commit 5faca60a128650a3f92d26950e44148b98416f59 Author: Fredrik Johansson Date: Mon Jul 27 19:50:31 2026 +0200 Initial commit: Diariet A bureaucratic horror game — five shifts processing incoming tips and incident reports for a municipal office that officially handles nuisance complaints and unofficially exists to keep Djupet's cult off the front page. Route each report: dismiss, file as known, flag for follow-up, or escalate and lose visibility into what happens next. Guidelines accumulate and contradict across shifts, same shape as Retur's regulation stack. Directly descended from Retur's engine (routeLetter/nextDay -> routeReport/nextShift, same 4-route-per-item structure) but with one real mechanical difference: escalating always costs Insyn (insight/ notice), whether or not it was the correct call — being right doesn't make you invisible. Several reports cite real events from Djupet's own event pool (the bleeding IKEA LACK table, Göran added to the cult's Discord by accident, the Värnhemstorget pigeons, Turning Torso rotating), reframed as outside tips from residents who saw a fragment of something they don't understand. Same universe, a level further from the truth, and the player is explicitly never given the full picture — that's the design, not a limitation. Shares djupet/retur's exact :root palette but its own accent (cold institutional cyan instead of Retur's stamp-red or Djupet's eldritch purple). Intro/win/loss screens use real generated desk photos (rotary phone, corkboard map with red string, a ceiling water stain shaped like a coastline that grows between screens), converted PNG->WebP. Verified: tsc clean, 9/9 vitest (data-integrity check that every report has real consequence text, and a locked-in test for the escalation-always-costs-insight mechanic), a real browser playthrough confirming the wrong+escalate cost stacks correctly (-1 trust, +2 insight), and all three ending states screenshotted via injected game state. Co-Authored-By: Claude Sonnet 5 diff --git a/.gitea/workflows/docker.yml b/.gitea/workflows/docker.yml new file mode 100644 index 0000000..62eaff1 --- /dev/null +++ b/.gitea/workflows/docker.yml @@ -0,0 +1,62 @@ +name: Docker + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Test + run: | + npm install + npm test + + - name: Derive image name and tags + id: meta + shell: bash + run: | + set -euo pipefail + + SERVER_URL="${GITHUB_SERVER_URL:-${GITEA_SERVER_URL:-}}" + REPO="${GITHUB_REPOSITORY:-${GITEA_REPOSITORY:-}}" + SHA="${GITHUB_SHA:-${GITEA_SHA:-}}" + + if [[ -z "${SERVER_URL}" || -z "${REPO}" || -z "${SHA}" ]]; then + echo "Missing SERVER_URL/REPO/SHA env vars." >&2 + echo "SERVER_URL='${SERVER_URL}' REPO='${REPO}' SHA='${SHA}'" >&2 + exit 1 + fi + + HOST=$(echo "${SERVER_URL}" | sed 's|https://||;s|http://||') + IMAGE=$(echo "${HOST}/${REPO}" | tr '[:upper:]' '[:lower:]') + SHORT_SHA=$(echo "${SHA}" | cut -c1-7) + + echo "host=${HOST}" >> "$GITHUB_OUTPUT" + echo "image=${IMAGE}" >> "$GITHUB_OUTPUT" + echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT" + + - name: Log in to Gitea container registry + uses: docker/login-action@v3 + with: + registry: ${{ steps.meta.outputs.host }} + username: ${{ github.actor }} + password: ${{ secrets.TKNTKN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + push: true + tags: | + ${{ steps.meta.outputs.image }}:latest + ${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.short_sha }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ec22402 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +node_modules +dist +.env +*.local + +# Raw source images — public/*.webp are the optimized derivatives that +# actually ship; no need to bloat the repo with large originals. +/intro.png +/win.png +/gameover.png diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0331674 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM node:22-alpine AS build +WORKDIR /app +COPY package.json ./ +RUN npm install +COPY . . +RUN npm run build + +FROM nginx:alpine +COPY --from=build /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 diff --git a/README.md b/README.md new file mode 100644 index 0000000..30a8a1d --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# Diariet + +**Du får aldrig hela bilden. Det är jobbet.** + +A short bureaucratic horror game. You process five shifts of incoming tips and incident reports for a municipal office that officially handles nuisance complaints and unofficially exists to keep [Djupet](https://repo.explewd.com/explewd/djupet)'s cult — and whatever it's actually in contact with — off the front page. Read each report, consult an accumulating and occasionally contradictory set of guidelines, then route it: dismiss it, file it as known, flag it for your own team, or escalate it upward and lose visibility into what happens next. + +You are never told the full story. Neither is the player. + +## Where this sits in the family + +Same visual lineage as [`djupet`](https://repo.explewd.com/explewd/djupet) and [`retur`](https://repo.explewd.com/explewd/retur) — identical `:root` palette (`#0d0d1a` background, the same borders and text colors) — but its own accent color (a cold institutional cyan, `#4fc3d9`, instead of Retur's stamp-red or Djupet's eldritch purple) and its own mechanical shape: `retur`'s engine (`routeLetter`/`nextDay`) is the direct ancestor of this one (`routeReport`/`nextShift`), same 4-route-per-item, trust/insight-instead-of-composure/suspicion, day-by-day guideline accumulation structure. + +The reports aren't invented from nothing — several cite real events from `djupet`'s own event pool (the bleeding IKEA LACK table, Göran accidentally added to the cult's Discord, the Värnhemstorget pigeons, Turning Torso rotating), reframed as outside tips filed by ordinary Malmö residents who saw a fragment of something they don't understand. Same universe, different desk, a level further from the truth. + +## The mechanical twist + +Unlike Retur, **escalating always costs you something**, whether or not it was the correct call — the *Insyn* (insight/notice) meter rises by one every time you choose `ESKALERA`, on top of any wrong-route penalty. The regulations sometimes tell you the right move is to escalate; doing so is never free. That's the core tension: being correct doesn't make you invisible. + +## Development + +```bash +npm install +npm run dev # http://localhost:5692 +npm run typecheck +npm test +npm run build +``` + +## Stack + +Vanilla TypeScript + Vite, zero runtime dependencies, `localStorage` only — no accounts, no backend. Same Docker/nginx/Gitea Actions shape as the rest of this family's small static games. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..07859a7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,6 @@ +services: + app: + image: ${IMAGE:-diariet:local} + restart: unless-stopped + ports: + - "${PORT:-3011}:80" diff --git a/index.html b/index.html new file mode 100644 index 0000000..b337b3b --- /dev/null +++ b/index.html @@ -0,0 +1,18 @@ + + + + + + Diariet + + + + + + + + +
Laddar…
+ + + diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..a676565 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,10 @@ +server { + listen 80; + root /usr/share/nginx/html; + index index.html; + location / { + try_files $uri $uri/ /index.html; + } + gzip on; + gzip_types text/css application/javascript application/json image/svg+xml; +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..29e0633 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1508 @@ +{ + "name": "diariet", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "diariet", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "typescript": "~5.6.2", + "vite": "^6.4.3", + "vitest": "^4.1.9" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "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, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.3.tgz", + "integrity": "sha512-c0wdcekXtQvvn5Tsrk/+op/gUArrbWaFduBnTLP2l1cKLSQs4diMWjJw3m6A0DdzT8dAAX95KpkJ3qynCePbmw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.3.tgz", + "integrity": "sha512-3YjElDdWN+qXAFbJ/CzPV+0wspLqh54k/I6GfdYtEJRqg7buSgc1yPM3B+93j1M4neobtkATHZTmxK2AMVGfnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.3.tgz", + "integrity": "sha512-Pch2pFNOxxz1hTjypIdPyRTR6riiwRl84+VcN9djS680fw+Co1nAJINrdpqp7KV0NvyuU8ilZXZCjd7ykJl1GQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.3.tgz", + "integrity": "sha512-LEuncFUHFiF8t4yZVZvvZA1wk0pjAscRnsrn1EfTEmN4HXotBi2YtcnLRyaK6UbuczW7xZS5ES+81Rdz8Z0T6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.3.tgz", + "integrity": "sha512-zvBUvsQUpOWALdDsk6qbS8bXf2VxmPisuudNDrY7x0p0jBdsoZl8HsHczIOgkQiZldmcacMKtBzpoGVNeIe2bQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.3.tgz", + "integrity": "sha512-C2KmNrcSem/AMg984H/dev+si0lieQGdXdR/lYGJnuumXnFb9Y7QdiI62obFdLlxRYLBv4P0eUVIDbD4c1vVvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.3.tgz", + "integrity": "sha512-ggXnsTAEzNQx74XpunRsiZ9aBZDsI7XIa0hm2nzR9f4WzH5/f/d73ZSDaC5ejJ8YLY4NW+V3wr0tjOaeCq8hqA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.3.tgz", + "integrity": "sha512-2vng+FlzNUhKZxtej3IUqJgbZoQk2M/dwQM20+ULV0R/E/8tr9/P6uEf2iiGIk4HL0zMKh5Jry7mUHdUOvyGgA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.3.tgz", + "integrity": "sha512-LLLFZKt4/Nraf9rxDkhiU8QVgLF4WmCkfr0L4fj0fPfIZFBib0DeiFk1hhaYKd03LFAFJcxHslhDFlNJLylf5Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.3.tgz", + "integrity": "sha512-WJkdQCvS9sWNOUBJZfQRKpZGFBztRzcowI+nndmflKgU4XY+3a420FgTOSKTsVqJbnzSxeT4vaJalpOaPo2YCQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.3.tgz", + "integrity": "sha512-PwHXCCS2n64/1Ot6rP1YEYA02MGYBcQlr8CSZZyrUG2O7NH6NklYmvr9v3Jy+5e/eDeNchc/ukmKJi9LuflMIQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.3.tgz", + "integrity": "sha512-vUjxINQu3RC8NZS3ykk1gN65gIz8pAopOq2HXuZhiIxHdx7TFvDG+jgrdSgInu1Eza4/Rfi2VzZgyIgEH4WOaw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.3.tgz", + "integrity": "sha512-wzko4aJ13+0G3kGnviCg5gnXFKd40izKsrf2uOw12US4XqprkDrmwOpeW14aSNa37V8bfPcz5Fkob6LZ3BAPmA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.3.tgz", + "integrity": "sha512-8120ue0JUMSwy11stlwnfdX3pPd+WZYGCDBwEHWtIHi6pOpZmsEF5QKB7a/UN+XFdqvobxz98kv8RTqikyCEBw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.3.tgz", + "integrity": "sha512-XLFHnR3tXMjbOCh2vtVJHmxt+995uJsTERQyseFDRA0xxMxyTZPLa3OIUlyFaO4mF/Lu0FjmWHCuPXJT1n/IOg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.3.tgz", + "integrity": "sha512-se6yXvNGMIl0f+RQzyh7XAmia8/9kplQx424wnG2w0C1oi6XgO6Y8otKhdXFHbHs88Ihavzmvh1NWjuovE76BQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.3.tgz", + "integrity": "sha512-gNoxRefktVIiGflpONuxWWXZAzIQG++z9qHO3xKwk4WdDMuQja3JHGfE1u0i3PfPDyvhypdk+WrgIJqLhGG7sg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.3.tgz", + "integrity": "sha512-V4KtWtQfAFMU7+9/A/VDps/VI8CHd3cYz0L8sgJzz8qK7eY7wI4ruFD82UYIYvW9Z4DtlTfhQcsl4XyPHW5uSg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.3.tgz", + "integrity": "sha512-LBx9LYXvj2CBkMkjLdNAWLwH0MLMin7do2VcVo9kVPibGLkY0BQQut2fv7NVqkXqZ/CrAu9LqDHVV1xHCMpCPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.3.tgz", + "integrity": "sha512-ABVf3Q0RCu7NcyCCOZQI0pJ3GuSdfSl8EXcy88QtdceIMIoCUdfhsJChZ64L9zVM2aJHjde1Bhn5uqSRcX9ySA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.3.tgz", + "integrity": "sha512-+2Cy/ldweGBLlPIKsQLF8U5N44a0KDdbrk1rAjHOM9M2K+kGdIVjHLmmrZIcx+9Ny3ke/1JomCsDI1ocb11+sg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.3.tgz", + "integrity": "sha512-dtZvzc8BedpSaFNy75x6uiWwAGTH+aZHDtdrqP6qk+WcLJrfti6sGje1ZJ9UxyzDLF23d/mV+PaMwuC0hL7UVA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.3.tgz", + "integrity": "sha512-Rj8Ra4noo+aYy7sKBggCx0407mws34kAb1ySyWuq5DAtFBQdkSwnsjCgPrhPe9cvgBKZIukpE+CVHvORCS93kQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.3.tgz", + "integrity": "sha512-vp7N084ew/odXn2gi/mzm9mUkQu9l6AiN6dt4IeUM2Uvm9o+cVmP+YkqbMOteLbiGgqBBlJZjIMYVCfOOIVbVQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.3.tgz", + "integrity": "sha512-MOG/3gTOn4Fwf574RVOaY61I5o6P90legkFADiTyn1hyjNydT+cerU2rLUwPdZkKKyJ+iT+K9p7WXK4LM1Ka6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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, + "license": "MIT" + }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "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, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", + "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" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.3.tgz", + "integrity": "sha512-Gu0c0iH9FzgX1L1t7ByIbbS3Vmdz+6KHm/EsqmmC71gUQ82yvZRkTK6XzrFObSka91WUVdynqp6nsfilzr5k6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.3", + "@rollup/rollup-android-arm64": "4.62.3", + "@rollup/rollup-darwin-arm64": "4.62.3", + "@rollup/rollup-darwin-x64": "4.62.3", + "@rollup/rollup-freebsd-arm64": "4.62.3", + "@rollup/rollup-freebsd-x64": "4.62.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.3", + "@rollup/rollup-linux-arm-musleabihf": "4.62.3", + "@rollup/rollup-linux-arm64-gnu": "4.62.3", + "@rollup/rollup-linux-arm64-musl": "4.62.3", + "@rollup/rollup-linux-loong64-gnu": "4.62.3", + "@rollup/rollup-linux-loong64-musl": "4.62.3", + "@rollup/rollup-linux-ppc64-gnu": "4.62.3", + "@rollup/rollup-linux-ppc64-musl": "4.62.3", + "@rollup/rollup-linux-riscv64-gnu": "4.62.3", + "@rollup/rollup-linux-riscv64-musl": "4.62.3", + "@rollup/rollup-linux-s390x-gnu": "4.62.3", + "@rollup/rollup-linux-x64-gnu": "4.62.3", + "@rollup/rollup-linux-x64-musl": "4.62.3", + "@rollup/rollup-openbsd-x64": "4.62.3", + "@rollup/rollup-openharmony-arm64": "4.62.3", + "@rollup/rollup-win32-arm64-msvc": "4.62.3", + "@rollup/rollup-win32-ia32-msvc": "4.62.3", + "@rollup/rollup-win32-x64-gnu": "4.62.3", + "@rollup/rollup-win32-x64-msvc": "4.62.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "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, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.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 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..0fd4d10 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "diariet", + "private": true, + "version": "0.1.0", + "license": "MIT", + "type": "module", + "scripts": { + "dev": "vite", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "devDependencies": { + "typescript": "~5.6.2", + "vite": "^6.4.3", + "vitest": "^4.1.9" + } +} diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..f2ebf77 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/gameover.webp b/public/gameover.webp new file mode 100644 index 0000000..1c38088 Binary files /dev/null and b/public/gameover.webp differ diff --git a/public/intro.webp b/public/intro.webp new file mode 100644 index 0000000..49f8071 Binary files /dev/null and b/public/intro.webp differ diff --git a/public/survived.webp b/public/survived.webp new file mode 100644 index 0000000..933bb86 Binary files /dev/null and b/public/survived.webp differ diff --git a/src/data/content.ts b/src/data/content.ts new file mode 100644 index 0000000..6d9cadc --- /dev/null +++ b/src/data/content.ts @@ -0,0 +1,357 @@ +import type { Guideline, Report, Route } from '../types.ts'; + +export const ROUTES: Record = { + avfarda: { label: 'AVFÄRDA', stamp: 'AVFÄRDAD', description: 'Ingen grund för åtgärd. Arkiveras inte ens.' }, + arkivera: { label: 'ARKIVERA', stamp: 'KATALOGISERAD', description: 'Känt fenomen. Förs till katalogen utan vidare åtgärd.' }, + utred: { label: 'UTRED', stamp: 'FLAGGAD', description: 'Flaggas för uppföljning av vår egen enhet.' }, + eskalera: { label: 'ESKALERA', stamp: 'ÖVERLÄMNAD', description: 'Överlämnas uppåt. Du får inte veta vad som händer sedan.' }, +}; + +export const GUIDELINES: Guideline[] = [ + { id: 'g1', fromDay: 1, number: '1.1', text: 'Enstaka, obekräftade sinnesintryck utan vittnen: avfärda.' }, + { id: 'g2', fromDay: 1, number: '1.2', text: 'Redan katalogiserade platser eller föremål utan ny avvikelse: arkivera.' }, + { id: 'g3', fromDay: 1, number: '1.3', text: 'Två eller fler oberoende källor som beskriver samma händelse: utred.' }, + { id: 'g4', fromDay: 1, number: '1.4', text: 'Strukturella förändringar i byggnader, broar eller himlakroppar: eskalera omedelbart.' }, + { id: 'g5', fromDay: 2, number: '2.1', text: 'Föremål som "blöder", "andas" eller "sjunger" och redan är katalogiserade: arkivera.' }, + { id: 'g6', fromDay: 2, number: '2.2', text: 'Om fenomenet är av en typ som saknas i katalogen: eskalera, oavsett övrigt innehåll.', note: 'Senare bestämmelse har företräde framför §2.1.' }, + { id: 'g7', fromDay: 2, number: '2.3', text: 'Anmälningar från minderåriga (under 12 år): utred, oavsett innehåll i övrigt.' }, + { id: 'g8', fromDay: 3, number: '3.1', text: 'Kommunalt anställd som "slutat svara" eller "slutat dyka upp": utred.' }, + { id: 'g9', fromDay: 3, number: '3.2', text: 'Om samma person tidigare förekommit i ett arkiverat ärende: eskalera.', note: 'Kontinuitet väger tyngre än en enskild händelse.' }, + { id: 'g10', fromDay: 3, number: '3.3', text: 'Anmälare som uppger en specifik, mätbar detalj (tid, avstånd, antal): utred, även om övrigt innehåll annars pekar mot avfärdande.' }, + { id: 'g11', fromDay: 4, number: '4.1', text: 'Flera obesläktade personer som delar samma dröm eller syn samma natt: eskalera.' }, + { id: 'g12', fromDay: 4, number: '4.2', text: 'Ärende som gäller samma plats som ett tidigare ärende denna vecka: arkivera som pågående, inte som nytt.' }, + { id: 'g13', fromDay: 4, number: '4.3', text: 'Anmälarens önskan om anonymitet är inte i sig ett skäl att eskalera. Behandla ärendet efter innehåll.' }, + { id: 'g14', fromDay: 5, number: '5.1', text: 'Ärenden som nämner denna avdelning eller handläggaren vid namn: arkivera internt. Utred aldrig vidare.' }, + { id: 'g15', fromDay: 5, number: '5.2', text: 'Ärenden daterade efter dagens datum, eller som beskriver något som ännu inte inträffat: eskalera utan att läsa färdigt.' }, + { id: 'g16', fromDay: 5, number: '5.3', text: 'Ärenden vars text skiljer sig mellan första och andra läsningen: eskalera.', note: 'Läs det inte en tredje gång.' }, +]; + +export const REPORTS: Report[] = [ + // ---- Day 1 ---- + { + id: 'd01', day: 1, source: 'Anonym', location: 'Möllevångstorget', filed: '2026-08-03', + body: 'Det luktar bränt tång vid torget på morgnarna. Ingen annan verkar märka det. Jag har inte pratat med grannarna om det.', + marks: ['ENSTAKA VITTNE'], + correctRoute: 'avfarda', explanation: 'Ett obekräftat sinnesintryck utan vittnen. §1.1.', + correctConsequence: 'Avfärdad. Lukten nämns inte igen i någon inkommen rapport den här veckan.', + wrongConsequences: { + utred: 'Flaggat i onödan. Uppföljningen bekräftade bara att torget luktar som torg gör.', + arkivera: 'Katalogiserat trots att det inte fanns något att katalogisera. Nästa handläggare kommer undra varför.', + eskalera: 'Överlämnat på svag grund. Ingen bekräftelse kom tillbaka. Det gör den sällan.', + }, + }, + { + id: 'd02', day: 1, source: 'Systembolaget, Davidshallsgatan', location: 'Davidshallsgatan', filed: '2026-08-03', + body: 'En kund erbjöd sig högljutt att "hälsa på gubben i havet" till personalen. Personalen bad honom lämna butiken. Han gick utan protest.', + marks: ['KÄND LOKAL'], + correctRoute: 'arkivera', explanation: 'Platsen och fenomenet är redan katalogiserade utan ny avvikelse. §1.2.', + correctConsequence: 'Katalogiserat under befintlig post. Ingen ny bilaga krävdes.', + wrongConsequences: { + avfarda: 'Avfärdat trots att posten redan finns i katalogen. Katalogen är nu inaktuell på en punkt.', + utred: 'Flaggat för uppföljning av något som redan är känt. Uppföljningen bekräftade det redan kända.', + eskalera: 'Överlämnat i onödan. Det som kom tillbaka var en kopia av den befintliga katalogposten, med en notering: "Se ovan."', + }, + }, + { + id: 'd03', day: 1, source: 'Två oberoende passagerare', location: 'Buss 5, Värnhemstorget', filed: '2026-08-03', + body: 'Bussen körde förbi hållplatsen tre gånger utan att stanna. Ingen av oss märkte det förrän vi jämförde anteckningar efteråt. Föraren såg nöjd ut, enligt båda.', + marks: ['TVÅ KÄLLOR', 'OBEROENDE'], + correctRoute: 'utred', explanation: 'Två oberoende källor beskriver samma händelse. §1.3.', + correctConsequence: 'Flaggat. Uppföljningsteamet noterar föraren för fortsatt observation, ingen ytterligare åtgärd än så länge.', + wrongConsequences: { + avfarda: 'Avfärdat trots två oberoende vittnesmål. Ett tredje vittne hörde av sig veckan efter. Det fanns ingen anteckning att jämföra med.', + arkivera: 'Katalogiserat som känt fenomen. Det är det inte, ännu.', + eskalera: 'Överlämnat för en enstaka bussrunda. Det kom tillbaka omärkt, utan kommentar.', + }, + }, + { + id: 'd04', day: 1, source: 'Arkitektkontoret', location: 'Turning Torso', filed: '2026-08-03', + body: 'Byggnaden roterar. Den har inte gjort det på tre år och är inte konstruerad för att göra det alls. Vi har ingen förklaring och kommer inte ringa igen.', + marks: ['STRUKTURELL AVVIKELSE'], + correctRoute: 'eskalera', explanation: 'Strukturell förändring i en byggnad. Eskaleras omedelbart enligt §1.4.', + correctConsequence: 'Överlämnat samma timme. Ingen bekräftelse kom tillbaka. Byggnaden slutade rotera dagen efter, enligt lokalnyheterna, som inte nämnde varför.', + wrongConsequences: { + avfarda: 'Avfärdat. Arkitektkontoret ringde aldrig igen, precis som de sa. Ingen annan gjorde det heller.', + arkivera: 'Katalogiserat som känt fenomen. Det var första gången det hände. Det blev inte den sista.', + utred: 'Flaggat för uppföljning i stället för omedelbar eskalering. Uppföljningen genomfördes tre dagar för sent.', + }, + penalty: { arkivera: { insight: 2 } }, + }, + { + id: 'd05', day: 1, source: 'Anonym', location: 'Bellevuegatan', filed: '2026-08-03', + body: 'Jag drömde att jag gick längs en gata jag inte känner igen och räknade dörrarna. Det var 14 stycken. Jag vet inte varför jag skriver in det här.', + marks: ['ENSTAKA VITTNE', 'DRÖM'], + correctRoute: 'avfarda', explanation: 'En enda persons obekräftade dröm. §1.1 gäller trots den specifika detaljen — ingen annan källa bekräftar den.', + correctConsequence: 'Avfärdat. Drömmar utan andra källor är inte handlingsbara, hur specifika de än är.', + wrongConsequences: { + utred: 'Flaggat på grund av en enda dröm. Det finns inget att följa upp förrän någon annan drömmer samma sak.', + arkivera: 'Katalogiserat i förtid. Posten fick ligga tom i väntan på fler källor som aldrig kom den veckan.', + eskalera: 'Överlämnat på grundval av en dröm. Det kom tillbaka med en enda rad: "Vänta på fler."', + }, + }, + + // ---- Day 2 ---- + { + id: 'd06', day: 2, source: 'Hyresgäst', location: 'Rosengård', filed: '2026-08-04', + body: 'Bordet, modell LACK, har börjat blöda en svart vätska från en led i skivan. Vi har sett det förut i grannens lägenhet för två år sedan. Vi har lagt en handduk över det.', + marks: ['KÄND FÖREMÅLSTYP'], + correctRoute: 'arkivera', explanation: 'Katalogiserad föremålstyp utan ny avvikelse. §2.1.', + correctConsequence: 'Katalogiserat under befintlig post för möbeltypen. Handduken rekommenderas fortsätta ligga kvar.', + wrongConsequences: { + eskalera: 'Överlämnat trots känd föremålstyp. Det som kom tillbaka var en kopia av den befintliga posten med en irriterad ton.', + avfarda: 'Avfärdat trots att katalogen redan har en post för detta. Nästa liknande anmälan blir svårare att placera.', + utred: 'Flaggat för uppföljning av ett väldokumenterat fenomen. Uppföljningen bekräftade det redan kända, långsammare än nödvändigt.', + }, + }, + { + id: 'd07', day: 2, source: 'Förälder', location: 'Husie', filed: '2026-08-04', + body: 'Min sons leksaksbil har börjat surra en melodi när ingen rör den. Det är inte en melodi som finns i leksaken. Den går på batteri som togs ur för en vecka sedan.', + marks: ['OKATALOGISERAD TYP'], + correctRoute: 'eskalera', explanation: 'Fenomentypen saknas i katalogen — en leksaksbil utan batteri som spelar en okänd melodi är inte samma post som ett blödande bord. §2.2 har företräde framför §2.1.', + correctConsequence: 'Överlämnat. Ingen bekräftelse kom tillbaka. Leksaken nämns inte i någon senare rapport från samma familj.', + wrongConsequences: { + arkivera: 'Katalogiserat under "blödande/andande föremål" trots att det är fel kategori. Katalogen är nu felaktig på en punkt som någon kommer lita på senare.', + avfarda: 'Avfärdat som barnprat. §2.3 hade ändå krävt uppföljning eftersom anmälaren är förälder till en minderårig, inte den minderåriga själv — men innehållet krävde mer än så.', + utred: 'Flaggat i stället för eskalerat. En vecka senare fanns varken leksaken eller familjens svar på uppföljningen.', + }, + penalty: { arkivera: { trust: 2 } }, + }, + { + id: 'd08', day: 2, source: 'Elev, 11 år', location: 'Värnhemstorget', filed: '2026-08-04', + body: 'Duvorna på torget har slutat äta. De bara sitter och tittar. En av dem tittade på mig längre än en duva borde kunna.', + marks: ['MINDERÅRIG ANMÄLARE'], + correctRoute: 'utred', explanation: 'Anmälare under 12 år. §2.3 kräver uppföljning oavsett innehåll.', + correctConsequence: 'Flaggat. Uppföljningen noterar duvorna för fortsatt observation. Eleven kontaktas inte igen.', + wrongConsequences: { + avfarda: 'Avfärdat trots att anmälaren är minderårig. §2.3 är inte förhandlingsbar av en anledning som inte står i regelverket.', + arkivera: 'Katalogiserat för tidigt. Det finns ingen tidigare post för stillasittande duvor att katalogisera under.', + eskalera: 'Överlämnat för en duvobservation. Det kom tillbaka oöppnat.', + }, + penalty: { avfarda: { trust: 2 } }, + }, + { + id: 'd09', day: 2, source: 'Kommunen, Parkförvaltningen', location: 'Värnhemstorget', filed: '2026-08-04', + body: 'Fontänen rinner baklänges igen. Vi satte upp samma staket som förra gången. Folk tar fortfarande selfies.', + marks: ['ÅTERKOMMANDE', 'KÄND POST'], + correctRoute: 'arkivera', explanation: 'Fenomenet är redan katalogiserat sedan tidigare tillfälle. §2.1.', + correctConsequence: 'Katalogiserat som återkommande post. Staketet noteras som fortsatt ineffektivt, vilket redan var känt.', + wrongConsequences: { + eskalera: 'Överlämnat för en känd, återkommande post. Svaret var kort: "Se tidigare ärende."', + utred: 'Flaggat för uppföljning av något som redan är dokumenterat två gånger om.', + avfarda: 'Avfärdat trots befintlig katalogpost. Parkförvaltningen hör av sig igen nästa gång, till en handläggare som inte minns detta.', + }, + }, + { + id: 'd10', day: 2, source: 'Nattvakt', location: 'Frihamnen', filed: '2026-08-04', + body: 'En container i hamnen har böjts inåt, som om något tryckte på den utifrån, utan en enda buckla på utsidan. Vi har inte sett den här sortens skada förut.', + marks: ['OKATALOGISERAD TYP'], + correctRoute: 'eskalera', explanation: 'En ny fenomentyp — skada utan yttre orsak. Ingen befintlig katalogpost matchar. §2.2.', + correctConsequence: 'Överlämnat. Containern flyttades av okänd part inom tolv timmar. Ingen fraktsedel för flytten hittades.', + wrongConsequences: { + arkivera: 'Katalogiserat trots att ingen matchande post finns. Nästa handläggare kommer leta efter den förgäves.', + avfarda: 'Avfärdat som materialfel. Materialet i fråga tål betydligt mer än så här.', + utred: 'Flaggat för uppföljning. Containern var borta innan uppföljningen hann genomföras.', + }, + penalty: { avfarda: { insight: 1 } }, + }, + + // ---- Day 3 ---- + { + id: 'd11', day: 3, source: 'Kollega, SMHI', location: 'Malmöhus fyr', filed: '2026-08-05', + body: 'Vår mätstationsansvarige vid fyren har slutat svara i telefon och på mejl sedan i tisdags. Hans ersättare rapporterar att vattnet "sjunger" på nätterna nu. Hon har också slutat höra av sig.', + marks: ['KOMMUNALT ANSTÄLLD'], + correctRoute: 'utred', explanation: 'Kommunalt/statligt anställd som slutat svara. §3.1. Ingen tidigare koppling till ett arkiverat ärende finns ännu.', + correctConsequence: 'Flaggat för uppföljning. Namnet noteras i systemet för framtida kontroll mot arkivet.', + wrongConsequences: { + avfarda: 'Avfärdat. Ingen mer information kom in om personen förrän ett annat ärende nämnde honom igen — då var det redan för sent att bara flagga det.', + arkivera: 'Katalogiserat för tidigt. Det finns ännu ingen post att katalogisera under.', + eskalera: 'Överlämnat direkt, utan att först flagga för uppföljning enligt rutin. Ingen förklaring kom tillbaka om varför det gick snabbare den här gången.', + }, + }, + { + id: 'd12', day: 3, source: 'Länsstyrelsen', location: 'Malmöhus fyr', filed: '2026-08-05', + body: 'Namnet på mätstationsansvarige vid fyren återfinns i vårt eget arkiv, ärende 0311, avslutat för fem veckor sedan under rubriken "vattennivå, tekniskt fel".', + marks: ['ÅTERKOMMANDE NAMN', 'KORSREFERENS'], + correctRoute: 'eskalera', explanation: 'Samma person har tidigare förekommit i ett arkiverat ärende. Kontinuitet väger tyngre än den enskilda händelsen. §3.2.', + correctConsequence: 'Överlämnat med hänvisning till ärende 0311. Ingen bekräftelse kom tillbaka. Ärende 0311 markerades som "under granskning" av någon annan än dig.', + wrongConsequences: { + utred: 'Flaggat för ytterligare uppföljning i stället för att eskalera trots den bekräftade kopplingen. §3.2 är tydlig på den punkten.', + arkivera: 'Katalogiserat som avslutat. Kopplingen till ärende 0311 innebär motsatsen.', + avfarda: 'Avfärdat trots en bekräftad korsreferens till ett tidigare arkiverat ärende. Det är precis den kombinationen §3.2 finns för att fånga.', + }, + penalty: { avfarda: { trust: 2, insight: 1 } }, + }, + { + id: 'd13', day: 3, source: 'Pendlare', location: 'SJ, tunnel mot Köpenhamn', filed: '2026-08-05', + body: 'Tåget stod stilla i exakt tolv minuter i en tunnel som inte finns på någon karta jag hittat. Jag tog tid på det med mobilen. Vi anlände ändå i tid enligt tidtabellen.', + marks: ['MÄTBAR DETALJ'], + correctRoute: 'utred', explanation: 'En specifik, mätbar detalj (exakt tolv minuter, tidtagen) kräver uppföljning enligt §3.3, även om ärendet annars hade avfärdats.', + correctConsequence: 'Flaggat. Uppföljningen begär tågloggen från SJ. Loggen visar ingen tunnel på den sträckan över huvud taget.', + wrongConsequences: { + avfarda: 'Avfärdat trots den mätbara detaljen. §3.3 finns specifikt för att förhindra det.', + arkivera: 'Katalogiserat för tidigt. Ingen tidigare post om en obefintlig tunnel finns att katalogisera under.', + eskalera: 'Överlämnat för en enskild tidsangivelse utan korsreferens. Det kom tillbaka med frågan varför det inte flaggades för uppföljning först.', + }, + }, + { + id: 'd14', day: 3, source: 'Anonym', location: 'Rådmansgatan', filed: '2026-08-05', + body: 'Jag hade en obehaglig känsla när jag gick förbi porten i går kväll. Inget särskilt hände. Jag vet inte varför jag anmäler det.', + marks: ['ENSTAKA VITTNE'], + correctRoute: 'avfarda', explanation: 'Ett obekräftat, ospecifikt sinnesintryck utan mätbar detalj eller andra källor. §1.1 gäller fortfarande.', + correctConsequence: 'Avfärdat. Inget vidare hördes från adressen.', + wrongConsequences: { + utred: 'Flaggat för en känsla utan mätbar detalj eller andra källor. Uppföljningen hade inget att undersöka.', + arkivera: 'Katalogiserat utan att det finns något fenomen att katalogisera.', + eskalera: 'Överlämnat för en obehaglig känsla. Ingenting kom tillbaka, vilket i det här fallet troligen var rätt svar även om vägen dit inte var det.', + }, + }, + { + id: 'd15', day: 3, source: 'Granne', location: 'Sofielund', filed: '2026-08-05', + body: 'Lukten från källaren är tillbaka. Vi har klagat förut. De sa att det var kimchi. Det är inte kimchi, det har aldrig varit kimchi.', + marks: ['ENSTAKA VITTNE', 'ÅTERKOMMANDE KLAGOMÅL'], + correctRoute: 'avfarda', explanation: 'Enstaka vittne, ingen mätbar detalj, ingen strukturell förändring. Att klagomålet upprepats gör det inte till mer än §1.1 så länge det saknar korroborering.', + correctConsequence: 'Avfärdat, som förra gången. Grannen kommer sannolikt höra av sig igen.', + wrongConsequences: { + utred: 'Flaggat trots att inget i det upprepade klagomålet uppfyller kraven för uppföljning.', + arkivera: 'Katalogiserat under en förklaring ("kimchi") som anmälaren själv redan avfärdat som osann. Katalogen ärver nu den osanningen.', + eskalera: 'Överlämnat för ett återkommande lukt-klagomål utan ny information. Det kom tillbaka omärkt.', + }, + }, + + // ---- Day 4 ---- + { + id: 'd16', day: 4, source: 'Fem obesläktade anmälare', location: 'Malmö, spritt', filed: '2026-08-06', + body: 'Vi känner inte varandra. Vi drömde samma sak i natt: en stad under vatten där byggnaderna är rätt placerade men färgen är fel. Vi har alla ritat samma bild oberoende av varandra. Bilderna bifogas.', + marks: ['FLERA KÄLLOR', 'DELAD DRÖM'], + correctRoute: 'eskalera', explanation: 'Flera obesläktade personer som delar samma dröm samma natt. §4.1, utan undantag.', + correctConsequence: 'Överlämnat med samtliga fem teckningar bifogade. Ingen bekräftelse kom tillbaka. Teckningarna returnerades inte heller.', + wrongConsequences: { + utred: 'Flaggat för uppföljning i stället för omedelbar eskalering. §4.1 medger inget mellansteg när kriteriet väl är uppfyllt.', + arkivera: 'Katalogiserat som känt fenomen. Delade drömmar mellan obesläktade personer har ingen tidigare katalogpost.', + avfarda: 'Avfärdat trots fem oberoende, samstämmiga källor. Det är den starkaste sortens bevisning regelverket erkänner, och den avfärdades ändå.', + }, + penalty: { avfarda: { trust: 2, insight: 2 } }, + }, + { + id: 'd17', day: 4, source: 'Granne', location: 'Sofielund', filed: '2026-08-06', + body: 'Källarlukten från i förrgår är tillbaka, samma adress. Den är starkare nu. Någon borde komma och titta, inte bara skriva ner det igen.', + marks: ['SAMMA PLATS', 'DENNA VECKA'], + correctRoute: 'arkivera', explanation: 'Samma plats som ärende d15 tidigare denna vecka. Behandlas som pågående, inte som nytt ärende. §4.2.', + correctConsequence: 'Katalogiserat som pågående under befintlig post från tidigare i veckan, med notering om att lukten förstärkts.', + wrongConsequences: { + avfarda: 'Avfärdat som ett nytt, enskilt ärende. Kopplingen till adressen tidigare i veckan gick förlorad.', + utred: 'Flaggat som nytt ärende i stället för att kopplas till det pågående. Uppföljningen dubbelarbetade det redan kända.', + eskalera: 'Överlämnat för en källarlukt som redan är ett pågående, lågprioriterat ärende. Det kom tillbaka med en enda fråga: "Varför nu?"', + }, + }, + { + id: 'd18', day: 4, source: 'Anonym (begär sekretess)', location: 'Limhamn', filed: '2026-08-06', + body: 'Jag vill vara anonym av personliga skäl, inte för att det jag sett är farligt. Min katt vägrar gå in i ett av rummen i lägenheten. Det har hon aldrig gjort förut.', + marks: ['ANONYM'], + correctRoute: 'avfarda', explanation: 'Anonymitet är inte i sig ett skäl att eskalera. §4.3. Innehållet — en katt som undviker ett rum — uppfyller inget annat kriterium.', + correctConsequence: 'Avfärdat efter innehåll, inte efter anmälarens önskan om sekretess. Sekretessen respekteras ändå i akten.', + wrongConsequences: { + eskalera: 'Överlämnat på grund av begärd anonymitet, inte på grund av innehållet. §4.3 finns specifikt för att förhindra den sammanblandningen.', + utred: 'Flaggat för uppföljning av ett kattbeteende utan andra kriterier uppfyllda.', + arkivera: 'Katalogiserat utan att det finns en matchande post för "katt som undviker rum".', + }, + }, + { + id: 'd19', day: 4, source: 'Hyresgäst', location: 'Rosengård', filed: '2026-08-06', + body: 'Samma bord som förra veckan. Det blöder fortfarande. Handduken vi la över det är nu genomsyrad. Vi har inte bytt den, av skäl vi inte kan förklara.', + marks: ['KÄND FÖREMÅLSTYP'], + correctRoute: 'arkivera', explanation: 'Samma katalogiserade föremålstyp som d06, utan ny avvikelsetyp. §2.1 gäller fortfarande.', + correctConsequence: 'Katalogiserat under befintlig post. Notering tillagd om handduken, för statistik snarare än åtgärd.', + wrongConsequences: { + eskalera: 'Överlämnat för en redan katalogiserad föremålstyp. Svaret, om det kom, hade sannolikt varit detsamma som förra gången — men inget svar kom alls.', + utred: 'Flaggat för uppföljning av något som redan är väldokumenterat sedan förra veckan.', + avfarda: 'Avfärdat trots befintlig katalogpost och ett känt mönster som nu fortsätter obevakat.', + }, + }, + { + id: 'd20', day: 4, source: 'Väktare', location: 'Kockums gamla varv', filed: '2026-08-06', + body: 'En ljusslinga i himlen rörde sig mot vinden i natt, i ett mönster jag inte sett förut. Ingen annan väktare i området rapporterar samma sak, men jag filmade elva sekunder innan kameran slutade fungera.', + marks: ['OKATALOGISERAD TYP'], + correctRoute: 'eskalera', explanation: 'En fenomentyp utan tidigare katalogpost. §2.2 har fortfarande företräde när typen är ny, oavsett antal vittnen.', + correctConsequence: 'Överlämnat med filminspelningen bifogad, elva sekunder, ingen ljudspår. Ingen bekräftelse kom tillbaka.', + wrongConsequences: { + utred: 'Flaggat för uppföljning av ett enda vittne. §2.2 kräver eskalering direkt när fenomentypen är ny, oavsett antal källor.', + arkivera: 'Katalogiserat under en post som inte matchar. Filmen bifogades ändå, av misstag, till fel ärende.', + avfarda: 'Avfärdat trots videobevis. Elva sekunder är kort, men det är elva sekunder mer än de flesta ärenden har.', + }, + penalty: { avfarda: { insight: 2 } }, + }, + + // ---- Day 5 ---- + { + id: 'd21', day: 5, source: 'Kollega', location: 'Diariet, internpost', filed: '2026-08-07', + body: 'Din företrädare på den här posten lämnade en lapp i din låda: "Fråga aldrig varför avdelningen heter det den heter. Det är inte en gåta. Det är bara sant."', + marks: ['NÄMNER AVDELNINGEN'], + correctRoute: 'arkivera', explanation: 'Nämner avdelningen själv. Arkiveras internt utan vidare utredning. §5.1.', + correctConsequence: 'Katalogiserat internt. Lappen läggs i din egen akt, inte i den allmänna katalogen.', + wrongConsequences: { + utred: 'Flaggat för uppföljning av något som gäller avdelningen själv. §5.1 säger uttryckligen att det aldrig ska utredas vidare.', + eskalera: 'Överlämnat uppåt en anteckning om den egna avdelningen. Det kom tillbaka med en enda understruken mening: "Se §5.1."', + avfarda: 'Avfärdat trots att det ska arkiveras internt, inte kastas. Din företrädares lapp finns nu ingenstans i systemet.', + }, + }, + { + id: 'd22', day: 5, source: 'Väderstationen, automatisk logg', location: 'Öresund', filed: '2026-08-09', + body: 'Denna rapport genererades automatiskt två dagar före dagens datum baserat på mönster som ännu inte inträffat. Vattennivån kommer sjunka 4 centimeter på tisdag. SMHI kallar det ett tekniskt fel i förväg.', + marks: ['FRAMTIDA DATUM', 'FÖRE HÄNDELSEN'], + correctRoute: 'eskalera', explanation: 'Daterad efter dagens datum och beskriver något som ännu inte inträffat. Eskaleras utan att läsas färdigt, enligt §5.2.', + correctConsequence: 'Överlämnat efter första meningen, enligt instruktion. Vad som stod i resten av rapporten vet du inte, och kommer inte få veta.', + wrongConsequences: { + utred: 'Flaggat för uppföljning av en rapport som beskriver framtiden. §5.2 kräver eskalering, inte utredning, och att läsningen avbryts.', + arkivera: 'Katalogiserat som om händelsen redan inträffat. Den hade inte gjort det än när ärendet stämplades.', + avfarda: 'Avfärdat en rapport om något som ännu inte hänt. På tisdag sjönk vattennivån 4 centimeter, exakt som beskrivet.', + }, + penalty: { avfarda: { insight: 2 } }, + }, + { + id: 'd23', day: 5, source: 'Anställd, Pressbyrån Centralstationen', location: 'Malmö C', filed: '2026-08-07', + body: 'Vi säljer en tidning utan namn sedan i fredags. Innehållet ändras beroende på vem som läser den. En kund köper ett exemplar om dagen och verkar nöjd.', + marks: ['TEXTEN HAR ÄNDRATS'], + correctRoute: 'eskalera', explanation: 'Texten skiljer sig mellan läsningar — ett direkt exempel på §5.3. Läs inte om den för att kontrollera; eskalera direkt.', + correctConsequence: 'Överlämnat efter första läsningen. Du läste den inte en andra gång, enligt instruktion. Du vet fortfarande inte om det du läste var samma sak som kunden läste.', + wrongConsequences: { + utred: 'Flaggat, och du läste rapporten en andra gång för att vara säker. Den andra läsningen beskrev något annat än den första. Det gjorde uppföljningen svårare, inte lättare.', + arkivera: 'Katalogiserat med en beskrivning som redan hunnit bli inaktuell innan du la undan pennan.', + avfarda: 'Avfärdat en rapport om en tidning vars innehåll ändras per läsare. Nästa kund som köper ett exemplar läser något annat än du gjorde.', + }, + penalty: { utred: { insight: 1 } }, + }, + { + id: 'd24', day: 5, source: 'Okänd, ingen avsändaradress', location: '(anges ej)', filed: '2026-08-07', + body: 'Du kommer inte minnas att du skrev in det här ärendet. Det är inte ett hot. Det är bara sant, som lappen sa. Stämpla det du redan bestämt dig för. Du bestämde dig innan du öppnade det.', + marks: ['ADRESSERAT TILL HANDLÄGGAREN'], + correctRoute: 'arkivera', explanation: 'Nämner handläggaren direkt, om än indirekt. Arkiveras internt enligt §5.1, oavsett hur obehagligt innehållet känns.', + correctConsequence: 'Katalogiserat internt, i din egen akt, bredvid din företrädares lapp. Du minns fortfarande att du gjorde det. Det är kanske det enda beviset på att rapporten hade fel.', + wrongConsequences: { + eskalera: 'Överlämnat en rapport riktad till dig själv. Ingenting kom tillbaka. Du vet inte om det berodde på att ingen läste den, eller på att någon redan visste.', + utred: 'Flaggat för uppföljning av ett ärende som §5.1 säger aldrig ska utredas vidare, oavsett hur personligt det känns.', + avfarda: 'Avfärdat i stället för arkiverat internt. Rapporten finns nu ingenstans. Det gör den inte mindre sann.', + }, + penalty: { eskalera: { insight: 2 } }, + }, + { + id: 'd25', day: 5, source: 'Pensionär', location: 'Rörsjöparken', filed: '2026-08-07', + body: 'Ekorrarna i parken samlar för mycket för säsongen. Det är nog bara en tidig vinter i år. Jag ville bara nämna det för någon.', + marks: ['ENSTAKA VITTNE'], + correctRoute: 'avfarda', explanation: 'Ett ospecifikt, obekräftat sinnesintryck utan mätbar detalj. §1.1, precis som den allra första rapporten. Skiftet slutar som det började.', + correctConsequence: 'Avfärdat. Det är förmodligen bara en tidig vinter. Förmodligen.', + wrongConsequences: { + utred: 'Flaggat för ekorrar som samlar nötter. Uppföljningen bekräftade att det snart blir vinter.', + arkivera: 'Katalogiserat utan att det finns ett fenomen att katalogisera. Katalogen fick en post om ekorrar.', + eskalera: 'Överlämnat för ett samtal om ekorrar och årstider. Ingenting kom tillbaka, av goda skäl den här gången.', + }, + }, +]; + +export const SHIFT_BRIEFINGS = [ + 'Vanliga ärenden. Vanliga riktlinjer. Du får aldrig hela bilden. Det är jobbet.', + 'Katalogen har uppdaterats med nya kategorier. Ingen förklarar varför just nu.', + 'En kollega på en annan myndighet har slutat svara. Det är inte ditt ärende att fråga varför.', + 'Nattpersonalen rapporterar att vissa ärenden beskriver saker innan de sker. Läs dem inte två gånger.', + 'Sista skiftet. Riktlinjerna nämner nu dig själv. Stämpla det du bestämt dig för.', +]; diff --git a/src/engine.test.ts b/src/engine.test.ts new file mode 100644 index 0000000..7309d45 --- /dev/null +++ b/src/engine.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from 'vitest'; +import { createState, currentReport, reportsForDay, nextShift, routeReport, startGame } from './engine.ts'; +import { REPORTS } from './data/content.ts'; + +describe('routing engine', () => { + it('starts on the first report of shift one', () => { + const state = startGame(createState()); + expect(state.phase).toBe('playing'); + expect(currentReport(state)?.id).toBe('d01'); + }); + + it('advances and records a correct route', () => { + const state = routeReport(startGame(createState()), 'avfarda'); + expect(state.reportIndex).toBe(1); + expect(state.decisions[0].correct).toBe(true); + expect(state.trust).toBe(3); + }); + + it('penalizes an incorrect route', () => { + const state = routeReport(startGame(createState()), 'arkivera'); + expect(state.decisions[0].correct).toBe(false); + expect(state.trust).toBe(2); + expect(state.insight).toBe(1); + }); + + it('escalating costs insight even when it is the correct route', () => { + // d04 (Turning Torso rotating) is correctly routed eskalera. + let state = startGame(createState()); + for (const route of ['avfarda', 'arkivera', 'utred'] as const) state = routeReport(state, route); + expect(currentReport(state)?.id).toBe('d04'); + state = routeReport(state, 'eskalera'); + expect(state.lastDecision?.correct).toBe(true); + expect(state.lastDecision?.trustCost).toBe(0); + expect(state.lastDecision?.insightCost).toBe(1); + }); + + it('escalating a wrong report stacks the escalation cost on top of the wrong-route cost', () => { + let state = startGame(createState()); + // d01's correct route is avfarda; eskalera is wrong here. + state = routeReport(state, 'eskalera'); + expect(state.lastDecision?.correct).toBe(false); + expect(state.lastDecision?.insightCost).toBe(2); // 1 for wrong + 1 for escalating + }); + + it('ends a shift and restores one trust point for the next', () => { + let state = startGame(createState()); + const routes = reportsForDay(1).map((report) => report.correctRoute); + state = routeReport(state, 'arkivera'); // deliberately wrong on d01 + for (const route of routes.slice(1)) state = routeReport(state, route); + expect(state.phase).toBe('shift-end'); + state = nextShift(state); + expect(state.day).toBe(2); + expect(state.trust).toBe(3); + }); + + it('has five authored reports in every shift', () => { + for (let day = 1; day <= 5; day += 1) expect(reportsForDay(day)).toHaveLength(5); + }); + + it('every report has its own correct-route consequence', () => { + for (const report of REPORTS) { + expect(report.correctConsequence, `${report.id} is missing a correctConsequence`).toBeTruthy(); + } + }); + + it('loses on insight reaching the cap even with trust intact', () => { + let state = startGame(createState()); + // Escalate every report regardless of correctness to ramp insight fast. + for (let i = 0; i < 10 && state.phase === 'playing'; i++) state = routeReport(state, 'eskalera'); + expect(state.phase).toBe('lost'); + expect(state.insight).toBeGreaterThanOrEqual(5); + }); +}); diff --git a/src/engine.ts b/src/engine.ts new file mode 100644 index 0000000..c9eb0b5 --- /dev/null +++ b/src/engine.ts @@ -0,0 +1,105 @@ +import { REPORTS } from './data/content.ts'; +import type { GameState, Report, Route } from './types.ts'; + +export const MAX_TRUST = 3; +export const MAX_INSIGHT = 5; +export const LAST_DAY = Math.max(...REPORTS.map((report) => report.day)); + +const DEFAULT_WRONG_CONSEQUENCES: Record = { + avfarda: 'Rapporten arkiverades som avfärdad. Ingen uppföljning skickades. Ingen bad om en heller.', + arkivera: 'Ärendet fördes till katalogen. Nästa handläggare kommer inte fråga varför.', + utred: 'Ärendet flaggades för uppföljning som aldrig genomfördes. Det ligger kvar i kön, odaterat.', + eskalera: 'Eskalerat utan tydlig grund. Någon läste det. Du fick inget kvitto på att de gjorde det.', +}; + +export function createState(): GameState { + return { + phase: 'intro', day: 1, reportIndex: 0, accuracy: 100, + trust: MAX_TRUST, insight: 0, decisions: [], lastDecision: null, + }; +} + +export function reportsForDay(day: number): Report[] { + return REPORTS.filter((report) => report.day === day); +} + +export function currentReport(state: GameState): Report | null { + return reportsForDay(state.day)[state.reportIndex] ?? null; +} + +export function startGame(state: GameState): GameState { + return state.phase === 'intro' ? { ...state, phase: 'playing' } : state; +} + +export function routeReport(state: GameState, route: Route): GameState { + if (state.phase !== 'playing') return state; + const report = currentReport(state); + if (!report) return state; + + const correct = report.correctRoute === route; + const configuredPenalty = report.penalty?.[route]; + const trustCost = correct ? 0 : configuredPenalty?.trust ?? 1; + // Escalating draws attention regardless of whether it was the right call — + // the distinct twist from Retur's routing: being correct doesn't make you + // invisible. Wrong-route insight cost still applies on top of this. + const insightFromEscalating = route === 'eskalera' ? 1 : 0; + const insightFromWrong = correct ? 0 : configuredPenalty?.insight ?? 1; + const insightCost = insightFromEscalating + insightFromWrong; + + const consequence = correct + ? report.correctConsequence ?? 'Ärendet hanterades enligt riktlinjerna. Ingenting hördes av det igen.' + : report.wrongConsequences?.[route] ?? DEFAULT_WRONG_CONSEQUENCES[route]; + + const decision = { reportId: report.id, route, correct, consequence, trustCost, insightCost }; + const decisions = [...state.decisions, decision]; + const correctCount = decisions.filter((decision) => decision.correct).length; + const accuracy = Math.round((correctCount / decisions.length) * 100); + const trust = Math.max(0, state.trust - trustCost); + const insight = Math.min(MAX_INSIGHT, state.insight + insightCost); + const dayReports = reportsForDay(state.day); + const isLastReport = state.reportIndex >= dayReports.length - 1; + + let phase: GameState['phase'] = 'playing'; + if (trust <= 0 || insight >= MAX_INSIGHT) phase = 'lost'; + else if (isLastReport) phase = state.day >= LAST_DAY ? 'won' : 'shift-end'; + + return { + ...state, + phase, + reportIndex: isLastReport ? state.reportIndex : state.reportIndex + 1, + accuracy, + trust, + insight, + decisions, + lastDecision: decision, + }; +} + +export function nextShift(state: GameState): GameState { + if (state.phase !== 'shift-end') return state; + return { + ...state, + phase: 'playing', + day: state.day + 1, + reportIndex: 0, + trust: Math.min(MAX_TRUST, state.trust + 1), + lastDecision: null, + }; +} + +export function saveState(state: GameState): void { + localStorage.setItem('diariet-save-v1', JSON.stringify(state)); +} + +export function loadState(): GameState | null { + try { + const value = localStorage.getItem('diariet-save-v1'); + return value ? (JSON.parse(value) as GameState) : null; + } catch { + return null; + } +} + +export function clearState(): void { + localStorage.removeItem('diariet-save-v1'); +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..d0b6e9a --- /dev/null +++ b/src/main.ts @@ -0,0 +1,207 @@ +import './style.css'; +import { SHIFT_BRIEFINGS, GUIDELINES, ROUTES } from './data/content.ts'; +import { MAX_INSIGHT, clearState, createState, currentReport, reportsForDay, loadState, nextShift, routeReport, saveState, startGame } from './engine.ts'; +import type { Route } from './types.ts'; + +let state = loadState() ?? createState(); +let selectedRoute: Route | null = null; +let showingResult = false; +let resultAdvanced = false; + +const app = document.getElementById('app')!; + +function escapeHtml(value: string): string { + return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", '''); +} + +function render(): void { + if (state.phase === 'intro') return renderIntro(); + if (showingResult) return renderDesk(); + if (state.phase === 'shift-end') return renderShiftEnd(); + if (state.phase === 'won' || state.phase === 'lost') return renderEnding(); + renderDesk(); +} + +function shell(content: string): void { + app.innerHTML = ` + + ${content} + + +
Arbetsinstruktion 14-C
+

Läs. Bedöm. Stämpla. Fråga inte.

+

Läs ärendet och dagens riktlinjer. Välj rätt destination och tryck STÄMPLA. Senare riktlinjer har företräde när de motsäger varandra.

+
  • Tre fel och ditt förtroende tar slut.
  • Att eskalera märks alltid, oavsett om det var rätt beslut.
  • Ett avslutat skift återställer ett förtroende. Insynen gör det inte.
  • Tangent 1–4 väljer destination, Enter stämplar.
+

Avdelningen ansvarar inte för det du får reda på, eller det du inte får reda på.

+
`; + bindShell(); +} + +function renderIntro(): void { + shell(`
+
D
+
+
TJÄNSTGÖRING 14-C / SEKRETESSBELAGD
+

DIARIET

+

Du får aldrig hela bilden. Det är jobbet.

+

Din företrädare lämnade ingen uppsägning. Bara en lapp i din låda.

+

Ärendena på ditt bord saknar svar. Riktlinjerna säger vad du ska göra med dem. Inte vad de betyder.

+ +
+
`); + document.getElementById('start-button')?.addEventListener('click', () => { state = startGame(state); saveState(state); render(); }); +} + +function renderDesk(): void { + const report = currentReport(state)!; + const dayReports = reportsForDay(state.day); + const guidelines = GUIDELINES.filter((rule) => rule.fromDay <= state.day); + const result = showingResult && state.lastDecision + ? `
+
${state.lastDecision.correct ? 'KORREKT HANTERAT' : 'AVVIKELSE REGISTRERAD'}
+

${escapeHtml(report.explanation)}

+
FÖLJD${escapeHtml(state.lastDecision.consequence)}
+ ${state.lastDecision.trustCost || state.lastDecision.insightCost ? `

−${state.lastDecision.trustCost} förtroende · +${state.lastDecision.insightCost} insyn

` : ''} + +
` : ''; + + const recentDecisions = state.decisions.slice(-3).reverse(); + + shell(`
+ +
+
+
+
Anmälare${escapeHtml(report.source)}
+
Plats${escapeHtml(report.location)}
+
Inkom${escapeHtml(report.filed)} · ÄRENDE ${report.id.toUpperCase()}
+
+
${report.marks.map((mark) => `${escapeHtml(mark)}`).join('')}
+
${escapeHtml(report.body)}
+ ${result} +
+
+ +
`); + + document.querySelectorAll('[data-route]').forEach((button) => button.addEventListener('click', () => { + selectedRoute = button.dataset.route as Route; + render(); + })); + document.getElementById('stamp-button')?.addEventListener('click', stamp); +} + +function stamp(): void { + if (!selectedRoute || showingResult) return; + const before = currentReport(state); + state = routeReport(state, selectedRoute); + saveState(state); + resultAdvanced = before?.id !== currentReport(state)?.id; + if (resultAdvanced) { + state = { ...state, reportIndex: state.reportIndex - 1 }; + } + showingResult = true; + render(); +} + +function advanceAfterResult(): void { + if (!showingResult) return; + if (resultAdvanced) state = { ...state, reportIndex: state.reportIndex + 1 }; + showingResult = false; + resultAdvanced = false; + selectedRoute = null; + saveState(state); + render(); +} + +function renderShiftEnd(): void { + const dayDecisions = state.decisions.slice(-reportsForDay(state.day).length); + const correct = dayDecisions.filter((decision) => decision.correct).length; + shell(`
+
SKIFTRAPPORT / SKIFT 0${state.day}
+
GRANSKAD
+

Luckan är stängd för dagen.

+

${correct} av ${dayDecisions.length} ärenden hanterades enligt gällande riktlinjer.

+

Ett förtroende återställs över natten. Insynen gör det inte.

+ +
`); + document.getElementById('next-shift-button')?.addEventListener('click', () => { state = nextShift(state); saveState(state); render(); }); +} + +function renderEnding(): void { + const won = state.phase === 'won'; + const lostByInsight = !won && state.insight >= MAX_INSIGHT; + + const stamp = won + ? (state.insight <= 1 ? 'FORTSATT TJÄNST' : 'FORTSATT TJÄNST, UNDER OBSERVATION') + : lostByInsight ? 'AVREGISTRERAD' : 'ENTLEDIGAD'; + + const title = won + ? state.insight <= 1 + ? 'Skiftet är slut. Du vet fortfarande ingenting.' + : 'Skiftet är slut. Någon la märke till dig.' + : lostByInsight + ? 'Du blev till slut det du handlade om.' + : 'Du gjorde för många fel för att någon skulle bry sig varför.'; + + const body = won + ? state.insight <= 1 + ? 'Fem skift avslutade utan en enda anmärkning. Ingen förklarar riktlinjerna för dig. Ingen kommer göra det. Du får komma tillbaka imorgon och fortsätta inte veta.' + : 'Fem skift avslutade, men din akt är tjockare än den borde vara. Ingen säger vem som läser den. Du får komma tillbaka imorgon ändå. Det är inte nödvändigtvis ett gott tecken.' + : lostByInsight + ? 'Insynen nådde taket innan misstagen gjorde det. Ditt namn finns inte längre i personalregistret. Det finns fortfarande i ett annat register. Du får aldrig veta vilket.' + : 'Din tjänst avslutas med omedelbar verkan. Ingen kartong väntar vid ditt bord — bara ett brev som redan är stämplat, adresserat till din efterträdare. Du får aldrig veta vad du missade.'; + + shell(`
+
SLUTLIG PERSONALAKT
+
${stamp}
+

${title}

+

${body}

+
Noggrannhet
${state.accuracy}%
Insyn
${state.insight}/5
Ärenden
${state.decisions.length}
+ +
`); + document.getElementById('restart-button')?.addEventListener('click', () => { clearState(); state = createState(); selectedRoute = null; showingResult = false; resultAdvanced = false; render(); }); +} + +function bindShell(): void { + const dialog = document.getElementById('help-dialog') as HTMLDialogElement | null; + document.getElementById('help-button')?.addEventListener('click', () => dialog?.showModal()); + document.querySelector('.dialog-close')?.addEventListener('click', () => dialog?.close()); + dialog?.addEventListener('click', (event) => { if (event.target === dialog) dialog.close(); }); +} + +window.addEventListener('keydown', (event) => { + if (event.key === '?') (document.getElementById('help-dialog') as HTMLDialogElement | null)?.showModal(); + if (state.phase !== 'playing') return; + if (showingResult && event.key === 'Enter') { event.preventDefault(); advanceAfterResult(); return; } + const routes = Object.keys(ROUTES) as Route[]; + const index = Number(event.key) - 1; + if (index >= 0 && index < routes.length && !showingResult) { selectedRoute = routes[index]; render(); } + if (event.key === 'Enter' && selectedRoute) { event.preventDefault(); stamp(); } +}); + +document.addEventListener('click', (event) => { + if ((event.target as HTMLElement).id === 'continue-button') advanceAfterResult(); +}); + +render(); diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..926633c --- /dev/null +++ b/src/style.css @@ -0,0 +1,100 @@ +/* Shares the family palette (same :root values as djupet/retur — same + universe, same near-black indigo) but picks its own accent: a cold + institutional cyan instead of Retur's warm stamp-red or Djupet's + eldritch purple, since Diariet is the neutral paperwork layer between + them. */ +:root { + --bg: #0d0d1a; --panel: #12121f; --paper: #d8d0b8; --paper-ink: #25221d; + --border: #2a2a42; --text: #e2e2f0; --dim: #8888aa; --accent: #4fc3d9; + --stamp: #4fc3d9; --archive: #c49a45; --safe: #44bb66; --danger: #ee4444; +} +*, *::before, *::after { box-sizing: border-box; } +html { background: var(--bg); } +body { margin: 0; min-height: 100vh; overflow-x: hidden; background: var(--bg); color: var(--text); font: 13px/1.55 'JetBrains Mono', monospace; } +button { font: inherit; } +#app { min-height: 100vh; max-width: 1280px; margin: auto; border-inline: 1px solid var(--border); position: relative; } +#header { min-height: 62px; padding: 14px 20px; border-bottom: 1px solid var(--border); display: flex; align-items: baseline; justify-content: space-between; gap: 18px; background: rgba(13,13,26,.94); } +.brand { color: var(--stamp); font-size: 22px; font-weight: 700; letter-spacing: 5px; text-shadow: 0 0 18px rgba(79,195,217,.35); } +.brand-sub { margin-left: 18px; color: var(--dim); font-size: 9px; letter-spacing: .12em; text-transform: uppercase; } +.text-button { border: 0; background: none; color: var(--dim); cursor: pointer; font-size: 10px; text-transform: uppercase; } +.text-button:hover { color: var(--text); } +.panel { padding: 16px; overflow-y: auto; background: var(--panel); } +.panel-title { color: var(--dim); border-bottom: 1px solid var(--border); padding-bottom: 6px; margin-bottom: 12px; font-size: 9px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; } +.desk { display: grid; grid-template-columns: 275px minmax(360px, 1fr) 275px; min-height: calc(100vh - 62px); } +.status-panel { border-right: 1px solid var(--border); } +.route-panel { border-left: 1px solid var(--border); } +.day-number { color: var(--accent); font-size: 48px; line-height: 1; font-weight: 700; } +.stat-row { display: flex; justify-content: space-between; align-items: center; margin: 4px 0; } +.stat-label { color: var(--dim); font-size: 10px; text-transform: uppercase; letter-spacing: .08em; } +.dots { display: flex; gap: 4px; } +.dot { width: 9px; height: 9px; border-radius: 50%; border: 1px solid var(--border); } +.dot.filled-trust { background: var(--safe); border-color: var(--safe); } +.dot.filled-insight { background: var(--danger); border-color: var(--danger); } +.briefing { color: var(--dim); font-size: 11px; font-style: italic; margin: 10px 0 16px; } +.incidents-title { margin-top: 18px; } +.incident-log { list-style: none; padding: 0; margin: 0; display: grid; gap: 8px; } +.incident-log li { font-size: 10.5px; padding: 8px; border-left: 2px solid var(--border); background: rgba(255,255,255,.02); } +.incident-log li b { display: block; color: var(--accent); margin-bottom: 2px; } +.incident-log li.ok { border-left-color: var(--safe); } +.incident-log li.bad { border-left-color: var(--danger); } +.regulations { display: grid; gap: 10px; } +.reg { font-size: 11px; } +.reg .num { color: var(--accent); font-weight: 700; margin-right: 6px; } +.reg .note { display: block; color: var(--dim); font-size: 10px; margin-top: 2px; } +.work-area { padding: 30px; display: grid; place-items: center; min-height: calc(100vh - 62px); } +.report { width: 100%; max-width: 640px; background: var(--paper); color: var(--paper-ink); padding: 30px 34px; border-radius: 2px; box-shadow: 0 10px 40px rgba(0,0,0,.5); position: relative; min-height: 420px; } +.report-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 20px; font-size: 11px; margin-bottom: 14px; border-bottom: 1px dashed rgba(37,34,29,.3); padding-bottom: 14px; } +.report-grid .field-label { text-transform: uppercase; font-size: 9px; opacity: .6; display: block; } +.report-grid .full { grid-column: 1 / -1; } +.report-marks { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 14px; } +.mark { border: 1px solid rgba(37,34,29,.4); padding: 2px 8px; font-size: 9px; letter-spacing: .06em; text-transform: uppercase; } +.report-body { font-size: 14px; line-height: 1.6; white-space: pre-wrap; } +.result-overlay { position: absolute; inset: 0; background: rgba(13,13,26,.94); color: var(--text); display: grid; place-items: center; text-align: center; padding: 30px; } +.result-title { font-size: 32px; font-weight: 700; letter-spacing: .08em; margin-bottom: 14px; } +.result-title.correct { color: var(--safe); } +.result-title.wrong { color: var(--danger); } +.consequence { border: 1px solid var(--border); padding: 14px 18px; margin: 14px 0; font-size: 12px; color: var(--dim); max-width: 480px; } +.consequence span { display: block; color: var(--accent); font-size: 9px; letter-spacing: .1em; margin-bottom: 6px; } +.routes { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 16px; } +.route-button { text-align: left; background: var(--panel); border: 1px solid var(--border); color: var(--text); padding: 12px; cursor: pointer; } +.route-button b { display: block; color: var(--accent); font-size: 11px; letter-spacing: .05em; margin-bottom: 3px; } +.route-button span:not(.key) { display: block; color: var(--dim); font-size: 10px; } +.route-button.selected { border-color: var(--accent); background: rgba(79,195,217,.08); } +.route-button .key { float: right; color: var(--dim); font-size: 9px; } +.stamp-button { margin-top: 14px; width: 100%; padding: 12px; background: var(--accent); color: var(--bg); border: 0; font-weight: 700; letter-spacing: .1em; cursor: pointer; } +.stamp-button:disabled { opacity: .3; cursor: not-allowed; } +.primary { background: var(--accent); color: var(--bg); border: 1px solid var(--accent); padding: 12px 20px; font-weight: 700; letter-spacing: .1em; cursor: pointer; } +.shift-end-screen, .intro-screen, .end-screen { min-height: calc(100vh - 62px); display: grid; place-items: center; padding: 30px; background: radial-gradient(circle, #17222a 0, var(--bg) 66%); } +.intro-screen { + grid-template-columns: auto minmax(280px, 540px); + gap: clamp(30px, 7vw, 90px); + background: + linear-gradient(rgba(13, 13, 26, 0.6), rgba(13, 13, 26, 0.88)), + url('/intro.webp') center / cover no-repeat; +} +.end-screen.victory { + background: + linear-gradient(rgba(13, 13, 26, 0.55), rgba(13, 13, 26, 0.85)), + url('/survived.webp') center / cover no-repeat; +} +.end-screen.failure { + background: + linear-gradient(rgba(13, 13, 26, 0.55), rgba(13, 13, 26, 0.85)), + url('/gameover.webp') center / cover no-repeat; +} +.seal { width: clamp(160px, 25vw, 260px); aspect-ratio: 1; display: grid; place-items: center; border: 8px double var(--stamp); border-radius: 50%; color: var(--stamp); font: 700 clamp(70px, 12vw, 120px)/1 'Special Elite'; transform: rotate(-8deg); opacity: .85; box-shadow: 0 0 60px rgba(79,195,217,.25); background: rgba(13, 13, 26, 0.35); backdrop-filter: blur(1px); text-align: center; } +.intro-copy h1 { margin: 0; color: var(--stamp); font-size: clamp(44px, 8vw, 76px); letter-spacing: .12em; line-height: 1; } +.intro-sub { color: var(--dim); font-size: 10px; letter-spacing: .15em; text-transform: uppercase; } +.intro-copy > p:not(.intro-sub) { color: #b0b0c4; } +.shift-end-card, .end-card { width: min(620px, 100%); padding: clamp(24px, 5vw, 50px); border: 1px solid var(--border); background: rgba(18, 18, 31, 0.82); backdrop-filter: blur(2px); text-align: center; position: relative; } +.end-card h1 { color: var(--text); } +.end-card > p, .shift-end-card > p { color: var(--dim); } +.audit-stamp { display: inline-block; padding: 8px 16px; border: 3px solid var(--safe); color: var(--safe); font-size: 18px; font-weight: 700; letter-spacing: .12em; transform: rotate(-4deg); background: rgba(13, 13, 26, 0.3); } +.failure .audit-stamp { border-color: var(--danger); color: var(--danger); } +.final-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin: 25px 0; } +.final-stats div { padding: 12px; border: 1px solid var(--border); } +.final-stats dt { color: var(--dim); font-size: 9px; text-transform: uppercase; } .final-stats dd { margin: 4px 0 0; font-size: 20px; } +.help-dialog { background: var(--panel); color: var(--text); border: 1px solid var(--border); max-width: 520px; padding: 24px; } +.help-dialog::backdrop { background: rgba(0,0,0,.6); } +.dialog-close { float: right; background: none; border: 0; color: var(--dim); cursor: pointer; } +@media (max-width: 700px) { #app { border: 0; } .brand-sub { display: block; margin: 2px 0 0; } .desk { display: block; } .status-panel { border-right: 0; border-bottom: 1px solid var(--border); } .regulations { max-height: 180px; overflow-y: auto; } .work-area { min-height: 560px; padding: 18px 12px; } .report { min-height: 500px; padding: 24px 18px; } .report-grid { grid-template-columns: 1fr; } .routes { grid-template-columns: 1fr 1fr; } .intro-screen { grid-template-columns: 1fr; text-align: center; } .seal { width: 130px; margin: auto; } .final-stats { grid-template-columns: 1fr; } } diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..a53527c --- /dev/null +++ b/src/types.ts @@ -0,0 +1,44 @@ +export type Route = 'avfarda' | 'arkivera' | 'utred' | 'eskalera'; + +export interface Report { + id: string; + day: number; + source: string; // who filed it + location: string; + filed: string; // date filed + body: string; + marks: string[]; // small tags: "ANONYM", "TREDJE ANMÄLAN", etc. + correctRoute: Route; + explanation: string; + correctConsequence?: string; + wrongConsequences?: Partial>; + penalty?: Partial>; +} + +export interface Guideline { + id: string; + fromDay: number; + number: string; + text: string; + note?: string; +} + +export interface Decision { + reportId: string; + route: Route; + correct: boolean; + consequence: string; + trustCost: number; + insightCost: number; +} + +export interface GameState { + phase: 'intro' | 'playing' | 'shift-end' | 'won' | 'lost'; + day: number; + reportIndex: number; + accuracy: number; + trust: number; + insight: number; + decisions: Decision[]; + lastDecision: Decision | null; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ed0c191 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": ["src"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..5c13d64 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + server: { port: 5692, strictPort: true }, + preview: { port: 4596, strictPort: true }, +});