summaryrefslogtreecommitdiff
path: root/frontend
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2025-11-15 18:00:09 +0000
committersoryu <soryu@soryu.co>2025-11-15 18:00:09 +0000
commit3e7b2beca1136a42700a7e1aebfe4c0fb2861a00 (patch)
tree6c896c31820681e360e50a73839fc2284c043dea /frontend
downloadsoryu-3e7b2beca1136a42700a7e1aebfe4c0fb2861a00.tar.gz
soryu-3e7b2beca1136a42700a7e1aebfe4c0fb2861a00.zip
Initial commit
Diffstat (limited to 'frontend')
-rw-r--r--frontend/README.md25
-rw-r--r--frontend/index.html14
-rw-r--r--frontend/package-lock.json1641
-rw-r--r--frontend/package.json27
-rw-r--r--frontend/public/.gitkeep0
-rw-r--r--frontend/public/__gaogao__56242cbde8f18ac64522e410bad04e68_waifu2x_art_noise2.pngbin0 -> 166561 bytes
-rw-r--r--frontend/public/a10-nerve-clips.svg26
-rw-r--r--frontend/public/ascii/ascii-flagged-3.txt65
-rw-r--r--frontend/public/ascii/ascii-flagged-7.txt15
-rw-r--r--frontend/public/ascii/ascii1.txt40
-rw-r--r--frontend/public/ascii/ascii2.txt26
-rw-r--r--frontend/public/ascii/ascii3.txt25
-rw-r--r--frontend/public/ascii/ascii4.txt33
-rw-r--r--frontend/public/ascii/ascii5.txt30
-rw-r--r--frontend/public/ascii/ascii6.txt32
-rw-r--r--frontend/public/ascii/ascii7.txt28
-rw-r--r--frontend/public/asuka-headset-logo.svg101
-rw-r--r--frontend/public/background-animation.gifbin0 -> 207908 bytes
-rw-r--r--frontend/public/kaomoji.txt3
-rw-r--r--frontend/public/logo/crane-logo-transparent.pngbin0 -> 142306 bytes
-rw-r--r--frontend/public/logo/crane-logo.pngbin0 -> 425121 bytes
-rw-r--r--frontend/public/nazrin-bg.jpgbin0 -> 198804 bytes
-rw-r--r--frontend/src/App.tsx19
-rw-r--r--frontend/src/components/BottomBar.tsx19
-rw-r--r--frontend/src/components/ChoiceMenu.tsx20
-rw-r--r--frontend/src/components/CityscapeBackground.tsx310
-rw-r--r--frontend/src/components/ConfigModal.tsx44
-rw-r--r--frontend/src/components/DialogueBox.tsx15
-rw-r--r--frontend/src/components/HeartLogo.tsx270
-rw-r--r--frontend/src/components/LandingPage.tsx219
-rw-r--r--frontend/src/components/LoadingScreen.tsx129
-rw-r--r--frontend/src/components/OrigamiDragonLogo.tsx180
-rw-r--r--frontend/src/components/TopBar.tsx24
-rw-r--r--frontend/src/components/VNApp.tsx228
-rw-r--r--frontend/src/components/VNInterface.tsx208
-rw-r--r--frontend/src/components/VNViewport.tsx22
-rw-r--r--frontend/src/main.tsx10
-rw-r--r--frontend/src/services/ws.ts69
-rw-r--r--frontend/src/stores/index.ts94
-rw-r--r--frontend/src/styles/pc98.css4353
-rw-r--r--frontend/src/types.ts11
-rw-r--r--frontend/tsconfig.json19
-rw-r--r--frontend/tsconfig.node.json9
-rw-r--r--frontend/tsconfig.node.tsbuildinfo1
-rw-r--r--frontend/tsconfig.tsbuildinfo1
-rw-r--r--frontend/vite-env.d.ts1
-rw-r--r--frontend/vite.config.d.ts2
-rw-r--r--frontend/vite.config.js6
-rw-r--r--frontend/vite.config.ts7
49 files changed, 8421 insertions, 0 deletions
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 0000000..1b07050
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,25 @@
+# PC-98 Visual Novel Skeleton (React + TypeScript + Vite)
+
+A responsive PC-98-style VN UI with CRT effects, dialogue box with nameplate, choice menu, and a WebSocket client stub for a Rust backend.
+
+Quick start
+1. cd pc98-vn
+2. npm install (or yarn / bun install)
+3. npm run dev (or yarn dev / bun run dev)
+
+Optional: Add a background image at public/bg.jpg. If absent, the app still runs using the base gradients.
+
+WebSocket wiring
+- Client URL is in src/services/ws.ts (default: ws://localhost:8080/ws).
+- Client sends on choice: { type: 'user_choice', id, label }.
+- Client expects messages like:
+ - { type: 'assistant', content: string }
+ - { type: 'choices', items: [{ id: string, label: string }, ...] }
+ - { type: 'bg', src: string }
+ - { type: 'name', value: string }
+ - { type: 'system', content: string }
+
+Notes
+- Aspect ratio ~16:10 for PC-98 feel (similar to 640x400). CRT scanlines and glow included.
+- Uses DotGothic16 as an optional pixel-like font from Google Fonts.
+- Portrait overlays can be positioned inside VNViewport's ui-layer.
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..4bb8d23
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
+ <title>PC-98 VN Skeleton</title>
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+ <link href="https://fonts.googleapis.com/css2?family=DotGothic16&display=swap" rel="stylesheet">
+ </head>
+ <body>
+ <div id="root"></div>
+ <script type="module" src="/src/main.tsx"></script>
+ </body>
+</html>
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
new file mode 100644
index 0000000..d7c37c7
--- /dev/null
+++ b/frontend/package-lock.json
@@ -0,0 +1,1641 @@
+{
+ "name": "pc98-vn",
+ "version": "0.0.1",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "pc98-vn",
+ "version": "0.0.1",
+ "dependencies": {
+ "@nanostores/react": "^1.0.0",
+ "@types/three": "^0.180.0",
+ "nanostores": "^1.0.1",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "three": "^0.180.0"
+ },
+ "devDependencies": {
+ "@types/node": "^24.3.1",
+ "@types/react": "^18.3.5",
+ "@types/react-dom": "^18.3.0",
+ "@vitejs/plugin-react": "^4.3.2",
+ "typescript": "^5.5.4",
+ "vite": "^5.4.0"
+ }
+ },
+ "node_modules/@ampproject/remapping": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
+ "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz",
+ "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz",
+ "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
+ "dev": true,
+ "dependencies": {
+ "@ampproject/remapping": "^2.2.0",
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.3",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-module-transforms": "^7.28.3",
+ "@babel/helpers": "^7.28.3",
+ "@babel/parser": "^7.28.3",
+ "@babel/template": "^7.27.2",
+ "@babel/traverse": "^7.28.3",
+ "@babel/types": "^7.28.2",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz",
+ "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/parser": "^7.28.3",
+ "@babel/types": "^7.28.2",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/compat-data": "^7.27.2",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+ "dev": true,
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
+ "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.28.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
+ "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz",
+ "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz",
+ "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz",
+ "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.3",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.28.3",
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.2",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.28.2",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz",
+ "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@dimforge/rapier3d-compat": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
+ "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.30",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz",
+ "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@nanostores/react": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@nanostores/react/-/react-1.0.0.tgz",
+ "integrity": "sha512-eDduyNy+lbQJMg6XxZ/YssQqF6b4OXMFEZMYKPJCCmBevp1lg0g+4ZRi94qGHirMtsNfAWKNwsjOhC+q1gvC+A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "engines": {
+ "node": "^20.0.0 || >=22.0.0"
+ },
+ "peerDependencies": {
+ "nanostores": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^1.0.0",
+ "react": ">=18.0.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.49.0.tgz",
+ "integrity": "sha512-rlKIeL854Ed0e09QGYFlmDNbka6I3EQFw7iZuugQjMb11KMpJCLPFL4ZPbMfaEhLADEL1yx0oujGkBQ7+qW3eA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.49.0.tgz",
+ "integrity": "sha512-cqPpZdKUSQYRtLLr6R4X3sD4jCBO1zUmeo3qrWBCqYIeH8Q3KRL4F3V7XJ2Rm8/RJOQBZuqzQGWPjjvFUcYa/w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.49.0.tgz",
+ "integrity": "sha512-99kMMSMQT7got6iYX3yyIiJfFndpojBmkHfTc1rIje8VbjhmqBXE+nb7ZZP3A5skLyujvT0eIUCUsxAe6NjWbw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.49.0.tgz",
+ "integrity": "sha512-y8cXoD3wdWUDpjOLMKLx6l+NFz3NlkWKcBCBfttUn+VGSfgsQ5o/yDUGtzE9HvsodkP0+16N0P4Ty1VuhtRUGg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.49.0.tgz",
+ "integrity": "sha512-3mY5Pr7qv4GS4ZvWoSP8zha8YoiqrU+e0ViPvB549jvliBbdNLrg2ywPGkgLC3cmvN8ya3za+Q2xVyT6z+vZqA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.49.0.tgz",
+ "integrity": "sha512-C9KzzOAQU5gU4kG8DTk+tjdKjpWhVWd5uVkinCwwFub2m7cDYLOdtXoMrExfeBmeRy9kBQMkiyJ+HULyF1yj9w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.49.0.tgz",
+ "integrity": "sha512-OVSQgEZDVLnTbMq5NBs6xkmz3AADByCWI4RdKSFNlDsYXdFtlxS59J+w+LippJe8KcmeSSM3ba+GlsM9+WwC1w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.49.0.tgz",
+ "integrity": "sha512-ZnfSFA7fDUHNa4P3VwAcfaBLakCbYaxCk0jUnS3dTou9P95kwoOLAMlT3WmEJDBCSrOEFFV0Y1HXiwfLYJuLlA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.49.0.tgz",
+ "integrity": "sha512-Z81u+gfrobVK2iV7GqZCBfEB1y6+I61AH466lNK+xy1jfqFLiQ9Qv716WUM5fxFrYxwC7ziVdZRU9qvGHkYIJg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.49.0.tgz",
+ "integrity": "sha512-zoAwS0KCXSnTp9NH/h9aamBAIve0DXeYpll85shf9NJ0URjSTzzS+Z9evmolN+ICfD3v8skKUPyk2PO0uGdFqg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.49.0.tgz",
+ "integrity": "sha512-2QyUyQQ1ZtwZGiq0nvODL+vLJBtciItC3/5cYN8ncDQcv5avrt2MbKt1XU/vFAJlLta5KujqyHdYtdag4YEjYQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.49.0.tgz",
+ "integrity": "sha512-k9aEmOWt+mrMuD3skjVJSSxHckJp+SiFzFG+v8JLXbc/xi9hv2icSkR3U7uQzqy+/QbbYY7iNB9eDTwrELo14g==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.49.0.tgz",
+ "integrity": "sha512-rDKRFFIWJ/zJn6uk2IdYLc09Z7zkE5IFIOWqpuU0o6ZpHcdniAyWkwSUWE/Z25N/wNDmFHHMzin84qW7Wzkjsw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.49.0.tgz",
+ "integrity": "sha512-FkkhIY/hYFVnOzz1WeV3S9Bd1h0hda/gRqvZCMpHWDHdiIHn6pqsY3b5eSbvGccWHMQ1uUzgZTKS4oGpykf8Tw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.49.0.tgz",
+ "integrity": "sha512-gRf5c+A7QiOG3UwLyOOtyJMD31JJhMjBvpfhAitPAoqZFcOeK3Kc1Veg1z/trmt+2P6F/biT02fU19GGTS529A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.49.0.tgz",
+ "integrity": "sha512-BR7+blScdLW1h/2hB/2oXM+dhTmpW3rQt1DeSiCP9mc2NMMkqVgjIN3DDsNpKmezffGC9R8XKVOLmBkRUcK/sA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.49.0.tgz",
+ "integrity": "sha512-hDMOAe+6nX3V5ei1I7Au3wcr9h3ktKzDvF2ne5ovX8RZiAHEtX1A5SNNk4zt1Qt77CmnbqT+upb/umzoPMWiPg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.49.0.tgz",
+ "integrity": "sha512-wkNRzfiIGaElC9kXUT+HLx17z7D0jl+9tGYRKwd8r7cUqTL7GYAvgUY++U2hK6Ar7z5Z6IRRoWC8kQxpmM7TDA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.49.0.tgz",
+ "integrity": "sha512-gq5aW/SyNpjp71AAzroH37DtINDcX1Qw2iv9Chyz49ZgdOP3NV8QCyKZUrGsYX9Yyggj5soFiRCgsL3HwD8TdA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.49.0.tgz",
+ "integrity": "sha512-gEtqFbzmZLFk2xKh7g0Rlo8xzho8KrEFEkzvHbfUGkrgXOpZ4XagQ6n+wIZFNh1nTb8UD16J4nFSFKXYgnbdBg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@tweenjs/tween.js": {
+ "version": "23.1.3",
+ "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
+ "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA=="
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true
+ },
+ "node_modules/@types/node": {
+ "version": "24.3.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz",
+ "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==",
+ "dev": true,
+ "dependencies": {
+ "undici-types": "~7.10.0"
+ }
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "dev": true
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.24",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz",
+ "integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==",
+ "dev": true,
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "dev": true,
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "node_modules/@types/stats.js": {
+ "version": "0.17.4",
+ "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
+ "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA=="
+ },
+ "node_modules/@types/three": {
+ "version": "0.180.0",
+ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.180.0.tgz",
+ "integrity": "sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg==",
+ "dependencies": {
+ "@dimforge/rapier3d-compat": "~0.12.0",
+ "@tweenjs/tween.js": "~23.1.3",
+ "@types/stats.js": "*",
+ "@types/webxr": "*",
+ "@webgpu/types": "*",
+ "fflate": "~0.8.2",
+ "meshoptimizer": "~0.22.0"
+ }
+ },
+ "node_modules/@types/webxr": {
+ "version": "0.5.23",
+ "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.23.tgz",
+ "integrity": "sha512-GPe4AsfOSpqWd3xA/0gwoKod13ChcfV67trvxaW2krUbgb9gxQjnCx8zGshzMl8LSHZlNH5gQ8LNScsDuc7nGQ=="
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/@webgpu/types": {
+ "version": "0.1.64",
+ "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.64.tgz",
+ "integrity": "sha512-84kRIAGV46LJTlJZWxShiOrNL30A+9KokD7RB3dRCIqODFjodS5tCD5yyiZ8kIReGVZSDfA3XkkwyyOIF6K62A=="
+ },
+ "node_modules/browserslist": {
+ "version": "4.25.4",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz",
+ "integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "caniuse-lite": "^1.0.30001737",
+ "electron-to-chromium": "^1.5.211",
+ "node-releases": "^2.0.19",
+ "update-browserslist-db": "^1.1.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001737",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001737.tgz",
+ "integrity": "sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ]
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true
+ },
+ "node_modules/csstype": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+ "dev": true
+ },
+ "node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.211",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.211.tgz",
+ "integrity": "sha512-IGBvimJkotaLzFnwIVgW9/UD/AOJ2tByUmeOrtqBfACSbAw5b1G0XpvdaieKyc7ULmbwXVx+4e4Be8pOPBrYkw==",
+ "dev": true
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/fflate": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
+ "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="
+ },
+ "node_modules/fsevents": {
+ "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": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/meshoptimizer": {
+ "version": "0.22.0",
+ "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.22.0.tgz",
+ "integrity": "sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg=="
+ },
+ "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==",
+ "dev": true
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/nanostores": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/nanostores/-/nanostores-1.0.1.tgz",
+ "integrity": "sha512-kNZ9xnoJYKg/AfxjrVL4SS0fKX++4awQReGqWnwTRHxeHGZ1FJFVgTqr/eMrNQdp0Tz7M7tG/TDaX8QfHDwVCw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "engines": {
+ "node": "^20.0.0 || >=22.0.0"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.19",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
+ "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
+ "dev": true
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.49.0.tgz",
+ "integrity": "sha512-3IVq0cGJ6H7fKXXEdVt+RcYvRCt8beYY9K1760wGQwSAHZcS9eot1zDG5axUbcp/kWRi5zKIIDX8MoKv/TzvZA==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.49.0",
+ "@rollup/rollup-android-arm64": "4.49.0",
+ "@rollup/rollup-darwin-arm64": "4.49.0",
+ "@rollup/rollup-darwin-x64": "4.49.0",
+ "@rollup/rollup-freebsd-arm64": "4.49.0",
+ "@rollup/rollup-freebsd-x64": "4.49.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.49.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.49.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.49.0",
+ "@rollup/rollup-linux-arm64-musl": "4.49.0",
+ "@rollup/rollup-linux-loongarch64-gnu": "4.49.0",
+ "@rollup/rollup-linux-ppc64-gnu": "4.49.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.49.0",
+ "@rollup/rollup-linux-riscv64-musl": "4.49.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.49.0",
+ "@rollup/rollup-linux-x64-gnu": "4.49.0",
+ "@rollup/rollup-linux-x64-musl": "4.49.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.49.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.49.0",
+ "@rollup/rollup-win32-x64-msvc": "4.49.0",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/three": {
+ "version": "0.180.0",
+ "resolved": "https://registry.npmjs.org/three/-/three-0.180.0.tgz",
+ "integrity": "sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w=="
+ },
+ "node_modules/typescript": {
+ "version": "5.9.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
+ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.10.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
+ "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
+ "dev": true
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
+ "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.4.19",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz",
+ "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
+ "dev": true,
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true
+ }
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..53e4c2c
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "pc98-vn",
+ "private": true,
+ "version": "0.0.1",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@nanostores/react": "^1.0.0",
+ "@types/three": "^0.180.0",
+ "nanostores": "^1.0.1",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "three": "^0.180.0"
+ },
+ "devDependencies": {
+ "@types/node": "^24.3.1",
+ "@types/react": "^18.3.5",
+ "@types/react-dom": "^18.3.0",
+ "@vitejs/plugin-react": "^4.3.2",
+ "typescript": "^5.5.4",
+ "vite": "^5.4.0"
+ }
+}
diff --git a/frontend/public/.gitkeep b/frontend/public/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/frontend/public/.gitkeep
diff --git a/frontend/public/__gaogao__56242cbde8f18ac64522e410bad04e68_waifu2x_art_noise2.png b/frontend/public/__gaogao__56242cbde8f18ac64522e410bad04e68_waifu2x_art_noise2.png
new file mode 100644
index 0000000..cce12f1
--- /dev/null
+++ b/frontend/public/__gaogao__56242cbde8f18ac64522e410bad04e68_waifu2x_art_noise2.png
Binary files differ
diff --git a/frontend/public/a10-nerve-clips.svg b/frontend/public/a10-nerve-clips.svg
new file mode 100644
index 0000000..d651857
--- /dev/null
+++ b/frontend/public/a10-nerve-clips.svg
@@ -0,0 +1,26 @@
+<svg width="60" height="24" viewBox="0 0 60 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <!-- Left clip -->
+ <g>
+ <!-- Main clip body with curved corner -->
+ <path d="M2 22 Q4 20 6 20 L12 2 L16 2 Q14 4 12 6 L6 22 Z" fill="#dc2626" stroke="#b91c1c" stroke-width="0.5"/>
+ <!-- Inner highlight -->
+ <path d="M3.5 20.5 Q5 19 6.5 19 L12.5 3.5 L14.5 3.5 Q13 5 11.5 6.5 L5.5 20.5 Z" fill="#ef4444" opacity="0.6"/>
+ <!-- Metal reflection -->
+ <path d="M4 19 Q5.5 18 7 18 L13 4 L14 4 Q12.5 5.5 11 7 L5 19 Z" fill="#fca5a5" opacity="0.3"/>
+ </g>
+
+ <!-- Right clip -->
+ <g>
+ <!-- Main clip body with curved corner -->
+ <path d="M58 22 Q56 20 54 20 L48 2 L44 2 Q46 4 48 6 L54 22 Z" fill="#dc2626" stroke="#b91c1c" stroke-width="0.5"/>
+ <!-- Inner highlight -->
+ <path d="M56.5 20.5 Q55 19 53.5 19 L47.5 3.5 L45.5 3.5 Q47 5 48.5 6.5 L54.5 20.5 Z" fill="#ef4444" opacity="0.6"/>
+ <!-- Metal reflection -->
+ <path d="M56 19 Q54.5 18 53 18 L47 4 L46 4 Q47.5 5.5 49 7 L55 19 Z" fill="#fca5a5" opacity="0.3"/>
+ </g>
+
+ <!-- Center connection hints (subtle) -->
+ <circle cx="30" cy="12" r="1" fill="#dc2626" opacity="0.4"/>
+ <circle cx="28" cy="12" r="0.5" fill="#ef4444" opacity="0.3"/>
+ <circle cx="32" cy="12" r="0.5" fill="#ef4444" opacity="0.3"/>
+</svg> \ No newline at end of file
diff --git a/frontend/public/ascii/ascii-flagged-3.txt b/frontend/public/ascii/ascii-flagged-3.txt
new file mode 100644
index 0000000..854f25d
--- /dev/null
+++ b/frontend/public/ascii/ascii-flagged-3.txt
@@ -0,0 +1,65 @@
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⠊⠁⠈⢘⣒⣤⠠⠰⣤⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠐⠀⢀⠀⠀⢐⡩⡽⣿⣟⡄⠉⣻⣷⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⢌⣴⡴⠊⣁⢔⣈⣰⡜⢉⣿⣿⡻⣤⣹⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⣲⡿⢋⣤⣞⣵⣿⡟⣡⣔⣯⣿⣧⣯⣷⣿⣿⣿⣿⣿⣦⠐⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣮⣞⣵⢿⣿⣿⣿⣷⣿⣿⣿⡿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⠐⢯⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⢡⠉⠜⣿⣿⣿⢻⣿⣿⣿⣿⣿⣿⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡸⣾⣿⣿⣿⣿⣿⣿⣿⣿⠟⠀⠂⠀⢈⣿⣷⣿⠀⣿⢻⣿⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠰⣿⣟⣻⣿⣿⣿⣿⣿⣿⣏⣀⠀⠀⠀⠸⣿⢿⡶⢀⡟⠈⣼⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⢃⣿⣭⣿⣿⣿⣿⣿⣿⡏⢍⣙⠛⠷⠦⡄⢻⣼⠋⣼⣁⣀⣹⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡎⢸⣿⣿⣿⣿⣿⣽⣿⠏⠻⠿⢿⣟⢦⢀⠀⠀⠉⠀⣉⣉⠉⢻⣿⣿⣿⣿⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠃⣸⣿⣿⣿⣿⣿⣿⡟⡈⠀⠀⠀⠀⢀⢮⠀⠀⠀⠚⡛⠃⠿⣾⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⢳⣿⣿⣿⣿⣷⣿⣿⠱⠀⠁⠀⠀⠀⣨⠆⠃⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⢿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠃⢞⣿⣿⣿⣿⣿⣿⣟⠠⠁⠀⠀⠀⠘⠣⣼⠀⢀⠀⠀⠀⠀⠀⣼⣿⣿⣿⡻⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢈⣼⣿⣿⣿⣿⣿⣿⣿⡄⠀⠀⠈⢷⣤⡤⢤⣠⣀⣀⠨⠀⠀⣰⣿⣿⣿⠏⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠰⠀⢼⡟⣽⣿⣿⣿⣿⣿⣿⣿⣆⢀⠀⠈⠱⠡⠔⠨⠎⠀⠀⣀⣼⣿⣿⣿⣻⠜⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠄⠀⢠⣿⢱⣿⣿⣿⣿⣿⣿⣿⣿⡜⠛⣧⡀⠀⠀⠀⠀⢀⢠⣾⣿⣿⣿⣿⣿⣯⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢼⣥⡿⣼⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⠉⠒⠀⠐⠀⠀⢸⢿⣿⣿⣿⣿⣿⡤⠀⠀⠘⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⠸⣼⠇⢸⣿⣿⣿⣿⣿⣿⢾⣯⡀⠀⠀⠀⠀⠀⠀⠀⠈⢸⣿⣟⣻⢟⣫⠥⠧⠤⠤⡀⠁⠐⠄⢀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡒⡀⠈⠻⡗⢹⣿⣿⣿⣿⣿⣿⣿⡇⣷⠀⠀⠀⠀⠀⣀⣀⣄⠾⠿⣷⠚⠉⠀⠘⡄⠣⠈⠔⠂⠀⠄⠀⠱⡄⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠐⠈⠀⠀⢠⣤⡁⠉⢿⠿⣿⣿⣿⣿⣿⡟⠉⠂⠀⡐⠢⠁⠀⠀⠀⠀⠀⢿⡆⠀⠀⠐⠀⠁⠀⠀⠀⠀⠀⠀⠀⠘⠆⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⠔⢁⡠⠤⠄⠂⠉⢫⡏⢷⣶⣷⣿⣿⣿⣿⣿⣟⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢫⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢘⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⢊⡖⠉⠀⠀⠀⠀⠀⢈⢲⠾⢿⣿⣿⣿⣿⣿⢶⡚⢁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡆⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠈⣿⠠⠁⠀⠀⠀⠀⡀⠆⣤⠚⡉⢿⡿⢿⠿⠟⡉⠔⠠⠈⠀⠀⠀⠀⠀⢀⠠⠈⠄⡀⢀⠀⢿⣇⠀⠀⠀⠀⠀⠄⠀⠀⠀⠀⠀⠀⠘⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⣏⠆⡀⠀⠀⢠⢰⡡⢂⠁⠠⢉⣿⠇⠆⡘⢠⠀⢂⠀⠀⠀⠀⠀⢀⡐⢢⠁⢎⡐⠄⠂⠀⠸⣿⣆⠀⠀⠀⠀⠈⠆⠀⠀⠀⠀⠀⠀⡆⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⢰⣏⠒⠠⢄⠣⣞⡟⡔⣂⠒⣀⢾⣿⢈⠱⣀⠃⠜⡀⠢⢀⠀⠀⢀⠲⣘⠢⡉⠤⠐⡈⠄⠁⠀⢸⣿⣄⠀⠀⠀⠀⠰⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⣬⢧⠉⡒⣌⠳⣽⣟⣺⡴⠖⢫⠿⠇⡈⠐⠠⢈⠂⡐⠁⠂⠐⣀⢸⡳⢌⡁⠆⠁⢂⠀⠀⠀⠀⠈⣿⡿⣷⣄⠀⠀⠣⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⢸⣃⠆⡱⢎⡽⣺⣿⠋⢀⠠⢻⣠⠀⠈⠡⠄⠀⠄⠀⠀⠈⠀⢈⣿⢆⢃⡐⠈⠀⠀⠀⠀⠀⠀⠀⣿⣏⣿⡝⣷⡀⠀⠁⠀⠀⠀⠀⠀⠇⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⢰⡇⢎⠵⣋⢶⡹⠁⠠⡀⢠⣻⣖⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⠎⡠⢀⠀⠀⠀⠀⠀⠀⠀⢠⣿⢺⡵⣛⠼⣋⢦⠀⠐⡀⠀⠀⠀⠄⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠸⡝⡬⢳⡍⡞⠁⣤⣇⣶⣷⣿⣿⡖⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⠆⠡⠀⠀⠀⠀⠀⠀⠀⠀⢸⡧⣗⢮⡱⢫⡜⢦⢣⠀⠄⠀⠀⢀⠃⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⣟⡼⡱⢎⢇⣾⣿⣿⣿⣟⣧⣿⣷⠐⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⢎⠡⠀⠀⠀⠀⠀⠀⠀⢀⣿⢫⡜⢎⡱⢣⠜⣢⢃⢆⠀⠀⠀⢰⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⢹⢶⡙⣾⣮⣿⣿⣿⣻⡾⣽⣛⣿⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⢂⠀⠀⠀⠀⠀⠀⢀⣾⢣⠣⡜⣌⠲⢡⠚⡄⢎⢸⠀⠀⠀⡜⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠨⡷⡹⣌⣿⣿⣿⣿⣷⣻⢧⡛⡼⢿⣧⡀⠀⠀⠀⠀⠀⠀⠀⢸⣇⠂⠀⠀⠀⠀⠀⢀⣼⢏⡲⢱⠘⡄⠣⢸⠇⡈⠔⡸⠀⠀⢀⠃⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⢷⣙⢦⣹⣿⣿⣿⣾⡽⢮⡕⡣⢍⣿⣧⡀⠀⠀⠀⠀⠀⠀⡌⠹⣧⡀⠀⠀⠀⢠⡾⣋⠎⡴⢁⠊⠄⡁⡾⠀⠐⢠⠁⠀⠀⣨⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣭⠖⡌⢿⣿⣿⣿⡽⣧⡛⡴⢃⠆⡋⡿⣦⡀⠀⠀⠀⡐⠀⠀⠀⠓⣄⣠⣼⣿⡳⣍⠚⠤⡁⠌⣀⠞⠀⢀⠜⠁⠀⠀⢀⠇⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⢌⠳⣈⠻⣟⣿⣿⣳⡽⣱⠍⡎⡔⡰⢨⢽⣷⣤⡼⠶⠖⠻⠛⠛⠋⠋⠺⣙⠻⠮⣏⣶⣼⣾⠭⠴⠒⠁⠀⠀⠀⠀⠘⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⣎⡓⠄⡂⠌⢻⣽⣟⣿⣷⡿⣴⣣⣷⠿⢛⠩⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠁⠀⠀⠀⠀⠀⠀⠀⠀⢸⡠⠀⠀⠀⡘⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢷⡉⢆⠡⢊⠦⣽⣾⣻⣟⡿⡻⡕⡒⢌⡁⠂⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡘⠀⠀⠀⢠⠃⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⡵⢂⠑⢪⣽⣳⣿⣵⢾⣱⠓⣬⡱⢆⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠁⠀⠀⢀⠆⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⠦⢌⢣⡟⣿⣿⣿⣯⣇⠋⢶⡹⢀⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠰⠀⠀⢀⠎⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠻⣆⠥⡚⡽⢯⡿⣷⣏⠞⡈⢗⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠃⢀⠎⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠐⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⢧⡱⣩⠟⣽⣻⣞⣯⠀⠍⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠪⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠐⠀⠀⠀⢀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡛⣜⣳⣟⡦⣃⠐⡀⠆⡌⠀⢤⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠁⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠳⣬⢋⡟⢶⢁⢂⠘⡴⣈⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠰⣀⠀⠀⠀⠀⠀⠀⠀⠀
+⠌⡀⠀⡁⠀⢀⠀⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⢼⡻⣌⠢⠀⠎⡵⢂⠡⠀⠀⠀⠀⠀⠈⣤⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⢢⠀⠀⠀⠀⠀⠀⠀
+⠂⠄⠀⡀⠄⠀⢀⠀⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⡈⣷⣫⢕⠢⠄⡁⠎⡵⡉⠄⠀⠀⠀⠀⠀⢳⣿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⡆⠀⠀⠀⠀⠀⠀
+⠌⠠⠐⠀⠀⠌⠀⠀⠀⠀⠐⠀⠀⠁⠀⠀⠀⠠⠁⣾⠵⡊⢅⠂⠄⠓⡬⢑⠠⠀⠀⠀⠀⠀⠈⠛⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠼⡀⠀⠀⠀⠀⠀
+⢂⠁⠠⢈⠀⠐⠈⢀⠠⠀⠀⠀⡀⠀⠀⠀⠀⠃⣼⣏⠳⠉⡄⠌⡀⠣⡘⠄⢂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⡿⠳⡀⠀⠀⠀⠀
+⠂⠌⢀⠂⡀⢂⠠⠀⠀⡀⠐⠀⠀⠀⠀⠀⠀⢼⡷⢎⠆⠁⠀⢂⠁⢂⠱⠈⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⠿⠁⠀⢣⠀⠀⠀⠀
+⠡⢈⠀⢂⠐⢀⠀⠁⠄⠀⠀⠄⠀⠈⠀⠀⢀⠹⣿⡆⠌⠀⠀⠈⢂⠄⠂⠡⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⢤⣎⠗⠋⠀⠀⠀⠀⠇⠀⠀⠀
+⣁⠂⡐⢀⠂⠄⢈⠀⠄⠈⡀⠄⠐⠀⠀⠀⡌⣠⠻⢿⣷⣦⣀⠀⠂⠠⠀⡁⠂⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⠤⡺⠭⠚⠁⠀⠀⠀⠀⠀⠀⠀⠡⡀⠀⠀
+⠄⡂⠐⡀⢂⠈⠠⢀⠂⠐⡀⠐⠈⡀⠀⠀⣳⣯⣛⠤⡈⠙⠛⠻⠷⣶⣶⣤⣁⣂⡀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⣠⣴⠲⣋⡔⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢡⠀⠀
+⢂⠅⢂⠐⠠⢈⠐⡀⠈⠄⡐⠈⠠⠀⠀⢀⣼⣳⢭⠲⣁⠂⠄⠀⠀⠀⠀⠙⢿⣿⣻⢿⡳⣖⠶⣶⣛⣿⣙⠳⣩⢡⢢⣱⠎⢀⡠⠆⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠀⠀
+⡌⢂⠄⠌⡐⡀⠂⠄⠡⠀⠄⠡⠀⠁⠀⢀⢹⣧⢫⠱⣀⠊⢀⠀⠀⠀⠀⠤⡈⠹⢯⣿⣱⢏⡾⣰⡙⢦⢣⠓⡔⢢⣳⢃⡾⢋⠔⠠⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⡄⠀
+⠆⡡⢈⠐⠠⢀⠡⠈⠄⠡⠈⠄⡁⠈⠀⠸⢸⣎⠧⢣⠐⡈⠀⠀⠀⠀⠀⠀⠈⠐⢌⡹⣟⣮⡳⣥⢫⢇⡏⠞⣌⣿⢷⠫⠜⡠⠈⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀
+⠜⣀⠂⠌⡐⢀⠂⠡⠈⠄⡁⢂⠄⠡⠀⡇⡿⡼⣙⠦⡑⠄⡁⠀⠀⠀⠀⠀⠀⠀⠀⠈⠢⣟⣿⣖⢯⣾⣭⣿⣾⢛⡌⢣⠘⢀⠁⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀
+⡱⢀⠌⡐⢀⠂⠌⡠⢁⠂⠄⡁⢂⠐⡀⡇⣿⡱⣍⠲⣁⠂⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠹⣿⡿⠿⢿⡟⡬⢃⠜⠠⠈⠄⠐⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀
+⡔⠡⠂⢌⠠⢈⠐⡀⠂⠌⡐⠠⢁⠂⠄⢻⣯⢳⡍⡖⢄⠃⡈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣧⢠⡿⣘⠱⡈⠌⠄⠡⢈⠐⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠇⠀
+⢌⡑⠈⡄⠂⢄⠂⠄⡁⠆⠠⡁⠢⢈⠐⢸⣯⢳⡝⡜⡠⠒⡀⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣸⢧⢡⠃⡜⠠⠈⠔⡁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢈⠀⠀
+⠆⡌⢡⠠⠉⡄⠨⠄⡁⢂⠡⠀⡅⠂⠌⣈⣿⢳⡞⡥⡑⢂⠐⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⢎⠢⡑⡀⠂⡍⠰⠀⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡎⠀⠀
+⢣⠘⡀⢆⠡⢂⢁⠂⠔⠠⡁⢡⠀⢃⠐⡀⣿⡳⡞⣥⠃⡌⠠⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⢎⠱⡐⣀⠣⢌⢁⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠁⠀⠀
+⢣⡑⢌⠂⡅⢢⠈⠔⡈⠔⡠⠁⢌⠐⡐⢀⢻⣵⠻⡴⡩⢄⠡⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢻⣌⠣⡐⢄⡊⠔⢂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡆⠀⠀⠀
+⢣⡘⢄⠣⢌⠰⣈⢂⠱⢈⠔⠡⣈⠐⡈⢄⠈⣯⣟⣱⡑⣂⠂⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣎⠱⣈⠦⡑⠊⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡘⠀⠀⠀⠀
+⢣⡘⢄⢊⡄⢣⠐⡌⢄⠣⢈⠥⢀⠒⠠⡈⠄⢻⣜⢧⠳⡄⢃⠐⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣇⠳⣄⢣⠱⡉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠂⠀⠀⠀⠀
+⠣⡜⡐⢢⠘⡄⢣⠘⡄⢊⠔⡈⠆⡘⢠⠐⡡⠈⣿⣎⠷⡘⠄⢂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣏⠶⣌⢣⠑⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡂⠀⠀⠀⠀⠀
+⣓⢤⠃⡥⠚⣄⠃⢎⠰⡁⢎⠰⢡⠘⣀⠒⠤⠑⣸⢮⢏⡵⢈⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣏⡞⡌⢆⠡⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⠀⠀⠀⠀⠀⠄
diff --git a/frontend/public/ascii/ascii-flagged-7.txt b/frontend/public/ascii/ascii-flagged-7.txt
new file mode 100644
index 0000000..256ff6e
--- /dev/null
+++ b/frontend/public/ascii/ascii-flagged-7.txt
@@ -0,0 +1,15 @@
+⣤⠀⣰⣿⠋⠀⡇⠀⠀⠀⠀⢸⠁⠀⠀⠈⠛⣗⠢⠤⠊⠉⠣⡀⡈⡇⠀⠀⠉⠣
+⢰⣾⣿⠟⠀⠀⠇⠀⠀⠀⠀⡘⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠈⢅⠘⢄⠀⢀⡀
+⠀⠉⡍⡇⠀⠀⣴⠀⠀⠀⠁⡇⠀⠀⠀⠀⠀⢸⡀⢄⣒⡠⠤⠤⠀⣣⡀⡩⢝⢕
+⠀⠀⣿⠁⠀⠀⢿⡀⠀⠀⠀⡇⠀⢀⡠⢄⣒⠌⠟⠉⠀⠉⠉⠉⢹⣿⣵⣾⢿⣥
+⠀⠀⢸⡶⢄⣸⡽⠁⠀⠀⠀⡧⣲⢵⣮⠗⠁⠀⠀⠀⠀⠀⠀⠀⢸⢁⣢⣜⡼⠟
+⠀⠀⠈⡿⠵⠶⢭⣄⣒⣒⣚⣿⣿⣿⠁⡞⠁⠀⢀⠎⡅⠀⠀⢠⠋⠉⠀⠀⠀⠀
+⠀⠀⠀⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⡋⡧⠃⠈⠀⠊⠀⠧⣀⠀⡌⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠸⠀⠀⡤⡀⠀⢠⢤⠀⠀⢇⠀⠙⠁⠄⠘⠓⠀⠀⠹⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠇⠄⠃⣇⠀⢸⠉⠤⢜⢸⠀⠀⠀⠀⠀⠀⠀⢀⠃⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⢠⠀⢠⠆⠀⠠⠶⠀⢸⢸⠀⠀⠀⠀⠀⠀⠀⠌⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠈⡄⠀⠀⠀⠀⠀⠀⠌⡨⠀⠀⠀⠀⠀⠀⠸⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⢠⠀⠀⠀⠀⠀⠀⡇⡆⠀⠀⠀⠀⠀⢀⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠈⡄⠀⠀⠀⠀⠀⡇⠇⠀⠀⠀⠀⠀⡘⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⢣⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⢀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⡆⣧⠀⠀⠀⠀⠈⡇⠀⠀⠀
diff --git a/frontend/public/ascii/ascii1.txt b/frontend/public/ascii/ascii1.txt
new file mode 100644
index 0000000..279dc9e
--- /dev/null
+++ b/frontend/public/ascii/ascii1.txt
@@ -0,0 +1,40 @@
+ define) (e g 0)(
+ e h 0) ( e w
+ set!) ( e l lambda
+ )( e @ display ); i
+ (e t magnitude )(e(! .
+ x ) (@"" )) (e %
+ modulo )(e m make-rectangular ) (e
+ j 00 ) (e b begin
+ ) (e c 0)(
+ e ? map ) ( e a; ()
+ apply ) (e ( r x ) ( do ((
+ z x (+(* z z ) x ) ) ( i 0 (+ i
+ 1 ) ) )( ( or ( >( t z ) 2) (>
+ i 26 )) i ) ) )( !) (e n (b let*)
+ ) (e : ( * 5 7 997 97 ) )( e d(
+ current-time) ) ( e k* ( * 89 991 73 3 ))
+ ( ! :) (e ($ ) ( list ( m g j )( m g c )(
+ m h j) ( m h c )( m (/(+ g h) 2 )( /
+ ( + j c )2))) ) (e (_ x y)(*( - x y)
+ ( - x y)))(e ~ (* 120))( e ( & x
+ ) (integer->char ( if(> x 25) (
+ * 8 4)(+ 65 x))) ) (e (u
+ )( n((f(?(l (p)( r p) ;
+ ) ($)))(y (/(a + f )5
+ )) )(>(/(a +(?( l (x )
+ (_ x y))f) )5 )~ ) )
+ )( e ( v k ) (k c
+ j 24 ( l( y) ( b( k
+ g h 79( l( x)(@ (& ( r( m x
+ y) ) ))) )( !) ( @ "\n")
+ )))) (e (k u v s f) ( do ( (;
+ i 00 (+ i 1 ))) ( ( >= i i s) )(
+ f(+ u (/ (*( - v u)i)(+ s -1 )) ) )) )( e(
+ q i)( +( *(b( w d (% ( +( * k* d) 1 )
+ : ))(/ d : ))( - 1 i) ) i)) (e(o) (n (
+ ( s(q .1) ) )( w j( q(- 00 1 ) ) )( w c
+ (+ j s ))(w g(q( - 01))) ( w h( + g
+ ( * s 1.5) )) ) ) ( o)( do (
+ )(( u )(v k) ) (
+ o) )
diff --git a/frontend/public/ascii/ascii2.txt b/frontend/public/ascii/ascii2.txt
new file mode 100644
index 0000000..4d81e61
--- /dev/null
+++ b/frontend/public/ascii/ascii2.txt
@@ -0,0 +1,26 @@
+⠀⢀⣒⠒⠆⠤⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⢠⡛⠛⠻⣷⣶⣦⣬⣕⡒⠤⢀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⡿⢿⣿⣿⣿⣿⣿⡿⠿⠿⣿⣳⠖⢋⣩⣭⣿⣶⡤⠶⠶⢶⣒⣲⢶⣉⣐⣒⣒⣒⢤⡀⠀⠀⠀⠀⠀⠀⠀
+⣿⠀⠉⣩⣭⣽⣶⣾⣿⢿⡏⢁⣴⠿⠛⠉⠁⠀⠀⠀⠀⠀⠀⠉⠙⠲⢭⣯⣟⡿⣷⣘⠢⡀⠀⠀⠀⠀⠀
+⠹⣷⣿⣿⣿⣿⣿⢟⣵⠋⢠⡾⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠻⣿⣿⣾⣦⣾⣢⠀⠀⠀⠀
+⠀⠹⣿⣿⣿⡿⣳⣿⠃⠀⣼⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢻⣿⣿⣿⠟⠀⠀⠀⠀
+⠀⠀⠹⣿⣿⣵⣿⠃⠀⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠹⣷⡄⠀⠀⠀⠀⠀
+⠀⠀⠀⠈⠛⣯⡇⠛⣽⣦⣿⠀⠀⠀⠀⢀⠔⠙⣄⠀⠀⠀⠀⠀⠀⣠⠳⡀⠀⠀⠀⠀⢿⡵⡀⠀⠀⠀⠀
+⠀⠀⠀⠀⣸⣿⣿⣿⠿⢿⠟⠀⠀⠀⢀⡏⠀⠀⠘⡄⠀⠀⠀⠀⢠⠃⠀⠹⡄⠀⠀⠀⠸⣿⣷⡀⠀⠀⠀
+⠀⠀⠀⢰⣿⣿⣿⣿⡀⠀⠀⠀⠀⠀⢸⠒⠤⢤⣀⣘⣆⠀⠀⠀⡏⢀⣀⡠⢷⠀⠀⠀⠀⣿⡿⠃⠀⠀⠀
+⠀⠀⠀⠸⣿⣿⠟⢹⣥⠀⠀⠀⠀⠀⣸⣀⣀⣤⣀⣀⠈⠳⢤⡀⡇⣀⣠⣄⣸⡆⠀⠀⠀⡏⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠁⠁⠀⢸⢟⡄⠀⠀⠀⠀⣿⣾⣿⣿⣿⣿⠁⠀⠈⠙⠙⣯⣿⣿⣿⡇⠀⠀⢠⠃⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠇⢨⢞⢆⠀⠀⠀⡿⣿⣿⣿⣿⡏⠀⠀⠀⠀⠀⣿⣿⣿⡿⡇⠀⣠⢟⡄⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⡼⠀⢈⡏⢎⠳⣄⠀⡇⠙⠛⠟⠛⠀⠀⠀⠀⠀⠀⠘⠻⠛⢱⢃⡜⡝⠈⠚⡄⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠘⣅⠁⢸⣋⠈⢣⡈⢷⠇⠀⠀⠀⠀⠀⣄⠀⠀⢀⡄⠀⠀⣠⣼⢯⣴⠇⣀⡀⢸⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠈⠳⡌⠛⣶⣆⣷⣿⣦⣄⣀⠀⠀⠀⠈⠉⠉⢉⣀⣤⡞⢛⣄⡀⢀⡨⢗⡦⠎⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠈⠑⠪⣿⠁⠀⠐⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣏⠉⠁⢸⠀⠀⠀⠄⠙⡆⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⣀⠤⠚⡉⢳⡄⠡⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣏⠁⣠⣧⣤⣄⣀⡀⡰⠁⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⢀⠔⠉⠀⠀⠀⠀⢀⣧⣠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣅⡀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⢸⠆⠀⠀⠀⣀⣼⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠟⠋⠁⣠⠖⠒⠒⠛⢿⣆⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠑⠤⠴⠞⢋⣵⣿⢿⣿⣿⣿⣿⣿⣿⠗⣀⠀⠀⠀⠀⠀⢰⠇⠀⠀⠀⠀⢀⡼⣶⣤⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠟⢛⣿⠀⠙⠲⠽⠛⠛⠵⠞⠉⠙⠳⢦⣀⣀⡞⠀⠀⠀⠀⡠⠋⠐⠣⠮⡁⠀
+⠀⠀⠀⠀⠀⠀⠀⢠⣎⡀⢀⣾⠇⢀⣠⡶⢶⠞⠋⠉⠉⠒⢄⡀⠉⠈⠉⠀⠀⠀⣠⣾⠀⠀⠀⠀⠀⢸⡀
+⠀⠀⠀⠀⠀⠀⠀⠘⣦⡀⠘⢁⡴⢟⣯⣞⢉⠀⠀⠀⠀⠀⠀⢹⠶⠤⠤⡤⢖⣿⡋⢇⠀⠀⠀⠀⠀⢸⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠵⠗⠺⠟⠖⢈⡣⡄⠀⠀⠀⠀⢀⣼⡤⣬⣽⠾⠋⠉⠑⠺⠧⣀⣤⣤⡠⠟⠃
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠛⠷⠶⠦⠶⠞⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ \ No newline at end of file
diff --git a/frontend/public/ascii/ascii3.txt b/frontend/public/ascii/ascii3.txt
new file mode 100644
index 0000000..a84762f
--- /dev/null
+++ b/frontend/public/ascii/ascii3.txt
@@ -0,0 +1,25 @@
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾⢟⣵⣶⣿⣿⠉⢍⢠⡈⠻⠶⣬⡙⢷⣮⠙⠿⣿⣷⣶⣄⠀⠀⢀⣠⢖⡡⠖⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠉⠒⠤⠄⣈⡑⠢⠤⢀⡴⠛⣵⣿⣿⠟⠉⢹⡶⠈⣌⢻⡄⠀⠘⣿⣷⣹⣷⣄⠹⣿⠛⢻⣷⣶⡯⠶⢿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⣹⠟⢲⡞⠁⣼⣿⣿⠏⠀⢠⢸⣇⡀⣿⡎⢿⣆⠸⡌⠻⣿⣿⣿⣧⠈⢷⡀⡿⣝⣧⠀⠈⢷⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⢠⡟⢀⡞⠀⣼⣿⡿⢃⠀⣠⠏⣼⣿⡇⢿⣧⢘⣿⣿⣧⠀⠙⠿⣿⣿⣷⡈⢷⡀⠘⣞⢧⡀⠘⣷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⣸⣷⡾⢡⢤⣯⣿⢣⢎⣾⡟⢠⣿⡿⣿⢸⡏⢿⣿⣿⣿⣷⡀⠘⣿⣿⣿⣷⡈⢷⡀⠘⣯⣳⣦⢹⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⣿⣿⣧⡟⣟⣾⣯⡟⣼⢻⢃⣿⡿⠁⠸⡌⣿⡀⠻⣟⣿⣿⣿⣄⠈⢿⣿⣿⣿⣌⢿⣄⠘⢷⣿⣿⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⣿⣿⣿⢹⣿⣿⢿⢱⡇⢸⣿⡿⠁⠀⣀⣷⣿⣧⠀⠙⢦⡙⠻⣿⣷⣦⣻⣿⣿⣿⣎⣿⣦⠀⢻⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⢸⣿⢧⣿⣿⡿⠈⢸⢇⣿⣿⣡⠔⠊⠁⠀⢳⣿⡆⠀⠀⠙⢦⡈⠙⠻⢿⣿⣿⣿⣿⣿⣿⣧⡀⠹⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⢸⣿⣼⣿⣿⣯⢇⣿⣸⣿⠁⠀⢀⠀⣀⠀⠀⠻⣿⡄⠀⠀⠀⢛⣽⢧⣤⣌⡿⣿⣿⣿⣿⣿⣿⣆⠘⢿⣄⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⣼⣿⣿⣿⣿⢿⣿⡇⣸⠃⢀⣾⣿⣿⣦⡀⠀⠀⠙⢿⡄⠀⠀⢼⣅⣼⣿⣿⡿⣯⣽⣿⢻⣿⣿⣿⣷⡀⠙⢦⡀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⣿⣿⣿⣿⣿⢸⣿⡇⣿⢙⣿⣏⣠⣿⣿⡇⠀⠀⠀⠈⠻⣆⠀⠸⡿⢿⣿⣿⠇⢹⠿⣿⡈⣿⣿⣿⣿⣿⣷⣾⣷⣄⠀⠀⠀⠀
+⠀⠀⠀⢸⣿⣿⣿⣿⣿⠸⣿⣷⣿⣿⡇⢿⠿⢿⡟⡋⠀⠀⠀⠀⠀⠈⠣⡀⠑⢤⣤⣶⠀⠀⠀⣿⡇⣿⣿⣿⣿⢹⡟⠿⣿⣿⣷⣤⡀⠀
+⠀⠀⢠⣿⣿⠏⣿⣿⣿⢀⣿⡏⣿⠉⠛⠊⠳⣤⣶⠇⠀⠀⠀⠀⠀⠀⠀⠈⠓⠀⠀⠀⠀⠀⠀⣿⣧⣿⣿⣿⣿⣷⣷⡀⠀⠈⠉⠉⠙⠓
+⢀⣴⣿⣿⠃⠀⣿⣿⣿⢸⣿⠐⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⢻⣿⣿⣿⡏⠿⣿⣿⣧⡀⠀⠀⠀⠀⠀
+⠋⣸⡿⠁⠀⠀⣿⣿⣿⣿⣿⡄⢸⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠇⢸⣿⣿⣿⣿⣦⣤⠟⠋⠉⠒⢦⡀⠀⠀
+⡴⠏⠀⠀⠀⠀⣿⡿⠋⢹⣿⣇⠘⣇⠙⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠋⠀⣿⣿⣿⣿⠛⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⡴⠁⠀⠀⢸⣿⣿⡄⢹⡄⠀⠑⠢⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡤⠊⠀⠀⢰⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⡼⠀⠀⠀⠀⠈⣿⠻⣿⣟⣿⣄⠀⠀⠀⠉⢲⡤⢄⡀⠀⠀⠀⡠⠞⡟⠀⣠⠀⠀⣾⡿⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠘⡄⠈⠻⣿⣿⠓⠤⠀⠀⠀⠙⠦⣈⠉⠒⠉⢀⡞⢀⠜⠁⠀⣸⠟⣠⣿⣿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⣿⠀⠀⢂⠀⠀⠀⠘⠆⠀⠀⠉⠓⣄⠀⠀⠀⠀⠀⠀⠉⢲⣤⣞⡊⠁⠀⠀⠚⠁⢰⢻⣿⡿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⢰⣿⣇⠀⠈⢆⢀⠀⠀⠀⠀⠀⠀⠀⠈⢢⡀⠀⠀⠀⠀⢠⠋⠘⡄⠈⠢⡀⠀⠀⢀⠇⢸⣿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⣾⣿⡟⣆⠀⠀⢠⣧⠀⠀⠀⠀⠀⠀⠀⠀⠑⠢⡀⠀⢠⠃⠰⠲⡜⡆⠀⠈⠢⣠⠎⠀⣿⡟⠀⠀⠀⠀⠀⠀⣠⠀⠀⠀⡄⠀⠀
+⠀⠀⠀⣿⣿⠁⣿⣦⠀⠀⠙⢆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠑⠃⠀⠘⠂⠃⠸⠀⠀⠀⠀⠀⢸⣿⡇⠀⠀⠀⠀⠀⢠⠃⠀⠀⠈⣇⠀⠀
+⠀⠀⠀⣿⣿⠀⣿⣿⣷⡄⠀⠈⢣⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⠀⠀⠀⠀⠀⠀⡾⠀⠀⠀⢠⣿⠀⠀
+⠀⠀⠀⣿⣿⠀⢻⣿⣿⣿⣦⣄⠀⡳⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⡿⠀⠀⠀⠀⢀⡜⢁⠄⠀⠀⢸⣿⠀⠀
diff --git a/frontend/public/ascii/ascii4.txt b/frontend/public/ascii/ascii4.txt
new file mode 100644
index 0000000..d90f854
--- /dev/null
+++ b/frontend/public/ascii/ascii4.txt
@@ -0,0 +1,33 @@
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⢃⡴⠋⠀⠀⠀⣀⣠⡤⠔⠒⠋⠁⠀⠀⠀⠐⠒⠚⠛⠻⢭⣗⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡞⠀⣀⡴⠒⠉⠁⠀⠀⠀⠀⠀⠀⣰⠖⠒⠦⣄⡀⠀⠀⠀⠀⠉⠙⠢⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⢈⡴⠎⠁⠀⠀⠀⠀⠀⠀⠀⠀⣀⡴⠋⠀⠀⠀⠀⠉⠳⣄⠀⠀⠀⠀⠀⠀⠙⠲⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⢀⠔⠉⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⠞⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⢀⡴⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⡞⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⢀⡞⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡞⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠐⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⣦⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⡾⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⡆⠀⠀⠀⠀⠳⣄⠀⠀⠀⠹⣎⠳⢤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⣰⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢣⠀⣀⠀⠀⠀⠈⠳⣄⠀⠀⠈⠳⣄⠙⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⡿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⢤⠀⠀⠀⠀⠀⠀⠀⠸⡇⠈⢢⡀⠀⢦⠀⠈⠳⣄⡀⠀⠈⠳⢄⡹⣆⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⠀⠀
+⠀⠐⢲⠗⢤⣄⡀⠀⠀⠀⠀⣀⣠⠤⣤⣏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡆⠈⡇⠀⠀⠀⠀⠀⠀⠀⣧⠀⠀⠳⡄⠈⢣⠀⠀⠀⠙⠢⣄⡀⠀⠙⠛⢷⡤⣄⣀⡀⠀⠀⣠⡷⠀⠀⠀
+⠀⢀⣼⠐⢻⣿⠟⢷⡀⠀⠚⠋⠉⠉⠳⡟⢷⡀⠀⠀⠀⠀⠀⠀⠀⢠⡇⠀⢸⡄⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⢹⡄⠈⢳⡀⠀⠀⠀⠀⢯⣓⠲⣤⣬⣷⡀⠈⠉⣩⡽⠋⠁⠀⠀⠀
+⣰⠊⡿⣠⢿⡿⣆⢀⢷⡀⠀⠀⠀⠀⣰⠇⠀⣧⠀⠀⠀⠀⠀⠀⠀⡾⠀⠀⠈⣇⠀⢀⠀⠀⠀⠀⢸⠀⠀⢀⣀⣻⡀⠀⢳⡄⠀⠀⠀⠀⢿⡆⣷⠀⠘⡏⠉⠉⠀⠀⠀⠀⠀⠀⠀
+⠁⢼⡿⠁⠀⠙⢿⢷⣹⡟⠲⠶⠖⠛⠁⠀⣰⠇⠀⠀⠀⠀⡶⠀⣸⠁⠀⠀⢀⣸⠀⠈⣇⠀⠀⠀⢸⠀⠀⠀⠀⠈⢧⠀⠀⠹⣄⠘⣄⠀⠈⣧⠸⡀⠀⢹⡄⠀⠀⠀⢀⡀⠀⠀⠀
+⠀⢸⡇⢷⠀⣴⢻⠂⡇⠙⠦⠤⣀⡤⠤⢾⡁⠀⠀⠀⠀⣼⠁⣰⣃⠤⠖⠛⠋⢸⡀⠀⢹⡀⠀⠀⢸⠀⠀⠀⠀⠀⠘⣆⠀⢸⣌⠢⡘⢆⠀⠸⣆⡇⠀⢸⡇⠀⣠⠔⠋⠀⠀⠀⠀
+⠀⢸⡇⠈⣷⠁⢸⡇⢻⡄⠀⠀⠀⠀⠀⢸⡇⠀⠀⠀⢰⠋⣰⠛⠁⠀⠀⠀⠀⠘⡇⠀⢸⡇⠀⠀⡾⠀⠀⠀⠀⠀⠀⢻⡀⢸⣽⡆⠘⣯⣧⠀⢻⡇⠀⢨⣧⠞⠁⠀⠀⠀⠀⠀⠀
+⠀⢸⢠⡞⠙⣆⢸⠃⠈⢷⠀⠀⠀⠀⠀⠀⣧⠀⠀⢠⣏⡼⠃⠀⠀⠀⠀⠀⠀⠀⣷⠀⡟⣧⠀⣸⠃⠀⠀⣀⣀⡀⠀⠀⣇⡸⣿⣿⠀⡏⠻⣇⠀⡇⢀⡼⠃⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⣼⢸⡀⠀⣨⣿⠂⠀⠈⢧⠀⠀⠀⠀⠀⢹⡀⣠⣿⠞⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⡼⠀⢿⣠⠃⢀⣾⣿⣷⡶⢷⡶⠀⣸⣧⡏⡏⡆⢸⠀⢸⡄⡵⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⡇⠈⠳⡼⠁⢸⠀⠀⠀⠈⢧⠀⠀⠀⠀⠀⢟⠃⠁⠀⠀⠀⢀⡠⣤⣖⣺⣤⠄⠋⠀⠀⡟⠁⠀⠈⠙⠉⠉⠁⠀⠀⠀⠟⣿⠀⣇⡇⡸⠀⢾⡟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⢠⠃⣴⠊⠳⣄⣸⠀⠀⠀⠀⠈⢳⡀⠀⢀⠀⠘⡇⠀⠀⣠⣶⣿⡿⠿⠛⠛⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⠇⠀⣿⡴⠃⣴⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠄
+⡞⢀⡇⠀⠀⣨⢿⠀⠀⠀⠀⣠⡴⠋⠀⠈⠳⣄⠈⠢⣌⠛⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⡆⠀⣯⡤⠞⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠄⠊⠀⠈
+⠀⢸⡇⣤⠊⠀⣾⠀⠀⠀⠘⢻⣿⠑⠲⣆⠀⠘⠳⣄⠈⠓⢦⣄⠠⠄⢦⣴⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣤⠤⠤⠼⠿⠗⠚⠉⠀⠀⠀⠀⠀⠀⠀⠀⢀⡠⠖⠉⠀⠀⠀⠀⠀
+⠀⢸⡟⠀⠳⠄⡇⠀⠀⠀⠀⠈⢾⡆⢀⡸⣦⠀⠀⠈⠻⡗⠒⠚⠋⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⢠⡞⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⡖⠋⠀⠀⠀⣰⠋⠀⠀⠀
+⢠⢾⠁⠀⠀⠀⡇⠀⠀⠀⠀⠀⢠⠙⠲⠭⣍⡷⠀⠀⠀⠹⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠛⢦⡀⣠⠞⠁⠀⠀⠀⠀
+⠁⠸⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⣸⠀⠀⠀⠀⢀⣀⡀⠀⠀⠹⣆⠀⣀⣀⠀⠀⠀⠀⠀⠀⠀⡴⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣄⠀⠀⠀⠀⠀⠀⠀⠀⠛⠁⠀⠀⠀⠀⠀⠀
+⠀⠆⠀⠀⠀⢸⠁⠀⠀⠀⠀⠀⣿⠀⠀⢠⠶⠭⠥⠭⣧⠀⠀⢻⣆⠀⠉⠛⠛⠲⣶⣒⠒⡾⠁⠀⠀⢸⣓⡶⠀⠀⢰⡿⠳⣶⣿⣿⡟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⢀⠀⠀⠀⢀⡟⠀⠀⠀⠀⠀⠀⡏⠀⠀⡏⠀⠀⠀⠀⠈⢧⡀⠀⢿⣦⣄⡀⠀⠀⠙⣿⡠⡇⠀⠀⠀⠀⠀⠀⠀⠀⠈⠻⠶⠾⠟⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⡸⠀⠀⠀⡼⠁⠀⠀⠀⠀⠀⢸⠁⠀⢠⠇⠀⠀⠀⠀⠀⠀⢳⡀⢸⣿⠳⢟⡦⣄⠀⢳⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⠀⠀⠀⠀⠀⢀⣴⡾⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠁⠀⠀⣰⠃⠀⠀⠀⠀⠀⢀⣿⠀⢀⡾⠀⢄⣀⠀⠀⠀⠀⠀⢷⡀⡏⡇⠀⠻⣆⡙⡿⢼⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠰⣧⣤⡄⠀⠀⠀⣾⣯⣾⠟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⣰⠃⠀⠀⠀⠀⠀⠀⡾⢹⢀⡞⠀⠀⠀⠈⠉⠓⢤⣀⠀⠀⣿⣿⡇⠀⠀⠈⠛⢦⡜⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠛⠷⠀⠀⠈⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⢠⡏⠀⠀⠀⠀⠀⢀⡼⠁⣾⠟⠀⠀⠀⠀⠀⠀⠀⠀⠈⠑⢦⣿⣿⠇⠀⠀⠀⠀⢨⠃⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡆⠀⠀
+⠠⠫⠁⠀⠀⠀⠀⢀⡞⠁⣸⡏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⢿⠿⢤⡀⠀⠀⠀⢸⠀⢳⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⠀⠀⠀⢀⠁⠀⠀
+⢁⠆⠀⠀⠀⠀⣠⠎⠀⣰⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⢤⣠⠶⠋⠉⠀⠀⠙⠦⡄⣀⡼⠀⠘⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⠁⠀⠀⢀⠂⠀⠀⠀
+⡞⠀⠀⠀⣀⠜⠁⢀⡴⢉⡇⠀⠀⠀⠀⠀⠀⠀⠘⢇⡘⣧⣄⣀⣀⣀⡤⠴⠚⠛⠁⣀⠘⢦⡙⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⠀⣠⠂⠀⠀⠀⠀
diff --git a/frontend/public/ascii/ascii5.txt b/frontend/public/ascii/ascii5.txt
new file mode 100644
index 0000000..44b442b
--- /dev/null
+++ b/frontend/public/ascii/ascii5.txt
@@ -0,0 +1,30 @@
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡤⠞⠋⣿⣿⣿⡆⠀⠀⠀⠀⠀⠀⠀⢸⣿⡿⠋⠉⢻⣿⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⣀⣀⣀⣀⣀⣠⠴⠛⣁⡤⠔⠀⠈⠛⠉⠀⢰⣶⣶⣤⠄⠀⠀⠈⣿⣿⣦⣴⣾⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⢠⠞⣡⣴⣾⣿⣿⢿⣋⡥⠴⠚⠉⠀⠀⠀⠀⢀⣀⣀⣀⣙⣋⡁⠀⠀⠀⠀⠘⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⢀⣿⣼⣿⣿⡿⠟⠋⠁⠀⠀⠀⢀⣀⡤⠴⠞⠛⠉⠉⠁⠀⠀⢈⡉⠛⠒⠦⢤⣀⠈⠻⢿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⢸⣿⣿⡿⠋⠀⣀⣀⣠⡤⠖⠛⠉⢀⣤⠀⠠⠀⠀⠀⠀⠀⠀⠀⠙⢆⠀⠀⠀⠀⠙⠷⣄⠀⠉⠉⠉⠉⠉⠀⣸⣿⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⣼⣿⣿⢳⣿⣿⣿⣿⠋⠀⠀⠀⣠⠋⠀⣠⠇⠀⠀⠀⠀⢰⠀⠀⢰⡀⢣⡀⠀⠀⠀⠘⢮⠳⣄⠀⠀⠀⠀⠀⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⢹⣿⣿⡾⣿⣿⣿⠋⠀⠀⠀⡴⠁⠀⡰⠃⠀⠀⠀⠀⠀⢸⠀⠀⠀⢣⠀⢳⠀⠀⢀⣀⣀⡆⠘⢧⠀⠀⠀⢠⣿⣿⣿⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⢀⣿⣿⣿⣧⢿⣿⠃⠀⠀⠀⡼⠁⠀⢠⠃⠀⡆⠀⠀⠀⠀⠸⠀⡇⠀⠈⡆⠈⢇⠀⢹⣿⣿⣷⣶⣮⣷⡀⠀⢸⣿⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⢀⣞⣿⣿⣿⣿⣾⠇⠀⠀⠀⢸⠁⠀⢀⡎⠀⢰⠁⠀⠀⠀⠀⡀⠀⡇⠀⠀⠀⠀⣸⣿⣿⣿⣿⣿⣿⣧⠈⢧⠀⣸⣿⣿⣿⣿⣿⣷⣤⠎⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⢠⣯⣿⣿⣿⣿⣿⢿⠀⠀⠀⠀⡇⠀⠀⢸⠁⠀⡿⠀⠀⠀⠀⠀⡇⢀⡇⠀⠀⢰⡀⠘⣿⣿⣿⣿⣿⣿⣿⠀⠀⢳⣿⣿⣿⣿⣿⣿⣿⡞⣦⠈⠁⠀⠀⠀⠀⠀
+⠀⣠⣿⣿⣿⣿⣿⣿⠃⡜⠀⡆⠀⠀⡇⠀⢀⡇⠀⢀⡇⠀⠀⠀⠀⢸⠀⢸⣇⠉⠉⠲⡧⣄⠙⢿⣿⣿⣿⠟⡇⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⠘⡇⡄⠀⠀⠀⠀⠀
+⢰⣿⣿⣿⣿⣿⣿⠃⠘⡇⢰⢧⠀⠀⡇⠀⠈⡇⠀⢸⠀⠀⠀⠀⢀⠇⠀⡿⢸⠀⠀⠀⣇⠈⠳⡄⠀⠀⠀⠀⢹⠀⠀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣇⡿⡇⠀⠀⠀⠀⠀
+⢹⣿⣿⣿⣿⡿⠁⠀⠀⡇⢸⢸⠀⢠⡇⠀⢸⡇⠀⢸⡇⢰⠀⢰⠈⠀⢠⠃⠈⡆⠀⠀⣿⠀⠀⡇⠀⠀⠀⠀⢸⠀⠀⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠇⠇⠀⠀⠀⠀⠀
+⠈⢻⣿⣿⡿⢁⠀⠀⠀⡇⠘⡇⠀⠘⡇⠀⢸⣇⠀⠘⣷⢸⠀⢸⠀⠀⢸⣸⣖⣛⣢⣀⠁⠇⠀⡕⡀⠀⠀⠀⠘⠀⠀⠸⣿⣿⣿⣿⣿⣿⣿⣿⠃⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⢹⡟⡠⠋⠀⠀⠀⣇⠃⡇⠀⠈⣷⠀⠸⣹⡆⠀⠙⣼⡆⠈⣇⠀⠸⠛⠛⢻⣿⣿⣿⣶⣤⣧⠃⠀⠀⠀⢸⠀⠀⠀⢹⣿⣿⣿⣿⣿⡿⠃⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⢠⡟⡴⠁⠀⠀⡠⢸⣿⠀⡇⠀⠀⣿⡀⠠⠧⠽⠄⠀⠀⢿⠀⠹⠧⠀⠀⠀⣤⣴⡿⣿⣿⠘⡿⠀⠀⢀⠀⠸⠀⠀⠀⠀⢻⣿⡿⣯⡿⠁⠀⠀⠀⠀⠀⠀⠀⠀⣰
+⢀⡏⣼⠁⢀⠀⡼⠁⢸⢸⠀⣷⠀⡀⣯⢷⠟⠛⠛⠓⠂⠀⠀⠀⠀⠀⠀⠀⠀⢻⣟⣛⣿⠏⢰⡇⠀⠀⢸⠀⢠⡆⠀⠀⠀⠈⠻⡿⠋⡗⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿
+⣼⣾⡏⠀⡾⡼⠁⠀⣿⢸⡇⢻⡄⢰⢹⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⠃⡄⠀⠈⠀⢸⡆⠀⠀⠀⡄⢀⡇⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿
+⣷⢻⡇⠀⣷⠃⢀⡜⢹⡄⣷⣨⠟⠛⠉⠉⢢⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡜⢸⠀⠃⠀⠀⠀⢸⢿⠀⠀⠀⢀⣸⡧⢰⠇⠀⠀⣀⡀⠀⠀⠀⠀⠀⠿
+⣿⠀⢧⠸⣿⢀⡞⠀⠀⣳⠟⠁⠀⠀⠀⠀⠀⢣⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡼⢀⣾⠁⠀⠀⡇⠀⣾⣿⣆⠀⠀⣿⣿⠁⣾⠐⣻⡿⠁⠀⠀⠀⠀⠀⠀⠀
+⠈⠃⠈⠧⣿⣾⠁⣠⣿⡇⠀⠀⠀⠀⠀⠀⠀⠸⣷⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⠟⠋⠘⠀⠀⠀⡇⢀⠇⡼⠹⡄⢠⣿⢁⠀⣿⠈⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⣸⠟⣿⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⡿⡇⠉⢙⣶⣤⣀⣀⣠⣴⣶⠿⠋⢸⠀⠀⠀⠀⠀⢸⠁⣸⡰⠃⡴⠻⣼⠘⡟⠀⡏⢸⠁⠀⠀⠀⡀⠀⠀⠀⠀⠀
+⠀⠀⠀⢀⡾⠁⣰⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠹⣦⡿⠾⣿⣼⡏⠉⠻⡁⠀⠀⠘⢦⡀⢀⡆⢀⡎⢰⠏⢠⠞⠁⠀⠘⡿⢁⣸⣅⢸⠀⠀⠀⠀⠚⠓⢆⠀⠀⠀
+⠀⠀⠀⢀⣧⣰⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣽⠛⢦⣿⢿⣇⠀⠀⠀⠀⠀⠀⣠⠞⠋⢃⡞⢡⢧⡔⠁⡠⠀⢀⢼⡷⠋⠁⣝⣎⣄⠀⠀⠀⠀⠀⠀⠱⡄⠀
+⠀⠀⠀⠚⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣄⠀⠠⢻⠇⣰⠋⠹⣾⡛⠲⢦⣤⡤⠒⠋⠁⢀⣴⠚⠤⠚⠈⣳⠞⠀⢀⡴⠋⠀⠀⠀⠙⣽⢻⣆⠀⠀⠀⠀⠀⠀⠹⠀
+⠀⠀⠀⣰⠟⡜⠳⣦⣤⣤⣤⡤⠔⠒⠋⠉⠁⠀⠀⢾⣟⠁⠀⠀⠈⠙⢶⡤⣤⣤⣤⠖⠚⠉⠁⠀⢀⣄⡾⠋⢁⡴⢻⠀⠀⠀⠀⠀⠀⢸⡀⠱⠱⣄⠀⠀⠀⠀⠈⡄
+⠀⠀⠀⣏⠀⡟⢦⣽⠻⠃⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⠟⠀⠀⠀⠀⢠⠞⠛⢺⡀⠀⠀⠀⠀⠀⢠⣿⠋⠀⡰⠋⢀⡞⠀⠀⠀⠀⠀⠀⢸⠀⠀⠘⠇⠱⡀⠀⠀⠀⠰
+⠀⠀⣠⢿⡀⣸⠀⠙⠳⢦⣤⣄⣀⣀⣀⣤⣴⠞⠋⠀⠀⠀⠀⠀⠀⠘⣦⣤⠞⡀⠀⠀⠀⠀⠀⣼⠟⢀⡞⠁⠀⠸⣇⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⢰⡯⠤⠗⠛⠀⠀⠀⠀⠀⠠⠀⠀⣇⣴⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣟⠜⠁⠀⠀⠀⠀⢠⢯⣠⠏⠀⠀⠀⠀⠘⣆⠀⠀⠀⠀⠀⣼⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣽⡃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⠋⠀⠀⠀⠀⠀⢀⣿⡴⡏⠀⠀⠀⠀⠀⠀⢸⡄⠀⠀⠀⣠⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀
diff --git a/frontend/public/ascii/ascii6.txt b/frontend/public/ascii/ascii6.txt
new file mode 100644
index 0000000..5e2c68e
--- /dev/null
+++ b/frontend/public/ascii/ascii6.txt
@@ -0,0 +1,32 @@
+⠀⠀⠀⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣷⢟⣭⣴⣾⣿⣿⠉⠭⠩⣤⡉⠻⠷⣦⣌⡙⠻⣶⣮⠙⠿⣿⣿⣳⣜⢦⡀⠀⠀⠀⠀⣀⡴⠋⣠⠔⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠒⢤⣄⡀⠀⠑⠢⢤⣀⠀⠀⠀⣠⣾⢟⣵⣿⣿⣿⡿⠋⢿⣆⣠⢓⡘⢿⡆⠀⠈⢹⣿⣶⡝⢿⣧⡀⠘⢷⣝⢮⢷⡕⣄⣀⡴⢚⣩⣶⡋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠉⠑⠒⠢⣤⣌⣉⠒⣾⠋⢀⣾⣿⣿⡿⠃⠀⠀⠸⡏⠀⢸⣷⠘⣿⡄⠀⢲⡝⢿⣿⣦⣻⣿⣦⡈⢿⣦⠀⠹⣮⢳⣾⠋⠉⠉⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⢠⣿⠋⢉⡿⠁⢀⣾⣿⣿⡿⠃⠀⢀⣼⢠⣿⣠⢸⣿⡇⢸⣿⣆⣘⢧⠈⠻⣿⣿⣿⣿⣷⡀⠙⣧⠘⣏⢧⡙⣦⡀⠀⠸⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⣾⡇⠀⣼⠁⠀⣾⣿⣿⠿⢁⠀⢀⡼⠁⣼⣿⣿⡈⣿⣷⡀⣹⣿⣾⣷⡀⠀⣘⠿⢿⣿⣿⣷⡄⠙⣧⠈⠈⢻⡜⢷⡀⠀⢹⣷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⢠⣏⡶⣼⠇⣠⣸⢏⣿⡿⢠⡏⡼⣿⠃⢰⣿⣿⢿⡇⢿⡟⢷⣷⣿⣿⣷⣿⣆⠀⠐⣿⣻⣿⣿⣿⣄⠸⣧⡀⠀⢻⣎⢷⣤⡀⢻⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⢸⣿⣿⡏⣰⢣⡟⣼⣿⢃⡏⣼⣷⡏⢠⣿⣿⡇⠈⣧⠸⣿⡀⢻⣯⢿⣿⣿⣿⣧⠀⠈⢿⣿⣿⣿⣿⣆⠙⣷⡄⠀⠹⣧⡻⣷⡜⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⢸⣿⣿⢸⡿⣼⣹⣿⡏⡾⢱⡟⢸⡇⣾⣿⡟⠀⠀⠹⡆⣿⣇⠀⠙⢷⡝⢿⣿⣿⣷⣤⡀⠻⣿⣿⣿⣿⣧⡙⣿⣄⠀⠹⣷⣻⣇⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⢸⣿⣧⣿⢇⣿⣿⣿⢱⠃⣾⠁⣸⣿⣿⡟⠀⠀⢀⣠⢿⡸⣿⡄⠀⠈⠻⣦⠉⠻⢿⣿⣿⣧⣽⣿⣿⣿⣿⣷⡘⣿⣧⡀⠘⢿⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠈⣿⣿⡏⣼⣿⣿⡿⢸⠀⣿⢀⣿⣿⡏⣠⡤⠚⠉⠀⠀⢳⣽⣷⡀⠀⠀⠈⠳⢄⠀⠉⠛⢿⣿⣿⣿⣿⣿⣿⣿⣾⣿⣷⡄⠈⢻⣿⣅⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⣿⡟⣸⣿⣿⣿⢇⡇⣰⡇⣿⣿⡟⠉⠁⠀⠀⠀⠀⠀⠈⢻⣿⣧⠀⠀⠀⠀⠀⡵⢒⣦⣖⡊⠙⠻⣿⣿⣿⣿⣿⣿⣿⣿⣦⠀⠹⣿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⢀⣿⢳⣿⣿⣿⣿⢸⣃⣿⠇⣿⡟⠀⢀⣠⢴⣂⣐⠦⠄⠀⠀⠹⣿⣧⠀⠀⠀⠀⢰⣿⠛⢻⣿⣿⣷⣮⡿⢿⣯⣿⣿⣿⣿⣿⣷⣄⠈⠻⣧⡀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⣸⢧⣿⣿⣿⣿⡿⣼⣿⣿⢀⣿⠀⣀⣼⣾⠿⣿⣿⣶⡄⠀⠀⠀⠈⢿⣧⠀⠀⠀⠚⣷⣤⣾⣿⣿⣷⠹⣿⣦⣿⣯⢹⣿⣿⣿⣿⣿⣧⣀⠈⢻⣄⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⡇⣿⣿⣿⢸⣿⢉⣿⢿⣅⣠⣿⣿⣿⡅⠀⠀⠀⠀⠀⠙⢷⡀⠀⠀⣿⠿⣿⣿⣿⣿⠀⣸⡿⢿⣿⡀⣿⣿⣿⣿⣿⣿⣿⣿⣷⣿⣧⣀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⢰⣿⣿⣿⣿⣿⣿⡇⡯⣿⣿⢸⣿⣿⡏⢸⣿⣿⣯⣿⠟⠷⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠘⢦⣐⣀⡢⢆⠀⠉⠀⢸⣿⡇⣿⣿⣿⣿⣿⡛⣿⢿⣿⣿⣿⣿⣷⣄⡀⠀⠀
+⠀⠀⠀⢀⣾⣿⣿⢿⣿⣿⣿⡇⠀⣿⢿⢿⡿⠿⠷⣄⠻⣄⣉⣉⣰⡏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠙⠛⠛⠁⠀⠀⠀⢞⣿⡆⣿⣿⣿⣿⣿⣧⡹⡆⠈⠉⠛⠛⠿⠿⠿⣦⣄
+⠀⠀⢠⣾⣿⣿⠏⢸⣿⣿⣿⡷⢸⣿⠘⢸⣿⠀⠀⠀⠀⠚⠛⠛⠛⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣧⣿⣿⣿⣿⣿⣿⣷⣻⡄⠀⠀⠀⠀⠀⠀⠀⠈
+⢀⣴⢟⣿⣿⠋⠀⢸⣿⣿⣿⡇⣸⣿⡆⠩⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⡏⣿⢾⣿⣿⣿⡌⠻⢿⣿⣿⣿⣄⠀⠀⠀⠀⠀⠀⠀
+⠋⢁⣾⡿⠁⠀⠀⢸⣿⣿⣿⣿⣿⣿⣇⠀⣿⣧⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡞⠀⣿⢸⣿⣿⣿⣿⣦⣤⣴⠟⠋⠉⠙⠲⣄⠀⠀⠀⠀
+⢀⣾⠋⠀⠀⠀⠀⢸⣿⣿⡿⠟⣿⣿⣿⠀⢸⡏⠳⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠞⠀⢠⣧⣿⣿⣿⡿⠿⠿⠋⠁⠀⠀⠀⠀⠀⠈⠁⠀⠀⠀
+⠋⠁⠀⠀⠀⠀⠀⢸⠟⠋⠀⠀⣿⣿⣿⣇⠀⣷⡀⠈⠳⢄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡴⠋⠀⠀⢸⣿⣿⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⡼⠋⠀⠀⠀⠀⢹⣿⣿⣿⡄⣸⣷⡀⠀⠀⠙⠲⢤⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⠞⠁⠀⠀⠀⢠⣿⣿⣿⣿⣿⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⢰⠇⠀⠀⠀⠀⠀⠀⣿⡈⢿⣿⣏⢸⣷⡄⠀⠀⠀⠀⠈⠻⣖⠤⢄⣀⠀⠀⠀⠀⣠⠖⢫⡏⠀⢀⡔⠀⠀⣸⣿⠟⣿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀⠸⣧⠀⠙⢿⣿⣿⣝⠶⣄⡀⠀⠀⠀⠈⠳⣄⡈⠉⠒⠒⠋⠁⢠⠏⢀⡴⠋⠀⠀⣰⡿⠋⣼⣿⣿⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⢸⡄⠀⠀⢠⠀⠀⠀⠀⠘⢆⠀⠀⠉⠛⠿⢆⡈⠉⠀⠀⠀⠀⠀⠈⠙⠲⢤⣀⠀⣰⣋⡴⠚⠀⠀⠀⡴⠛⠁⣸⢹⣿⣻⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⣾⣇⠀⠀⠘⣆⠀⠀⠀⠀⠈⠃⠀⠀⠀⠀⠀⠻⡄⠀⠀⠀⠀⠀⠀⠀⠀⢀⡽⢷⠓⠢⣄⠀⠀⠀⠀⠀⠀⢰⠇⣾⣟⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⢸⣿⣿⡆⠀⠀⠘⠆⠲⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⠀⠀⠀⢠⠟⠀⠈⢧⠀⠈⠳⣄⠀⠀⠀⢠⠇⠀⣿⣻⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⣾⣿⣿⢹⡆⠀⠀⠀⢦⢳⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢦⡀⠀⠀⢀⡎⠀⡔⠲⡌⢳⡀⠀⠈⠳⣄⣰⠏⠀⢸⣿⡿⠀⠀⠀⠀⠀⠀⠀⠀⣀⡄⠀⠀⠀⡄⠀⠀⠀
+⠀⠀⠀⠀⣿⣿⡟⢸⣿⣦⠀⠀⠀⠳⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠑⠦⠟⠀⠀⢧⣀⡼⠀⡇⠀⠀⠀⠀⠁⠀⢀⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⢰⠏⠀⠀⠀⠘⣿⠀⠀⠀
+⠀⠀⠀⢸⣿⣿⡇⠘⣿⣿⣷⡀⠀⠀⠈⢳⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⢸⣿⣿⠃⠀⠀⠀⠀⠀⠀⠀⡏⠀⠀⠀⠀⠀⣿⠀⠀⠀
+⠀⠀⠀⠸⣿⣿⠃⠀⣿⣿⣿⣿⣦⡀⠀⠀⠳⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⣸⣿⣿⠀⠀⠀⠀⠀⠀⠀⢰⠏⠀⠀⠀⠀⢸⣿⡇⠀⠀
+⠀⠀⠀⠀⣿⣿⠀⠀⢿⣿⣿⣿⣿⡟⣦⡀⢠⡹⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⡇⠀⠀⠀⠀⠀⢀⡰⠃⣠⠆⠀⠀⠀⣸⢻⡇⠀⠀
diff --git a/frontend/public/ascii/ascii7.txt b/frontend/public/ascii/ascii7.txt
new file mode 100644
index 0000000..e3f323b
--- /dev/null
+++ b/frontend/public/ascii/ascii7.txt
@@ -0,0 +1,28 @@
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⠀⠀⠀⠀⢀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠊⠀⠀⠀⠀⡴⠂⠀⠀⠀⠀⢦⠀⠀⠀⠀⠙⠢⡀⠀⢀⠈⠱⡀⠀⠀⠀⠘⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠞⠀⠀⠀⠀⠀⡸⠁⠀⠀⢀⠀⠀⠀⢣⠀⠀⠀⠀⠀⠘⢆⠀⠉⠂⡘⣦⠀⠀⠀⠀⠱⡄⠀⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⠀⢠⠋⠀⠀⢠⠄⠀⢠⠇⠀⠀⠀⢸⠀⠀⠀⠀⢧⠀⠀⠀⠀⠀⠀⢣⠀⠀⢙⣿⣇⠀⠀⠀⠀⠘⡄⠀⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⢀⠏⠀⠀⠀⡏⠀⠀⢸⠀⠀⠀⠀⠘⡄⠀⠀⠀⠘⣆⠀⠀⠀⠀⠀⠀⢣⠀⢄⠈⢿⡇⠀⠀⠀⠀⣸⡆⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⡞⡘⠀⠀⢰⠃⠀⠀⡞⠀⠀⠀⠀⠀⢳⠀⠀⠀⠀⣿⡄⠀⠀⠀⠀⠀⠈⡆⠀⠑⣾⣿⠀⠀⢀⣼⠟⣧⠀⠀⠀⠀⠀
+⠼⠀⠀⠀⠀⠀⢰⠃⡇⠀⠀⡾⡄⠀⠀⣷⠀⠀⠀⠀⠀⢸⢧⠀⠀⠀⣹⢿⠀⠀⠀⠀⠀⠀⢹⠈⠢⣼⣾⣇⣴⠛⠁⠀⢹⠀⠀⠀⠀⠀
+⠀⠀⢢⠀⠀⠀⢸⢸⠁⠀⠀⠃⡧⢄⣰⡿⡄⠀⠀⠀⠀⢨⣀⠷⣖⠉⢹⠀⣧⠀⠀⠀⠀⠀⠸⡆⠢⣀⣿⡟⢿⣤⣀⣀⣸⡆⠀⠀⠀⠀
+⠉⠒⢤⣧⠀⠀⢺⢸⠀⠀⢈⡆⢳⠀⢘⡟⢻⡂⠀⠀⠀⢹⠀⢀⣼⡷⣟⡛⠻⣶⣤⡀⠀⠀⠀⡿⣀⣸⠟⠷⡀⠙⠻⠿⣿⠃⠀⠀⠀⠀
+⠀⠀⠀⠉⠓⠀⢸⢸⡄⠀⠘⡇⢸⣷⣾⢟⣲⠷⣤⡀⠀⢸⡆⠉⢸⠃⠀⢻⡄⢯⡿⠀⠀⢠⣴⠃⢹⠁⢠⡄⠘⣆⠀⠀⢿⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⣀⡀⢸⣿⡇⠀⠀⢱⠈⣿⣁⡏⣈⣧⠈⠙⢤⡐⢇⠀⠸⣎⠛⣹⡇⢺⠁⠀⠀⣼⣿⢠⠃⢠⠟⡋⢠⠟⡅⠀⣼⠀⠀⠀⠀⠀
+⠀⠀⠀⢸⣿⠁⠀⣿⣿⣄⠀⠀⢧⢹⡈⢧⣉⡿⠀⠀⠀⠉⠺⠄⠀⠈⠛⠉⠀⠈⡇⠀⢀⣿⠏⢀⣀⣸⡞⢀⠇⠀⢡⠀⡇⠀⠀⠀⠀⠀
+⣤⠶⢻⡿⠿⠆⠀⠸⡇⢻⣧⠀⠈⣷⣷⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⢀⡾⠇⠀⠉⠀⢀⣠⠜⠀⠀⠸⡄⡇⠀⠀⠀⠀⠀
+⠀⢀⣸⠃⠀⠀⠀⠀⠘⠀⠻⣧⡀⢹⡇⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡀⢹⠏⠀⠀⣨⢛⣿⣿⣿⠀⠀⠀⠸⣇⡀⠀⠀⠀⠀⠀
+⠾⢋⣥⣶⠟⠀⠀⠀⠀⠀⠀⠈⠋⠛⢷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⠛⠋⠀⢀⡴⠁⢸⣿⣿⣿⡄⠀⠀⠀⣿⡇⠀⠀⠀⠀⠀
+⠀⠉⠡⡿⣶⣶⣶⠂⠀⠀⠀⠀⠀⠀⠀⠑⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠏⠀⠀⢸⣿⣿⢹⣇⠀⠀⠀⢹⡇⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠙⢿⡏⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣱⣦⣶⠿⠛⠋⠉⠀⠀⠀⠀⢀⣴⡿⠁⠀⠀⠀⢸⣿⣿⣿⣿⡄⠀⠀⠘⣧⠀⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⠀⡤⠤⢤⣀⣔⣶⠤⠔⣚⠭⢻⠘⡏⢱⣦⣄⣀⣀⣠⣴⣾⣿⠏⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⣧⠀⠀⠀⢻⡄⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⠀⣰⠗⢀⣉⣉⠹⠿⣾⡍⠀⠀⠈⡖⣷⠈⣿⣿⣿⣿⣿⣿⠟⠁⠀⠀⠀⠀⢀⡴⠊⠁⠀⠀⠉⠙⢦⡀⠀⠈⢇⠀⠀⠀⠀
+⠀⠀⠀⠀⠀⣾⢋⡠⠋⡁⠀⠀⡠⠟⠁⠀⠀⢀⣱⣿⣧⣸⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀⢠⠋⠀⠀⠀⠀⢠⠀⠀⠀⠱⡄⠀⠸⡄⠀⠀⠀
+⠀⠀⠀⠀⠀⡇⠀⠀⠈⢀⠔⡏⠀⠀⡤⠊⠉⠀⠀⢸⠋⠉⢉⠹⡿⠋⢇⠀⠀⠀⠀⢀⠇⠀⠀⠀⠀⠀⠀⣆⠀⠀⠀⠹⡀⠀⢣⠀⠀⠀
+⠀⠀⠀⠀⠀⠇⠀⠀⠀⢸⢰⠃⠀⡜⠀⠀⠀⢠⠀⠙⣤⠞⢡⠞⠀⢀⡼⠀⠀⣀⡠⢾⠀⠀⠀⠀⠀⠀⠀⡉⠀⠀⠀⠀⢣⠀⠘⡆⠀⠀
+⠀⠀⠀⠀⠀⢸⠀⠀⠀⢸⣿⠀⢸⠁⠑⢄⠀⢸⢀⠔⠁⣰⠁⠀⠀⠘⠃⠜⠁⠀⢀⡆⠀⠀⢀⡠⠒⠉⠁⠉⠑⢄⠀⠀⠈⡆⠀⢹⠀⠀
+⠀⠀⠀⠀⠀⣠⡆⠀⠀⠼⣿⣧⡇⠀⠀⠀⠑⡷⠋⢀⡜⠉⠦⣀⡀⠀⣀⡠⠔⠊⠉⡇⡠⠖⠁⠀⠀⠀⠀⠀⠀⠈⢢⠀⠀⢱⠀⠀⣇⠀
+⠀⠀⠀⠀⣼⣷⡇⠀⢀⠜⣡⣿⠃⠀⠀⢀⡜⠁⢠⠎⠀⠀⠀⠈⢉⡉⠁⠀⠀⠀⢸⡇⠀⢀⠆⠀⠀⠀⠀⠀⠀⠀⠀⠱⡀⢸⠀⢇⠸⡀
+⠀⠀⠀⠘⣧⡉⠓⠒⢁⠜⠩⣇⢀⠀⣠⠎⠀⣠⠃⠀⠀⠰⢀⠔⠁⠀⠀⠀⠀⠀⡟⡇⢀⡌⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⢸⠀⢸⠀⡇
+⠀⠀⠀⡴⣇⠈⠁⠀⠉⢀⠀⡼⠠⡶⠉⠉⠈⠉⠉⠑⠒⠒⠒⠒⠒⠒⠒⠒⠒⠚⠁⡇⡜⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡴⣿⡄⠀⡇⢸
+⠀⠀⠀⢦⡈⣉⡡⠖⠊⠁⠀⣑⢠⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡼⢠⢛⣷⠀⡇⠘
diff --git a/frontend/public/asuka-headset-logo.svg b/frontend/public/asuka-headset-logo.svg
new file mode 100644
index 0000000..f8ba4c0
--- /dev/null
+++ b/frontend/public/asuka-headset-logo.svg
@@ -0,0 +1,101 @@
+<svg width="100" height="80" viewBox="0 0 100 80" xmlns="http://www.w3.org/2000/svg">
+ <defs>
+ <!-- White/silver gradient for the main interface -->
+ <linearGradient id="whiteGradient" x1="0%" y1="0%" x2="100%" y2="100%">
+ <stop offset="0%" style="stop-color:#ffffff;stop-opacity:1" />
+ <stop offset="50%" style="stop-color:#f0f0f0;stop-opacity:1" />
+ <stop offset="100%" style="stop-color:#d0d0d0;stop-opacity:1" />
+ </linearGradient>
+
+ <!-- Orange/yellow gradient for accent parts -->
+ <linearGradient id="orangeGradient" x1="0%" y1="0%" x2="100%" y2="100%">
+ <stop offset="0%" style="stop-color:#ffaa00;stop-opacity:1" />
+ <stop offset="50%" style="stop-color:#ff8800;stop-opacity:1" />
+ <stop offset="100%" style="stop-color:#ee6600;stop-opacity:1" />
+ </linearGradient>
+
+ <!-- Red gradient for warning/active elements -->
+ <radialGradient id="redGradient" cx="50%" cy="50%" r="50%">
+ <stop offset="0%" style="stop-color:#ff3333;stop-opacity:1" />
+ <stop offset="100%" style="stop-color:#cc0000;stop-opacity:1" />
+ </radialGradient>
+
+ <!-- Shadow filter -->
+ <filter id="shadow" x="-50%" y="-50%" width="200%" height="200%">
+ <feDropShadow dx="1" dy="2" stdDeviation="2" flood-color="#000000" flood-opacity="0.4"/>
+ </filter>
+
+ <!-- Glow filter -->
+ <filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
+ <feGaussianBlur stdDeviation="2" result="coloredBlur"/>
+ <feMerge>
+ <feMergeNode in="coloredBlur"/>
+ <feMergeNode in="SourceGraphic"/>
+ </feMerge>
+ </filter>
+ </defs>
+
+ <!-- Left A10 nerve connector (the distinctive cat-ear like protrusion) -->
+ <g transform="translate(15, 25)">
+ <!-- Main white interface body -->
+ <ellipse cx="0" cy="5" rx="12" ry="18" fill="url(#whiteGradient)" filter="url(#shadow)"/>
+ <!-- Orange accent stripe -->
+ <rect x="-8" y="-8" width="16" height="4" fill="url(#orangeGradient)" rx="2"/>
+ <!-- Top connector detail -->
+ <rect x="-6" y="-12" width="12" height="6" fill="url(#whiteGradient)" rx="3"/>
+ <!-- Interface ports -->
+ <circle cx="-4" cy="2" r="2" fill="#333333"/>
+ <circle cx="4" cy="2" r="2" fill="#333333"/>
+ <circle cx="0" cy="8" r="2" fill="#333333"/>
+ <!-- Small indicator lights -->
+ <circle cx="-4" cy="-4" r="1.5" fill="url(#redGradient)" filter="url(#glow)"/>
+ <circle cx="4" cy="-4" r="1.5" fill="url(#redGradient)" filter="url(#glow)"/>
+ </g>
+
+ <!-- Right A10 nerve connector (mirrored) -->
+ <g transform="translate(85, 25)">
+ <!-- Main white interface body -->
+ <ellipse cx="0" cy="5" rx="12" ry="18" fill="url(#whiteGradient)" filter="url(#shadow)"/>
+ <!-- Orange accent stripe -->
+ <rect x="-8" y="-8" width="16" height="4" fill="url(#orangeGradient)" rx="2"/>
+ <!-- Top connector detail -->
+ <rect x="-6" y="-12" width="12" height="6" fill="url(#whiteGradient)" rx="3"/>
+ <!-- Interface ports -->
+ <circle cx="-4" cy="2" r="2" fill="#333333"/>
+ <circle cx="4" cy="2" r="2" fill="#333333"/>
+ <circle cx="0" cy="8" r="2" fill="#333333"/>
+ <!-- Small indicator lights -->
+ <circle cx="-4" cy="-4" r="1.5" fill="url(#redGradient)" filter="url(#glow)"/>
+ <circle cx="4" cy="-4" r="1.5" fill="url(#redGradient)" filter="url(#glow)"/>
+ </g>
+
+ <!-- Central connecting band (thin, barely visible like in the reference) -->
+ <path d="M 27 20 Q 50 12 73 20"
+ stroke="url(#whiteGradient)"
+ stroke-width="2"
+ fill="none"
+ opacity="0.7"/>
+
+ <!-- Central forehead interface (small white rectangular piece) -->
+ <g transform="translate(50, 15)">
+ <rect x="-8" y="-3" width="16" height="6" fill="url(#whiteGradient)" rx="2" filter="url(#shadow)"/>
+ <rect x="-6" y="-2" width="12" height="4" fill="url(#orangeGradient)" rx="1"/>
+ <!-- Central indicator -->
+ <circle cx="0" cy="0" r="1.5" fill="url(#redGradient)" filter="url(#glow)"/>
+ </g>
+
+ <!-- Additional connecting wires/cables (very thin lines) -->
+ <path d="M 27 25 Q 35 22 43 24" stroke="#666666" stroke-width="1" fill="none" opacity="0.5"/>
+ <path d="M 73 25 Q 65 22 57 24" stroke="#666666" stroke-width="1" fill="none" opacity="0.5"/>
+
+ <!-- Bottom neural interface details -->
+ <g transform="translate(15, 40)">
+ <rect x="-2" y="0" width="4" height="8" fill="url(#whiteGradient)" rx="1"/>
+ <rect x="-1" y="2" width="2" height="4" fill="url(#orangeGradient)"/>
+ </g>
+
+ <g transform="translate(85, 40)">
+ <rect x="-2" y="0" width="4" height="8" fill="url(#whiteGradient)" rx="1"/>
+ <rect x="-1" y="2" width="2" height="4" fill="url(#orangeGradient)"/>
+ </g>
+</svg> \ No newline at end of file
diff --git a/frontend/public/background-animation.gif b/frontend/public/background-animation.gif
new file mode 100644
index 0000000..f59418c
--- /dev/null
+++ b/frontend/public/background-animation.gif
Binary files differ
diff --git a/frontend/public/kaomoji.txt b/frontend/public/kaomoji.txt
new file mode 100644
index 0000000..572027f
--- /dev/null
+++ b/frontend/public/kaomoji.txt
@@ -0,0 +1,3 @@
+ᗜ˰ᗜ
+ᗜ▵ᗜ
+ᗜˬᗜ
diff --git a/frontend/public/logo/crane-logo-transparent.png b/frontend/public/logo/crane-logo-transparent.png
new file mode 100644
index 0000000..9ea5fbd
--- /dev/null
+++ b/frontend/public/logo/crane-logo-transparent.png
Binary files differ
diff --git a/frontend/public/logo/crane-logo.png b/frontend/public/logo/crane-logo.png
new file mode 100644
index 0000000..0978467
--- /dev/null
+++ b/frontend/public/logo/crane-logo.png
Binary files differ
diff --git a/frontend/public/nazrin-bg.jpg b/frontend/public/nazrin-bg.jpg
new file mode 100644
index 0000000..ba42485
--- /dev/null
+++ b/frontend/public/nazrin-bg.jpg
Binary files differ
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
new file mode 100644
index 0000000..0a0e008
--- /dev/null
+++ b/frontend/src/App.tsx
@@ -0,0 +1,19 @@
+import React from 'react'
+import { useStore } from '@nanostores/react'
+import { LandingPage } from './components/LandingPage'
+import { VNInterface } from './components/VNInterface'
+import { isLoggedInStore, login, logout } from './stores'
+
+export default function App() {
+ const isLoggedIn = useStore(isLoggedInStore)
+
+ return (
+ <>
+ {isLoggedIn ? (
+ <VNInterface onLogout={logout} />
+ ) : (
+ <LandingPage onLogin={login} />
+ )}
+ </>
+ )
+}
diff --git a/frontend/src/components/BottomBar.tsx b/frontend/src/components/BottomBar.tsx
new file mode 100644
index 0000000..b9f19ed
--- /dev/null
+++ b/frontend/src/components/BottomBar.tsx
@@ -0,0 +1,19 @@
+import React from 'react'
+
+type Props = {
+ onSkip?: () => void
+ onAuto?: () => void
+ onLog?: () => void
+ location?: string
+}
+
+export const BottomBar: React.FC<Props> = ({ onSkip, onAuto, onLog, location = 'Tokyo' }) => {
+ return (
+ <div className="bottombar">
+ <button className="mini-btn" onClick={onSkip}>SKIP</button>
+ <button className="mini-btn" onClick={onAuto}>AUTO</button>
+ <button className="mini-btn" onClick={onLog}>LOG</button>
+ <div className="spacer" />
+ </div>
+ )
+}
diff --git a/frontend/src/components/ChoiceMenu.tsx b/frontend/src/components/ChoiceMenu.tsx
new file mode 100644
index 0000000..0de86f6
--- /dev/null
+++ b/frontend/src/components/ChoiceMenu.tsx
@@ -0,0 +1,20 @@
+import React from 'react'
+import { Choice } from '../types'
+
+type Props = {
+ choices: Choice[]
+ onSelect: (id: string) => void
+}
+
+export const ChoiceMenu: React.FC<Props> = ({ choices, onSelect }) => {
+ if (!choices.length) return null
+ return (
+ <div className="choice-menu">
+ {choices.map((c) => (
+ <button key={c.id} className="choice-item" onClick={() => onSelect(c.id)}>
+ {c.label}
+ </button>
+ ))}
+ </div>
+ )
+}
diff --git a/frontend/src/components/CityscapeBackground.tsx b/frontend/src/components/CityscapeBackground.tsx
new file mode 100644
index 0000000..82a679d
--- /dev/null
+++ b/frontend/src/components/CityscapeBackground.tsx
@@ -0,0 +1,310 @@
+import React, { useRef, useEffect } from 'react'
+import * as THREE from 'three'
+
+interface CityscapeBackgroundProps {
+ className?: string
+}
+
+export function CityscapeBackground({ className }: CityscapeBackgroundProps) {
+ const canvasRef = useRef<HTMLCanvasElement>(null)
+ const sceneRef = useRef<THREE.Scene>()
+ const rendererRef = useRef<THREE.WebGLRenderer>()
+ const cameraRef = useRef<THREE.PerspectiveCamera>()
+ const animationIdRef = useRef<number>()
+ const speedLinesRef = useRef<THREE.LineSegments>()
+ const mouseRef = useRef(new THREE.Vector2())
+ const startTimeRef = useRef<number>(Date.now())
+
+ useEffect(() => {
+ if (!canvasRef.current) {
+ console.log('CityscapeBackground: Canvas ref not available')
+ return
+ }
+
+ console.log('FuturistBackground: Initializing 3D futurist scene')
+
+ // Scene setup with futurist art movement atmosphere
+ const scene = new THREE.Scene()
+ scene.background = new THREE.Color(0x000000) // Black background
+ scene.fog = new THREE.Fog(0x000000, 30, 120) // Black fog for depth
+
+ // Camera setup for isometric view
+ const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 200)
+ camera.position.set(30, 25, 30)
+ camera.lookAt(0, 0, 0)
+
+ // Renderer setup
+ const renderer = new THREE.WebGLRenderer({
+ canvas: canvasRef.current,
+ antialias: false, // Disable for better performance
+ alpha: false // Disable alpha for opaque background
+ })
+ renderer.setSize(window.innerWidth, window.innerHeight)
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5)) // Limit pixel ratio for performance
+
+ // Store references
+ sceneRef.current = scene
+ rendererRef.current = renderer
+ cameraRef.current = camera
+
+ // Futurist art movement lighting - dramatic contrasts inspired by paintings
+ const ambientLight = new THREE.AmbientLight(0x1a1a1a, 0.4) // Warm grey ambient
+ scene.add(ambientLight)
+
+ // Bold primary light - warm like industrial sunlight
+ const primaryLight = new THREE.DirectionalLight(0xffaa33, 1.8) // Golden orange
+ primaryLight.position.set(-25, 35, 20)
+ scene.add(primaryLight)
+
+ // Dynamic red light for energy and motion
+ const dynamicLight = new THREE.DirectionalLight(0xdd2222, 1.2) // Bold red
+ dynamicLight.position.set(20, 10, -15)
+ scene.add(dynamicLight)
+
+ // Contrasting blue for depth and drama
+ const contrastLight = new THREE.PointLight(0x2244bb, 0.8, 50) // Deep blue
+ contrastLight.position.set(-30, 5, 0)
+ scene.add(contrastLight)
+
+ // Yellow accent for vibrant highlights
+ const accentLight = new THREE.PointLight(0xdddd22, 0.7, 40) // Yellow
+ accentLight.position.set(25, 20, -5)
+ scene.add(accentLight)
+
+ // Green industrial light
+ const industrialLight = new THREE.PointLight(0x22aa22, 0.6, 45) // Industrial green
+ industrialLight.position.set(0, -5, 30)
+ scene.add(industrialLight)
+
+ // Create speed lines
+ createSpeedLines()
+
+ console.log('FuturistBackground: Dynamic futurist art created, starting animation')
+
+ // Reset start time
+ startTimeRef.current = Date.now()
+
+ // Animation loop
+ const animate = () => {
+ animationIdRef.current = requestAnimationFrame(animate)
+
+ const time = Date.now() * 0.001 // Convert to seconds for smoother animation
+
+ // Animate speed lines
+ animateSpeedLines(time)
+
+ // Apply interactive effects
+ applyFuturistInteractions()
+
+ // Dramatic camera movement inspired by futurist dynamism
+ const cameraSpeed = time * 0.2
+ camera.position.x = 20 + Math.sin(cameraSpeed * 1.3) * 12 + Math.cos(cameraSpeed * 0.7) * 6
+ camera.position.y = 15 + Math.cos(cameraSpeed * 0.9) * 8 + Math.sin(cameraSpeed * 1.1) * 4
+ camera.position.z = 25 + Math.sin(cameraSpeed * 0.6) * 15 + Math.cos(cameraSpeed * 1.4) * 8
+
+ // Dynamic look-at point for more dramatic perspective shifts
+ const lookAtTarget = new THREE.Vector3(
+ Math.sin(cameraSpeed * 0.8) * 5,
+ Math.cos(cameraSpeed * 0.5) * 3,
+ Math.sin(cameraSpeed * 1.2) * 8
+ )
+ camera.lookAt(lookAtTarget)
+
+ renderer.render(scene, camera)
+ }
+
+ animate()
+
+ // Mouse tracking for interactive effects
+ const handleMouseMove = (event: MouseEvent) => {
+ // Convert screen coordinates to normalized device coordinates (-1 to +1)
+ mouseRef.current.set(
+ (event.clientX / window.innerWidth) * 2 - 1,
+ -(event.clientY / window.innerHeight) * 2 + 1
+ )
+ }
+
+ // Handle window resize
+ const handleResize = () => {
+ if (camera && renderer) {
+ camera.aspect = window.innerWidth / window.innerHeight
+ camera.updateProjectionMatrix()
+ renderer.setSize(window.innerWidth, window.innerHeight)
+ }
+ }
+
+ window.addEventListener('mousemove', handleMouseMove)
+ window.addEventListener('resize', handleResize)
+
+ // Cleanup
+ return () => {
+ if (animationIdRef.current) {
+ cancelAnimationFrame(animationIdRef.current)
+ }
+ window.removeEventListener('mousemove', handleMouseMove)
+ window.removeEventListener('resize', handleResize)
+
+ // Dispose of Three.js objects
+ scene.clear()
+ renderer.dispose()
+ }
+ }, [])
+
+ const applyFuturistInteractions = () => {
+ const camera = cameraRef.current!
+ const raycaster = new THREE.Raycaster()
+
+ // Convert mouse coordinates to world position
+ raycaster.setFromCamera(mouseRef.current, camera)
+
+ // Interactive speed line distortion
+ if (speedLinesRef.current) {
+ const positions = speedLinesRef.current.geometry.attributes.position.array as Float32Array
+
+ for (let i = 0; i < positions.length; i += 6) { // Step by 6 for line pairs
+ const linePos = new THREE.Vector3(positions[i], positions[i + 1], positions[i + 2])
+ const mouseWorldPos = new THREE.Vector3()
+ raycaster.ray.at(linePos.z, mouseWorldPos)
+
+ const distance = linePos.distanceTo(mouseWorldPos)
+ const distortRadius = 15
+
+ if (distance < distortRadius) {
+ const distortStrength = (distortRadius - distance) / distortRadius
+
+ // Create dramatic wave distortion around mouse
+ const waveX = Math.sin(Date.now() * 0.02 + i) * distortStrength * 2.0
+ const waveY = Math.cos(Date.now() * 0.025 + i) * distortStrength * 1.5
+
+ positions[i] += waveX
+ positions[i + 1] += waveY
+ positions[i + 3] += waveX * 0.8 // End point follows with slight lag
+ positions[i + 4] += waveY * 0.8
+ }
+ }
+
+ speedLinesRef.current.geometry.attributes.position.needsUpdate = true
+ }
+ }
+
+
+ const createSpeedLines = () => {
+ const scene = sceneRef.current!
+
+ // Create sharp, dramatic speed lines
+ const motionVertices = []
+ const motionColors = []
+
+ // High-contrast speed colors
+ const speedColors = [
+ { r: 1.0, g: 1.0, b: 1.0 }, // Brilliant white
+ { r: 1.0, g: 0.1, b: 0.1 }, // Sharp red
+ { r: 1.0, g: 0.8, b: 0.0 }, // Electric yellow
+ { r: 0.0, g: 1.0, b: 1.0 }, // Cyan
+ { r: 1.0, g: 0.0, b: 1.0 }, // Magenta
+ ]
+
+ // Create 300 sharp speed lines for intense motion
+ for (let i = 0; i < 300; i++) {
+ // Start point - spread across space
+ const startX = (Math.random() - 0.5) * 100
+ const startY = (Math.random() - 0.5) * 40
+ const startZ = Math.random() * -200
+
+ // End point - long streaks suggesting extreme speed
+ const endX = startX + (Math.random() - 0.5) * 20
+ const endY = startY + (Math.random() - 0.5) * 8
+ const endZ = startZ + 20 + Math.random() * 40 // Longer streaks
+
+ motionVertices.push(startX, startY, startZ)
+ motionVertices.push(endX, endY, endZ)
+
+ // High-contrast colors for sharp appearance
+ const colorIndex = Math.floor(Math.random() * speedColors.length)
+ const color = speedColors[colorIndex]
+
+ motionColors.push(color.r, color.g, color.b)
+ motionColors.push(color.r, color.g, color.b)
+ }
+
+ const motionGeometry = new THREE.BufferGeometry()
+ motionGeometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(motionVertices), 3))
+ motionGeometry.setAttribute('color', new THREE.BufferAttribute(new Float32Array(motionColors), 3))
+
+ const motionMaterial = new THREE.LineBasicMaterial({
+ vertexColors: true,
+ transparent: true,
+ opacity: 0.9,
+ linewidth: 2
+ })
+
+ const speedLines = new THREE.LineSegments(motionGeometry, motionMaterial)
+ speedLinesRef.current = speedLines
+ scene.add(speedLines)
+ }
+
+
+
+
+
+
+ const animateSpeedLines = (time: number) => {
+ if (!speedLinesRef.current) return
+
+ const positions = speedLinesRef.current.geometry.attributes.position.array as Float32Array
+
+ for (let i = 0; i < positions.length; i += 6) { // Step by 6 since we have line pairs
+ // Extreme speed effect - lines blazing toward camera
+ positions[i + 2] += 6.0 + Math.sin(time * 2 + i) * 3.0 // High variable speed
+ positions[i + 5] += 6.0 + Math.sin(time * 2 + i) * 3.0 // End point matches
+
+ // Intense lateral movement for speed blur
+ const lateralMotion = Math.sin(time * 1.5 + i * 0.15) * 0.8
+ positions[i] += lateralMotion
+ positions[i + 3] += lateralMotion
+
+ // Sharp vertical oscillation for dynamic energy
+ const verticalMotion = Math.cos(time * 2.2 + i * 0.12) * 0.6
+ positions[i + 1] += verticalMotion
+ positions[i + 4] += verticalMotion
+
+ // Reset lines that have blazed past camera
+ if (positions[i + 2] > 100) {
+ positions[i + 2] = -250 - Math.random() * 100
+ positions[i + 5] = positions[i + 2] + 20 + Math.random() * 40
+
+ positions[i] = (Math.random() - 0.5) * 100
+ positions[i + 1] = (Math.random() - 0.5) * 40
+ positions[i + 3] = positions[i] + (Math.random() - 0.5) * 20
+ positions[i + 4] = positions[i + 1] + (Math.random() - 0.5) * 8
+ }
+ }
+
+ speedLinesRef.current.geometry.attributes.position.needsUpdate = true
+
+ // Rapid rotation for intense motion blur
+ speedLinesRef.current.rotation.x += 0.008
+ speedLinesRef.current.rotation.y += 0.012
+ speedLinesRef.current.rotation.z += 0.025
+ }
+
+
+
+ return (
+ <canvas
+ ref={canvasRef}
+ className={className}
+ style={{
+ position: 'fixed',
+ top: 0,
+ left: 0,
+ width: '100%',
+ height: '100%',
+ zIndex: 0,
+ pointerEvents: 'auto',
+ opacity: 1,
+ visibility: 'visible',
+ }}
+ />
+ )
+} \ No newline at end of file
diff --git a/frontend/src/components/ConfigModal.tsx b/frontend/src/components/ConfigModal.tsx
new file mode 100644
index 0000000..e7b1f9f
--- /dev/null
+++ b/frontend/src/components/ConfigModal.tsx
@@ -0,0 +1,44 @@
+import React from 'react'
+
+type Props = {
+ isOpen: boolean
+ onClose: () => void
+ skipIntro: boolean
+ onSkipIntroChange: (skip: boolean) => void
+}
+
+export const ConfigModal: React.FC<Props> = ({ isOpen, onClose, skipIntro, onSkipIntroChange }) => {
+ if (!isOpen) return null
+
+ return (
+ <div className="modal-overlay" onClick={onClose}>
+ <div className="config-modal" onClick={e => e.stopPropagation()}>
+ <div className="modal-header">
+ <h2>Configuration</h2>
+ <button className="close-btn" onClick={onClose}>×</button>
+ </div>
+
+ <div className="modal-content">
+ <div className="config-option">
+ <label className="config-label">
+ <input
+ type="checkbox"
+ checked={skipIntro}
+ onChange={e => onSkipIntroChange(e.target.checked)}
+ className="config-checkbox"
+ />
+ <span className="config-text">Skip Intro</span>
+ </label>
+ <div className="config-description">
+ Skip the loading screen animation on startup
+ </div>
+ </div>
+ </div>
+
+ <div className="modal-footer">
+ <button className="modal-btn" onClick={onClose}>Close</button>
+ </div>
+ </div>
+ </div>
+ )
+} \ No newline at end of file
diff --git a/frontend/src/components/DialogueBox.tsx b/frontend/src/components/DialogueBox.tsx
new file mode 100644
index 0000000..ad5975a
--- /dev/null
+++ b/frontend/src/components/DialogueBox.tsx
@@ -0,0 +1,15 @@
+import React from 'react'
+
+type Props = {
+ name?: string
+ text: string
+}
+
+export const DialogueBox: React.FC<Props> = ({ name = 'You', text }) => {
+ return (
+ <div className="dialogue vn-text-box">
+ <div className="nameplate">{name}</div>
+ <div className="dialogue-text">{text}</div>
+ </div>
+ )
+}
diff --git a/frontend/src/components/HeartLogo.tsx b/frontend/src/components/HeartLogo.tsx
new file mode 100644
index 0000000..2d407f7
--- /dev/null
+++ b/frontend/src/components/HeartLogo.tsx
@@ -0,0 +1,270 @@
+import React, { useState, useEffect } from 'react'
+
+interface HeartLogoProps {
+ size?: 'small' | 'medium' | 'large' | 'header' | 'header-no-rays'
+ className?: string
+ onClick?: () => void
+}
+
+export function HeartLogo({ size = 'small', className = '', onClick }: HeartLogoProps) {
+ const [animationOffset, setAnimationOffset] = useState(0)
+ const [isAnimating, setIsAnimating] = useState(false)
+
+ // Click handler to toggle animation
+ const handleClick = () => {
+ setIsAnimating(!isAnimating)
+ if (onClick) {
+ onClick()
+ }
+ }
+
+ // Animation for header beams
+ useEffect(() => {
+ if (size !== 'header-no-rays' || !isAnimating) return
+
+ let animationId: number
+ let startTime = Date.now()
+
+ const animate = () => {
+ const elapsed = Date.now() - startTime
+ const rotationSpeed = 0.02 // Slow rotation speed
+ const offset = (elapsed * rotationSpeed) % 360
+ setAnimationOffset(offset)
+ animationId = requestAnimationFrame(animate)
+ }
+
+ animationId = requestAnimationFrame(animate)
+
+ return () => {
+ if (animationId) {
+ cancelAnimationFrame(animationId)
+ }
+ }
+ }, [size, isAnimating])
+ if (size === 'header') {
+ // Header version with rays filling the entire header rectangle
+ return (
+ <div className={`heart-logo ${size} ${className}`}>
+ <div className="heart-logo-rectangle">
+ <div className="heart-logo-content">
+ <div className="heart-logo-rays">
+ <svg viewBox="0 0 100 100" className="heart-logo-rays-svg" preserveAspectRatio="none">
+ {/* Header rays matching loading screen exactly */}
+ <g transform="translate(50, 50)">
+ {[...Array(16)].map((_, i) => {
+ const angle = (i * 22.5);
+ const rayWidth = 2; // Much thinner rays
+
+ // Create triangular ray path - thin rays extending to header edges
+ const startAngle = angle - rayWidth;
+ const endAngle = angle + rayWidth;
+
+ const innerRadius = 0; // Start from center (heart center)
+ const outerRadius = 200; // Extend far beyond to reach all header edges
+
+ const x1 = Math.cos(startAngle * Math.PI / 180) * innerRadius;
+ const y1 = Math.sin(startAngle * Math.PI / 180) * innerRadius;
+ const x2 = Math.cos(endAngle * Math.PI / 180) * innerRadius;
+ const y2 = Math.sin(endAngle * Math.PI / 180) * innerRadius;
+ const x3 = Math.cos(startAngle * Math.PI / 180) * outerRadius;
+ const y3 = Math.sin(startAngle * Math.PI / 180) * outerRadius;
+ const x4 = Math.cos(endAngle * Math.PI / 180) * outerRadius;
+ const y4 = Math.sin(endAngle * Math.PI / 180) * outerRadius;
+
+ return (
+ <polygon
+ key={i}
+ points={`${x1},${y1} ${x2},${y2} ${x4},${y4} ${x3},${y3}`}
+ fill={i % 2 === 0 ? "#8B0000" : "#000000"}
+ opacity="0.9"
+ />
+ );
+ })}
+ </g>
+ </svg>
+ </div>
+ <div className="heart-logo-outline">
+ <svg viewBox="0 0 100 100" className="heart-logo-svg">
+ <path d="M50,85 C50,85 10,60 10,35 C10,18 20,8 35,8 C42,8 50,13 50,25 C50,13 58,8 65,8 C80,8 90,18 90,35 C90,60 50,85 50,85 Z"
+ fill="#8B0000"
+ stroke="#8B0000"
+ strokeWidth="2"/>
+ </svg>
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ if (size === 'header-no-rays') {
+ // Header version with small red beams - compact sunlight effect
+ return (
+ <div className={`heart-logo ${size} ${className}`} onClick={handleClick} style={{ cursor: 'pointer' }}>
+ <div className="heart-logo-content">
+ <div className="heart-logo-rays">
+ <svg viewBox="-200 -67 400 134" className="heart-logo-rays-svg" preserveAspectRatio="xMidYMid slice">
+ {/* Red beams extending across entire header rectangle - optimized for wide screens */}
+ <g>
+ {[...Array(32)].map((_, i) => {
+ const angle = (i * 11.25) + animationOffset; // 32 rays, 11.25 degrees apart, animated
+ const rayWidth = 2; // Much narrower rays to show background between them
+
+ // Create triangular ray path
+ const startAngle = angle - rayWidth;
+ const endAngle = angle + rayWidth;
+
+ const innerRadius = 0; // Start from heart center
+
+ // Calculate intersection with rectangle bounds for wide header
+ const getIntersectionWithRect = (angleInDegrees: number) => {
+ const rad = angleInDegrees * Math.PI / 180;
+ const dx = Math.cos(rad);
+ const dy = Math.sin(rad);
+
+ // Header rectangle bounds (wide format ~3:1 ratio)
+ const rectBoundsX = 200; // Full width
+ const rectBoundsY = 67; // Height to match header proportions
+
+ // Calculate intersection with each edge
+ let t = Infinity;
+
+ // Right edge (x = rectBoundsX)
+ if (dx > 0) {
+ t = Math.min(t, rectBoundsX / dx);
+ }
+ // Left edge (x = -rectBoundsX)
+ if (dx < 0) {
+ t = Math.min(t, -rectBoundsX / dx);
+ }
+ // Top edge (y = -rectBoundsY)
+ if (dy < 0) {
+ t = Math.min(t, -rectBoundsY / dy);
+ }
+ // Bottom edge (y = rectBoundsY)
+ if (dy > 0) {
+ t = Math.min(t, rectBoundsY / dy);
+ }
+
+ return { x: dx * t, y: dy * t };
+ };
+
+ const x1 = Math.cos(startAngle * Math.PI / 180) * innerRadius;
+ const y1 = Math.sin(startAngle * Math.PI / 180) * innerRadius;
+ const x2 = Math.cos(endAngle * Math.PI / 180) * innerRadius;
+ const y2 = Math.sin(endAngle * Math.PI / 180) * innerRadius;
+
+ const edge1 = getIntersectionWithRect(startAngle);
+ const edge2 = getIntersectionWithRect(endAngle);
+
+ return (
+ <polygon
+ key={i}
+ points={`${x1},${y1} ${x2},${y2} ${edge2.x},${edge2.y} ${edge1.x},${edge1.y}`}
+ fill={i % 2 === 0 ? "#DC2626" : "#B91C1C"}
+ opacity="0.6"
+ />
+ );
+ })}
+ </g>
+ </svg>
+ </div>
+ <div className="heart-logo-outline">
+ <svg viewBox="0 0 100 100" className="heart-logo-svg">
+ <path d="M50,85 C50,85 10,60 10,35 C10,18 20,8 35,8 C42,8 50,13 50,25 C50,13 58,8 65,8 C80,8 90,18 90,35 C90,60 50,85 50,85 Z"
+ fill="#8B0000"
+ stroke="#8B0000"
+ strokeWidth="2"/>
+ </svg>
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ // Original square version for other sizes
+ return (
+ <div className={`heart-logo ${size} ${className}`}>
+ <div className="heart-logo-rectangle">
+ <div className="heart-logo-content">
+ <div className="heart-logo-rays">
+ <svg viewBox="-100 -67 200 134" className="heart-logo-rays-svg">
+ {/* Rising Sun flag style rays - triangular rays extending to rectangle edges */}
+ <g>
+ {[...Array(16)].map((_, i) => {
+ const angle = (i * 22.5);
+ const rayWidth = 11.25; // Half the angle between rays for triangular shape
+
+ // Create triangular ray path
+ const startAngle = angle - rayWidth;
+ const endAngle = angle + rayWidth;
+
+ const innerRadius = 0; // Start from center (heart center)
+
+ // Calculate intersection with rectangle bounds
+ // Rectangle aspect ratio 120:80 = 3:2, so use different bounds for x and y
+ const getIntersectionWithRect = (angleInDegrees: number) => {
+ const rad = angleInDegrees * Math.PI / 180;
+ const dx = Math.cos(rad);
+ const dy = Math.sin(rad);
+
+ // Rectangle bounds matching the 120x80 aspect ratio (3:2)
+ const rectBoundsX = 100; // Full width
+ const rectBoundsY = 67; // 2/3 of width to maintain 3:2 ratio
+
+ // Calculate intersection with each edge
+ let t = Infinity;
+
+ // Right edge (x = rectBoundsX)
+ if (dx > 0) {
+ t = Math.min(t, rectBoundsX / dx);
+ }
+ // Left edge (x = -rectBoundsX)
+ if (dx < 0) {
+ t = Math.min(t, -rectBoundsX / dx);
+ }
+ // Top edge (y = -rectBoundsY)
+ if (dy < 0) {
+ t = Math.min(t, -rectBoundsY / dy);
+ }
+ // Bottom edge (y = rectBoundsY)
+ if (dy > 0) {
+ t = Math.min(t, rectBoundsY / dy);
+ }
+
+ return { x: dx * t, y: dy * t };
+ };
+
+ const x1 = Math.cos(startAngle * Math.PI / 180) * innerRadius;
+ const y1 = Math.sin(startAngle * Math.PI / 180) * innerRadius;
+ const x2 = Math.cos(endAngle * Math.PI / 180) * innerRadius;
+ const y2 = Math.sin(endAngle * Math.PI / 180) * innerRadius;
+
+ const edge1 = getIntersectionWithRect(startAngle);
+ const edge2 = getIntersectionWithRect(endAngle);
+
+ return (
+ <polygon
+ key={i}
+ points={`${x1},${y1} ${x2},${y2} ${edge2.x},${edge2.y} ${edge1.x},${edge1.y}`}
+ fill={i % 2 === 0 ? "#8B0000" : "#000000"}
+ opacity="0.9"
+ />
+ );
+ })}
+ </g>
+ </svg>
+ </div>
+ <div className="heart-logo-outline">
+ <svg viewBox="0 0 100 100" className="heart-logo-svg">
+ <path d="M50,85 C50,85 10,60 10,35 C10,18 20,8 35,8 C42,8 50,13 50,25 C50,13 58,8 65,8 C80,8 90,18 90,35 C90,60 50,85 50,85 Z"
+ fill="#8B0000"
+ stroke="#8B0000"
+ strokeWidth="2"/>
+ </svg>
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+} \ No newline at end of file
diff --git a/frontend/src/components/LandingPage.tsx b/frontend/src/components/LandingPage.tsx
new file mode 100644
index 0000000..f5dc55c
--- /dev/null
+++ b/frontend/src/components/LandingPage.tsx
@@ -0,0 +1,219 @@
+import React, { useState, useEffect } from 'react'
+import { LoadingScreen } from './LoadingScreen'
+import { HeartLogo } from './HeartLogo'
+// Using direct PNG logo on landing header
+
+interface LandingPageProps {
+ onLogin: () => void
+}
+
+export function LandingPage({ onLogin }: LandingPageProps) {
+ const [loading, setLoading] = useState(false)
+ const [showLanding, setShowLanding] = useState(false)
+ const [isStandby, setIsStandby] = useState(true) // false = LIVE, true = STDBY
+ const [asciiArt, setAsciiArt] = useState('')
+ const [isGlitching, setIsGlitching] = useState(false)
+ // Note: Removed VN preview and ASCII features to focus on an Art Deco/Nouveau landing.
+
+ // Auto-fade in landing page content after component mounts
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ setShowLanding(true)
+ }, 500) // Delay before fading in content
+
+ return () => clearTimeout(timer)
+ }, [])
+
+ // Removed VN dialogue preview / typing effect.
+
+ // Load ASCII art for overlay in hero (right/bottom)
+ useEffect(() => {
+ fetch('/ascii/ascii1.txt')
+ .then((res) => res.text())
+ .then((txt) => setAsciiArt(txt.replace(/\n+$/, '')))
+ .catch(() => setAsciiArt(''))
+ }, [])
+
+ // Occasionally trigger a brief glitch on the word "futurist"
+ useEffect(() => {
+ let armTimer: number | undefined
+ let activeTimer: number | undefined
+ let cancelled = false
+
+ const arm = () => {
+ // Random delay between glitches: 0.8s–2s (slightly punchier)
+ const delay = 800 + Math.random() * 1200
+ armTimer = window.setTimeout(() => {
+ setIsGlitching(true)
+ // Glitch duration: 150ms–350ms (snappier)
+ const dur = 150 + Math.random() * 200
+ activeTimer = window.setTimeout(() => {
+ setIsGlitching(false)
+ if (!cancelled) arm()
+ }, dur)
+ }, delay)
+ }
+
+ arm()
+
+ return () => {
+ cancelled = true
+ if (armTimer) clearTimeout(armTimer)
+ if (activeTimer) clearTimeout(activeTimer)
+ }
+ }, [])
+
+
+ // Handle loading screen completion
+ const handleLoadingComplete = () => {
+ // Call the original onLogin immediately to transition to VN page
+ // Don't reset loading states as we're leaving the landing page
+ onLogin()
+ }
+
+ // Handle login button click
+ const handleLogin = () => {
+ setLoading(true)
+ }
+
+ // Removed ASCII art effects and rendering.
+
+ return (
+ <div>
+ {loading && (
+ <LoadingScreen onComplete={handleLoadingComplete} />
+ )}
+
+ {/* Floating Header Bar - Hidden during loading */}
+ {!loading && (
+ <div className={`floating-header ${showLanding ? 'fade-in' : 'hidden'}`}>
+ <div className="header-content">
+ <div className="brand">
+ <img
+ src="/logo/crane-logo-transparent.png"
+ alt="Soryu"
+ height={40}
+ className="brand-mark"
+ onError={(e) => { const img = (e.currentTarget as HTMLImageElement); img.onerror = null; img.src = '/logo/crane-logo.png'; }}
+ />
+ </div>
+ <div className="header-center">
+ <HeartLogo size="header-no-rays" className="header-heart" />
+ </div>
+
+ <div className="system-info">
+ <div className="info-item">
+ <span className="info-label">System:</span>
+ <span
+ className="info-value live-status clickable"
+ onClick={() => setIsStandby(!isStandby)}
+ title="Click to toggle between LIVE and STANDBY"
+ >
+ <span className={`status-dot ${isStandby ? 'standby' : 'live'}`}></span>
+ {isStandby ? 'STDBY' : 'LIVE'}
+ </span>
+ </div>
+ <div className="info-item">
+ <span className="info-label">Version:</span>
+ <span className="info-value">v1.0.0</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ )}
+
+ <div className={`modern-landing-page manga-style ${showLanding && !loading ? 'fade-in' : 'hidden'}`}>
+ {/* Retro-futuristic page background */}
+ <div className="rf-page-bg" aria-hidden="true">
+ <div className="rf-page-speedlines layer-a"></div>
+ <div className="rf-page-speedlines layer-b"></div>
+ </div>
+ {/* Taisho Magazine Cover Backdrop */}
+ <div className="taisho-cover">
+ <div className="cover-backdrop" aria-hidden="true"></div>
+
+ {/* Cover Content Grid */}
+ <div className="cover-content">
+ {/* Vertical Masthead (magazine-style) */}
+ <div className="masthead">
+ <div className="masthead-vertical">
+ <span className="jp">そりゅう</span>
+ <span className="en">SORYU</span>
+ </div>
+ <div className="issue-badge">かはいい Vol.01</div>
+ </div>
+
+ {/* Central Hero - full-frame retro-futuristic */}
+ <div className="hero">
+ <div className="hero-frame taisho-modern-frame">
+ <div className="hero-inner hero-fill">
+ {/* Retro-futuristic racing hero content */}
+ <div className="rf-hero" aria-hidden="true">
+ <div className="rf-speedlines layer-a"></div>
+ <div className="rf-speedlines layer-b"></div>
+ <div className="rf-hero-content">
+ <div className="rf-badge">Engineering • Systems • para bellum</div>
+ <h2 className="rf-headline">
+ Mission driven{' '}
+ <span
+ className={`glitch-word ${isGlitching ? 'is-glitching' : ''}`}
+ data-text="futurist"
+ >
+ futurist
+ </span>{' '}
+ technology
+ </h2>
+ <p className="rf-tagline">Avant-garde. Aesthetic Engineering. A Race of Steel</p>
+ <div className="rf-stats">
+ <div className="rf-stat"><span className="label">Velocity</span><span className="value">0–603 km/h</span></div>
+ <div className="rf-stat"><span className="label">Energy</span><span className="value">32 MJ</span></div>
+ <div className="rf-stat"><span className="label">Flow</span><span className="value">MAX</span></div>
+ </div>
+ </div>
+ <div className="rf-accent-diagonal"></div>
+ {/* ASCII overlay (transparent, gradient text) */}
+ {asciiArt && (
+ <div className="rf-ascii-overlay" aria-hidden="true">
+ {asciiArt.split('\n').map((line, i) => (
+ <div key={i} className="ascii-line">
+ {Array.from(line).map((ch, j) => (
+ <span
+ key={`${i}-${j}`}
+ className="ascii-char"
+ style={{
+ '--delay': `${(i * 0.08 + j * 0.01)}s`,
+ } as React.CSSProperties}
+ >
+ {ch}
+ </span>
+ ))}
+ </div>
+ ))}
+ </div>
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+
+ {/* CTA */}
+ <div className="cta-area">
+ <button className="taisho-cta" onClick={handleLogin}>
+ <span className="cta-icon">▶</span>
+ <span className="cta-text">Enter</span>
+ </button>
+ </div>
+ {/* Visitor Counter */}
+ <div className="visit-counter" aria-label="visitor counter">
+ <img
+ src="https://count.getloli.com/get/@soryu-landing?theme=booru-jaypee&darkmode=0"
+ alt="visit counter"
+ referrerPolicy="no-referrer-when-downgrade"
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+}
diff --git a/frontend/src/components/LoadingScreen.tsx b/frontend/src/components/LoadingScreen.tsx
new file mode 100644
index 0000000..5f33d00
--- /dev/null
+++ b/frontend/src/components/LoadingScreen.tsx
@@ -0,0 +1,129 @@
+import React, { useEffect, useState, useRef } from 'react'
+
+type Props = {
+ onComplete: () => void
+}
+
+export const LoadingScreen: React.FC<Props> = ({ onComplete }) => {
+ const [fadeOut, setFadeOut] = useState(false)
+ const [mounted, setMounted] = useState(true)
+ const [subtitle, setSubtitle] = useState('')
+ const [animatedText, setAnimatedText] = useState('')
+ const subtitleLoaded = useRef(false)
+
+ useEffect(() => {
+ console.log('Loading screen mounted - starting timer...')
+
+ // Set fixed subtitle and animate it
+ if (!subtitleLoaded.current) {
+ subtitleLoaded.current = true
+
+ const fixedSubtitle = 'Whisper of the Heart'
+ setSubtitle(fixedSubtitle)
+
+ // Animate text character by character
+ let currentIndex = 0
+ const animateText = () => {
+ if (currentIndex <= fixedSubtitle.length) {
+ setAnimatedText(fixedSubtitle.slice(0, currentIndex))
+ currentIndex++
+ setTimeout(animateText, 50) // 50ms delay between characters
+ }
+ }
+
+ // Start animation after heart appears (1000ms = 0.2s heart delay + 0.8s heart animation)
+ setTimeout(animateText, 1000)
+ }
+
+ // Start fade after 4 seconds (longer to show ray spinning)
+ const fadeTimer = setTimeout(() => {
+ console.log('4 seconds passed - Starting fade out...')
+ setFadeOut(true)
+ }, 4000)
+
+ // Complete after fade finishes (4s show + 1s fade = 5s total)
+ const completeTimer = setTimeout(() => {
+ console.log('5 seconds passed - Loading screen completing...')
+ setMounted(false)
+ onComplete()
+ }, 5000)
+
+ return () => {
+ console.log('Cleaning up timers...')
+ clearTimeout(fadeTimer)
+ clearTimeout(completeTimer)
+ }
+ }, []) // Empty dependency array - run once on mount
+
+ const handleClick = () => {
+ console.log('Loading screen clicked, completing early...')
+ setFadeOut(true)
+ setTimeout(() => {
+ setMounted(false)
+ onComplete()
+ }, 1000)
+ }
+
+ console.log('LoadingScreen render - fadeOut:', fadeOut, 'mounted:', mounted)
+
+ if (!mounted) return null
+
+ return (
+ <div className={`loading-screen ${fadeOut ? 'fade-out' : ''}`} onClick={handleClick}>
+ <div className="loading-logo">
+ <div className="heart-container">
+ <div className="sun-rays loading-animate-rays">
+ <svg viewBox="0 0 100 100" className="rays-svg">
+ {/* Rising Sun flag style rays - triangular rays extending to edges */}
+ <g transform="translate(50, 50)">
+ {[...Array(16)].map((_, i) => {
+ const angle = (i * 22.5);
+ const nextAngle = ((i + 1) * 22.5);
+ const rayWidth = 11.25; // Half the angle between rays for triangular shape
+
+ // Create triangular ray path
+ const startAngle = angle - rayWidth;
+ const endAngle = angle + rayWidth;
+
+ const innerRadius = 0; // Start from center (heart center)
+ const outerRadius = 70; // Extend to screen edge
+
+ const x1 = Math.cos(startAngle * Math.PI / 180) * innerRadius;
+ const y1 = Math.sin(startAngle * Math.PI / 180) * innerRadius;
+ const x2 = Math.cos(endAngle * Math.PI / 180) * innerRadius;
+ const y2 = Math.sin(endAngle * Math.PI / 180) * innerRadius;
+ const x3 = Math.cos(startAngle * Math.PI / 180) * outerRadius;
+ const y3 = Math.sin(startAngle * Math.PI / 180) * outerRadius;
+ const x4 = Math.cos(endAngle * Math.PI / 180) * outerRadius;
+ const y4 = Math.sin(endAngle * Math.PI / 180) * outerRadius;
+
+ return (
+ <polygon
+ key={i}
+ points={`${x1},${y1} ${x2},${y2} ${x4},${y4} ${x3},${y3}`}
+ fill={i % 2 === 0 ? "#8B0000" : "#000000"}
+ opacity="0.9"
+ />
+ );
+ })}
+ </g>
+ </svg>
+ </div>
+ <div className="heart-outline loading-animate-heart">
+ <svg viewBox="0 0 100 100" className="heart-svg">
+ <path d="M50,85 C50,85 10,60 10,35 C10,18 20,8 35,8 C42,8 50,13 50,25 C50,13 58,8 65,8 C80,8 90,18 90,35 C90,60 50,85 50,85 Z"
+ fill="#8B0000"
+ stroke="#8B0000"
+ strokeWidth="2"/>
+ </svg>
+ </div>
+ <div className="logo-text loading-animate-text">soryu</div>
+ <div className="loading-subtitle loading-animate-subtitle">{animatedText}</div>
+ <div className="loading-dots loading-animate-text">
+ <span>.</span><span>.</span><span>.</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+} \ No newline at end of file
diff --git a/frontend/src/components/OrigamiDragonLogo.tsx b/frontend/src/components/OrigamiDragonLogo.tsx
new file mode 100644
index 0000000..6a7a5fb
--- /dev/null
+++ b/frontend/src/components/OrigamiDragonLogo.tsx
@@ -0,0 +1,180 @@
+import React from 'react'
+
+type Variant = 'mark' | 'ribbonS' | 'badge' | 'crane' | 'craneOutline'
+
+type Props = {
+ variant?: Variant
+ height?: number
+ color?: string
+ className?: string
+ title?: string
+ strokeWidth?: number
+}
+
+/**
+ * Origami-style dragon logo for small header placement.
+ * - Uses currentColor by default so it adapts to surrounding text color.
+ * - Keep height around 20–24px in compact headers.
+ */
+export const OrigamiDragonLogo: React.FC<Props> = ({
+ variant = 'crane',
+ height = 20,
+ color = 'currentColor',
+ className = '',
+ title = 'Soryu logo',
+ strokeWidth = 12,
+}) => {
+ if (variant === 'craneOutline') {
+ // Outline rendition inspired by provided licensed crane icon
+ return (
+ <svg
+ className={className}
+ role="img"
+ aria-label={title}
+ width={height}
+ height={height}
+ viewBox="0 0 100 100"
+ xmlns="http://www.w3.org/2000/svg"
+ style={{ display: 'inline-block', verticalAlign: 'middle', color }}
+ >
+ <title>{title}</title>
+ <g fill="none" stroke="currentColor" strokeWidth={strokeWidth} strokeLinejoin="round" strokeLinecap="round">
+ {/* Left tail → left wing base → body pivot */}
+ <path d="M10 60 L36 76 L18 42 L42 58" />
+ {/* Left wing upstroke */}
+ <path d="M42 58 L26 12" />
+ {/* Left wing inner crease to pivot */}
+ <path d="M26 12 L50 62" />
+ {/* Body lower crease */}
+ <path d="M30 76 L50 62" />
+ {/* Center mast (rear triangle) */}
+ <path d="M50 62 L58 16 L64 70" />
+ {/* Right wing outer edge */}
+ <path d="M50 62 L72 30 L66 78" />
+ {/* Right wing inner crease */}
+ <path d="M72 30 L60 52" />
+ {/* Neck and head */}
+ <path d="M66 78 L82 54 L90 40 L96 56" />
+ {/* Neck inner crease */}
+ <path d="M82 54 L72 44" />
+ </g>
+ </svg>
+ )
+ }
+ if (variant === 'crane') {
+ // Origami crane silhouette with a dragon head
+ return (
+ <svg
+ className={className}
+ role="img"
+ aria-label={title}
+ width={height}
+ height={height}
+ viewBox="0 0 100 100"
+ xmlns="http://www.w3.org/2000/svg"
+ style={{ display: 'inline-block', verticalAlign: 'middle', color }}
+ >
+ <title>{title}</title>
+ {/* Left/up wing */}
+ <polygon points="46,48 22,24 40,52" fill="currentColor" opacity="0.95" />
+ {/* Right/down wing */}
+ <polygon points="52,52 78,76 58,54" fill="currentColor" opacity="0.78" />
+ {/* Body (diamond) */}
+ <polygon points="44,52 52,46 58,52 50,60" fill="currentColor" opacity="0.88" />
+ {/* Tail */}
+ <polygon points="44,60 34,70 46,64" fill="currentColor" opacity="0.7" />
+ {/* Neck */}
+ <polygon points="58,46 76,40 74,44 56,50" fill="currentColor" opacity="0.9" />
+ {/* Dragon head (top plane) */}
+ <polygon points="76,40 90,38 82,46" fill="currentColor" opacity="0.95" />
+ {/* Dragon jaw plane */}
+ <polygon points="74,44 90,46 82,50" fill="currentColor" opacity="0.8" />
+ {/* Small horn */}
+ <polygon points="86,36 92,38 88,42" fill="currentColor" opacity="0.85" />
+ </svg>
+ )
+ }
+ if (variant === 'mark') {
+ // Angular origami dragon head/neck, faceted into simple planes
+ return (
+ <svg
+ className={className}
+ role="img"
+ aria-label={title}
+ width={height}
+ height={height}
+ viewBox="0 0 100 100"
+ xmlns="http://www.w3.org/2000/svg"
+ style={{ display: 'inline-block', verticalAlign: 'middle', color }}
+ >
+ <title>{title}</title>
+ {/* Upper head */}
+ <polygon points="60,22 86,16 73,34 58,30" fill="currentColor" opacity="0.95" />
+ {/* Snout / jaw plane */}
+ <polygon points="58,30 73,34 90,30 71,45" fill="currentColor" opacity="0.8" />
+ {/* Neck planes */}
+ <polygon points="58,30 71,45 56,60 40,66 30,72 22,64 46,48" fill="currentColor" opacity="0.9" />
+ <polygon points="22,64 30,72 18,74 26,60" fill="currentColor" opacity="0.65" />
+ {/* Small ear/horn suggestion */}
+ <polygon points="78,18 86,16 82,24" fill="currentColor" opacity="0.7" />
+ </svg>
+ )
+ }
+
+ if (variant === 'ribbonS') {
+ // Folded ribbon forming an angular "S" silhouette
+ return (
+ <svg
+ className={className}
+ role="img"
+ aria-label={title}
+ width={height}
+ height={height}
+ viewBox="0 0 100 100"
+ xmlns="http://www.w3.org/2000/svg"
+ style={{ display: 'inline-block', verticalAlign: 'middle', color }}
+ >
+ <title>{title}</title>
+ {/* Top fold */}
+ <polygon points="22,32 62,18 70,26 30,40" fill="currentColor" opacity="0.95" />
+ {/* Middle fold (turning back) */}
+ <polygon points="30,40 70,26 60,46 20,60" fill="currentColor" opacity="0.8" />
+ {/* Lower fold */}
+ <polygon points="22,64 60,46 68,54 28,70" fill="currentColor" opacity="0.9" />
+ </svg>
+ )
+ }
+
+ // badge
+ return (
+ <svg
+ className={className}
+ role="img"
+ aria-label={title}
+ width={height}
+ height={height}
+ viewBox="0 0 100 100"
+ xmlns="http://www.w3.org/2000/svg"
+ style={{ display: 'inline-block', verticalAlign: 'middle', color }}
+ >
+ <title>{title}</title>
+ {/* Hex container */}
+ <polygon
+ points="50,6 83,25 83,75 50,94 17,75 17,25"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="8"
+ strokeLinejoin="round"
+ opacity="0.9"
+ />
+ {/* Mini mark inside */}
+ <g transform="translate(0,2) scale(0.85) translate(10,6)">
+ <polygon points="60,22 86,16 73,34 58,30" fill="currentColor" opacity="0.95" />
+ <polygon points="58,30 73,34 90,30 71,45" fill="currentColor" opacity="0.8" />
+ <polygon points="58,30 71,45 56,60 40,66 30,72 22,64 46,48" fill="currentColor" opacity="0.9" />
+ </g>
+ </svg>
+ )
+}
+
+export default OrigamiDragonLogo
diff --git a/frontend/src/components/TopBar.tsx b/frontend/src/components/TopBar.tsx
new file mode 100644
index 0000000..89d7206
--- /dev/null
+++ b/frontend/src/components/TopBar.tsx
@@ -0,0 +1,24 @@
+import React from 'react'
+
+type Props = {
+ title?: string
+ status?: string
+}
+
+export const TopBar: React.FC<Props> = ({ title = 'PC-98 VISUAL NOVEL', status = 'IDLE' }) => {
+ return (
+ <div className="topbar window-border">
+ <div className="topbar-left">
+ <span className="led led-green" />
+ <span className="menu-item">FILE</span>
+ <span className="menu-item">SAVE</span>
+ <span className="menu-item">LOAD</span>
+ <span className="menu-item">CONFIG</span>
+ </div>
+ <div className="topbar-title">{title}</div>
+ <div className="topbar-right">
+ <span className="status">{status}</span>
+ </div>
+ </div>
+ )
+}
diff --git a/frontend/src/components/VNApp.tsx b/frontend/src/components/VNApp.tsx
new file mode 100644
index 0000000..0f73d2c
--- /dev/null
+++ b/frontend/src/components/VNApp.tsx
@@ -0,0 +1,228 @@
+import React, { useEffect, useMemo, useRef, useState } from 'react'
+import { TopBar } from './TopBar'
+import { VNViewport } from './VNViewport'
+import { DialogueBox } from './DialogueBox'
+import { ChoiceMenu } from './ChoiceMenu'
+import { BottomBar } from './BottomBar'
+import { LoadingScreen } from './LoadingScreen'
+import { ConfigModal } from './ConfigModal'
+import { VNWebSocket } from '../services/ws'
+import { ChatMessage, Choice } from '../types'
+// Using direct PNG logo in VN header
+
+const DEFAULT_CHOICES: Choice[] = [
+ { id: 'greet', label: '"Hello?"' },
+ { id: 'who', label: '"Who are you?"' },
+ { id: 'silence', label: '(Stay silent)' },
+]
+
+export function VNApp() {
+ const [loading, setLoading] = useState(true)
+ const [loadingComplete, setLoadingComplete] = useState(false)
+ const [messages, setMessages] = useState<ChatMessage[]>([
+ { id: 'm1', role: 'assistant', content: 'A warm CRT glow fills the room. A figure turns towards you...' },
+ ])
+ const [choices, setChoices] = useState<Choice[]>(DEFAULT_CHOICES)
+ const [status, setStatus] = useState('OFFLINE')
+ const [name, setName] = useState('???')
+ const [bg, setBg] = useState('/__gaogao__56242cbde8f18ac64522e410bad04e68_waifu2x_art_noise2.png')
+ const [money, setMoney] = useState(15000)
+ const [currentTime, setCurrentTime] = useState(new Date())
+ const [location, setLocation] = useState('Tokyo')
+ const [weather, setWeather] = useState('Sunny 22°C')
+ const [configModalOpen, setConfigModalOpen] = useState(false)
+ const [skipIntro, setSkipIntro] = useState(() => {
+ const saved = localStorage.getItem('skipIntro')
+ return saved === 'true'
+ })
+
+ const ws = useMemo(() => new VNWebSocket('ws://localhost:8080/ws'), [])
+ const lastId = useRef(2)
+
+ // Check if intro should be skipped on initial load
+ useEffect(() => {
+ if (skipIntro) {
+ setLoading(false)
+ setLoadingComplete(true)
+ // Set skip intro to true after first time loading screen is shown
+ localStorage.setItem('skipIntro', 'true')
+ }
+ }, [])
+
+ // Handle skip intro toggle
+ const handleSkipIntroChange = (skip: boolean) => {
+ setSkipIntro(skip)
+ localStorage.setItem('skipIntro', skip.toString())
+ }
+
+ // Handle loading screen completion
+ const handleLoadingComplete = () => {
+ setLoading(false)
+ setLoadingComplete(true)
+ // Set skip intro to true after first time loading screen is shown
+ if (!skipIntro) {
+ setSkipIntro(true)
+ localStorage.setItem('skipIntro', 'true')
+ }
+ }
+
+ useEffect(() => {
+ ws.on('open', () => setStatus('ONLINE'))
+ ws.on('close', () => setStatus('OFFLINE'))
+ ws.on('message', (data) => {
+ // Expecting { type: 'assistant'|'choices'|'bg'|'name'|'system', ... }
+ if (data?.type === 'assistant') {
+ pushMessage('assistant', data.content || '')
+ } else if (data?.type === 'choices') {
+ setChoices((data.items || []).map((it: any, idx: number) => ({ id: it.id ?? String(idx), label: it.label ?? String(it) })))
+ } else if (data?.type === 'bg') {
+ setBg(data.src || '/bg.jpg')
+ } else if (data?.type === 'name') {
+ setName(data.value || name)
+ } else if (data?.type === 'system') {
+ pushMessage('system', data.content || '')
+ }
+ })
+ ws.connect()
+ return () => ws.close()
+ }, [ws])
+
+ // Update clock every second
+ useEffect(() => {
+ const timer = setInterval(() => {
+ setCurrentTime(new Date())
+ }, 1000)
+ return () => clearInterval(timer)
+ }, [])
+
+ function pushMessage(role: 'user' | 'assistant' | 'system', content: string) {
+ lastId.current += 1
+ setMessages((prev) => [...prev, { id: 'm' + lastId.current, role, content }])
+ }
+
+ function handleChoice(id: string) {
+ const choice = choices.find(c => c.id === id)
+ const content = choice?.label || id
+ pushMessage('user', content)
+ ws.send({ type: 'user_choice', id, label: content })
+ setChoices([])
+ setTimeout(() => {
+ pushMessage('assistant', `The figure nods: "${content}"...`)
+ setChoices(DEFAULT_CHOICES)
+ setName('Guide')
+ }, 600)
+ }
+
+ const lastAssistant = messages.slice().reverse().find(m => m.role !== 'user')
+
+ return (
+ <div>
+ {!loadingComplete && (
+ <LoadingScreen
+ onComplete={handleLoadingComplete}
+ />
+ )}
+ <div className={`main-interface ${loadingComplete ? 'visible' : 'hidden'}`}>
+ <div className="screen" role="application">
+ <div className="left-pillar">
+ <div className="pillar-image">
+ Character A
+ </div>
+ <div className="pillar-bottom-section">
+ <div className="pillar-buttons">
+ <button className="pillar-btn" onClick={() => {}}>SAVE</button>
+ <button className="pillar-btn" onClick={() => {}}>LOAD</button>
+ <button className="pillar-btn" onClick={() => setConfigModalOpen(true)}>CONFIG</button>
+ <button className="pillar-btn" onClick={() => {}}>EXIT</button>
+ </div>
+ </div>
+ </div>
+
+ <div className="screen-content">
+ <div className="screen-header header-footer-border">
+ <div className="brand">
+ <img
+ src="/logo/crane-logo-transparent.png"
+ alt="Soryu"
+ height={24}
+ className="brand-mark"
+ onError={(e) => { const img = (e.currentTarget as HTMLImageElement); img.onerror = null; img.src = '/logo/crane-logo.png'; }}
+ />
+ </div>
+ </div>
+ <VNViewport bgSrc={bg}>
+ <div className="ui-layer">
+ <div className="weather-display">
+ {location} - {weather}
+ </div>
+ </div>
+ </VNViewport>
+ <DialogueBox name={name} text={lastAssistant?.content || ''} />
+ <div className="text-input-area">
+ <input
+ type="text"
+ className="text-input"
+ placeholder="Type your response..."
+ onKeyPress={(e) => {
+ if (e.key === 'Enter') {
+ const target = e.target as HTMLInputElement;
+ if (target.value.trim()) {
+ pushMessage('user', target.value);
+ target.value = '';
+ }
+ }
+ }}
+ />
+ </div>
+ <div className="screen-footer header-footer-border">
+ <div className="desktop-time">{new Date().toLocaleDateString('ja-JP', { year: 'numeric', month: 'long', day: 'numeric' })} {new Date().toLocaleTimeString('ja-JP', { hour12: false })}</div>
+ <div className="mobile-time-money">
+ <div className="mobile-clock">
+ {currentTime.toLocaleDateString('ja-JP', { year: 'numeric', month: 'long', day: 'numeric' })} {currentTime.toLocaleTimeString('ja-JP', { hour12: false })}
+ </div>
+ <div className="mobile-money">
+ {money.toLocaleString()}
+ </div>
+ </div>
+ <div className="mobile-buttons">
+ <button className="mobile-btn" onClick={() => {}}>SAVE</button>
+ <button className="mobile-btn" onClick={() => {}}>LOAD</button>
+ <button className="mobile-btn" onClick={() => setConfigModalOpen(true)}>CONFIG</button>
+ <button className="mobile-btn" onClick={() => {}}>EXIT</button>
+ </div>
+ </div>
+ <BottomBar
+ onSkip={() => {}}
+ onAuto={() => {}}
+ onLog={() => {}}
+ location={location}
+ />
+ </div>
+
+ <div className="right-pillar">
+ <div className="pillar-image">
+ Character B
+ </div>
+ <div className="pillar-bottom-section">
+ <div className="pillar-display digital-clock">
+ {currentTime.toLocaleDateString('ja-JP', { year: 'numeric', month: 'long', day: 'numeric' })}
+ <br />
+ {currentTime.toLocaleTimeString('ja-JP', { hour12: false })}
+ </div>
+ <div className="pillar-display yen-counter">
+ {money.toLocaleString()}
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <ConfigModal
+ isOpen={configModalOpen}
+ onClose={() => setConfigModalOpen(false)}
+ skipIntro={skipIntro}
+ onSkipIntroChange={handleSkipIntroChange}
+ />
+ </div>
+ )
+}
diff --git a/frontend/src/components/VNInterface.tsx b/frontend/src/components/VNInterface.tsx
new file mode 100644
index 0000000..be71d27
--- /dev/null
+++ b/frontend/src/components/VNInterface.tsx
@@ -0,0 +1,208 @@
+import React, { useEffect } from 'react'
+import { useStore } from '@nanostores/react'
+import {
+ isStandbyStore,
+ currentTimeStore,
+ weatherStore,
+ showChoicesStore,
+ showSettingsModalStore,
+ isVisibleStore,
+ yenBalanceStore,
+ toggleStandby,
+ toggleShowChoices,
+ updateTime
+} from '../stores'
+
+interface VNInterfaceProps {
+ onLogout: () => void
+}
+
+export function VNInterface({ onLogout }: VNInterfaceProps) {
+ const isStandby = useStore(isStandbyStore)
+ const currentTime = useStore(currentTimeStore)
+ const weather = useStore(weatherStore)
+ const showChoices = useStore(showChoicesStore)
+ const showSettingsModal = useStore(showSettingsModalStore)
+ const isVisible = useStore(isVisibleStore)
+ const yenBalance = useStore(yenBalanceStore)
+
+ // Fade in effect on mount
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ isVisibleStore.set(true)
+ }, 100)
+ return () => clearTimeout(timer)
+ }, [])
+
+ // Update clock every second (Japan Time)
+ useEffect(() => {
+ const timer = setInterval(() => {
+ const now = new Date()
+ // Convert to Japan Time (UTC+9)
+ const japanTime = new Date(now.getTime() + (now.getTimezoneOffset() * 60000) + (9 * 3600000))
+ updateTime()
+ }, 1000)
+ return () => clearInterval(timer)
+ }, [])
+
+ return (
+ <div className={`vn-interface ${isVisible ? 'fade-in' : 'fade-out'}`}>
+ {/* Background */}
+ <div className="vn-background">
+ <img
+ src="/__gaogao__56242cbde8f18ac64522e410bad04e68_waifu2x_art_noise2.png"
+ alt="Background image"
+ className="background-image"
+ />
+ </div>
+
+ {/* Combined Info Panel (Top Right) */}
+ <div className="floating-info-panel">
+ <div className="info-panel-content">
+ {/* Weather Section */}
+ <div className="weather-section">
+ <div className="weather-icon">🌤️</div>
+ <div className="weather-details">
+ <div className="weather-location">Tokyo</div>
+ <div className="weather-temp">22°C Sunny</div>
+ </div>
+ </div>
+
+ {/* Time Section */}
+ <div className="time-section">
+ <div className="japan-date">{currentTime.toLocaleDateString('ja-JP', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ weekday: 'short'
+ })}</div>
+ <div className="japan-time">{currentTime.toLocaleTimeString('ja-JP', {
+ hour12: false,
+ hour: '2-digit',
+ minute: '2-digit'
+ })}</div>
+ </div>
+
+ {/* Status Section */}
+ <div className="status-section">
+ <div className="status-item">
+ <span className="info-label">Balance:</span>
+ <span className="info-value yen-balance">¥{yenBalance.toLocaleString()}</span>
+ </div>
+ <div className="status-item">
+ <span className="info-label">System:</span>
+ <span
+ className="info-value live-status clickable"
+ onClick={toggleStandby}
+ title="Click to toggle between LIVE and STANDBY"
+ >
+ <span className={`status-dot ${isStandby ? 'standby' : 'live'}`}></span>
+ {isStandby ? 'STDBY' : 'LIVE'}
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ {/* Main VN Viewport */}
+ <div className="vn-viewport">
+ <div className="vn-content">
+ </div>
+ </div>
+
+ {/* Dialogue Panel (Bottom) */}
+ <div className="floating-dialogue-panel">
+ <div className="dialogue-content">
+ <div className="dialogue-speaker">???</div>
+ <div className="dialogue-text">
+ A warm CRT glow fills the room. A figure turns towards you...
+ </div>
+ </div>
+ </div>
+
+ {/* Input/Choice Panel (Bottom) */}
+ <div className="floating-input-panel">
+ <div className="input-content">
+ {!showChoices ? (
+ // Text Input Mode
+ <input
+ type="text"
+ className="vn-text-input"
+ placeholder="Type your response..."
+ onKeyPress={(e) => {
+ if (e.key === 'Enter') {
+ const target = e.target as HTMLInputElement;
+ if (target.value.trim()) {
+ console.log('User input:', target.value);
+ target.value = '';
+ }
+ }
+ }}
+ />
+ ) : (
+ // Choice Options Mode
+ <div className="choice-buttons">
+ <button className="choice-btn" onClick={() => console.log('Choice: Hello?')}>"Hello?"</button>
+ <button className="choice-btn" onClick={() => console.log('Choice: Who are you?')}>"Who are you?"</button>
+ <button className="choice-btn" onClick={() => console.log('Choice: Stay silent')}>(Stay silent)</button>
+ </div>
+ )}
+
+ {/* Toggle Button */}
+ <button
+ className="toggle-input-btn"
+ onClick={toggleShowChoices}
+ title={showChoices ? "Switch to text input" : "Switch to choice options"}
+ >
+ {showChoices ? "⎀" : "≡"}
+ </button>
+ </div>
+ </div>
+
+ {/* Floating Settings Button */}
+ <button className="floating-logout-btn" onClick={() => showSettingsModalStore.set(true)}>
+ <span className="btn-icon">⚙</span>
+ <span className="btn-text">Settings</span>
+ </button>
+
+ {/* Settings Modal */}
+ {showSettingsModal && (
+ <div className="modal-overlay" onClick={() => showSettingsModalStore.set(false)}>
+ <div className="settings-modal" onClick={(e) => e.stopPropagation()}>
+ <div className="modal-header">
+ <h2 className="modal-title">Settings</h2>
+ <button className="modal-close-btn" onClick={() => showSettingsModalStore.set(false)}>
+ ×
+ </button>
+ </div>
+ <div className="modal-content">
+ <div className="settings-section">
+ <h3>Display Options</h3>
+ <div className="setting-item">
+ <label>
+ <input type="checkbox" defaultChecked /> Enable animations
+ </label>
+ </div>
+ </div>
+ <div className="settings-section">
+ <h3>Audio</h3>
+ <div className="setting-item">
+ <label>Master Volume</label>
+ <input type="range" min="0" max="100" defaultValue="75" />
+ </div>
+ </div>
+ </div>
+ <div className="modal-footer">
+ <button className="modal-btn secondary" onClick={() => showSettingsModalStore.set(false)}>
+ Cancel
+ </button>
+ <button className="modal-btn logout" onClick={() => { showSettingsModalStore.set(false); onLogout(); }}>
+ Logout
+ </button>
+ </div>
+ </div>
+ </div>
+ )}
+ </div>
+ )
+}
diff --git a/frontend/src/components/VNViewport.tsx b/frontend/src/components/VNViewport.tsx
new file mode 100644
index 0000000..fe01264
--- /dev/null
+++ b/frontend/src/components/VNViewport.tsx
@@ -0,0 +1,22 @@
+import React from 'react'
+
+type Props = {
+ bgSrc?: string
+ children?: React.ReactNode
+}
+
+export const VNViewport: React.FC<Props> = ({ bgSrc = '/bg.jpg', children }) => {
+ return (
+ <div className="viewport">
+ <div className="viewport-inner">
+ <div className="bg">
+ {bgSrc && <img src={bgSrc} alt="Background" />}
+ </div>
+ {children}
+ </div>
+ <div className="scanlines" aria-hidden="true" />
+ <div className="crt-glow" aria-hidden="true" />
+ <div className="dither-pattern" aria-hidden="true" />
+ </div>
+ )
+}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
new file mode 100644
index 0000000..ca5a10c
--- /dev/null
+++ b/frontend/src/main.tsx
@@ -0,0 +1,10 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import App from './App'
+import './styles/pc98.css'
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+ <React.StrictMode>
+ <App />
+ </React.StrictMode>
+)
diff --git a/frontend/src/services/ws.ts b/frontend/src/services/ws.ts
new file mode 100644
index 0000000..7d10512
--- /dev/null
+++ b/frontend/src/services/ws.ts
@@ -0,0 +1,69 @@
+// Minimal WS client with auto-reconnect for Rust backend interoperability.
+// Point URL to your Rust server: ws://localhost:8080/ws (example)
+
+type Listener = (data: any) => void
+
+export class VNWebSocket {
+ private url: string
+ private ws: WebSocket | null = null
+ private reconnectDelay = 1000
+ private maxReconnectDelay = 8000
+ private shouldReconnect = true
+ private listeners: Record<string, Listener[]> = {}
+
+ constructor(url: string) {
+ this.url = url
+ }
+
+ on(event: 'open' | 'close' | 'error' | 'message', cb: Listener) {
+ if (!this.listeners[event]) this.listeners[event] = []
+ this.listeners[event].push(cb)
+ }
+
+ private emit(event: string, data?: any) {
+ ;(this.listeners[event] || []).forEach(cb => cb(data))
+ }
+
+ connect() {
+ this.ws = new WebSocket(this.url)
+
+ this.ws.addEventListener('open', () => {
+ this.emit('open')
+ this.reconnectDelay = 1000
+ })
+
+ this.ws.addEventListener('message', (evt) => {
+ try {
+ const parsed = JSON.parse(evt.data)
+ this.emit('message', parsed)
+ } catch {
+ this.emit('message', evt.data)
+ }
+ })
+
+ this.ws.addEventListener('close', () => {
+ this.emit('close')
+ if (this.shouldReconnect) {
+ setTimeout(() => this.connect(), this.reconnectDelay)
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay)
+ }
+ })
+
+ this.ws.addEventListener('error', (e) => {
+ this.emit('error', e)
+ this.ws?.close()
+ })
+ }
+
+ send(data: any) {
+ const payload = typeof data === 'string' ? data : JSON.stringify(data)
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
+ this.ws.send(payload)
+ }
+ }
+
+ close() {
+ this.shouldReconnect = false
+ this.ws?.close()
+ }
+}
diff --git a/frontend/src/stores/index.ts b/frontend/src/stores/index.ts
new file mode 100644
index 0000000..58f461c
--- /dev/null
+++ b/frontend/src/stores/index.ts
@@ -0,0 +1,94 @@
+import { atom } from 'nanostores'
+import { ChatMessage, Choice } from '../types'
+
+// Authentication state
+export const isLoggedInStore = atom<boolean>(false)
+
+// VN Interface state
+export const isStandbyStore = atom<boolean>(false)
+export const currentTimeStore = atom<Date>(new Date())
+export const weatherStore = atom<string>('Tokyo - Sunny 22°C')
+export const showChoicesStore = atom<boolean>(false)
+export const showSettingsModalStore = atom<boolean>(false)
+export const isVisibleStore = atom<boolean>(false)
+export const yenBalanceStore = atom<number>(15000)
+
+// VN Page state
+export const loadingStore = atom<boolean>(true)
+export const loadingCompleteStore = atom<boolean>(false)
+export const messagesStore = atom<ChatMessage[]>([
+ { id: 'm1', role: 'assistant', content: 'A warm CRT glow fills the room. A figure turns towards you...' },
+])
+export const choicesStore = atom<Choice[]>([
+ { id: 'greet', label: '"Hello?"' },
+ { id: 'who', label: '"Who are you?"' },
+ { id: 'silence', label: '(Stay silent)' },
+])
+export const statusStore = atom<string>('OFFLINE')
+export const nameStore = atom<string>('???')
+export const backgroundStore = atom<string>('/__gaogao__56242cbde8f18ac64522e410bad04e68_waifu2x_art_noise2.png')
+export const locationStore = atom<string>('Tokyo')
+export const configModalOpenStore = atom<boolean>(false)
+export const skipIntroStore = atom<boolean>(
+ (() => {
+ const saved = localStorage.getItem('skipIntro')
+ return saved === 'true'
+ })()
+)
+
+// Actions
+export const login = () => {
+ isLoggedInStore.set(true)
+}
+
+export const logout = () => {
+ isLoggedInStore.set(false)
+}
+
+export const toggleStandby = () => {
+ isStandbyStore.set(!isStandbyStore.get())
+}
+
+export const toggleShowChoices = () => {
+ showChoicesStore.set(!showChoicesStore.get())
+}
+
+export const updateTime = () => {
+ currentTimeStore.set(new Date())
+}
+
+export const addMessage = (message: ChatMessage) => {
+ messagesStore.set([...messagesStore.get(), message])
+}
+
+export const setChoices = (choices: Choice[]) => {
+ choicesStore.set(choices)
+}
+
+export const clearChoices = () => {
+ choicesStore.set([])
+}
+
+export const setBackground = (src: string) => {
+ backgroundStore.set(src)
+}
+
+export const setName = (name: string) => {
+ nameStore.set(name)
+}
+
+export const setStatus = (status: string) => {
+ statusStore.set(status)
+}
+
+export const setSkipIntro = (skip: boolean) => {
+ skipIntroStore.set(skip)
+ localStorage.setItem('skipIntro', skip.toString())
+}
+
+export const setLoadingComplete = (complete: boolean) => {
+ loadingCompleteStore.set(complete)
+ if (complete) {
+ loadingStore.set(false)
+ }
+} \ No newline at end of file
diff --git a/frontend/src/styles/pc98.css b/frontend/src/styles/pc98.css
new file mode 100644
index 0000000..63e1996
--- /dev/null
+++ b/frontend/src/styles/pc98.css
@@ -0,0 +1,4353 @@
+@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Dela+Gothic+One&family=Coral+Pixels&family=Notable&family=Noto+Serif+JP:wght@300;400;700;900&display=swap');
+
+:root {
+ --bg: #000033;
+ --bg-gradient: linear-gradient(180deg, #000033 0%, #000066 100%);
+ --fg: #ffffff;
+ --accent: #ff6699;
+ --accent2: #66ccff;
+ --accent3: #ffcc66;
+ --text-box-bg: rgba(0, 0, 51, 0.9);
+ --text-box-border: #66ccff;
+ --interface-border: #000000;
+ --name-bg: #ff6699;
+ --name-fg: #ffffff;
+ --choice-bg: rgba(0, 0, 51, 0.8);
+ --choice-border: #66ccff;
+ --scanline-opacity: 0.1;
+ --shadow: rgba(0, 0, 0, 0.7);
+ --text-shadow: rgba(0, 0, 0, 0.8);
+}
+
+* { box-sizing: border-box; }
+html, body, #root { height: 100%; }
+
+body {
+ margin: 0;
+ background: #000000;
+ color: var(--fg);
+ font-family: 'MS Gothic', 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', 'メイリオ', Meiryo, 'Courier New', monospace;
+ font-weight: 400;
+ font-size: 16px;
+}
+
+/* Remove app-root since we're using fixed positioning */
+
+/* Main interface container with fixed pillar borders */
+.screen {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ display: flex;
+ background: var(--bg);
+ overflow: hidden;
+}
+
+/* Left pillar border with character image */
+.left-pillar {
+ width: 200px;
+ height: 100vh;
+ background: linear-gradient(180deg, #000000, #000000, #000000);
+ border-right: 4px solid var(--interface-border);
+ position: relative;
+ flex-shrink: 0;
+ box-shadow: inset -4px 0 8px rgba(0,0,0,0.2);
+}
+
+/* Right pillar border with character image */
+.right-pillar {
+ width: 200px;
+ height: 100vh;
+ background: linear-gradient(180deg, #000000, #000000, #000000);
+ border-left: 4px solid var(--interface-border);
+ position: relative;
+ flex-shrink: 0;
+ box-shadow: inset 4px 0 8px rgba(0,0,0,0.2);
+}
+
+/* Character images */
+.pillar-image {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 160px;
+ height: 240px;
+ background: linear-gradient(180deg, #000000, #000000);
+ border: 3px solid var(--interface-border);
+ border-radius: 8px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ font-family: 'MS Gothic', monospace;
+ font-size: 11px;
+ color: #555;
+ box-shadow:
+ inset 0 0 15px rgba(0,0,0,0.2),
+ 0 4px 8px rgba(0,0,0,0.3);
+ text-align: center;
+ line-height: 1.3;
+}
+
+/* Add character silhouette */
+.pillar-image::before {
+ content: '';
+ width: 80px;
+ height: 120px;
+ background: linear-gradient(180deg,
+ #999 0%,
+ #777 20%,
+ #666 40%,
+ #555 60%,
+ #777 80%,
+ #999 100%);
+ border-radius: 40px 40px 20px 20px;
+ margin-bottom: 8px;
+ box-shadow: inset 0 0 10px rgba(0,0,0,0.4);
+}
+
+.left-pillar .pillar-image {
+ background: linear-gradient(135deg, #000000, #000000, #000000);
+}
+
+.right-pillar .pillar-image {
+ background: linear-gradient(225deg, #000000, #000000, #000000);
+}
+
+/* Bottom corner UI elements */
+.pillar-bottom-section {
+ position: absolute;
+ bottom: 20px;
+ width: 180px;
+ left: 10px;
+ background: linear-gradient(180deg, #000000, #000000, #000000);
+ border: 3px solid var(--interface-border);
+ border-radius: 6px;
+ padding: 12px;
+ box-shadow:
+ inset 0 0 8px rgba(0,0,0,0.2),
+ 0 3px 6px rgba(0,0,0,0.3);
+}
+
+.pillar-bottom-section::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ pointer-events: none;
+}
+
+/* Button grid in left pillar */
+.pillar-buttons {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 6px;
+ position: relative;
+ z-index: 1;
+}
+
+.pillar-btn {
+ appearance: none;
+ background: linear-gradient(180deg, #000000, #000000, #000000);
+ border: 2px outset #000000;
+ color: #ffffff;
+ font-family: 'MS Gothic', monospace;
+ font-size: 10px;
+ font-weight: bold;
+ padding: 8px 4px;
+ text-align: center;
+ cursor: pointer;
+ box-shadow: inset 0 0 4px rgba(255,255,255,0.3);
+ text-shadow: 1px 1px 0 rgba(0,0,0,0.5);
+}
+
+.pillar-btn:hover {
+ background: linear-gradient(180deg, #000000, #000000, #000000);
+ border: 2px inset #000000;
+}
+
+.pillar-btn:active {
+ background: linear-gradient(180deg, #000000, #000000, #000000);
+ box-shadow: inset 2px 2px 4px rgba(0,0,0,0.3);
+}
+
+/* Digital display in right pillar */
+.pillar-display {
+ background: #000000;
+ border: 3px solid #ffffff;
+ border-radius: 4px;
+ padding: 8px;
+ font-family: 'Courier New', monospace;
+ color: #00ff00;
+ text-shadow: 0 0 8px #00ff00;
+ font-size: 14px;
+ font-weight: bold;
+ letter-spacing: 1px;
+ text-align: center;
+ box-shadow: 0 0 4px rgba(0,255,0,0.3);
+}
+
+.digital-clock {
+ margin-bottom: 8px;
+ font-size: 16px;
+}
+
+.yen-counter {
+ font-size: 12px;
+ color: #ffff00;
+ text-shadow: 0 0 6px #ffff00;
+}
+
+.yen-counter::before {
+ content: '¥ ';
+ color: #ffffff;
+ text-shadow: 0 0 4px #ffffff;
+}
+
+/* Main content area - much wider */
+.screen-content {
+ flex: 1;
+ display: grid;
+ grid-template-rows: auto 1fr auto auto auto;
+ gap: 0;
+ background: #000000;
+ position: relative;
+ min-width: 0;
+ max-width: none;
+ height: 100vh;
+}
+
+/* Header and footer styling */
+.header-footer-border {
+ position: relative;
+ background: linear-gradient(180deg, #000000, #000000, #000000);
+ border: 2px solid var(--interface-border);
+}
+
+.header-footer-border::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 100%;
+}
+
+.screen-header {
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 16px;
+ background: linear-gradient(180deg, #000000, #000000);
+ border-bottom: 2px solid var(--interface-border);
+ color: #ffffff;
+ font-family: 'Sylfaen', serif;
+ font-weight: bold;
+ font-size: 14px;
+}
+
+/* Header brand: origami dragon mark + word */
+.screen-header .brand {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ color: #ffffff; /* white lineart + text */
+}
+
+/* (brand text removed) */
+
+.screen-header .brand-mark {
+ transition: transform 150ms ease-out;
+ opacity: 1;
+ will-change: transform, filter;
+}
+
+.screen-header .brand:hover .brand-mark {
+ animation: logo-glitch 700ms steps(8, end) infinite;
+ opacity: 1;
+}
+
+/* (brand text removed) */
+
+/* (brand text animation removed) */
+
+.screen-footer {
+ height: 24px;
+ display: flex;
+ align-items: center;
+ padding: 0 16px;
+ background: linear-gradient(180deg, #000000, #000000);
+ border-top: 2px solid var(--interface-border);
+ color: #ffffff;
+ font-family: 'MS Gothic', monospace;
+ font-size: 12px;
+}
+
+.vn-text-box {
+ background: var(--text-box-bg);
+ border: 2px solid var(--text-box-border);
+ backdrop-filter: blur(2px);
+ -webkit-backdrop-filter: blur(2px);
+}
+
+.topbar {
+ display: none;
+}
+
+.viewport {
+ position: relative;
+ overflow: hidden;
+ display: grid;
+ align-items: stretch;
+ height: 100%;
+}
+.viewport-inner { position: relative; inset: 0; }
+.bg { position: absolute; inset: 0; }
+.bg img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ object-position: center 25%;
+ image-rendering: pixelated;
+}
+
+/* PC-98 color palette overlay */
+.bg::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: linear-gradient(
+ 45deg,
+ rgba(255, 0, 255, 0.05) 0%,
+ rgba(0, 255, 255, 0.03) 25%,
+ rgba(255, 255, 0, 0.02) 50%,
+ rgba(255, 0, 0, 0.03) 75%,
+ rgba(0, 0, 255, 0.05) 100%
+ );
+ mix-blend-mode: overlay;
+ pointer-events: none;
+}
+
+.ui-layer { position: relative; inset: 0; display: contents; }
+
+.scanlines {
+ pointer-events: none;
+ position: absolute;
+ inset: 0;
+ mix-blend-mode: multiply;
+}
+.crt-glow {
+ pointer-events: none;
+ position: absolute;
+ inset: -2%;
+ background: radial-gradient(ellipse at center, rgba(255,102,153,0.08), rgba(0,0,0,0) 60%);
+ filter: blur(4px);
+}
+
+/* PC-98 characteristic dithering pattern */
+.dither-pattern {
+ pointer-events: none;
+ position: absolute;
+ inset: 0;
+ opacity: 0.08;
+ background-image:
+ radial-gradient(circle at 1px 1px, rgba(255,255,255,0.8) 0.5px, transparent 0),
+ radial-gradient(circle at 3px 3px, rgba(0,0,0,0.3) 0.5px, transparent 0);
+ background-size: 6px 6px, 4px 4px;
+ mix-blend-mode: overlay;
+}
+
+/* Additional PC-98 color quantization effect */
+.viewport::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ mix-blend-mode: color-burn;
+ pointer-events: none;
+ z-index: 1;
+}
+
+/* Add retro CRT curvature effect */
+.screen::before {
+ content: '';
+ pointer-events: none;
+ position: absolute;
+ inset: -5px;
+ background: radial-gradient(
+ ellipse at center,
+ transparent 50%,
+ rgba(0,0,0,0.1) 80%,
+ rgba(0,0,0,0.3) 100%
+ );
+ border-radius: 8px;
+}
+
+.dialogue {
+ position: absolute;
+ bottom: 120px;
+ left: 16px;
+ right: 16px;
+ height: 140px;
+ padding: 20px 24px 20px 24px;
+ display: grid;
+ align-items: start;
+ font-size: clamp(14px, 2.4vmin, 18px);
+ line-height: 1.6;
+ background: var(--text-box-bg);
+ border: 3px solid var(--text-box-border);
+ border-radius: 8px;
+ box-shadow: 0 4px 20px var(--shadow);
+ z-index: 10;
+}
+.nameplate {
+ position: absolute;
+ top: -16px;
+ left: 24px;
+ padding: 4px 16px;
+ background: var(--name-bg);
+ color: var(--name-fg);
+ font-size: clamp(12px, 1.8vmin, 14px);
+ font-weight: bold;
+ border: 2px solid var(--text-box-border);
+ border-radius: 4px;
+ text-shadow: 1px 1px 2px var(--text-shadow);
+}
+.dialogue-text {
+ white-space: pre-wrap;
+ color: var(--fg);
+ text-shadow: 1px 1px 2px var(--text-shadow);
+ font-family: 'MS Gothic', monospace;
+ font-weight: 400;
+ letter-spacing: 0.5px;
+}
+
+/* Text input area */
+.text-input-area {
+ position: absolute;
+ bottom: 65px;
+ left: 16px;
+ right: 16px;
+ z-index: 5;
+}
+
+.text-input {
+ width: 100%;
+ height: 44px;
+ padding: 12px 16px;
+ background: var(--text-box-bg);
+ border: 3px solid var(--text-box-border);
+ border-radius: 6px;
+ color: var(--fg);
+ font-family: 'MS Gothic', monospace;
+ font-size: clamp(13px, 2.0vmin, 16px);
+ backdrop-filter: blur(2px);
+ -webkit-backdrop-filter: blur(2px);
+ box-shadow: 0 2px 8px var(--shadow);
+ text-shadow: 1px 1px 2px var(--text-shadow);
+}
+
+.text-input::placeholder {
+ color: rgba(255, 255, 255, 0.6);
+ font-style: italic;
+}
+
+.text-input:focus {
+ outline: none;
+ border-color: var(--accent);
+ box-shadow: 0 0 8px rgba(255, 102, 153, 0.4);
+}
+.choice-item {
+ appearance: none;
+ border: 2px solid var(--choice-border);
+ background: var(--choice-bg);
+ color: var(--fg);
+ padding: 12px 20px;
+ text-align: left;
+ font-family: 'MS Gothic', monospace;
+ font-size: clamp(13px, 2.0vmin, 16px);
+ font-weight: normal;
+ letter-spacing: 0.5px;
+ border-radius: 6px;
+ backdrop-filter: blur(2px);
+ -webkit-backdrop-filter: blur(2px);
+ text-shadow: 1px 1px 2px var(--text-shadow);
+ transition: all 0.2s ease;
+}
+.choice-item:hover {
+ background: var(--accent);
+ border-color: var(--accent);
+ color: #ffffff;
+ cursor: pointer;
+ transform: translateX(4px);
+ box-shadow: 0 2px 12px rgba(255, 102, 153, 0.4);
+}
+.choice-item:active {
+ transform: translateX(2px);
+ box-shadow: 0 1px 6px rgba(255, 102, 153, 0.6);
+}
+
+.bottombar {
+ position: absolute;
+ bottom: 26px;
+ right: 24px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ background: transparent;
+ border: none;
+ height: auto;
+ padding: 0;
+}
+.mini-btn {
+ appearance: none;
+ border: 2px solid var(--choice-border);
+ background: var(--choice-bg);
+ color: var(--fg);
+ font-family: 'MS Gothic', monospace;
+ font-size: clamp(10px, 1.6vmin, 12px);
+ font-weight: normal;
+ padding: 6px 12px;
+ min-width: 50px;
+ border-radius: 4px;
+ backdrop-filter: blur(2px);
+ -webkit-backdrop-filter: blur(2px);
+ text-shadow: 1px 1px 2px var(--text-shadow);
+}
+.mini-btn:hover {
+ background: var(--accent2);
+ border-color: var(--accent2);
+ cursor: pointer;
+ transform: translateY(-1px);
+ box-shadow: 0 2px 8px rgba(102, 204, 255, 0.3);
+}
+.mini-btn:active {
+ transform: translateY(0);
+ box-shadow: none;
+}
+.spacer { display: none; }
+.location-display {
+ font-family: 'MS Gothic', monospace;
+ font-size: clamp(9px, 1.2vmin, 10px);
+ color: var(--accent3);
+ font-weight: bold;
+ text-shadow: 1px 1px 2px var(--text-shadow);
+}
+
+/* Weather display in viewport */
+.weather-display {
+ position: absolute;
+ top: 18px;
+ right: 16px;
+ padding: 8px 12px;
+ background: rgba(0, 0, 51, 0.8);
+ border: 2px solid var(--interface-border);
+ border-radius: 4px;
+ color: var(--accent2);
+ font-family: 'MS Gothic', monospace;
+ font-size: clamp(11px, 1.6vmin, 13px);
+ font-weight: bold;
+ text-shadow: 1px 1px 2px var(--text-shadow);
+ backdrop-filter: blur(2px);
+ -webkit-backdrop-filter: blur(2px);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
+ z-index: 20;
+}
+
+/* Add PC-98 style text selection */
+::selection {
+ background: var(--accent);
+ color: #000000;
+}
+
+/* Enhance button focus states */
+button:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 1px;
+}
+
+/* Add subtle animation to LEDs */
+.led {
+ animation: led-pulse 2s ease-in-out infinite alternate;
+}
+
+@keyframes led-pulse {
+ from { opacity: 0.8; }
+ to { opacity: 1; }
+}
+
+/* Add PC-98 style loading animation */
+.loading {
+ position: relative;
+}
+
+.loading::after {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 20px;
+ height: 20px;
+ margin: -10px 0 0 -10px;
+ border: 2px solid var(--accent);
+ border-radius: 50%;
+ border-top-color: transparent;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+/* Modern Landing Page Styles */
+.modern-landing-page {
+ position: fixed;
+ top: 120px;
+ left: 0;
+ width: 100vw;
+ height: calc(100vh - 120px);
+ background: linear-gradient(180deg, #0a0a0f 0%, #0c0d16 100%);
+ overflow: hidden;
+ opacity: 0;
+ transition: opacity 1s ease-in;
+}
+
+.modern-landing-page.manga-style { background: none; }
+
+/* Retro-futuristic full-page speedlines */
+.rf-page-bg { position: absolute; inset: 0; z-index: 0; overflow: hidden; }
+.rf-page-speedlines { position: absolute; inset: -40% -40%; opacity: 0.12; mix-blend-mode: screen; }
+.rf-page-speedlines.layer-a { background: repeating-linear-gradient(80deg, #66ccff22 0 6px, transparent 6px 16px); animation: rf-page-move 12s linear infinite; }
+.rf-page-speedlines.layer-b { background: repeating-linear-gradient(100deg, #ff669922 0 8px, transparent 8px 22px); animation: rf-page-move 18s linear infinite; }
+@keyframes rf-page-move { from { transform: translateX(0); } to { transform: translateX(-240px); } }
+
+.modern-landing-page.fade-in {
+ opacity: 1;
+}
+
+.modern-landing-page.hidden {
+ opacity: 0;
+ pointer-events: none;
+}
+
+/* Floating Header Bar */
+.floating-header {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ width: 100vw;
+ height: 120px;
+ background: rgba(0, 0, 0, 0.8);
+ backdrop-filter: blur(15px);
+ border-bottom: 3px solid rgba(255, 255, 255, 0.8);
+ z-index: 100;
+ display: flex;
+ align-items: center;
+ padding: 20px 40px;
+ opacity: 0;
+ transition: opacity 1s ease-in;
+}
+
+.floating-header.fade-in {
+ opacity: 1;
+}
+
+.floating-header.hidden {
+ opacity: 0;
+ pointer-events: none;
+}
+
+/* Light mode overrides for landing header */
+/* (Reverted site-wide light overrides; counter handles its own appearance) */
+
+.header-content {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+}
+
+/* Brand in the floating header (top-left) */
+.floating-header .brand {
+ position: absolute;
+ top: 40px;
+ left: 40px;
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ color: #ffffff; /* white lineart + text */
+ z-index: 21;
+}
+
+/* (brand text removed) */
+
+.floating-header .brand-mark {
+ transition: transform 150ms ease-out;
+ opacity: 1;
+ will-change: transform, filter;
+}
+
+.floating-header .brand:hover .brand-mark {
+ animation: logo-glitch 700ms steps(8, end) infinite;
+ opacity: 1;
+}
+
+/* Glitch effect for logo mark on hover */
+@keyframes logo-glitch {
+ 0% { transform: translate(0, 0); filter: none; }
+ 10% { transform: translate(-1px, 0.5px); filter: drop-shadow(-1px 0 red) drop-shadow(1px 0 cyan); }
+ 20% { transform: translate(1px, -0.5px); filter: drop-shadow(-1px 0 red) drop-shadow(1px 0 cyan); }
+ 30% { transform: translate(0.5px, 1px); filter: drop-shadow(-2px 0 magenta) drop-shadow(2px 0 cyan); }
+ 40% { transform: translate(-0.5px, -1px); filter: drop-shadow(-1px 0 blue) drop-shadow(1px 0 red); }
+ 50% { transform: translate(0, 0); filter: none; }
+ 60% { transform: translate(1px, 0.5px); filter: drop-shadow(-1px 0 magenta) drop-shadow(1px 0 lime); }
+ 70% { transform: translate(-1px, -0.5px); filter: drop-shadow(-2px 0 red) drop-shadow(2px 0 cyan); }
+ 80% { transform: translate(0.5px, -1px); filter: drop-shadow(-1px 0 magenta) drop-shadow(1px 0 cyan); }
+ 90% { transform: translate(-0.5px, 1px); filter: drop-shadow(-1px 0 red) drop-shadow(1px 0 cyan); }
+ 100% { transform: translate(0, 0); filter: none; }
+}
+
+/* Respect reduced motion preferences */
+@media (prefers-reduced-motion: reduce) {
+ .screen-header .brand:hover .brand-mark,
+ .floating-header .brand:hover .brand-mark {
+ animation: none;
+ filter: none;
+ }
+}
+
+/* (brand text removed) */
+
+.header-center {
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ z-index: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.heart-logo.header-no-rays {
+ width: 100%;
+ height: 100%;
+ position: relative;
+}
+
+.heart-logo.header-no-rays .heart-logo-content {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+}
+
+.heart-logo.header-no-rays .heart-logo-rays {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 1;
+}
+
+.heart-logo.header-no-rays .heart-logo-rays-svg {
+ width: 100%;
+ height: 100%;
+}
+
+.heart-logo.header-no-rays .heart-logo-outline {
+ position: relative;
+ z-index: 2;
+ width: 60px;
+ height: 60px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.heart-logo.header-no-rays .heart-logo-svg {
+ width: 100%;
+ height: 100%;
+}
+
+/* Video Background */
+.video-background, .ascii-background {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ overflow: hidden;
+ z-index: 1;
+}
+
+.ascii-background {
+ background: rgba(0, 0, 0, 0.3);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.ascii-art {
+ font-family: 'Courier New', 'MS Gothic', monospace;
+ font-size: clamp(6px, 1.2vmin, 12px);
+ line-height: 1.1;
+ text-align: center;
+ margin: 0;
+ padding: 20px;
+ position: relative;
+}
+
+.ascii-line {
+ display: block;
+ white-space: pre;
+}
+
+.ascii-char {
+ display: inline-block;
+ color: transparent;
+ background: linear-gradient(
+ var(--gradient-direction, 45deg),
+ #ff6b6b,
+ #4ecdc4,
+ #45b7d1,
+ #96ceb4,
+ #feca57,
+ #ff9ff3,
+ #54a0ff,
+ #5f27cd
+ );
+ background-size: 600% 600%;
+ background-clip: text;
+ -webkit-background-clip: text;
+ animation: gradient-shift 4s ease-in-out infinite;
+ animation-delay: var(--delay, 0s);
+ transition: transform 0.05s ease-out;
+ transform-origin: center;
+ position: relative;
+}
+
+.ascii-char::before {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 1ch;
+ height: 1.1em;
+ transform: translate(-50%, -50%);
+ pointer-events: none;
+}
+
+@keyframes gradient-shift {
+ 0% {
+ background-position: 0% 50%;
+ }
+ 25% {
+ background-position: 100% 0%;
+ }
+ 50% {
+ background-position: 100% 100%;
+ }
+ 75% {
+ background-position: 0% 100%;
+ }
+ 100% {
+ background-position: 0% 50%;
+ }
+}
+
+/* Large Static Text */
+.large-text {
+ position: absolute;
+ left: 50%;
+ top: 20%;
+ transform: translate(-50%, -50%);
+ font-family: 'Inter', 'SF Pro Display', system-ui, sans-serif;
+ font-size: clamp(72px, 10vw, 180px);
+ font-weight: 700;
+ color: #ffffff;
+ white-space: nowrap;
+ z-index: 3;
+ pointer-events: none;
+ line-height: 1.1;
+ text-align: center;
+ letter-spacing: -0.02em;
+ background: linear-gradient(135deg, #ffffff 0%, #f0f0f0 50%, #e0e0e0 100%);
+ background-clip: text;
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ text-shadow: 0 4px 20px rgba(255, 255, 255, 0.3);
+ filter: drop-shadow(0 0 30px rgba(255, 255, 255, 0.2));
+ overflow: hidden;
+}
+
+/* Subtitle Text */
+.subtitle-text {
+ position: absolute;
+ left: 50%;
+ top: 40%;
+ transform: translate(-50%, -50%);
+ font-family: 'Inter', 'SF Pro Display', system-ui, sans-serif;
+ font-size: clamp(16px, 2.5vmin, 24px);
+ font-weight: 400;
+ color: rgba(255, 255, 255, 0.8);
+ white-space: nowrap;
+ z-index: 3;
+ pointer-events: none;
+ text-align: center;
+ letter-spacing: 0.02em;
+ opacity: 0.9;
+ animation: subtleFadeIn 2s ease-out 0.5s both;
+}
+
+@keyframes subtleFadeIn {
+ from {
+ opacity: 0;
+ transform: translate(-50%, calc(-50% + 20px));
+ }
+ to {
+ opacity: 0.9;
+ transform: translate(-50%, -50%);
+ }
+}
+
+/* Manga Panel Layout */
+.manga-background {
+ position: absolute;
+ inset: 0;
+ overflow: hidden;
+ z-index: 1;
+}
+
+/* Animated speed lines radiating from center */
+.speed-lines {
+ position: absolute;
+ inset: 0;
+ background: repeating-linear-gradient(
+ 0deg,
+ transparent,
+ transparent 10px,
+ rgba(0, 0, 0, 0.03) 10px,
+ rgba(0, 0, 0, 0.03) 11px
+ );
+ animation: speed-lines-move 0.5s linear infinite;
+}
+
+@keyframes speed-lines-move {
+ from { transform: translateY(0); }
+ to { transform: translateY(11px); }
+}
+
+/* Halftone dot overlay for manga texture */
+.halftone-overlay {
+ position: absolute;
+ inset: 0;
+ background-image:
+ radial-gradient(circle, rgba(0, 0, 0, 0.15) 1px, transparent 1px);
+ background-size: 4px 4px;
+ opacity: 0.2;
+ pointer-events: none;
+}
+
+/* Focus lines emanating from center */
+.focus-lines {
+ position: absolute;
+ inset: 0;
+ background:
+ repeating-conic-gradient(
+ from 0deg at 50% 50%,
+ transparent 0deg,
+ transparent 5deg,
+ rgba(0, 0, 0, 0.05) 5deg,
+ rgba(0, 0, 0, 0.05) 6deg
+ );
+ animation: focus-pulse 3s ease-in-out infinite;
+}
+
+@keyframes focus-pulse {
+ 0%, 100% { opacity: 0.3; }
+ 50% { opacity: 0.6; }
+}
+
+/* Main manga panel container */
+.manga-panel-container {
+ position: relative;
+ z-index: 2;
+ display: grid;
+ grid-template-columns: 1.2fr 0.8fr;
+ grid-template-rows: 1fr auto;
+ gap: 20px;
+ padding: 40px;
+ height: 100%;
+ max-width: 1600px;
+ margin: 0 auto;
+}
+
+/* Individual manga panel base styles */
+.manga-panel {
+ position: relative;
+ background: #ffffff;
+ border: 4px solid #000000;
+ box-shadow:
+ inset 0 0 20px rgba(0, 0, 0, 0.1),
+ 8px 8px 0px rgba(0, 0, 0, 0.3),
+ 12px 12px 0px rgba(0, 0, 0, 0.15);
+ overflow: hidden;
+ transform: rotate(-0.5deg);
+ transition: transform 0.3s ease;
+}
+
+.manga-panel:hover {
+ transform: rotate(0deg) scale(1.02);
+ z-index: 10;
+}
+
+/* Panel borders with variable thickness */
+.panel-border {
+ position: absolute;
+ inset: 0;
+ border: 6px solid #000000;
+ pointer-events: none;
+ z-index: 10;
+}
+
+.panel-border::before {
+ content: '';
+ position: absolute;
+ inset: 6px;
+ border: 2px solid #000000;
+}
+
+.panel-border::after {
+ content: '';
+ position: absolute;
+ inset: 12px;
+ border: 1px solid rgba(0, 0, 0, 0.3);
+}
+
+.panel-content {
+ position: relative;
+ padding: 40px;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ z-index: 5;
+}
+
+/* Hero Panel - Main focus area */
+.manga-panel-hero {
+ grid-column: 1;
+ grid-row: 1;
+ transform: rotate(0.8deg);
+ background: radial-gradient(ellipse at top left, #ffffff 0%, #f8f8f8 100%);
+}
+
+.manga-title {
+ font-family: 'Dela Gothic One', 'Noto Sans JP', sans-serif;
+ text-align: center;
+ margin: 0;
+ position: relative;
+ z-index: 6;
+}
+
+.title-kanji {
+ display: block;
+ font-size: clamp(80px, 12vw, 150px);
+ font-weight: 900;
+ color: #000000;
+ text-shadow:
+ 4px 4px 0px #ffffff,
+ 8px 8px 0px rgba(0, 0, 0, 0.3);
+ line-height: 0.9;
+ letter-spacing: 0.05em;
+ margin-bottom: 10px;
+}
+
+.title-romaji {
+ display: block;
+ font-family: 'Impact', 'Arial Black', sans-serif;
+ font-size: clamp(28px, 4vw, 48px);
+ font-weight: 900;
+ color: #000000;
+ letter-spacing: 0.3em;
+ text-transform: uppercase;
+ border-top: 4px solid #000000;
+ border-bottom: 4px solid #000000;
+ padding: 8px 0;
+ background: linear-gradient(90deg, transparent 0%, #fff 20%, #fff 80%, transparent 100%);
+}
+
+/* Speech Bubble */
+.speech-bubble {
+ position: absolute;
+ bottom: 60px;
+ right: 60px;
+ background: #ffffff;
+ border: 3px solid #000000;
+ border-radius: 20px;
+ padding: 20px 30px;
+ max-width: 300px;
+ box-shadow: 4px 4px 0px rgba(0, 0, 0, 0.2);
+ z-index: 7;
+}
+
+.bubble-text {
+ font-family: 'Noto Sans JP', sans-serif;
+ font-size: clamp(14px, 1.5vw, 18px);
+ font-weight: 600;
+ color: #000000;
+ margin: 0;
+ line-height: 1.6;
+ letter-spacing: 0.02em;
+}
+
+.bubble-tail {
+ position: absolute;
+ bottom: -15px;
+ right: 40px;
+ width: 0;
+ height: 0;
+ border-left: 15px solid transparent;
+ border-right: 15px solid transparent;
+ border-top: 20px solid #000000;
+}
+
+.bubble-tail::before {
+ content: '';
+ position: absolute;
+ bottom: 3px;
+ left: -12px;
+ width: 0;
+ height: 0;
+ border-left: 12px solid transparent;
+ border-right: 12px solid transparent;
+ border-top: 17px solid #ffffff;
+}
+
+/* Action Lines - radiating from center */
+.action-lines {
+ position: absolute;
+ inset: 0;
+ z-index: 1;
+}
+
+.action-line {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 200%;
+ height: 3px;
+ background: linear-gradient(90deg,
+ transparent 0%,
+ rgba(0, 0, 0, 0.3) 50%,
+ transparent 100%
+ );
+ transform-origin: 0% 50%;
+ transform: rotate(var(--angle)) translateX(-50%);
+ animation: action-line-pulse 2s ease-in-out infinite;
+ animation-delay: var(--delay);
+}
+
+@keyframes action-line-pulse {
+ 0%, 100% { opacity: 0.3; width: 180%; }
+ 50% { opacity: 0.7; width: 220%; }
+}
+
+/* Side Panel with ASCII Art */
+.manga-panel-side {
+ grid-column: 2;
+ grid-row: 1;
+ transform: rotate(-1.2deg);
+ background: linear-gradient(135deg, #fafafa 0%, #f0f0f0 100%);
+}
+
+.ascii-art-manga {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+}
+
+.ascii-art-manga .ascii-art {
+ font-family: 'Courier New', monospace;
+ font-size: clamp(6px, 0.8vw, 10px);
+ line-height: 1.1;
+ color: #000000;
+ filter: drop-shadow(2px 2px 0px rgba(0, 0, 0, 0.1));
+}
+
+.ascii-char {
+ display: inline-block;
+ transition: transform 0.05s ease-out;
+ transform-origin: center;
+}
+
+/* Manga SFX Text */
+.sfx-text {
+ position: absolute;
+ font-family: 'Dela Gothic One', 'Noto Sans JP', sans-serif;
+ font-weight: 900;
+ color: #000000;
+ text-shadow:
+ 2px 2px 0px #ffffff,
+ 4px 4px 0px rgba(0, 0, 0, 0.3);
+ z-index: 8;
+ pointer-events: none;
+}
+
+.sfx-1 {
+ bottom: 20px;
+ right: 20px;
+ font-size: clamp(24px, 3vw, 36px);
+ opacity: 0.6;
+ animation: sfx-throb 1.5s ease-in-out infinite;
+}
+
+@keyframes sfx-throb {
+ 0%, 100% { transform: scale(1); }
+ 50% { transform: scale(1.1); }
+}
+
+/* Bottom Panel with CTA */
+.manga-panel-bottom {
+ grid-column: 1 / 3;
+ grid-row: 2;
+ transform: rotate(0.3deg);
+ min-height: 200px;
+ background: linear-gradient(to right, #ffffff 0%, #fafafa 50%, #ffffff 100%);
+}
+
+.manga-panel-bottom .panel-content {
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ gap: 40px;
+}
+
+/* Narrative Box */
+.narrative-box {
+ flex: 1;
+ background: #000000;
+ color: #ffffff;
+ padding: 20px 30px;
+ border: 3px solid #000000;
+ box-shadow: inset 0 0 20px rgba(255, 255, 255, 0.1);
+ position: relative;
+}
+
+.narrative-box::before {
+ content: '';
+ position: absolute;
+ top: -8px;
+ left: 20px;
+ width: 40px;
+ height: 3px;
+ background: #ffffff;
+}
+
+.narrative-text {
+ font-family: 'Noto Serif JP', serif;
+ font-size: clamp(16px, 2vw, 22px);
+ font-style: italic;
+ margin: 0;
+ line-height: 1.7;
+ letter-spacing: 0.05em;
+}
+
+/* Manga CTA Button */
+.manga-cta {
+ position: relative;
+ background: #000000;
+ color: #ffffff;
+ border: 4px solid #000000;
+ font-family: 'Impact', 'Arial Black', sans-serif;
+ font-size: clamp(18px, 2.5vw, 28px);
+ font-weight: 900;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ padding: 20px 50px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ box-shadow:
+ 4px 4px 0px rgba(0, 0, 0, 0.3),
+ 8px 8px 0px rgba(0, 0, 0, 0.15);
+ display: flex;
+ align-items: center;
+ gap: 15px;
+ z-index: 6;
+}
+
+.manga-cta::before {
+ content: '';
+ position: absolute;
+ inset: -8px;
+ border: 2px solid #000000;
+ pointer-events: none;
+}
+
+.manga-cta:hover {
+ transform: translate(2px, 2px);
+ box-shadow:
+ 2px 2px 0px rgba(0, 0, 0, 0.3),
+ 4px 4px 0px rgba(0, 0, 0, 0.15);
+}
+
+.manga-cta:active {
+ transform: translate(4px, 4px);
+ box-shadow:
+ 0px 0px 0px rgba(0, 0, 0, 0.3);
+}
+
+.cta-kaomoji {
+ font-size: clamp(24px, 3vw, 32px);
+ animation: kaomoji-bounce 0.6s ease-in-out infinite;
+}
+
+@keyframes kaomoji-bounce {
+ 0%, 100% { transform: translateY(0); }
+ 50% { transform: translateY(-4px); }
+}
+
+.cta-text {
+ font-size: clamp(18px, 2.5vw, 28px);
+ letter-spacing: 0.15em;
+}
+
+/* Button impact lines */
+.button-impact-lines {
+ position: absolute;
+ inset: -20px;
+ pointer-events: none;
+ z-index: -1;
+}
+
+/* Impact burst effect */
+.impact-burst {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 100px;
+ height: 100px;
+ transform: translate(-50%, -50%);
+ pointer-events: none;
+ z-index: 5;
+}
+
+.burst-line {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 60px;
+ height: 4px;
+ background: linear-gradient(90deg, #000000 0%, transparent 100%);
+ transform-origin: 0% 50%;
+ transform: rotate(var(--burst-angle)) translateX(-30px);
+ opacity: 0.4;
+ animation: burst-expand 1.5s ease-out infinite;
+}
+
+@keyframes burst-expand {
+ 0% { width: 40px; opacity: 0.6; }
+ 100% { width: 80px; opacity: 0; }
+}
+
+/* Floating manga effects */
+.manga-effects {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ z-index: 20;
+}
+
+.effect-star {
+ position: absolute;
+ font-size: clamp(30px, 4vw, 50px);
+ color: #000000;
+ opacity: 0.15;
+ animation: star-twinkle 2s ease-in-out infinite;
+}
+
+.effect-star-1 {
+ top: 10%;
+ right: 15%;
+ animation-delay: 0s;
+}
+
+.effect-star-2 {
+ top: 40%;
+ left: 10%;
+ animation-delay: 0.7s;
+}
+
+.effect-star-3 {
+ bottom: 20%;
+ right: 20%;
+ animation-delay: 1.4s;
+}
+
+@keyframes star-twinkle {
+ 0%, 100% { transform: scale(1) rotate(0deg); opacity: 0.15; }
+ 50% { transform: scale(1.3) rotate(90deg); opacity: 0.3; }
+}
+
+.sfx-floating {
+ top: 15%;
+ left: 50%;
+ transform: translateX(-50%);
+ font-size: clamp(40px, 6vw, 70px);
+ opacity: 0.1;
+ animation: menacing-float 4s ease-in-out infinite;
+}
+
+@keyframes menacing-float {
+ 0%, 100% { transform: translateX(-50%) translateY(0) rotate(-2deg); }
+ 50% { transform: translateX(-50%) translateY(-20px) rotate(2deg); }
+}
+
+/* ========== VN Preview Styles ========== */
+
+/* VN Preview Content Container */
+.vn-preview-content {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: center;
+ height: 100%;
+ padding: 20px;
+ gap: 15px;
+}
+
+/* Character Name Badge */
+.vn-character-name {
+ background: #000000;
+ color: #ffffff;
+ padding: 8px 24px;
+ border: 3px solid #000000;
+ font-family: 'Noto Serif JP', serif;
+ font-size: clamp(16px, 2vw, 20px);
+ font-weight: 700;
+ letter-spacing: 2px;
+ text-transform: uppercase;
+ box-shadow:
+ 0 4px 8px rgba(0, 0, 0, 0.3),
+ inset 0 0 20px rgba(255, 255, 255, 0.1);
+ position: relative;
+ z-index: 10;
+ animation: name-badge-glow 2s ease-in-out infinite;
+}
+
+@keyframes name-badge-glow {
+ 0%, 100% { box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3), inset 0 0 20px rgba(255, 255, 255, 0.1); }
+ 50% { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5), inset 0 0 30px rgba(255, 255, 255, 0.2); }
+}
+
+/* Character Visual Area */
+.vn-character-visual {
+ flex: 1;
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ min-height: 200px;
+}
+
+.character-silhouette {
+ width: clamp(150px, 20vw, 250px);
+ height: clamp(200px, 30vw, 350px);
+ background: linear-gradient(180deg, #1a1a1a 0%, #000000 100%);
+ border: 3px solid #000000;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ box-shadow:
+ 0 8px 16px rgba(0, 0, 0, 0.4),
+ inset 0 0 40px rgba(255, 255, 255, 0.05);
+ animation: silhouette-breathe 3s ease-in-out infinite;
+}
+
+@keyframes silhouette-breathe {
+ 0%, 100% { transform: scale(1); }
+ 50% { transform: scale(1.02); }
+}
+
+.silhouette-glow {
+ position: absolute;
+ inset: -10px;
+ background: radial-gradient(ellipse at center, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
+ border-radius: 8px;
+ animation: glow-pulse 2.5s ease-in-out infinite;
+ pointer-events: none;
+}
+
+@keyframes glow-pulse {
+ 0%, 100% { opacity: 0.3; }
+ 50% { opacity: 0.6; }
+}
+
+.character-placeholder {
+ font-size: clamp(60px, 10vw, 100px);
+ color: #333333;
+ font-weight: 900;
+ text-shadow: 0 4px 8px rgba(0, 0, 0, 0.5);
+ animation: placeholder-float 3s ease-in-out infinite;
+}
+
+@keyframes placeholder-float {
+ 0%, 100% { transform: translateY(0); }
+ 50% { transform: translateY(-10px); }
+}
+
+/* VN Textbox */
+.vn-textbox {
+ width: 100%;
+ background: #000000;
+ border: 4px solid #000000;
+ box-shadow:
+ 0 -4px 12px rgba(0, 0, 0, 0.3),
+ inset 0 0 30px rgba(255, 255, 255, 0.1);
+ position: relative;
+ z-index: 10;
+ min-height: 100px;
+}
+
+.textbox-border {
+ position: absolute;
+ inset: -4px;
+ border: 2px solid #333333;
+ pointer-events: none;
+}
+
+.textbox-content {
+ padding: 20px 25px;
+ position: relative;
+}
+
+.dialogue-text {
+ font-family: 'Noto Serif JP', serif;
+ font-size: clamp(14px, 1.8vw, 18px);
+ line-height: 1.8;
+ color: #ffffff;
+ margin: 0;
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
+ min-height: 40px;
+}
+
+/* Typing Cursor */
+.typing-cursor {
+ display: inline-block;
+ margin-left: 2px;
+ animation: cursor-blink 0.8s steps(2) infinite;
+}
+
+@keyframes cursor-blink {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0; }
+}
+
+/* Continue Indicator */
+.continue-indicator {
+ position: absolute;
+ bottom: 10px;
+ right: 15px;
+ font-size: 20px;
+ color: #ffffff;
+ animation: indicator-bounce 1s ease-in-out infinite;
+}
+
+@keyframes indicator-bounce {
+ 0%, 100% { transform: translateY(0); opacity: 0.7; }
+ 50% { transform: translateY(-5px); opacity: 1; }
+}
+
+/* ========== End VN Preview Styles ========== */
+
+
+.video-background video {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.background-gif, .background-image {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ object-position: center;
+}
+
+.video-placeholder {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(45deg, #1a1a2e 0%, #16213e 50%, #0a0a0f 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 2;
+}
+
+/* ================== Taisho Magazine Cover Layout ================== */
+.taisho-cover {
+ position: absolute;
+ inset: 0;
+ overflow: hidden;
+ color: #1a0f08;
+}
+
+.taisho-cover, .taisho-cover * {
+ box-sizing: border-box;
+}
+
+.taisho-cover {
+ --paper: #f7f1e1;
+ --ink: #1a0f08;
+ --ink-20: rgba(26, 15, 8, 0.12);
+ --crimson: #a83a32;
+ --teal: #2f6b6d;
+ --mustard: #c9a876;
+ --indigo: #2b3a67;
+ --shadow: rgba(26, 15, 8, 0.25);
+}
+
+.taisho-cover .cover-backdrop { display: none; }
+
+.taisho-cover .paper-tone {
+ position: absolute;
+ inset: 0;
+ background:
+ radial-gradient(120% 120% at 100% 0%, rgba(0,0,0,0.04), transparent 60%),
+ radial-gradient(120% 120% at 0% 100%, rgba(0,0,0,0.05), transparent 60%),
+ var(--paper);
+}
+
+/* Ichimatsu (checker) pattern replacing circular waves */
+.taisho-cover .pattern-ichimatsu {
+ position: absolute;
+ inset: 0;
+ opacity: 0.18;
+ background:
+ linear-gradient(45deg, var(--ink-20) 25%, transparent 25%, transparent 75%, var(--ink-20) 75%, var(--ink-20)) 0 0/22px 22px,
+ linear-gradient(45deg, var(--ink-20) 25%, transparent 25%, transparent 75%, var(--ink-20) 75%, var(--ink-20)) 11px 11px/22px 22px;
+ pointer-events: none;
+ mix-blend-mode: multiply;
+}
+
+.taisho-cover .halftone-overlay {
+ position: absolute;
+ inset: 0;
+ background: repeating-linear-gradient(135deg, rgba(26, 15, 8, 0.08) 0 1px, transparent 1px 6px);
+ opacity: 0.25;
+ pointer-events: none;
+}
+
+.taisho-cover .cover-content {
+ position: relative;
+ z-index: 1;
+ height: 100%;
+ display: grid;
+ grid-template-columns: 1fr 220px; /* hero left, masthead right */
+ grid-template-rows: 1fr auto; /* CTA sits below hero */
+ grid-template-areas:
+ "hero masthead"
+ "hero cta";
+ gap: 28px;
+ padding: 40px 48px;
+}
+
+/* Masthead */
+.masthead { grid-area: masthead; display: flex; flex-direction: column; align-items: center; gap: 18px; }
+.masthead-vertical {
+ writing-mode: vertical-rl;
+ text-orientation: mixed;
+ font-family: 'DotGothic16', system-ui, sans-serif;
+ background: linear-gradient(180deg, var(--ink) 0%, #000 100%);
+ color: #fff;
+ border: 6px double #fff;
+ box-shadow: 0 12px 30px var(--shadow);
+ padding: 24px 10px;
+ letter-spacing: 4px;
+}
+.masthead-vertical .jp { font-size: 42px; line-height: 1; }
+.masthead-vertical .en { font-size: 12px; margin-top: 10px; opacity: 0.8; letter-spacing: 2px; }
+.issue-badge {
+ background: var(--mustard);
+ color: #1a0f08;
+ font-family: 'Noto Serif JP', serif;
+ font-weight: 700;
+ padding: 6px 10px;
+ border: 2px solid var(--ink);
+ box-shadow: 2px 2px 0 rgba(26,15,8,0.25);
+}
+
+/* Hero */
+.hero { grid-area: hero; display: flex; align-items: center; justify-content: center; }
+.hero-frame {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ min-height: 420px;
+ background: #fff;
+ /* No panel borders; keep only drop shadow */
+ box-shadow: 0 22px 40px var(--shadow);
+ overflow: hidden;
+}
+.hero-pattern.asanoha {
+ position: absolute; inset: 0; opacity: 0.15;
+ background:
+ repeating-conic-gradient(from 0deg, var(--indigo) 0 6deg, transparent 6deg 12deg),
+ repeating-conic-gradient(from 30deg, var(--teal) 0 6deg, transparent 6deg 12deg);
+ mix-blend-mode: multiply;
+}
+
+/* Seigaiha (wave) pattern overlay */
+.pattern-seigaiha {
+ position: absolute;
+ left: 0; right: 0; bottom: 0;
+ height: 38%;
+ pointer-events: none;
+ opacity: 0.18;
+ mix-blend-mode: multiply;
+ -webkit-mask-image: linear-gradient(to top, rgba(0,0,0,1), rgba(0,0,0,0));
+ mask-image: linear-gradient(to top, rgba(0,0,0,1), rgba(0,0,0,0));
+}
+.pattern-seigaiha path {
+ stroke: rgba(26, 15, 8, 0.22);
+ stroke-width: 1.2;
+ fill: none;
+ vector-effect: non-scaling-stroke;
+}
+.hero-inner { position: relative; z-index: 1; height: 100%; display: flex; flex-direction: column; }
+.hero-textbox { margin-top: auto; }
+
+/* Full-bleed hero fill */
+.hero-inner.hero-fill { position: absolute; inset: 0; padding: 0; display: block; }
+
+/* Art Deco/Nouveau hero content */
+.deco-hero-content {
+ display: grid;
+ grid-template-rows: auto auto auto auto auto auto; /* tighten rows; rf block is self-contained */
+ align-items: start;
+ justify-items: center;
+ padding: 40px 28px;
+ color: var(--ink);
+}
+.deco-headline {
+ font-family: 'Notable', 'Dela Gothic One', sans-serif;
+ font-weight: 700;
+ letter-spacing: 0.2em;
+ font-size: clamp(28px, 4vw, 48px);
+ margin: 0;
+ text-transform: uppercase;
+}
+.deco-divider {
+ width: min(280px, 60%);
+ height: 10px;
+ margin: 14px 0 10px 0;
+ background:
+ linear-gradient(90deg, transparent 0%, var(--mustard) 20%, var(--mustard) 80%, transparent 100%);
+ position: relative;
+}
+.deco-divider::before,
+.deco-divider::after {
+ content: '';
+ position: absolute;
+ top: 50%;
+ width: 10px;
+ height: 10px;
+ border: 2px solid var(--ink);
+ background: #fff;
+ transform: translateY(-50%) rotate(45deg);
+}
+.deco-divider::before { left: -12px; }
+.deco-divider::after { right: -12px; }
+.deco-subtitle {
+ font-family: 'Noto Serif JP', serif;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ font-size: clamp(14px, 2.2vw, 18px);
+ margin: 6px 0 8px 0;
+}
+.deco-blurb {
+ font-family: 'Inter', system-ui, sans-serif;
+ font-size: clamp(12px, 1.8vw, 16px);
+ line-height: 1.6;
+ text-align: center;
+ max-width: 52ch;
+ margin: 0;
+}
+
+/* ASCII art in hero */
+.ascii-hero {
+ display: inline-block;
+ background: #fff;
+ border: 3px double var(--ink);
+ box-shadow: 0 8px 16px rgba(26,15,8,0.12);
+ padding: 8px;
+}
+.ascii-hero-art {
+ margin: 0;
+ white-space: pre;
+ font-family: 'Courier New', 'MS Gothic', monospace;
+ font-size: clamp(6px, 0.95vmin, 11px);
+ line-height: 1.05;
+ color: #000;
+ text-align: left;
+}
+
+/* Retro-futuristic racing hero */
+.rf-hero {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ background:
+ linear-gradient(180deg, #0a0a0f 0%, #0c0d16 100%);
+ border: 3px double #111;
+ overflow: hidden;
+}
+.rf-speedlines {
+ position: absolute;
+ inset: 0;
+ background:
+ repeating-linear-gradient(
+ 75deg,
+ rgba(102, 204, 255, 0.06) 0 4px,
+ transparent 4px 12px
+ );
+ transform: translateX(0);
+ animation: rf-dash 2.8s linear infinite;
+ mix-blend-mode: screen;
+}
+.rf-speedlines.layer-b {
+ opacity: 0.5;
+ background:
+ repeating-linear-gradient(
+ 105deg,
+ rgba(255, 102, 153, 0.05) 0 6px,
+ transparent 6px 16px
+ );
+ animation-duration: 3.8s;
+}
+@keyframes rf-dash {
+ from { transform: translateX(0); }
+ to { transform: translateX(-120px); }
+}
+.rf-accent-diagonal {
+ position: absolute;
+ right: -15%;
+ top: -10%;
+ width: 60%;
+ height: 160%;
+ background: linear-gradient(180deg, rgba(102,204,255,0.15), rgba(255,102,153,0.15));
+ transform: rotate(-18deg);
+ filter: blur(8px);
+}
+.rf-hero-content {
+ position: relative;
+ z-index: 2;
+ display: grid;
+ gap: 10px;
+ padding: 32px;
+ justify-items: start;
+ color: #e6faff;
+}
+.rf-ascii-overlay {
+ position: absolute;
+ right: 10px;
+ bottom: 10px;
+ z-index: 1; /* above speedlines/accent, below hero text */
+ pointer-events: none;
+ font-family: 'Courier New', 'MS Gothic', monospace;
+ font-size: clamp(6px, 0.95vmin, 11px);
+ line-height: 1.05;
+ white-space: pre;
+ text-align: left;
+ max-width: 48%;
+ max-height: 70%;
+ overflow: hidden;
+}
+.rf-badge {
+ font-family: 'Notable', sans-serif;
+ font-size: 11px;
+ letter-spacing: 0.18em;
+ padding: 6px 10px;
+ border: 1px solid rgba(102,204,255,0.6);
+ background: rgba(10,12,20,0.6);
+}
+.rf-headline {
+ margin: 0;
+ font-family: 'Dela Gothic One', sans-serif;
+ font-weight: 700;
+ font-size: clamp(32px, 6vw, 72px);
+ letter-spacing: 0.04em;
+}
+
+/* Glitch effect for the word "futurist" in the hero headline */
+.glitch-word {
+ position: relative;
+ display: inline-block;
+}
+.glitch-word::before,
+.glitch-word::after {
+ content: attr(data-text);
+ position: absolute;
+ top: 0;
+ left: 0;
+ opacity: 0;
+ pointer-events: none;
+}
+.glitch-word.is-glitching {
+ text-shadow: -2px 0 #ff00c1, 2px 0 #00fff9;
+ animation: glitch-main 0.35s steps(10) 1;
+}
+.glitch-word.is-glitching::before {
+ opacity: 0.85;
+ color: #ff00c1;
+ mix-blend-mode: screen;
+ animation: glitch-before 0.35s steps(10) 1;
+}
+.glitch-word.is-glitching::after {
+ opacity: 0.85;
+ color: #00fff9;
+ mix-blend-mode: screen;
+ animation: glitch-after 0.35s steps(10) 1;
+}
+
+@keyframes glitch-main {
+ 0% { transform: none; }
+ 12% { transform: translate(3px, -2px) skew(0.6deg); }
+ 24% { transform: translate(-3px, 2px) skew(-0.4deg); }
+ 36% { transform: translate(2px, 0) skew(0.3deg); }
+ 48% { transform: translate(-2px, 2px) skew(-0.3deg); }
+ 60% { transform: translate(2px, -2px) skew(0.4deg); }
+ 72% { transform: translate(0, 2px) skew(-0.3deg); }
+ 84% { transform: translate(-2px, 0) skew(0.2deg); }
+ 100% { transform: none; }
+}
+
+@keyframes glitch-before {
+ 0% { transform: translate(-3px, -2px); clip-path: inset(0 0 85% 0); }
+ 20% { transform: translate(-4px, 1px); clip-path: inset(15% 0 60% 0); }
+ 40% { transform: translate(-3px, 0); clip-path: inset(40% 0 40% 0); }
+ 60% { transform: translate(-5px, -2px); clip-path: inset(60% 0 20% 0); }
+ 80% { transform: translate(-2px, 2px); clip-path: inset(80% 0 5% 0); }
+ 100% { transform: translate(0, 0); clip-path: inset(0 0 0 0); }
+}
+
+@keyframes glitch-after {
+ 0% { transform: translate(3px, 2px); clip-path: inset(85% 0 0 0); }
+ 20% { transform: translate(4px, -2px); clip-path: inset(60% 0 15% 0); }
+ 40% { transform: translate(3px, 0); clip-path: inset(40% 0 40% 0); }
+ 60% { transform: translate(5px, 2px); clip-path: inset(20% 0 60% 0); }
+ 80% { transform: translate(2px, -2px); clip-path: inset(5% 0 80% 0); }
+ 100% { transform: translate(0, 0); clip-path: inset(0 0 0 0); }
+}
+.rf-tagline {
+ margin: 0;
+ font-family: 'Inter', system-ui, sans-serif;
+ font-size: clamp(14px, 2vw, 22px);
+ color: #c7eaff;
+ opacity: 0.9;
+}
+.rf-stats {
+ display: flex;
+ gap: 14px;
+ margin-top: 6px;
+}
+.rf-stat {
+ font-family: 'Orbitron', monospace;
+ font-size: clamp(12px, 1.6vw, 18px);
+ color: #9fe3ff;
+ border-left: 2px solid rgba(102,204,255,0.6);
+ padding-left: 8px;
+}
+.rf-stat .label { opacity: 0.7; margin-right: 6px; }
+.rf-stat .value { color: #ffffff; }
+
+@media (max-width: 1024px) {
+ .ascii-hero { height: 100%; }
+}
+
+/* Gordian knot illustration */
+.deco-illustration { margin: 6px 0 2px 0; }
+.gordian-svg { width: min(520px, 80%); height: auto; }
+.gordian-svg path, .gordian-svg line, .gordian-svg polygon {
+ stroke: var(--ink);
+ stroke-width: 2.2;
+ fill: none;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+}
+.gordian-svg polygon { fill: rgba(201,168,118,0.2); }
+.gordian-svg .blade { stroke-width: 2.4; }
+.gordian-svg .guard { fill: rgba(201,168,118,0.35); stroke: var(--ink); stroke-width: 1.6; }
+.gordian-svg .rope-texture { stroke-width: 1.2; stroke-dasharray: 2 6; opacity: 0.35; }
+/* Rope appearance: outer dark edge and inner rope tone */
+.gordian-svg .rope-outer { stroke: rgba(26,15,8,0.35); stroke-width: 7; }
+.gordian-svg .rope-inner { stroke: #e8dcc3; stroke-width: 5; }
+.gordian-svg .rope-rib { stroke: rgba(26,15,8,0.25); stroke-width: 1.2; stroke-dasharray: 3 6; }
+.gordian-svg .notch { stroke: #fdf9f0; stroke-width: 8; stroke-linecap: round; opacity: 0.98; }
+/* New katana styling */
+.gordian-svg .katana-blade-outline { fill: #f4f4f4; stroke: var(--ink); stroke-width: 1.4; }
+.gordian-svg .katana-hamon { stroke: rgba(26,15,8,0.35); stroke-width: 1.2; opacity: 0.6; }
+.gordian-svg .katana-guard { fill: rgba(201,168,118,0.45); stroke: var(--ink); stroke-width: 1.2; }
+.gordian-svg .katana-grip { stroke: var(--ink); stroke-width: 5; }
+.gordian-svg .katana-wrap { stroke: #e8dcc3; stroke-width: 2; stroke-dasharray: 3 5; }
+
+/* Anime figure illustration */
+.anime-svg { width: min(520px, 80%); height: auto; }
+.anime-svg .silhouette { fill: rgba(26,15,8,0.85); stroke: var(--ink); stroke-width: 1.2; }
+.anime-svg .eye { stroke: #f4f4f4; stroke-width: 2; fill: none; stroke-linecap: round; }
+.anime-svg .eye-glint { stroke: #ffffff; stroke-width: 1.4; stroke-linecap: round; }
+
+/* Feature grid inside hero */
+.deco-feature-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 16px;
+ width: 100%;
+ max-width: 960px;
+ margin-top: 18px;
+}
+
+.deco-feature-card {
+ background: #fff;
+ border: 3px double var(--ink);
+ background-image:
+ linear-gradient(180deg, rgba(201,168,118,0.12), transparent 40%),
+ radial-gradient(120% 120% at 50% 0%, rgba(26,15,8,0.06), transparent 60%);
+ padding: 16px 18px;
+ text-align: center;
+ box-shadow: 0 8px 16px rgba(26,15,8,0.12);
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+}
+
+.deco-feature-card:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 12px 24px rgba(26,15,8,0.18);
+}
+
+.deco-icon {
+ display: none;
+}
+.deco-icon-svg {
+ width: 26px;
+ height: 26px;
+ margin-bottom: 8px;
+ stroke: var(--ink);
+ fill: none;
+ stroke-width: 2;
+}
+
+.deco-card-title {
+ font-family: 'Notable', sans-serif;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ font-size: 13px;
+ margin-bottom: 6px;
+}
+
+.deco-card-desc {
+ font-family: 'Inter', system-ui, sans-serif;
+ font-size: 12px;
+ line-height: 1.5;
+ color: #333;
+ margin: 0;
+}
+
+/* Taishō-modern frame with Deco elements */
+.taisho-modern-frame {
+ position: relative;
+ background: linear-gradient(180deg, #ffffff 0%, #fdf9f0 100%);
+}
+
+/* Inner gold accent border */
+.taisho-modern-frame::before {
+ content: '';
+ position: absolute;
+ inset: 0; /* hug the panel edge */
+ border: 3px double var(--mustard);
+ box-shadow: inset 0 0 0 2px #fff; /* subtle inner light line */
+ z-index: 2; /* above hero content */
+ pointer-events: none;
+}
+
+@media (max-width: 1024px) {
+ .hero-frame { min-height: 380px; }
+}
+
+@media (max-width: 1024px) {
+ .deco-feature-grid {
+ grid-template-columns: 1fr;
+ max-width: 560px;
+ }
+}
+
+.price-badge {
+ position: absolute;
+ top: 14px;
+ right: 14px;
+ background: #fff;
+ border: 3px solid var(--ink);
+ width: 84px; height: 84px;
+ border-radius: 50%;
+ display: grid; place-items: center;
+ box-shadow: 6px 6px 0 rgba(26,15,8,0.25);
+ text-align: center;
+}
+.price-badge .yen { font-family: 'DotGothic16', system-ui, sans-serif; font-size: 18px; display: block; }
+.price-badge .free { font-family: 'Impact','Arial Black',sans-serif; font-size: 14px; letter-spacing: 2px; }
+
+/* Coverlines removed */
+
+/* (ASCII section removed for updated design) */
+/* .ascii-section / .ascii-frame / .ascii-art-container not used */
+
+/* CTA */
+.cta-area { grid-area: cta; align-self: end; display: flex; justify-content: flex-end; }
+.taisho-cta {
+ position: relative;
+ background: var(--ink);
+ color: #fff;
+ border: 0;
+ outline: 0;
+ padding: 18px 28px;
+ display: inline-flex;
+ align-items: center;
+ gap: 12px;
+ box-shadow:
+ 0 0 0 2px #fff inset,
+ 8px 8px 0 rgba(26,15,8,0.25);
+ text-transform: uppercase;
+ letter-spacing: 0.15em;
+ cursor: pointer;
+ transform: rotate(-1deg);
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
+}
+.taisho-cta:hover { transform: rotate(0deg) translate(1px,1px); box-shadow: 0 0 0 2px #fff inset, 6px 6px 0 rgba(26,15,8,0.25); }
+.cta-icon { font-size: 18px; }
+.cta-text { font-size: 14px; }
+.cta-kaomoji { font-size: 18px; opacity: 0.85; }
+
+/* Barcode */
+.barcode {
+ grid-column: 1 / 2;
+ grid-row: 3 / 4;
+ align-self: end;
+ width: 160px;
+ height: 44px;
+ background:
+ repeating-linear-gradient(
+ 90deg,
+ #000 0 2px,
+ transparent 2px 4px,
+ #000 4px 6px,
+ transparent 6px 9px
+ );
+ border: 2px solid #000;
+ background-clip: padding-box;
+}
+
+/* Visitor counter (count.getloli.com) */
+.visit-counter {
+ position: static;
+ grid-column: 1;
+ grid-row: 3;
+ align-self: start; /* above barcode in the same cell */
+ justify-self: start;
+ margin-left: 0;
+ margin-bottom: 6px;
+ opacity: 0.98;
+}
+.visit-counter img {
+ height: 84px; /* default/base size */
+ image-rendering: auto;
+}
+
+/* Responsiveness */
+@media (max-width: 1024px) {
+ .taisho-cover .cover-content {
+ grid-template-columns: 1fr;
+ grid-template-areas:
+ "masthead"
+ "hero"
+ "cta"
+ "counter";
+ gap: 22px;
+ padding: 24px;
+ }
+ .masthead { flex-direction: row; justify-content: space-between; }
+ .masthead-vertical { writing-mode: initial; padding: 10px 16px; letter-spacing: 3px; }
+ .masthead-vertical .jp { font-size: 28px; }
+ .masthead-vertical .en { margin-top: 0; margin-left: 10px; font-size: 12px; }
+ .hero-frame { min-height: 360px; }
+ .cta-area { justify-content: center; }
+ /* Counter handled in explicit medium/mobile blocks */
+}
+
+
+.placeholder-content {
+ text-align: center;
+ color: rgba(255, 255, 255, 0.6);
+}
+
+.placeholder-icon {
+ font-size: 64px;
+ margin-bottom: 20px;
+ opacity: 0.5;
+}
+
+.placeholder-content p {
+ font-family: 'Orbitron', monospace;
+ font-size: 18px;
+ margin: 10px 0;
+}
+
+.placeholder-subtitle {
+ font-size: 14px !important;
+ opacity: 0.7;
+}
+
+/* Floating Login Button */
+.floating-login-btn {
+ position: fixed;
+ bottom: 120px;
+ right: 40px;
+ background: linear-gradient(135deg, rgba(255, 102, 153, 0.15), rgba(255, 102, 153, 0.05));
+ border: 2px solid rgba(255, 102, 153, 0.6);
+ border-radius: 4px;
+ color: #ffffff;
+ font-family: 'Orbitron', monospace;
+ font-size: 14px;
+ font-weight: 600;
+ padding: 12px 16px;
+ cursor: pointer;
+ transition: all 0.4s cubic-bezier(0.165, 0.84, 0.44, 1);
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ backdrop-filter: blur(20px);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ width: 160px;
+ height: 80px;
+ z-index: 200;
+ overflow: visible;
+ box-shadow: 0 8px 32px rgba(255, 102, 153, 0.2),
+ 0 0 0 1px rgba(255, 255, 255, 0.1) inset;
+}
+
+.floating-login-btn .btn-icon {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 20px;
+ height: 20px;
+ font-size: 20px;
+ transition: transform 0.3s ease;
+}
+
+.floating-login-btn .btn-text {
+ opacity: 1;
+ white-space: nowrap;
+ transform: translateX(0);
+ transition: all 0.3s ease;
+ margin: 0;
+ pointer-events: auto;
+ font-weight: 600;
+}
+
+.floating-login-btn:hover {
+ border-color: rgba(255, 102, 153, 0.8);
+ background: linear-gradient(135deg, rgba(255, 102, 153, 0.25), rgba(255, 102, 153, 0.1));
+ transform: translateY(-4px) scale(1.02);
+ box-shadow:
+ 0 16px 40px rgba(255, 102, 153, 0.4),
+ 0 0 0 1px rgba(255, 255, 255, 0.2) inset,
+ 0 8px 32px rgba(255, 102, 153, 0.2);
+}
+
+.floating-login-btn:hover .btn-icon {
+ transform: scale(1.1) rotate(-5deg);
+}
+
+.floating-login-btn:active {
+ transform: translateY(-2px) scale(0.98);
+ transition: all 0.1s ease;
+}
+
+/* Floating Settings Button */
+.floating-settings-btn {
+ position: fixed;
+ bottom: 40px;
+ right: 40px;
+ background: linear-gradient(135deg, rgba(102, 204, 255, 0.1), rgba(102, 204, 255, 0.05));
+ border: 2px solid rgba(102, 204, 255, 0.4);
+ border-radius: 4px;
+ color: #ffffff;
+ font-family: 'Orbitron', monospace;
+ font-size: 14px;
+ font-weight: 600;
+ padding: 10px 16px;
+ cursor: pointer;
+ transition: all 0.4s cubic-bezier(0.165, 0.84, 0.44, 1);
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ backdrop-filter: blur(20px);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ width: 160px;
+ height: 80px;
+ z-index: 200;
+ overflow: visible;
+ box-shadow: 0 6px 24px rgba(102, 204, 255, 0.15),
+ 0 0 0 1px rgba(255, 255, 255, 0.08) inset;
+}
+
+.floating-settings-btn .btn-icon {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 18px;
+ height: 18px;
+ font-size: 18px;
+ transition: transform 0.3s ease;
+}
+
+.floating-settings-btn .btn-text {
+ opacity: 1;
+ white-space: nowrap;
+ transform: translateX(0);
+ transition: all 0.3s ease;
+ margin: 0;
+ pointer-events: auto;
+ font-weight: 500;
+}
+
+.floating-settings-btn:hover {
+ border-color: rgba(102, 204, 255, 0.6);
+ background: linear-gradient(135deg, rgba(102, 204, 255, 0.2), rgba(102, 204, 255, 0.08));
+ transform: translateY(-3px) scale(1.02);
+ box-shadow:
+ 0 12px 32px rgba(102, 204, 255, 0.3),
+ 0 0 0 1px rgba(255, 255, 255, 0.15) inset,
+ 0 6px 24px rgba(102, 204, 255, 0.15);
+}
+
+.floating-settings-btn:hover .btn-icon {
+ animation: pulse-bounce 0.6s ease-in-out infinite;
+}
+
+@keyframes pulse-bounce {
+ 0%, 100% {
+ transform: scale(1);
+ }
+ 50% {
+ transform: scale(1.2);
+ }
+}
+
+.floating-settings-btn:active {
+ transform: translateY(-1px) scale(0.98);
+ transition: all 0.1s ease;
+}
+
+.brand-section {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 8px;
+ width: 100%;
+}
+
+.brand-title-container {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ width: 100%;
+ height: 60px;
+}
+
+.ekg-canvas {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 1;
+ opacity: 0.6;
+}
+
+.bottom-heartbeat {
+ position: fixed;
+ bottom: 30px;
+ right: 30px;
+ width: 200px;
+ height: 100px;
+ z-index: 50;
+ pointer-events: none;
+ background: linear-gradient(135deg, rgba(248, 244, 240, 0.95) 0%, rgba(232, 223, 213, 0.95) 100%);
+ border: 3px solid #1a0f08;
+ border-radius: 0;
+ padding: 12px;
+ backdrop-filter: blur(10px);
+ box-shadow:
+ 0 0 0 1px #c9a876,
+ 0 10px 30px rgba(26, 15, 8, 0.3),
+ inset 0 0 20px rgba(201, 168, 118, 0.1);
+ clip-path: polygon(8px 0, 100% 0, 100% calc(100% - 8px), calc(100% - 8px) 100%, 0 100%, 0 8px);
+}
+
+/* Hide on mobile devices */
+@media (max-width: 768px) {
+ .bottom-heartbeat {
+ display: none;
+ }
+}
+
+/* Phone sizes */
+@media (max-width: 480px) {
+ .visit-counter img { height: 96px; }
+}
+
+.bottom-ekg-canvas {
+ width: 100%;
+ height: 100%;
+ opacity: 1;
+ background: linear-gradient(135deg, rgba(26, 15, 8, 0.05) 0%, rgba(139, 111, 71, 0.08) 100%);
+ border-radius: 0;
+ border: 1px solid rgba(139, 111, 71, 0.2);
+}
+
+.bottom-heartbeat::before {
+ content: "心電図";
+ position: absolute;
+ top: 8px;
+ left: 12px;
+ font-size: 10px;
+ color: #1a0f08;
+ font-family: 'Noto Serif JP', serif;
+ font-weight: 700;
+ letter-spacing: 2px;
+ opacity: 0.7;
+}
+
+.bottom-heartbeat::after {
+ content: "◆ 72 拍/分";
+ position: absolute;
+ bottom: 8px;
+ right: 12px;
+ font-size: 9px;
+ color: #8b6f47;
+ font-family: 'Noto Serif JP', serif;
+ font-weight: 600;
+ letter-spacing: 1px;
+}
+
+.brand-title {
+ position: relative;
+ z-index: 2;
+ font-family: 'Dela Gothic One', cursive;
+ font-size: clamp(32px, 6vmin, 48px);
+ font-weight: 400;
+ color: #ffffff;
+ letter-spacing: 0;
+ margin: 0;
+ margin-bottom: 6px;
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
+ cursor: pointer;
+ perspective: 1000px;
+ transform-style: preserve-3d;
+}
+
+.title-text {
+ display: inline-block;
+ transition: transform 0.6s ease;
+ backface-visibility: hidden;
+}
+
+.title-text-hover {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ transform: rotateY(180deg);
+ transition: transform 0.6s ease;
+ backface-visibility: hidden;
+}
+
+.brand-title:hover .title-text {
+ transform: rotateY(180deg);
+}
+
+.brand-title:hover .title-text-hover {
+ transform: rotateY(0deg);
+}
+
+.brand-subtitle {
+ font-family: 'Notable', sans-serif;
+ font-size: clamp(10px, 1.4vmin, 12px);
+ color: var(--accent2);
+ font-weight: normal;
+ letter-spacing: 2px;
+ margin: 0;
+ margin-bottom: 0;
+ opacity: 0.8;
+}
+
+.brand-line {
+ position: relative;
+ width: 100vw;
+ height: 60px;
+ margin: 8px 0;
+ transform: translateX(-40px);
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+
+.main-menu {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ width: 100%;
+ max-width: 320px;
+ align-self: center;
+}
+
+.menu-btn {
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(102, 204, 255, 0.3);
+ color: #ffffff;
+ font-family: 'Orbitron', monospace;
+ font-size: 14px;
+ font-weight: 600;
+ padding: 12px;
+ cursor: pointer;
+ transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1);
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ backdrop-filter: blur(10px);
+ position: relative;
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 12px;
+ width: 48px;
+ height: 48px;
+ border-radius: 4px;
+}
+
+.menu-btn::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(90deg, transparent 0%, rgba(102, 204, 255, 0.1) 50%, transparent 100%);
+ transition: left 0.5s ease;
+}
+
+.menu-btn:hover::before {
+ left: 100%;
+}
+
+.menu-btn .btn-text {
+ opacity: 0;
+ width: 0;
+ overflow: hidden;
+ white-space: nowrap;
+ transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1);
+}
+
+.menu-btn:hover {
+ border-color: #66ccff;
+ background: rgba(102, 204, 255, 0.1);
+ transform: translateY(-2px);
+ box-shadow:
+ 0 10px 30px rgba(102, 204, 255, 0.2),
+ 0 0 20px rgba(102, 204, 255, 0.1);
+ width: auto;
+ padding: 12px 24px;
+}
+
+.menu-btn:hover .btn-text {
+ opacity: 1;
+ width: auto;
+ margin-left: 8px;
+}
+
+.menu-btn.primary {
+ border-color: #ff6699;
+ background: rgba(255, 102, 153, 0.1);
+}
+
+.menu-btn.primary:hover {
+ border-color: #ff6699;
+ background: rgba(255, 102, 153, 0.2);
+ box-shadow:
+ 0 10px 30px rgba(255, 102, 153, 0.3),
+ 0 0 20px rgba(255, 102, 153, 0.2);
+ width: auto;
+ padding: 12px 24px;
+}
+
+.btn-icon {
+ font-size: 16px;
+ opacity: 0.8;
+}
+
+.btn-text {
+ flex: 1;
+ text-align: left;
+}
+
+.system-info {
+ position: absolute;
+ top: 40px;
+ right: 40px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ font-family: monospace;
+ font-size: 12px;
+ opacity: 0.6;
+ z-index: 20;
+}
+
+.info-item {
+ display: flex;
+ justify-content: space-between;
+ gap: 20px;
+}
+
+.info-label {
+ color: #66ccff;
+ font-family: 'MS Gothic', monospace;
+ font-size: 10px;
+ font-weight: 600;
+}
+
+.info-value {
+ color: #ffffff;
+ font-family: 'MS Gothic', monospace;
+ font-size: 11px;
+ font-weight: bold;
+}
+
+.live-status {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.clickable {
+ cursor: pointer;
+ transition: opacity 0.2s ease;
+}
+
+.clickable:hover {
+ opacity: 0.8;
+}
+
+.status-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ transition: all 0.3s ease;
+}
+
+.status-dot.live {
+ background-color: #ff4444;
+ animation: recording-pulse 2s ease-in-out infinite alternate;
+}
+
+.status-dot.standby {
+ background-color: #66ccff;
+ opacity: 1;
+ box-shadow: 0 0 4px rgba(102, 204, 255, 0.6);
+}
+
+@keyframes recording-pulse {
+ from {
+ opacity: 0.4;
+ box-shadow: 0 0 4px rgba(255, 68, 68, 0.4);
+ }
+ to {
+ opacity: 1;
+ box-shadow: 0 0 12px rgba(255, 68, 68, 0.8);
+ }
+}
+
+
+
+@keyframes line-glow {
+ from {
+ box-shadow: 0 0 10px rgba(102, 204, 255, 0.5);
+ }
+ to {
+ box-shadow: 0 0 20px rgba(102, 204, 255, 0.8);
+ }
+}
+
+@keyframes scan-lines {
+ 0% {
+ transform: translateY(0);
+ }
+ 100% {
+ transform: translateY(4px);
+ }
+}
+
+/* Desktop Responsiveness */
+@media (min-width: 1024px) {
+
+ /* Desktop button group - use modern layout */
+ .mobile-button-group {
+ position: fixed;
+ bottom: 40px;
+ left: 50%;
+ transform: translateX(-50%);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 16px;
+ z-index: 200;
+ }
+
+ .floating-login-btn {
+ position: static;
+ bottom: auto;
+ right: auto;
+ left: auto;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ }
+
+ .floating-settings-btn {
+ position: static;
+ bottom: auto;
+ right: auto;
+ left: auto;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ }
+}
+
+/* Mobile Responsiveness */
+@media (max-width: 768px) {
+ .floating-header {
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 100px;
+ padding: 15px;
+ }
+
+ /* Allow scrolling on mobile: stop constraining to viewport */
+ .modern-landing-page {
+ position: relative;
+ top: 0;
+ margin-top: 100px; /* account for fixed header height */
+ height: auto;
+ min-height: calc(100dvh - 100px);
+ width: 100%;
+ overflow: visible;
+ }
+
+ /* Let the cover size to content so page can scroll */
+ .taisho-cover {
+ position: relative;
+ inset: auto;
+ overflow: visible;
+ }
+
+ .taisho-cover .cover-content {
+ height: auto;
+ /* Extra spacing between rows */
+ gap: 28px;
+ }
+
+ /* (Removed earlier overlap hacks; barcode handled explicitly below) */
+
+ .header-content {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 15px;
+ }
+
+ .brand-section {
+ align-items: flex-start;
+ }
+
+ .brand-title {
+ letter-spacing: 4px;
+ }
+
+ .brand-line {
+ width: 100vw;
+ transform: translateX(-15px);
+ margin: 6px 0;
+ height: 40px;
+ }
+
+ .video-background, .ascii-background {
+ top: 0;
+ }
+
+ .ascii-art {
+ font-size: clamp(8px, 2vmin, 16px);
+ line-height: 1.1;
+ padding: 15px;
+ }
+
+ /* Move barcode to its own row below CTA to avoid overlaps */
+ .taisho-cover .cover-content {
+ grid-template-areas:
+ "masthead"
+ "hero"
+ "coverlines"
+ "cta"
+ "barcode";
+ }
+ .barcode {
+ grid-area: barcode;
+ justify-self: start;
+ align-self: start;
+ margin-top: 8px;
+ }
+ .taisho-cover .cover-content {
+ grid-template-areas:
+ "masthead"
+ "hero"
+ "coverlines"
+ "cta"
+ "counter"
+ "barcode";
+ }
+ .visit-counter {
+ position: static;
+ grid-area: counter;
+ align-self: start;
+ justify-self: center; /* center horizontally on mobile */
+ margin-top: 6px;
+ }
+ .visit-counter img { height: 84px; }
+
+ .large-text {
+ font-size: clamp(44px, 10vw, 90px);
+ top: 18%;
+ }
+
+ /* Taisho mobile layout */
+ .taisho-content {
+ grid-template-columns: 1fr;
+ gap: 0;
+ padding: 0;
+ }
+
+ .taisho-content::before,
+ .taisho-content::after {
+ display: none;
+ }
+
+ .taisho-left {
+ padding: 60px 30px;
+ gap: 25px;
+ }
+
+ .taisho-left::before,
+ .taisho-left::after {
+ display: none;
+ }
+
+ .taisho-title {
+ font-size: clamp(48px, 12vw, 80px);
+ line-height: 0.9;
+ }
+
+ .taisho-title-latin {
+ font-size: clamp(14px, 3vw, 20px);
+ margin-top: 12px;
+ }
+
+ .taisho-title-latin::before {
+ left: -25px;
+ font-size: 10px;
+ }
+
+ .taisho-divider {
+ width: 140px;
+ margin: 20px 0;
+ }
+
+ .taisho-divider::after {
+ left: 70px;
+ }
+
+ .taisho-tagline {
+ font-size: clamp(20px, 4vw, 28px);
+ padding-left: 15px;
+ }
+
+ .taisho-tagline::before {
+ font-size: 45px;
+ top: -10px;
+ left: -8px;
+ }
+
+ .taisho-subtitle {
+ font-size: clamp(14px, 2.5vw, 18px);
+ padding-left: 15px;
+ border-left-width: 2px;
+ }
+
+ .taisho-cta {
+ padding: 18px 40px;
+ font-size: 13px;
+ width: 100%;
+ justify-content: center;
+ margin-top: 20px;
+ }
+
+ .cta-icon {
+ font-size: 20px;
+ }
+
+ .cta-text {
+ font-size: 13px;
+ }
+
+ .taisho-right {
+ padding: 60px 30px;
+ }
+
+ .taisho-right::before,
+ .taisho-right::after {
+ display: none;
+ }
+
+ .code-frame {
+ padding: 30px;
+ border-width: 8px;
+ box-shadow:
+ 0 0 0 2px #c9a876,
+ 0 0 0 10px #1a0f08,
+ 0 20px 40px rgba(26, 15, 8, 0.4),
+ inset 0 0 60px rgba(201, 168, 118, 0.08);
+ }
+
+ .code-frame::after {
+ font-size: 12px;
+ top: 15px;
+ left: 15px;
+ }
+
+ .ascii-art-container {
+ padding: 25px;
+ max-height: 400px;
+ }
+
+ .ascii-art-container .ascii-art {
+ font-size: clamp(7px, 1.5vw, 10px);
+ }
+
+ /* Mobile button group container */
+ .mobile-button-group {
+ position: fixed;
+ bottom: 40px;
+ left: 50%;
+ transform: translateX(-50%);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 16px;
+ z-index: 200;
+ }
+
+ .floating-login-btn {
+ position: static;
+ width: 120px;
+ height: 60px;
+ min-width: auto;
+ padding: 8px 12px;
+ bottom: auto;
+ left: auto;
+ transform: none;
+ flex-direction: column;
+ gap: 4px;
+ }
+
+ .floating-login-btn .btn-text {
+ opacity: 1;
+ transform: translateX(0);
+ pointer-events: auto;
+ }
+
+ .floating-login-btn:hover {
+ padding: 8px 12px;
+ width: 120px;
+ height: 60px;
+ }
+
+ .floating-settings-btn {
+ position: static;
+ bottom: auto;
+ left: auto;
+ width: 120px;
+ height: 60px;
+ flex-direction: column;
+ gap: 4px;
+ }
+
+ .floating-settings-btn:hover {
+ padding: 8px 12px;
+ width: 120px;
+ height: 60px;
+ }
+
+ .system-info {
+ font-size: 10px;
+ }
+}
+
+/* VN Interface Styles */
+.vn-interface {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background: #000000;
+ overflow: hidden;
+ z-index: 1000;
+ transition: opacity 0.8s ease-in-out;
+}
+
+.vn-interface.fade-out {
+ opacity: 0;
+}
+
+.vn-interface.fade-in {
+ opacity: 1;
+}
+
+.vn-background {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ overflow: hidden;
+ z-index: 1;
+}
+
+/* Combined Info Panel (Top Right) */
+.floating-info-panel {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ width: 180px;
+ height: 160px;
+ background: rgba(0, 0, 0, 0.8);
+ backdrop-filter: blur(15px);
+ border: 1px solid rgba(102, 204, 255, 0.3);
+ z-index: 100;
+ padding: 15px;
+}
+
+.info-panel-content {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ height: 100%;
+}
+
+/* Weather Section */
+.weather-section {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.weather-icon {
+ font-size: 16px;
+ opacity: 0.8;
+}
+
+.weather-details {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.weather-location {
+ font-family: 'MS Gothic', monospace;
+ font-size: 10px;
+ color: #66ccff;
+ font-weight: 600;
+}
+
+.weather-temp {
+ font-family: 'MS Gothic', monospace;
+ font-size: 12px;
+ color: #ffffff;
+ font-weight: bold;
+}
+
+/* Time Section */
+.time-section {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ align-items: center;
+ padding: 6px 0;
+ border-top: 1px solid rgba(102, 204, 255, 0.2);
+ border-bottom: 1px solid rgba(102, 204, 255, 0.2);
+}
+
+.japan-date {
+ font-family: 'MS Gothic', monospace;
+ font-size: 10px;
+ color: #66ccff;
+ font-weight: 600;
+}
+
+.japan-time {
+ font-family: 'MS Gothic', monospace;
+ font-size: 14px;
+ color: #ffffff;
+ font-weight: bold;
+ letter-spacing: 1px;
+}
+
+/* Status Section */
+.status-section {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.status-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.yen-balance {
+ color: #ffcc00;
+ font-weight: bold;
+}
+
+/* VN Viewport */
+.vn-viewport {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 50;
+}
+
+.vn-content {
+ text-align: center;
+ z-index: 60;
+}
+
+.vn-brand-title {
+ font-family: 'Dela Gothic One', cursive;
+ font-size: clamp(52px, 9vmin, 78px);
+ font-weight: 400;
+ color: #ffffff;
+ letter-spacing: 0;
+ margin: 0;
+ margin-bottom: 16px;
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
+}
+
+.vn-brand-subtitle {
+ font-family: 'Notable', sans-serif;
+ font-size: clamp(14px, 2.5vmin, 18px);
+ color: var(--accent2);
+ font-weight: normal;
+ letter-spacing: 2px;
+ margin: 0;
+ opacity: 0.8;
+}
+
+/* Floating Dialogue Panel */
+.floating-dialogue-panel {
+ position: fixed;
+ bottom: 170px;
+ left: 20px;
+ right: 20px;
+ height: 150px;
+ background: rgba(0, 0, 0, 0.9);
+ backdrop-filter: blur(15px);
+ border: 1px solid rgba(102, 204, 255, 0.3);
+ z-index: 100;
+ padding: 20px;
+}
+
+.dialogue-content {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ height: 100%;
+}
+
+.dialogue-speaker {
+ font-family: 'MS Gothic', monospace;
+ font-size: 14px;
+ color: #ff6699;
+ font-weight: bold;
+}
+
+.dialogue-text {
+ font-family: 'MS Gothic', monospace;
+ font-size: 16px;
+ color: #ffffff;
+ line-height: 1.5;
+ flex: 1;
+}
+
+/* Floating Input Panel */
+.floating-input-panel {
+ position: fixed;
+ bottom: 20px;
+ left: 20px;
+ right: 20px;
+ min-height: 70px;
+ max-height: 130px;
+ background: rgba(0, 0, 0, 0.8);
+ backdrop-filter: blur(15px);
+ border: 1px solid rgba(102, 204, 255, 0.3);
+ z-index: 100;
+ padding: 15px 20px;
+ display: flex;
+ align-items: stretch;
+ transition: all 0.3s ease;
+}
+
+.input-content {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.vn-text-input {
+ width: 100%;
+ height: 40px;
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(102, 204, 255, 0.3);
+ color: #ffffff;
+ font-family: 'MS Gothic', monospace;
+ font-size: 14px;
+ padding: 0 15px;
+ outline: none;
+ transition: all 0.3s ease;
+}
+
+.vn-text-input::placeholder {
+ color: rgba(255, 255, 255, 0.4);
+ font-family: 'MS Gothic', monospace;
+}
+
+.vn-text-input:focus {
+ border-color: #66ccff;
+ background: rgba(102, 204, 255, 0.1);
+ box-shadow: 0 0 10px rgba(102, 204, 255, 0.3);
+}
+
+/* Choice Buttons (for toggle mode) */
+.choice-buttons {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ width: 100%;
+ min-height: 90px;
+ max-height: 90px;
+ overflow-y: auto;
+}
+
+.choice-btn {
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(102, 204, 255, 0.3);
+ color: #ffffff;
+ font-family: 'MS Gothic', monospace;
+ font-size: 12px;
+ padding: 8px 12px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ text-align: left;
+ flex-shrink: 0;
+ min-height: 24px;
+}
+
+.choice-btn:hover {
+ border-color: #66ccff;
+ background: rgba(102, 204, 255, 0.1);
+ transform: translateX(2px);
+}
+
+/* Toggle Button */
+.toggle-input-btn {
+ background: rgba(102, 204, 255, 0.1);
+ border: 1px solid rgba(102, 204, 255, 0.3);
+ color: #ffffff;
+ font-size: 16px;
+ width: 50px;
+ height: 40px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.toggle-input-btn:hover {
+ border-color: #66ccff;
+ background: rgba(102, 204, 255, 0.2);
+ box-shadow: 0 0 5px rgba(102, 204, 255, 0.3);
+}
+
+/* Floating Settings Button (VN Interface) */
+.vn-interface .floating-logout-btn {
+ position: fixed;
+ bottom: 350px;
+ right: 30px;
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(102, 204, 255, 0.3);
+ color: #ffffff;
+ font-family: 'Orbitron', monospace;
+ font-size: 14px;
+ font-weight: 600;
+ padding: 12px;
+ cursor: pointer;
+ transition: all 0.25s ease;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ backdrop-filter: blur(10px);
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ width: 48px;
+ height: 48px;
+ z-index: 200;
+ overflow: visible;
+}
+
+.vn-interface .floating-logout-btn .btn-icon {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+}
+
+.vn-interface .floating-logout-btn .btn-text {
+ opacity: 0;
+ white-space: nowrap;
+ transform: translateX(-10px);
+ transition: transform 0.25s ease;
+ margin-left: 8px;
+ pointer-events: none;
+}
+
+.vn-interface .floating-logout-btn:hover {
+ border-color: #66ccff;
+ background: rgba(102, 204, 255, 0.1);
+ transform: translateY(-2px);
+ box-shadow:
+ 0 10px 30px rgba(102, 204, 255, 0.2),
+ 0 0 20px rgba(102, 204, 255, 0.1);
+ width: auto;
+ min-width: 110px;
+ padding: 12px 16px;
+}
+
+.vn-interface .floating-logout-btn:hover .btn-text {
+ opacity: 1;
+ transform: translateX(0);
+ pointer-events: auto;
+ transition: opacity 0.2s ease 0.05s, transform 0.25s ease;
+}
+
+.vn-interface .floating-logout-btn:active {
+ transform: translateY(-1px);
+}
+
+/* Mobile Responsiveness for VN Interface */
+@media (max-width: 768px) {
+ .floating-info-panel {
+ top: 10px;
+ right: 10px;
+ width: 140px;
+ height: 130px;
+ padding: 10px;
+ }
+
+ .info-panel-content {
+ gap: 6px;
+ }
+
+ .weather-section {
+ gap: 6px;
+ }
+
+ .weather-icon {
+ font-size: 12px;
+ }
+
+ .weather-location {
+ font-size: 8px;
+ }
+
+ .weather-temp {
+ font-size: 10px;
+ }
+
+ .time-section {
+ padding: 4px 0;
+ }
+
+ .japan-date {
+ font-size: 8px;
+ }
+
+ .japan-time {
+ font-size: 12px;
+ }
+
+ .status-section {
+ gap: 3px;
+ }
+
+ .info-label {
+ font-size: 8px;
+ }
+
+ .info-value {
+ font-size: 9px;
+ }
+
+ .floating-dialogue-panel {
+ bottom: 120px;
+ left: 10px;
+ right: 10px;
+ height: 120px;
+ padding: 15px;
+ }
+
+ .floating-input-panel {
+ bottom: 10px;
+ left: 10px;
+ right: 10px;
+ min-height: 60px;
+ max-height: 110px;
+ padding: 10px 15px;
+ }
+
+ /* (Removed special choices-mode spacing; not needed for barcode fix) */
+
+ .vn-text-input {
+ height: 35px;
+ font-size: 13px;
+ padding: 0 12px;
+ }
+
+ .choice-buttons {
+ min-height: 75px;
+ max-height: 75px;
+ gap: 4px;
+ }
+
+ .choice-btn {
+ font-size: 11px;
+ padding: 6px 8px;
+ min-height: 20px;
+ }
+
+ .toggle-input-btn {
+ width: 45px;
+ height: 35px;
+ font-size: 14px;
+ }
+
+ .vn-interface .floating-logout-btn {
+ bottom: 280px;
+ right: 20px;
+ width: 44px;
+ height: 44px;
+ }
+
+ .vn-interface .floating-logout-btn:hover {
+ padding: 12px 16px;
+ width: auto;
+ min-width: 100px;
+ }
+}
+
+/* Settings Modal */
+.modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.8);
+ backdrop-filter: blur(5px);
+ z-index: 1000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ animation: fadeIn 0.3s ease;
+}
+
+.settings-modal {
+ background: rgba(0, 0, 0, 0.95);
+ border: 2px solid rgba(102, 204, 255, 0.5);
+ border-radius: 0;
+ max-width: 500px;
+ width: 90%;
+ max-height: 80vh;
+ overflow-y: auto;
+ animation: slideIn 0.3s ease;
+ backdrop-filter: blur(15px);
+}
+
+.modal-header {
+ padding: 20px;
+ border-bottom: 1px solid rgba(102, 204, 255, 0.3);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.modal-title {
+ font-family: 'MS Gothic', monospace;
+ font-size: 18px;
+ font-weight: bold;
+ color: #66ccff;
+ margin: 0;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+}
+
+.modal-close-btn {
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(102, 204, 255, 0.3);
+ color: #ffffff;
+ font-size: 18px;
+ font-weight: bold;
+ cursor: pointer;
+ padding: 0;
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.3s ease;
+ border-radius: 0;
+ font-family: 'MS Gothic', monospace;
+ backdrop-filter: blur(10px);
+}
+
+.modal-close-btn:hover {
+ background: rgba(102, 204, 255, 0.1);
+ border-color: #66ccff;
+ color: #66ccff;
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(102, 204, 255, 0.2);
+}
+
+.modal-close-btn:active {
+ transform: translateY(0);
+ box-shadow: 0 2px 6px rgba(102, 204, 255, 0.2);
+}
+
+.modal-content {
+ padding: 20px;
+}
+
+.settings-section {
+ margin-bottom: 25px;
+}
+
+.settings-section h3 {
+ font-family: 'MS Gothic', monospace;
+ font-size: 14px;
+ color: #66ccff;
+ margin: 0 0 15px 0;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ border-bottom: 1px solid rgba(102, 204, 255, 0.2);
+ padding-bottom: 5px;
+}
+
+.setting-item {
+ margin-bottom: 15px;
+}
+
+.setting-item label {
+ font-family: 'MS Gothic', monospace;
+ font-size: 13px;
+ color: #ffffff;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ cursor: pointer;
+}
+
+.setting-item input[type="checkbox"] {
+ appearance: none;
+ width: 18px;
+ height: 18px;
+ border: 1px solid rgba(102, 204, 255, 0.3);
+ background: rgba(255, 255, 255, 0.05);
+ position: relative;
+ cursor: pointer;
+ border-radius: 0;
+ transition: all 0.3s ease;
+}
+
+.setting-item input[type="checkbox"]:checked {
+ background: rgba(102, 204, 255, 0.2);
+ border-color: #66ccff;
+}
+
+.setting-item input[type="checkbox"]:checked::after {
+ content: '✓';
+ position: absolute;
+ top: -2px;
+ left: 2px;
+ color: #66ccff;
+ font-size: 14px;
+ font-weight: bold;
+ font-family: 'MS Gothic', monospace;
+}
+
+.setting-item input[type="range"] {
+ flex: 1;
+ background: rgba(255, 255, 255, 0.1);
+ border: 1px solid rgba(102, 204, 255, 0.3);
+ height: 6px;
+ outline: none;
+ appearance: none;
+ border-radius: 0;
+}
+
+.setting-item input[type="range"]::-webkit-slider-thumb {
+ appearance: none;
+ width: 16px;
+ height: 16px;
+ background: #66ccff;
+ cursor: pointer;
+ border-radius: 0;
+ border: none;
+ transition: all 0.3s ease;
+}
+
+.setting-item input[type="range"]::-webkit-slider-thumb:hover {
+ background: #80d4ff;
+ transform: scale(1.1);
+}
+
+.modal-footer {
+ padding: 20px;
+ border-top: 1px solid rgba(102, 204, 255, 0.3);
+ display: flex;
+ gap: 15px;
+ justify-content: flex-end;
+}
+
+.modal-btn {
+ font-family: 'MS Gothic', monospace;
+ font-size: 13px;
+ font-weight: bold;
+ padding: 12px 20px;
+ border: 1px solid rgba(102, 204, 255, 0.3);
+ background: rgba(255, 255, 255, 0.05);
+ color: #ffffff;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ border-radius: 0;
+ backdrop-filter: blur(10px);
+}
+
+.modal-btn:hover {
+ border-color: #66ccff;
+ background: rgba(102, 204, 255, 0.1);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(102, 204, 255, 0.2);
+}
+
+.modal-btn:active {
+ transform: translateY(0);
+ box-shadow: 0 2px 6px rgba(102, 204, 255, 0.2);
+}
+
+.modal-btn.secondary {
+ background: rgba(255, 255, 255, 0.08);
+ border-color: rgba(255, 255, 255, 0.2);
+}
+
+.modal-btn.secondary:hover {
+ background: rgba(255, 255, 255, 0.15);
+ border-color: rgba(255, 255, 255, 0.4);
+ box-shadow: 0 4px 12px rgba(255, 255, 255, 0.1);
+}
+
+.modal-btn.logout {
+ background: rgba(255, 100, 100, 0.1);
+ border-color: rgba(255, 100, 100, 0.5);
+ color: #ff6464;
+}
+
+.modal-btn.logout:hover {
+ background: rgba(255, 100, 100, 0.2);
+ border-color: #ff6464;
+ box-shadow: 0 4px 12px rgba(255, 100, 100, 0.3);
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+@keyframes slideIn {
+ from {
+ opacity: 0;
+ transform: translateY(-20px) scale(0.95);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+}
+
+@media (max-width: 768px) {
+ .settings-modal {
+ width: 95%;
+ max-height: 85vh;
+ }
+
+ .modal-header, .modal-content, .modal-footer {
+ padding: 15px;
+ }
+
+ .modal-title {
+ font-size: 16px;
+ }
+
+ .modal-footer {
+ flex-direction: column;
+ }
+
+ .modal-btn {
+ width: 100%;
+ }
+}
+
+/* Loading Screen */
+.loading-screen {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background: #000000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 9999;
+ opacity: 1;
+ transition: opacity 1s ease-out;
+ pointer-events: auto;
+ cursor: pointer;
+}
+
+.loading-screen.fade-out {
+ opacity: 0 !important;
+ pointer-events: none;
+ transition: opacity 1s ease-out;
+}
+
+.loading-screen.hide {
+ display: none;
+}
+
+.main-interface {
+ opacity: 0;
+ transition: opacity 0.5s ease-in;
+}
+
+.main-interface.visible {
+ opacity: 1;
+}
+
+.main-interface.hidden {
+ opacity: 0;
+}
+
+.loading-logo {
+ text-align: center;
+ color: var(--fg);
+ font-family: 'MS Gothic', monospace;
+}
+
+.heart-container {
+ position: relative;
+ display: inline-block;
+}
+
+.sun-rays {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 140vmax;
+ height: 140vmax;
+ z-index: 0;
+ animation: rotate-rays 15s linear infinite;
+}
+
+.rays-svg {
+ width: 100%;
+ height: 100%;
+ filter: drop-shadow(0 0 20px #8B0000) drop-shadow(0 0 40px #8B0000);
+}
+
+.heart-outline {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 450px;
+ height: 450px;
+ z-index: 1;
+}
+
+.heart-svg {
+ width: 100%;
+ height: 100%;
+ filter: drop-shadow(0 0 15px #8B0000) drop-shadow(0 0 30px #8B0000);
+}
+
+.heart-container .logo-text,
+.heart-container .loading-subtitle,
+.heart-container .loading-dots {
+ position: relative;
+ z-index: 3;
+}
+
+@keyframes rotate-rays {
+ from {
+ transform: translate(-50%, -50%) rotate(0deg);
+ }
+ to {
+ transform: translate(-50%, -50%) rotate(360deg);
+ }
+}
+
+/* Staggered loading animations */
+.loading-animate-heart {
+ opacity: 0;
+ transform: translate(-50%, -50%) scale(0.3);
+ animation: heart-appear 0.8s ease-out 0.2s forwards;
+}
+
+.loading-animate-rays {
+ opacity: 0;
+ animation: rays-appear 0.6s ease-out 0.8s forwards, rotate-rays 15s linear 1.4s infinite;
+}
+
+.loading-animate-text {
+ opacity: 0;
+ transform: translateY(20px);
+ animation: text-appear 0.6s ease-out 1.2s forwards;
+}
+
+.loading-animate-subtitle {
+ opacity: 0;
+ transform: translateY(20px);
+ animation: text-appear 0.6s ease-out 1.0s forwards;
+}
+
+@keyframes heart-appear {
+ from {
+ opacity: 0;
+ transform: translate(-50%, -50%) scale(0.3);
+ }
+ to {
+ opacity: 1;
+ transform: translate(-50%, -50%) scale(1);
+ }
+}
+
+@keyframes rays-appear {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+@keyframes text-appear {
+ from {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.logo-text {
+ font-size: clamp(48px, 8vmin, 72px);
+ font-weight: bold;
+ font-family: 'Dela Gothic One', cursive;
+ color: #ffffff;
+ margin-bottom: 16px;
+ letter-spacing: 8px;
+}
+
+.loading-subtitle {
+ font-size: clamp(14px, 2.5vmin, 18px);
+ color: var(--accent2);
+ margin-bottom: 24px;
+ letter-spacing: 2px;
+ opacity: 0.8;
+}
+
+.loading-dots {
+ font-size: 24px;
+ color: var(--accent3);
+ letter-spacing: 4px;
+}
+
+.loading-dots span {
+ animation: dot-blink 1.5s ease-in-out infinite;
+}
+
+.loading-dots span:nth-child(2) {
+ animation-delay: 0.3s;
+}
+
+.loading-dots span:nth-child(3) {
+ animation-delay: 0.6s;
+}
+
+@keyframes logo-glow {
+ from {
+ text-shadow:
+ 0 0 10px var(--accent),
+ 0 0 20px var(--accent),
+ 0 0 30px var(--accent);
+ }
+ to {
+ text-shadow:
+ 0 0 15px var(--accent),
+ 0 0 25px var(--accent),
+ 0 0 40px var(--accent);
+ }
+}
+
+@keyframes dot-blink {
+ 0%, 20% { opacity: 0; }
+ 50% { opacity: 1; }
+ 100% { opacity: 0; }
+}
+
+.loading-instruction {
+ margin-top: 32px;
+ font-size: clamp(12px, 2vmin, 16px);
+ color: var(--accent3);
+ text-align: center;
+ opacity: 0.8;
+ animation: pulse 2s ease-in-out infinite;
+}
+
+@keyframes pulse {
+ 0%, 100% { opacity: 0.5; }
+ 50% { opacity: 1; }
+}
+
+/* Mobile responsive design */
+@media (max-width: 1024px) {
+ /* Hide pillar borders on smaller screens */
+ .left-pillar,
+ .right-pillar {
+ display: none;
+ }
+
+ .screen {
+ display: block;
+ position: relative;
+ width: 100vw;
+ height: 100vh;
+ }
+
+ .screen-content {
+ width: 100%;
+ height: 100vh;
+ max-width: none;
+ }
+
+ /* Move elements to bottom border */
+ .screen-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 8px 16px;
+ height: 48px;
+ }
+
+ /* Hide desktop time in mobile */
+ .desktop-time {
+ display: none;
+ }
+
+ .mobile-time-money {
+ display: flex;
+ gap: 12px;
+ font-family: 'MS Gothic', monospace;
+ font-size: 12px;
+ background: rgba(0, 0, 0, 0.8);
+ padding: 6px 12px;
+ border-radius: 4px;
+ border: 1px solid var(--interface-border);
+ position: relative;
+ z-index: 2;
+ }
+
+ .mobile-time-money::before {
+ display: none;
+ }
+
+ .mobile-clock {
+ color: var(--accent2);
+ font-weight: bold;
+ text-shadow: 0 0 4px var(--accent2);
+ }
+
+ .mobile-money {
+ color: var(--accent3);
+ font-weight: bold;
+ text-shadow: 0 0 4px var(--accent3);
+ }
+
+ .mobile-money::before {
+ content: '¥ ';
+ color: var(--fg);
+ text-shadow: none;
+ }
+
+ .mobile-buttons {
+ display: flex;
+ gap: 8px;
+ }
+
+ .mobile-btn {
+ appearance: none;
+ background: linear-gradient(180deg, #000000, #000000, #000000);
+ border: 2px outset #000000;
+ color: #ffffff;
+ font-family: 'MS Gothic', monospace;
+ font-size: 9px;
+ font-weight: bold;
+ padding: 4px 8px;
+ text-align: center;
+ cursor: pointer;
+ box-shadow: inset 0 0 4px rgba(255,255,255,0.3);
+ text-shadow: 1px 1px 0 rgba(0,0,0,0.5);
+ }
+
+ .mobile-btn:hover {
+ background: linear-gradient(180deg, #000000, #000000, #000000);
+ border: 2px inset #000000;
+ }
+
+ /* Hide desktop bottom bar when mobile elements are shown */
+ .bottombar {
+ display: none;
+ }
+
+ /* Adjust dialogue and input positioning */
+ .dialogue {
+ bottom: 140px;
+ margin: 0 8px;
+ }
+
+ .text-input-area {
+ bottom: 80px;
+ left: 8px;
+ right: 8px;
+ }
+}
+
+/* Config Modal */
+.modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.8);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 10000;
+ backdrop-filter: blur(4px);
+ -webkit-backdrop-filter: blur(4px);
+}
+
+.config-modal {
+ background: linear-gradient(180deg, #000000, #000000, #000000);
+ border: 4px solid var(--interface-border);
+ border-radius: 8px;
+ min-width: 400px;
+ max-width: 90vw;
+ max-height: 90vh;
+ overflow: auto;
+ box-shadow:
+ inset 0 0 15px rgba(0,0,0,0.1),
+ 0 8px 32px rgba(0,0,0,0.6);
+ position: relative;
+}
+
+.modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px 20px;
+ background: linear-gradient(180deg, #000000, #000000);
+ border-bottom: 3px solid var(--interface-border);
+}
+
+.modal-header h2 {
+ margin: 0;
+ font-family: 'Sylfaen', serif;
+ font-size: 18px;
+ font-weight: bold;
+ color: #ffffff;
+ text-shadow: 1px 1px 0 rgba(0,0,0,0.5);
+}
+
+.close-btn {
+ appearance: none;
+ background: linear-gradient(180deg, #000000, #000000, #000000);
+ border: 2px outset #000000;
+ color: #ffffff;
+ font-family: 'MS Gothic', monospace;
+ font-size: 18px;
+ font-weight: bold;
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ border-radius: 4px;
+ box-shadow: inset 0 0 4px rgba(255,255,255,0.3);
+}
+
+.close-btn:hover {
+ background: linear-gradient(180deg, #000000, #000000, #000000);
+ border: 2px inset #000000;
+}
+
+.modal-content {
+ padding: 24px;
+ background: linear-gradient(180deg, #000000, #000000, #000000);
+}
+
+.config-option {
+ margin-bottom: 20px;
+}
+
+.config-label {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ cursor: pointer;
+ font-family: 'MS Gothic', monospace;
+ font-size: 14px;
+ font-weight: bold;
+ color: #ffffff;
+}
+
+.config-checkbox {
+ appearance: none;
+ width: 20px;
+ height: 20px;
+ border: 3px inset #000000;
+ background: #ffffff;
+ border-radius: 2px;
+ position: relative;
+ cursor: pointer;
+}
+
+.config-checkbox:checked {
+ background: var(--accent2);
+ border: 3px inset var(--accent2);
+}
+
+.config-checkbox:checked::after {
+ content: '✓';
+ position: absolute;
+ top: -2px;
+ left: 2px;
+ color: #000000;
+ font-size: 14px;
+ font-weight: bold;
+}
+
+.config-text {
+ user-select: none;
+}
+
+.config-description {
+ margin-top: 8px;
+ margin-left: 32px;
+ font-family: 'MS Gothic', monospace;
+ font-size: 12px;
+ color: #555;
+ font-style: italic;
+}
+
+.modal-footer {
+ padding: 16px 24px;
+ background: linear-gradient(180deg, #000000, #000000);
+ border-top: 3px solid var(--interface-border);
+ display: flex;
+ justify-content: flex-end;
+}
+
+.modal-btn {
+ appearance: none;
+ background: linear-gradient(180deg, #000000, #000000, #000000);
+ border: 2px outset #000000;
+ color: #ffffff;
+ font-family: 'MS Gothic', monospace;
+ font-size: 12px;
+ font-weight: bold;
+ padding: 8px 16px;
+ text-align: center;
+ cursor: pointer;
+ box-shadow: inset 0 0 4px rgba(255,255,255,0.3);
+ text-shadow: 1px 1px 0 rgba(0,0,0,0.5);
+ border-radius: 4px;
+}
+
+.modal-btn:hover {
+ background: linear-gradient(180deg, #000000, #000000, #000000);
+ border: 2px inset #000000;
+}
+
+/* Desktop view - hide mobile elements */
+@media (min-width: 1025px) {
+ .mobile-time-money,
+ .mobile-buttons {
+ display: none;
+ }
+
+ /* Hide desktop time in footer when pillar borders are visible */
+ .desktop-time {
+ visibility: hidden;
+ }
+}
+/* Tablet/medium layout */
+@media (min-width: 769px) and (max-width: 1024px) {
+ .taisho-cover .cover-content {
+ grid-template-columns: 1fr;
+ grid-template-areas:
+ "masthead"
+ "hero"
+ "cta"
+ "counter";
+ gap: 24px;
+ }
+ .visit-counter {
+ position: static;
+ grid-area: counter;
+ justify-self: start;
+ align-self: start;
+ margin-left: 0;
+ }
+ .visit-counter img { height: 72px; }
+}
+
+/* Desktop only: place counter above barcode, away from center frame */
+@media (min-width: 1025px) {
+ .visit-counter {
+ position: absolute;
+ left: 12px;
+ bottom: 24px;
+ z-index: 220;
+ }
+ .visit-counter img {
+ height: 88px;
+ max-width: 240px;
+ }
+}
+
+/* Hide ASCII overlay on small screens to avoid clutter */
+@media (max-width: 900px) {
+ .rf-ascii-overlay {
+ display: none;
+ }
+}
diff --git a/frontend/src/types.ts b/frontend/src/types.ts
new file mode 100644
index 0000000..c6d1263
--- /dev/null
+++ b/frontend/src/types.ts
@@ -0,0 +1,11 @@
+export type Role = 'user' | 'assistant' | 'system'
+export type ChatMessage = {
+ id: string
+ role: Role
+ content: string
+}
+
+export type Choice = {
+ id: string
+ label: string
+}
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 0000000..1144b49
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "Bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "allowSyntheticDefaultImports": true,
+ "forceConsistentCasingInFileNames": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json
new file mode 100644
index 0000000..d33d10f
--- /dev/null
+++ b/frontend/tsconfig.node.json
@@ -0,0 +1,9 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "types": ["node"]
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/frontend/tsconfig.node.tsbuildinfo b/frontend/tsconfig.node.tsbuildinfo
new file mode 100644
index 0000000..5a18318
--- /dev/null
+++ b/frontend/tsconfig.node.tsbuildinfo
@@ -0,0 +1 @@
+{"fileNames":["./node_modules/typescript/lib/lib.d.ts","./node_modules/typescript/lib/lib.es5.d.ts","./node_modules/typescript/lib/lib.es2015.d.ts","./node_modules/typescript/lib/lib.es2016.d.ts","./node_modules/typescript/lib/lib.es2017.d.ts","./node_modules/typescript/lib/lib.es2018.d.ts","./node_modules/typescript/lib/lib.es2019.d.ts","./node_modules/typescript/lib/lib.es2020.d.ts","./node_modules/typescript/lib/lib.dom.d.ts","./node_modules/typescript/lib/lib.webworker.importscripts.d.ts","./node_modules/typescript/lib/lib.scripthost.d.ts","./node_modules/typescript/lib/lib.es2015.core.d.ts","./node_modules/typescript/lib/lib.es2015.collection.d.ts","./node_modules/typescript/lib/lib.es2015.generator.d.ts","./node_modules/typescript/lib/lib.es2015.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.promise.d.ts","./node_modules/typescript/lib/lib.es2015.proxy.d.ts","./node_modules/typescript/lib/lib.es2015.reflect.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2016.array.include.d.ts","./node_modules/typescript/lib/lib.es2016.intl.d.ts","./node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","./node_modules/typescript/lib/lib.es2017.date.d.ts","./node_modules/typescript/lib/lib.es2017.object.d.ts","./node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2017.string.d.ts","./node_modules/typescript/lib/lib.es2017.intl.d.ts","./node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","./node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","./node_modules/typescript/lib/lib.es2018.intl.d.ts","./node_modules/typescript/lib/lib.es2018.promise.d.ts","./node_modules/typescript/lib/lib.es2018.regexp.d.ts","./node_modules/typescript/lib/lib.es2019.array.d.ts","./node_modules/typescript/lib/lib.es2019.object.d.ts","./node_modules/typescript/lib/lib.es2019.string.d.ts","./node_modules/typescript/lib/lib.es2019.symbol.d.ts","./node_modules/typescript/lib/lib.es2019.intl.d.ts","./node_modules/typescript/lib/lib.es2020.bigint.d.ts","./node_modules/typescript/lib/lib.es2020.date.d.ts","./node_modules/typescript/lib/lib.es2020.promise.d.ts","./node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2020.string.d.ts","./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2020.intl.d.ts","./node_modules/typescript/lib/lib.es2020.number.d.ts","./node_modules/typescript/lib/lib.esnext.disposable.d.ts","./node_modules/typescript/lib/lib.esnext.float16.d.ts","./node_modules/typescript/lib/lib.decorators.d.ts","./node_modules/typescript/lib/lib.decorators.legacy.d.ts","./node_modules/@types/node/compatibility/iterators.d.ts","./node_modules/@types/node/globals.typedarray.d.ts","./node_modules/@types/node/buffer.buffer.d.ts","./node_modules/@types/node/globals.d.ts","./node_modules/@types/node/web-globals/abortcontroller.d.ts","./node_modules/@types/node/web-globals/domexception.d.ts","./node_modules/@types/node/web-globals/events.d.ts","./node_modules/undici-types/utility.d.ts","./node_modules/undici-types/header.d.ts","./node_modules/undici-types/readable.d.ts","./node_modules/undici-types/fetch.d.ts","./node_modules/undici-types/formdata.d.ts","./node_modules/undici-types/connector.d.ts","./node_modules/undici-types/client-stats.d.ts","./node_modules/undici-types/client.d.ts","./node_modules/undici-types/errors.d.ts","./node_modules/undici-types/dispatcher.d.ts","./node_modules/undici-types/global-dispatcher.d.ts","./node_modules/undici-types/global-origin.d.ts","./node_modules/undici-types/pool-stats.d.ts","./node_modules/undici-types/pool.d.ts","./node_modules/undici-types/handlers.d.ts","./node_modules/undici-types/balanced-pool.d.ts","./node_modules/undici-types/h2c-client.d.ts","./node_modules/undici-types/agent.d.ts","./node_modules/undici-types/mock-interceptor.d.ts","./node_modules/undici-types/mock-call-history.d.ts","./node_modules/undici-types/mock-agent.d.ts","./node_modules/undici-types/mock-client.d.ts","./node_modules/undici-types/mock-pool.d.ts","./node_modules/undici-types/mock-errors.d.ts","./node_modules/undici-types/proxy-agent.d.ts","./node_modules/undici-types/env-http-proxy-agent.d.ts","./node_modules/undici-types/retry-handler.d.ts","./node_modules/undici-types/retry-agent.d.ts","./node_modules/undici-types/api.d.ts","./node_modules/undici-types/cache-interceptor.d.ts","./node_modules/undici-types/interceptors.d.ts","./node_modules/undici-types/util.d.ts","./node_modules/undici-types/cookies.d.ts","./node_modules/undici-types/patch.d.ts","./node_modules/undici-types/websocket.d.ts","./node_modules/undici-types/eventsource.d.ts","./node_modules/undici-types/diagnostics-channel.d.ts","./node_modules/undici-types/content-type.d.ts","./node_modules/undici-types/cache.d.ts","./node_modules/undici-types/index.d.ts","./node_modules/@types/node/web-globals/fetch.d.ts","./node_modules/@types/node/web-globals/navigator.d.ts","./node_modules/@types/node/web-globals/storage.d.ts","./node_modules/@types/node/assert.d.ts","./node_modules/@types/node/assert/strict.d.ts","./node_modules/@types/node/async_hooks.d.ts","./node_modules/@types/node/buffer.d.ts","./node_modules/@types/node/child_process.d.ts","./node_modules/@types/node/cluster.d.ts","./node_modules/@types/node/console.d.ts","./node_modules/@types/node/constants.d.ts","./node_modules/@types/node/crypto.d.ts","./node_modules/@types/node/dgram.d.ts","./node_modules/@types/node/diagnostics_channel.d.ts","./node_modules/@types/node/dns.d.ts","./node_modules/@types/node/dns/promises.d.ts","./node_modules/@types/node/domain.d.ts","./node_modules/@types/node/events.d.ts","./node_modules/@types/node/fs.d.ts","./node_modules/@types/node/fs/promises.d.ts","./node_modules/@types/node/http.d.ts","./node_modules/@types/node/http2.d.ts","./node_modules/@types/node/https.d.ts","./node_modules/@types/node/inspector.d.ts","./node_modules/@types/node/module.d.ts","./node_modules/@types/node/net.d.ts","./node_modules/@types/node/os.d.ts","./node_modules/@types/node/path.d.ts","./node_modules/@types/node/perf_hooks.d.ts","./node_modules/@types/node/process.d.ts","./node_modules/@types/node/punycode.d.ts","./node_modules/@types/node/querystring.d.ts","./node_modules/@types/node/readline.d.ts","./node_modules/@types/node/readline/promises.d.ts","./node_modules/@types/node/repl.d.ts","./node_modules/@types/node/sea.d.ts","./node_modules/@types/node/sqlite.d.ts","./node_modules/@types/node/stream.d.ts","./node_modules/@types/node/stream/promises.d.ts","./node_modules/@types/node/stream/consumers.d.ts","./node_modules/@types/node/stream/web.d.ts","./node_modules/@types/node/string_decoder.d.ts","./node_modules/@types/node/test.d.ts","./node_modules/@types/node/timers.d.ts","./node_modules/@types/node/timers/promises.d.ts","./node_modules/@types/node/tls.d.ts","./node_modules/@types/node/trace_events.d.ts","./node_modules/@types/node/tty.d.ts","./node_modules/@types/node/url.d.ts","./node_modules/@types/node/util.d.ts","./node_modules/@types/node/v8.d.ts","./node_modules/@types/node/vm.d.ts","./node_modules/@types/node/wasi.d.ts","./node_modules/@types/node/worker_threads.d.ts","./node_modules/@types/node/zlib.d.ts","./node_modules/@types/node/index.d.ts","./node_modules/@types/estree/index.d.ts","./node_modules/rollup/dist/rollup.d.ts","./node_modules/rollup/dist/parseast.d.ts","./node_modules/vite/types/hmrpayload.d.ts","./node_modules/vite/types/customevent.d.ts","./node_modules/vite/types/hot.d.ts","./node_modules/vite/dist/node/types.d-agj9qkwt.d.ts","./node_modules/esbuild/lib/main.d.ts","./node_modules/source-map-js/source-map.d.ts","./node_modules/postcss/lib/previous-map.d.ts","./node_modules/postcss/lib/input.d.ts","./node_modules/postcss/lib/css-syntax-error.d.ts","./node_modules/postcss/lib/declaration.d.ts","./node_modules/postcss/lib/root.d.ts","./node_modules/postcss/lib/warning.d.ts","./node_modules/postcss/lib/lazy-result.d.ts","./node_modules/postcss/lib/no-work-result.d.ts","./node_modules/postcss/lib/processor.d.ts","./node_modules/postcss/lib/result.d.ts","./node_modules/postcss/lib/document.d.ts","./node_modules/postcss/lib/rule.d.ts","./node_modules/postcss/lib/node.d.ts","./node_modules/postcss/lib/comment.d.ts","./node_modules/postcss/lib/container.d.ts","./node_modules/postcss/lib/at-rule.d.ts","./node_modules/postcss/lib/list.d.ts","./node_modules/postcss/lib/postcss.d.ts","./node_modules/postcss/lib/postcss.d.mts","./node_modules/vite/dist/node/runtime.d.ts","./node_modules/vite/types/importglob.d.ts","./node_modules/vite/types/metadata.d.ts","./node_modules/vite/dist/node/index.d.ts","./node_modules/@babel/types/lib/index.d.ts","./node_modules/@types/babel__generator/index.d.ts","./node_modules/@babel/parser/typings/babel-parser.d.ts","./node_modules/@types/babel__template/index.d.ts","./node_modules/@types/babel__traverse/index.d.ts","./node_modules/@types/babel__core/index.d.ts","./node_modules/@vitejs/plugin-react/dist/index.d.ts","./vite.config.ts"],"fileIdsList":[[54,105,187],[54,105],[54,105,187,188,189,190,191],[54,105,187,189],[54,102,105],[54,104,105],[105],[54,105,110,139],[54,105,106,111,116,124,136,147],[54,105,106,107,116,124],[54,105,108,148],[54,105,109,110,117,125],[54,105,110,136,144],[54,105,111,113,116,124],[54,104,105,112],[54,105,113,114],[54,105,115,116],[54,104,105,116],[54,105,116,117,118,136,147],[54,105,116,117,118,131,136,139],[54,98,105,113,116,119,124,136,147],[54,105,116,117,119,120,124,136,144,147],[54,105,119,121,136,144,147],[52,53,54,55,56,57,58,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153],[54,105,116,122],[54,105,123,147],[54,105,113,116,124,136],[54,105,125],[54,105,126],[54,104,105,127],[54,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153],[54,105,129],[54,105,130],[54,105,116,131,132],[54,105,131,133,148,150],[54,105,116,136,137,139],[54,105,138,139],[54,105,136,137],[54,105,139],[54,105,140],[54,102,105,136,141],[54,105,116,142,143],[54,105,142,143],[54,105,110,124,136,144],[54,105,145],[54,105,124,146],[54,105,119,130,147],[54,105,110,148],[54,105,136,149],[54,105,123,150],[54,105,151],[54,98,105],[54,105,116,118,127,136,139,147,149,150,152],[54,105,136,153],[54,105,186,192],[54,105,178],[54,105,176,178],[54,105,167,175,176,177,179,181],[54,105,165],[54,105,168,173,178,181],[54,105,164,181],[54,105,168,169,172,173,174,181],[54,105,168,169,170,172,173,181],[54,105,165,166,167,168,169,173,174,175,177,178,179,181],[54,105,181],[54,105,163,165,166,167,168,169,170,172,173,174,175,176,177,178,179,180],[54,105,163,181],[54,105,168,170,171,173,174,181],[54,105,172,181],[54,105,173,174,178,181],[54,105,166,176],[54,105,156,185],[54,105,155,156],[54,65,68,71,72,105,147],[54,68,105,136,147],[54,68,72,105,147],[54,105,136],[54,62,105],[54,66,105],[54,64,65,68,105,147],[54,105,124,144],[54,105,154],[54,62,105,154],[54,64,68,105,124,147],[54,59,60,61,63,67,105,116,136,147],[54,68,76,105],[54,60,66,105],[54,68,92,93,105],[54,60,63,68,105,139,147,154],[54,68,105],[54,64,68,105,147],[54,59,105],[54,62,63,64,66,67,68,69,70,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,93,94,95,96,97,105],[54,68,85,88,105,113],[54,68,76,77,78,105],[54,66,68,77,79,105],[54,67,105],[54,60,62,68,105],[54,68,72,77,79,105],[54,72,105],[54,66,68,71,105,147],[54,60,64,68,76,105],[54,68,85,105],[54,62,68,92,105,139,152,154],[54,105,116,117,119,120,121,124,136,144,147,153,154,156,157,158,159,160,161,162,182,183,184,185],[54,105,158,159,160,161],[54,105,158,159,160],[54,105,158],[54,105,159],[54,105,156],[54,105,186,193]],"fileInfos":[{"version":"a7297ff837fcdf174a9524925966429eb8e5feecc2cc55cc06574e6b092c1eaa","impliedFormat":1},{"version":"c430d44666289dae81f30fa7b2edebf186ecc91a2d4c71266ea6ae76388792e1","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"080941d9f9ff9307f7e27a83bcd888b7c8270716c39af943532438932ec1d0b9","affectsGlobalScope":true,"impliedFormat":1},{"version":"80e18897e5884b6723488d4f5652167e7bb5024f946743134ecc4aa4ee731f89","affectsGlobalScope":true,"impliedFormat":1},{"version":"cd034f499c6cdca722b60c04b5b1b78e058487a7085a8e0d6fb50809947ee573","affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fb0f136d372979348d59b3f5020b4cdb81b5504192b1cacff5d1fbba29378aa1","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"a680117f487a4d2f30ea46f1b4b7f58bef1480456e18ba53ee85c2746eeca012","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"51ad4c928303041605b4d7ae32e0c1ee387d43a24cd6f1ebf4a2699e1076d4fa","affectsGlobalScope":true,"impliedFormat":1},{"version":"4245fee526a7d1754529d19227ecbf3be066ff79ebb6a380d78e41648f2f224d","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"d153a11543fd884b596587ccd97aebbeed950b26933ee000f94009f1ab142848","affectsGlobalScope":true,"impliedFormat":1},{"version":"c0671b50bb99cc7ad46e9c68fa0e7f15ba4bc898b59c31a17ea4611fab5095da","affectsGlobalScope":true,"impliedFormat":1},{"version":"d802f0e6b5188646d307f070d83512e8eb94651858de8a82d1e47f60fb6da4e2","affectsGlobalScope":true,"impliedFormat":1},{"version":"aa83e100f0c74a06c9d24f40a096c9e9cc3c02704250d01541e22c0ae9264eda","affectsGlobalScope":true,"impliedFormat":1},{"version":"1db0b7dca579049ca4193d034d835f6bfe73096c73663e5ef9a0b5779939f3d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"456fa0c0ab68731564917642b977c71c3b7682240685b118652fb9253c9a6429","affectsGlobalScope":true,"impliedFormat":1},{"version":"cdcf9ea426ad970f96ac930cd176d5c69c6c24eebd9fc580e1572d6c6a88f62c","impliedFormat":1},{"version":"23cd712e2ce083d68afe69224587438e5914b457b8acf87073c22494d706a3d0","impliedFormat":1},{"version":"487b694c3de27ddf4ad107d4007ad304d29effccf9800c8ae23c2093638d906a","impliedFormat":1},{"version":"e525f9e67f5ddba7b5548430211cae2479070b70ef1fd93550c96c10529457bd","impliedFormat":1},{"version":"ccf4552357ce3c159ef75f0f0114e80401702228f1898bdc9402214c9499e8c0","impliedFormat":1},{"version":"c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","impliedFormat":1},{"version":"68834d631c8838c715f225509cfc3927913b9cc7a4870460b5b60c8dbdb99baf","impliedFormat":1},{"version":"4bc0794175abedf989547e628949888c1085b1efcd93fc482bccd77ee27f8b7c","impliedFormat":1},{"version":"3c8e93af4d6ce21eb4c8d005ad6dc02e7b5e6781f429d52a35290210f495a674","impliedFormat":1},{"version":"2c9875466123715464539bfd69bcaccb8ff6f3e217809428e0d7bd6323416d01","impliedFormat":1},{"version":"ea6bc8de8b59f90a7a3960005fd01988f98fd0784e14bc6922dde2e93305ec7d","impliedFormat":1},{"version":"36107995674b29284a115e21a0618c4c2751b32a8766dd4cb3ba740308b16d59","impliedFormat":1},{"version":"914a0ae30d96d71915fc519ccb4efbf2b62c0ddfb3a3fc6129151076bc01dc60","impliedFormat":1},{"version":"33e981bf6376e939f99bd7f89abec757c64897d33c005036b9a10d9587d80187","impliedFormat":1},{"version":"6c8e442ba33b07892169a14f7757321e49ab0f1032d676d321a1fdab8a67d40c","impliedFormat":1},{"version":"b41767d372275c154c7ea6c9d5449d9a741b8ce080f640155cc88ba1763e35b3","impliedFormat":1},{"version":"1cd673d367293fc5cb31cd7bf03d598eb368e4f31f39cf2b908abbaf120ab85a","impliedFormat":1},{"version":"af13e99445f37022c730bfcafcdc1761e9382ce1ea02afb678e3130b01ce5676","impliedFormat":1},{"version":"3825bf209f1662dfd039010a27747b73d0ef379f79970b1d05601ec8e8a4249f","impliedFormat":1},{"version":"0b6e25234b4eec6ed96ab138d96eb70b135690d7dd01f3dd8a8ab291c35a683a","impliedFormat":1},{"version":"9666f2f84b985b62400d2e5ab0adae9ff44de9b2a34803c2c5bd3c8325b17dc0","impliedFormat":1},{"version":"da52342062e70c77213e45107921100ba9f9b3a30dd019444cf349e5fb3470c4","impliedFormat":1},{"version":"e9ace91946385d29192766bf783b8460c7dbcbfc63284aa3c9cae6de5155c8bc","impliedFormat":1},{"version":"40b463c6766ca1b689bfcc46d26b5e295954f32ad43e37ee6953c0a677e4ae2b","impliedFormat":1},{"version":"249b9cab7f5d628b71308c7d9bb0a808b50b091e640ba3ed6e2d0516f4a8d91d","impliedFormat":1},{"version":"1e30c045732e7db8f7a82cf90b516ebe693d2f499ce2250a977ec0d12e44a529","impliedFormat":1},{"version":"84b736594d8760f43400202859cda55607663090a43445a078963031d47e25e7","impliedFormat":1},{"version":"499e5b055a5aba1e1998f7311a6c441a369831c70905cc565ceac93c28083d53","impliedFormat":1},{"version":"54c3e2371e3d016469ad959697fd257e5621e16296fa67082c2575d0bf8eced0","impliedFormat":1},{"version":"beb8233b2c220cfa0feea31fbe9218d89fa02faa81ef744be8dce5acb89bb1fd","impliedFormat":1},{"version":"78b29846349d4dfdd88bd6650cc5d2baaa67f2e89dc8a80c8e26ef7995386583","impliedFormat":1},{"version":"5d0375ca7310efb77e3ef18d068d53784faf62705e0ad04569597ae0e755c401","impliedFormat":1},{"version":"59af37caec41ecf7b2e76059c9672a49e682c1a2aa6f9d7dc78878f53aa284d6","impliedFormat":1},{"version":"addf417b9eb3f938fddf8d81e96393a165e4be0d4a8b6402292f9c634b1cb00d","impliedFormat":1},{"version":"e38d4fdf79e1eadd92ed7844c331dbaa40f29f21541cfee4e1acff4db09cda33","impliedFormat":1},{"version":"8bd86b8e8f6a6aa6c49b71e14c4ffe1211a0e97c80f08d2c8cc98838006e4b88","impliedFormat":1},{"version":"7c10a32ae6f3962672e6869ee2c794e8055d8225ef35c91c0228e354b4e5d2d3","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"99f569b42ea7e7c5fe404b2848c0893f3e1a56e0547c1cd0f74d5dbb9a9de27e","impliedFormat":1},{"version":"830171b27c5fdf9bcbe4cf7d428fcf3ae2c67780fb7fbdccdf70d1623d938bc4","affectsGlobalScope":true,"impliedFormat":1},{"version":"f9501cc13ce624c72b61f12b3963e84fad210fbdf0ffbc4590e08460a3f04eba","affectsGlobalScope":true,"impliedFormat":1},{"version":"e7721c4f69f93c91360c26a0a84ee885997d748237ef78ef665b153e622b36c1","affectsGlobalScope":true,"impliedFormat":1},{"version":"bbcfd9cd76d92c3ee70475270156755346c9086391e1b9cb643d072e0cf576b8","impliedFormat":1},{"version":"7394959e5a741b185456e1ef5d64599c36c60a323207450991e7a42e08911419","impliedFormat":1},{"version":"72c1f5e0a28e473026074817561d1bc9647909cf253c8d56c41d1df8d95b85f7","impliedFormat":1},{"version":"003ec918ec442c3a4db2c36dc0c9c766977ea1c8bcc1ca7c2085868727c3d3f6","affectsGlobalScope":true,"impliedFormat":1},{"version":"a6310806c6aa3154773976dd083a15659d294700d9ad8f6b8a2e10c3dc461ff1","impliedFormat":1},{"version":"c4e8e8031808b158cfb5ac5c4b38d4a26659aec4b57b6a7e2ba0a141439c208c","impliedFormat":1},{"version":"2c91d8366ff2506296191c26fd97cc1990bab3ee22576275d28b654a21261a44","affectsGlobalScope":true,"impliedFormat":1},{"version":"5524481e56c48ff486f42926778c0a3cce1cc85dc46683b92b1271865bcf015a","impliedFormat":1},{"version":"db39d9a16e4ddcd8a8f2b7b3292b362cc5392f92ad7ccd76f00bccf6838ac7de","affectsGlobalScope":true,"impliedFormat":1},{"version":"289e9894a4668c61b5ffed09e196c1f0c2f87ca81efcaebdf6357cfb198dac14","impliedFormat":1},{"version":"25a1105595236f09f5bce42398be9f9ededc8d538c258579ab662d509aa3b98e","impliedFormat":1},{"version":"5078cd62dbdf91ae8b1dc90b1384dec71a9c0932d62bdafb1a811d2a8e26bef2","impliedFormat":1},{"version":"a2e2bbde231b65c53c764c12313897ffdfb6c49183dd31823ee2405f2f7b5378","impliedFormat":1},{"version":"ad1cc0ed328f3f708771272021be61ab146b32ecf2b78f3224959ff1e2cd2a5c","impliedFormat":1},{"version":"62f572306e0b173cc5dfc4c583471151f16ef3779cf27ab96922c92ec82a3bc8","affectsGlobalScope":true,"impliedFormat":1},{"version":"622b67a408a881e15ab38043547563b9d29ca4b46f5b7a7e4a4fc3123d25d19f","impliedFormat":1},{"version":"2617f1d06b32c7b4dfd0a5c8bc7b5de69368ec56788c90f3d7f3e3d2f39f0253","impliedFormat":1},{"version":"bd8b644c5861b94926687618ec2c9e60ad054d334d6b7eb4517f23f53cb11f91","impliedFormat":1},{"version":"bcbabfaca3f6b8a76cb2739e57710daf70ab5c9479ab70f5351c9b4932abf6bd","impliedFormat":1},{"version":"77fced47f495f4ff29bb49c52c605c5e73cd9b47d50080133783032769a9d8a6","impliedFormat":1},{"version":"966dd0793b220e22344c944e0f15afafdc9b0c9201b6444ea0197cd176b96893","impliedFormat":1},{"version":"c54f0b30a787b3df16280f4675bd3d9d17bf983ae3cd40087409476bc50b922d","affectsGlobalScope":true,"impliedFormat":1},{"version":"0f5cda0282e1d18198e2887387eb2f026372ebc4e11c4e4516fef8a19ee4d514","impliedFormat":1},{"version":"e99b0e71f07128fc32583e88ccd509a1aaa9524c290efb2f48c22f9bf8ba83b1","impliedFormat":1},{"version":"76957a6d92b94b9e2852cf527fea32ad2dc0ef50f67fe2b14bd027c9ceef2d86","impliedFormat":1},{"version":"5e9f8c1e042b0f598a9be018fc8c3cb670fe579e9f2e18e3388b63327544fe16","affectsGlobalScope":true,"impliedFormat":1},{"version":"a8a99a5e6ed33c4a951b67cc1fd5b64fd6ad719f5747845c165ca12f6c21ba16","affectsGlobalScope":true,"impliedFormat":1},{"version":"a58a15da4c5ba3df60c910a043281256fa52d36a0fcdef9b9100c646282e88dd","impliedFormat":1},{"version":"b36beffbf8acdc3ebc58c8bb4b75574b31a2169869c70fc03f82895b93950a12","impliedFormat":1},{"version":"de263f0089aefbfd73c89562fb7254a7468b1f33b61839aafc3f035d60766cb4","impliedFormat":1},{"version":"70b57b5529051497e9f6482b76d91c0dcbb103d9ead8a0549f5bab8f65e5d031","impliedFormat":1},{"version":"8c81fd4a110490c43d7c578e8c6f69b3af01717189196899a6a44f93daa57a3a","impliedFormat":1},{"version":"1013eb2e2547ad8c100aca52ef9df8c3f209edee32bb387121bb3227f7c00088","impliedFormat":1},{"version":"e07c573ac1971ea89e2c56ff5fd096f6f7bba2e6dbcd5681d39257c8d954d4a8","impliedFormat":1},{"version":"363eedb495912790e867da6ff96e81bf792c8cfe386321e8163b71823a35719a","impliedFormat":1},{"version":"37ba7b45141a45ce6e80e66f2a96c8a5ab1bcef0fc2d0f56bb58df96ec67e972","impliedFormat":1},{"version":"125d792ec6c0c0f657d758055c494301cc5fdb327d9d9d5960b3f129aff76093","impliedFormat":1},{"version":"dba28a419aec76ed864ef43e5f577a5c99a010c32e5949fe4e17a4d57c58dd11","affectsGlobalScope":true,"impliedFormat":1},{"version":"ea713aa14a670b1ea0fbaaca4fd204e645f71ca7653a834a8ec07ee889c45de6","impliedFormat":1},{"version":"07199a85560f473f37363d8f1300fac361cda2e954caf8a40221f83a6bfa7ade","impliedFormat":1},{"version":"9705cd157ffbb91c5cab48bdd2de5a437a372e63f870f8a8472e72ff634d47c1","affectsGlobalScope":true,"impliedFormat":1},{"version":"ae86f30d5d10e4f75ce8dcb6e1bd3a12ecec3d071a21e8f462c5c85c678efb41","impliedFormat":1},{"version":"982efeb2573605d4e6d5df4dc7e40846bda8b9e678e058fc99522ab6165c479e","impliedFormat":1},{"version":"e03460fe72b259f6d25ad029f085e4bedc3f90477da4401d8fbc1efa9793230e","impliedFormat":1},{"version":"4286a3a6619514fca656089aee160bb6f2e77f4dd53dc5a96b26a0b4fc778055","impliedFormat":1},{"version":"c9231cf03fd7e8cfd78307eecbd24ff3f0fa55d0f6d1108c4003c124d168adc4","affectsGlobalScope":true,"impliedFormat":1},{"version":"2d5d50cd0667d9710d4d2f6e077cc4e0f9dc75e106cccaea59999b36873c5a0d","affectsGlobalScope":true,"impliedFormat":1},{"version":"784490137935e1e38c49b9289110e74a1622baf8a8907888dcbe9e476d7c5e44","impliedFormat":1},{"version":"42180b657831d1b8fead051698618b31da623fb71ff37f002cb9d932cfa775f1","impliedFormat":1},{"version":"4f98d6fb4fe7cbeaa04635c6eaa119d966285d4d39f0eb55b2654187b0b27446","impliedFormat":1},{"version":"f8529fe0645fd9af7441191a4961497cc7638f75a777a56248eac6a079bb275d","affectsGlobalScope":true,"impliedFormat":1},{"version":"4445f6ce6289c5b2220398138da23752fd84152c5c95bb8b58dedefc1758c036","impliedFormat":1},{"version":"c39866300d394f45f82d3352ecfc1649aa1d5a47decdc5c76708cde15251c43d","impliedFormat":1},{"version":"151ff381ef9ff8da2da9b9663ebf657eac35c4c9a19183420c05728f31a6761d","impliedFormat":1},{"version":"8ccaa1a30e1c213a5ea06fc5388cc0846026c179d1400eceef42f94db200fc90","affectsGlobalScope":true,"impliedFormat":1},{"version":"a660aa95476042d3fdcc1343cf6bb8fdf24772d31712b1db321c5a4dcc325434","impliedFormat":1},{"version":"282f98006ed7fa9bb2cd9bdbe2524595cfc4bcd58a0bb3232e4519f2138df811","impliedFormat":1},{"version":"6222e987b58abfe92597e1273ad7233626285bc2d78409d4a7b113d81a83496b","impliedFormat":1},{"version":"cbe726263ae9a7bf32352380f7e8ab66ee25b3457137e316929269c19e18a2be","impliedFormat":1},{"version":"8b96046bf5fb0a815cba6b0880d9f97b7f3a93cf187e8dcfe8e2792e97f38f87","impliedFormat":99},{"version":"bacf2c84cf448b2cd02c717ad46c3d7fd530e0c91282888c923ad64810a4d511","affectsGlobalScope":true,"impliedFormat":1},{"version":"402e5c534fb2b85fa771170595db3ac0dd532112c8fa44fc23f233bc6967488b","impliedFormat":1},{"version":"8885cf05f3e2abf117590bbb951dcf6359e3e5ac462af1c901cfd24c6a6472e2","impliedFormat":1},{"version":"333caa2bfff7f06017f114de738050dd99a765c7eb16571c6d25a38c0d5365dc","impliedFormat":1},{"version":"e61df3640a38d535fd4bc9f4a53aef17c296b58dc4b6394fd576b808dd2fe5e6","impliedFormat":1},{"version":"459920181700cec8cbdf2a5faca127f3f17fd8dd9d9e577ed3f5f3af5d12a2e4","impliedFormat":1},{"version":"4719c209b9c00b579553859407a7e5dcfaa1c472994bd62aa5dd3cc0757eb077","impliedFormat":1},{"version":"7ec359bbc29b69d4063fe7dad0baaf35f1856f914db16b3f4f6e3e1bca4099fa","impliedFormat":1},{"version":"70790a7f0040993ca66ab8a07a059a0f8256e7bb57d968ae945f696cbff4ac7a","impliedFormat":1},{"version":"d1b9a81e99a0050ca7f2d98d7eedc6cda768f0eb9fa90b602e7107433e64c04c","impliedFormat":1},{"version":"a022503e75d6953d0e82c2c564508a5c7f8556fad5d7f971372d2d40479e4034","impliedFormat":1},{"version":"b215c4f0096f108020f666ffcc1f072c81e9f2f95464e894a5d5f34c5ea2a8b1","impliedFormat":1},{"version":"644491cde678bd462bb922c1d0cfab8f17d626b195ccb7f008612dc31f445d2d","impliedFormat":1},{"version":"dfe54dab1fa4961a6bcfba68c4ca955f8b5bbeb5f2ab3c915aa7adaa2eabc03a","impliedFormat":1},{"version":"1251d53755b03cde02466064260bb88fd83c30006a46395b7d9167340bc59b73","impliedFormat":1},{"version":"47865c5e695a382a916b1eedda1b6523145426e48a2eae4647e96b3b5e52024f","impliedFormat":1},{"version":"4cdf27e29feae6c7826cdd5c91751cc35559125e8304f9e7aed8faef97dcf572","impliedFormat":1},{"version":"331b8f71bfae1df25d564f5ea9ee65a0d847c4a94baa45925b6f38c55c7039bf","impliedFormat":1},{"version":"2a771d907aebf9391ac1f50e4ad37952943515eeea0dcc7e78aa08f508294668","impliedFormat":1},{"version":"0146fd6262c3fd3da51cb0254bb6b9a4e42931eb2f56329edd4c199cb9aaf804","impliedFormat":1},{"version":"183f480885db5caa5a8acb833c2be04f98056bdcc5fb29e969ff86e07efe57ab","impliedFormat":99},{"version":"82e687ebd99518bc63ea04b0c3810fb6e50aa6942decd0ca6f7a56d9b9a212a6","impliedFormat":99},{"version":"7f698624bbbb060ece7c0e51b7236520ebada74b747d7523c7df376453ed6fea","impliedFormat":1},{"version":"8f07f2b6514744ac96e51d7cb8518c0f4de319471237ea10cf688b8d0e9d0225","impliedFormat":1},{"version":"257b83faa134d971c738a6b9e4c47e59bb7b23274719d92197580dd662bfafc3","impliedFormat":99},{"version":"a28ac3e717907284b3910b8e9b3f9844a4e0b0a861bea7b923e5adf90f620330","impliedFormat":1},{"version":"b6d03c9cfe2cf0ba4c673c209fcd7c46c815b2619fd2aad59fc4229aaef2ed43","impliedFormat":1},{"version":"82e5a50e17833a10eb091923b7e429dc846d42f1c6161eb6beeb964288d98a15","impliedFormat":1},{"version":"670a76db379b27c8ff42f1ba927828a22862e2ab0b0908e38b671f0e912cc5ed","impliedFormat":1},{"version":"13b77ab19ef7aadd86a1e54f2f08ea23a6d74e102909e3c00d31f231ed040f62","impliedFormat":1},{"version":"069bebfee29864e3955378107e243508b163e77ab10de6a5ee03ae06939f0bb9","impliedFormat":1},{"version":"26e0ffceb2198feb1ef460d5d14111c69ad07d44c5a67fd4bfeb74c969aa9afb","impliedFormat":99},{"version":"2d8b8a9c0291a6635f5efef200ae0fae8bfd0f5d57b3e456125e71aea661be6a","signature":"4b96dd19fd2949d28ce80e913412b0026dc421e5bf6c31d87c7b5eb11b5753b4"}],"root":[194],"options":{"composite":true,"module":99},"referencedMap":[[189,1],[187,2],[192,3],[188,1],[190,4],[191,1],[155,2],[102,5],[103,5],[104,6],[54,7],[105,8],[106,9],[107,10],[52,2],[108,11],[109,12],[110,13],[111,14],[112,15],[113,16],[114,16],[115,17],[116,18],[117,19],[118,20],[55,2],[53,2],[119,21],[120,22],[121,23],[154,24],[122,25],[123,26],[124,27],[125,28],[126,29],[127,30],[128,31],[129,32],[130,33],[131,34],[132,34],[133,35],[134,2],[135,2],[136,36],[138,37],[137,38],[139,39],[140,40],[141,41],[142,42],[143,43],[144,44],[145,45],[146,46],[147,47],[148,48],[149,49],[150,50],[151,51],[56,2],[57,2],[58,2],[99,52],[100,2],[101,2],[152,53],[153,54],[193,55],[162,2],[179,56],[177,57],[178,58],[166,59],[167,57],[174,60],[165,61],[170,62],[180,2],[171,63],[176,64],[182,65],[181,66],[164,67],[172,68],[173,69],[168,70],[175,56],[169,71],[157,72],[156,73],[163,2],[1,2],[50,2],[51,2],[9,2],[13,2],[12,2],[3,2],[14,2],[15,2],[16,2],[17,2],[18,2],[19,2],[20,2],[21,2],[4,2],[22,2],[23,2],[5,2],[24,2],[28,2],[25,2],[26,2],[27,2],[29,2],[30,2],[31,2],[6,2],[32,2],[33,2],[34,2],[35,2],[7,2],[39,2],[36,2],[37,2],[38,2],[40,2],[8,2],[41,2],[46,2],[47,2],[42,2],[43,2],[44,2],[45,2],[2,2],[48,2],[49,2],[11,2],[10,2],[76,74],[87,75],[74,76],[88,77],[97,78],[65,79],[66,80],[64,81],[96,82],[91,83],[95,84],[68,85],[84,86],[67,87],[94,88],[62,89],[63,83],[69,90],[70,2],[75,91],[73,90],[60,92],[98,93],[89,94],[79,95],[78,90],[80,96],[82,97],[77,98],[81,99],[92,82],[71,100],[72,101],[83,102],[61,77],[86,103],[85,90],[90,2],[59,2],[93,104],[186,105],[183,106],[161,107],[159,108],[158,2],[160,109],[184,2],[185,110],[194,111]],"latestChangedDtsFile":"./vite.config.d.ts","version":"5.9.2"} \ No newline at end of file
diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo
new file mode 100644
index 0000000..fd99352
--- /dev/null
+++ b/frontend/tsconfig.tsbuildinfo
@@ -0,0 +1 @@
+{"root":["./src/app.tsx","./src/main.tsx","./src/types.ts","./src/components/bottombar.tsx","./src/components/choicemenu.tsx","./src/components/cityscapebackground.tsx","./src/components/configmodal.tsx","./src/components/dialoguebox.tsx","./src/components/landingpage.tsx","./src/components/loadingscreen.tsx","./src/components/topbar.tsx","./src/components/vnapp.tsx","./src/components/vninterface.tsx","./src/components/vnviewport.tsx","./src/services/ws.ts","./src/stores/index.ts"],"version":"5.9.2"} \ No newline at end of file
diff --git a/frontend/vite-env.d.ts b/frontend/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/frontend/vite-env.d.ts
@@ -0,0 +1 @@
+/// <reference types="vite/client" />
diff --git a/frontend/vite.config.d.ts b/frontend/vite.config.d.ts
new file mode 100644
index 0000000..340562a
--- /dev/null
+++ b/frontend/vite.config.d.ts
@@ -0,0 +1,2 @@
+declare const _default: import("vite").UserConfig;
+export default _default;
diff --git a/frontend/vite.config.js b/frontend/vite.config.js
new file mode 100644
index 0000000..db2454e
--- /dev/null
+++ b/frontend/vite.config.js
@@ -0,0 +1,6 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react()],
+});
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
new file mode 100644
index 0000000..5a33944
--- /dev/null
+++ b/frontend/vite.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react()],
+})