diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000000000000000000000000000000000000..7fe3db7a17f7dd6ac53d55a64f0b42e98c8f88c5
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,126 @@
+# ═══════════════════════════════════════════════════════════
+# MAC — MBM AI Cloud | Local Server Configuration
+# ═══════════════════════════════════════════════════════════
+# Copy this to .env: cp .env.example .env
+# Then run: docker compose up -d
+# ═══════════════════════════════════════════════════════════
+
+# ── App ───────────────────────────────────────────────────
+MAC_ENV=development
+MAC_HOST=0.0.0.0
+MAC_PORT=8000
+MAC_DEBUG=false
+MAC_SECRET_KEY=change-me-to-random-string
+MAC_CORS_ORIGINS=["*"]
+MAC_WORKERS=4 # Uvicorn worker processes
+
+# ── Network binding ────────────────────────────────────────
+# Set APP_HOST to a specific IP to restrict which interface the app listens on.
+# Leave as 0.0.0.0 to accept connections on all interfaces.
+# The installer sets this to the system's configured static IP.
+APP_HOST=0.0.0.0
+APP_PORT=80
+
+# ── Database (PostgreSQL — persistent storage) ────────────
+DATABASE_URL=postgresql+asyncpg://mac:mac_password@localhost:5432/mac_db
+PGADMIN_PORT=5050
+PGADMIN_DEFAULT_EMAIL=admin@mbm.local
+PGADMIN_DEFAULT_PASSWORD=ChangeThisStrongPassword!
+
+# ── Redis (rate limiting & caching) ──────────────────────
+REDIS_URL=redis://localhost:6379/0
+
+# ── JWT Auth ──────────────────────────────────────────────
+JWT_SECRET_KEY=change-me-jwt-secret-random-string
+JWT_ALGORITHM=HS256
+JWT_ACCESS_TOKEN_EXPIRE_MINUTES=1440
+
+# ── vLLM Local GPU Inference ─────────────────────────────
+# Each model runs its own vLLM instance on a separate port.
+# Docker Compose sets these automatically via service names.
+VLLM_BASE_URL=http://localhost:8001
+VLLM_SPEED_URL=http://localhost:8001
+VLLM_CODE_URL=http://localhost:8002
+VLLM_REASONING_URL=http://localhost:8003
+VLLM_INTELLIGENCE_URL=http://localhost:8004
+VLLM_API_KEY=
+VLLM_TIMEOUT=120 # HTTP timeout (seconds) for LLM requests
+VLLM_HEALTH_TIMEOUT=5 # Timeout for model health checks
+
+# ── Model Registry ────────────────────────────────────────
+# Override the entire model list with a JSON array (leave empty for defaults)
+# Each object needs: id, name, served_name, url_key, category,
+# parameters, context_length, capabilities (list), specialty.
+MAC_MODELS_JSON=
+
+# Only enable specific models from the built-in list (comma-separated IDs)
+# Example: MAC_ENABLED_MODELS=qwen2.5:7b,qwen2.5-coder:7b
+MAC_ENABLED_MODELS=
+
+# Which model ID the "auto" keyword falls back to (empty = first code model)
+MAC_AUTO_FALLBACK=
+
+# Default max_tokens when the client doesn't specify
+MAC_DEFAULT_MAX_TOKENS=2048
+
+# ── Open-source model auto-download (first app use) ─────
+# Set to true to prefetch Hugging Face models into local cache after first use.
+# Limit=0 means all detected open-source model repos.
+MAC_MODEL_AUTO_DOWNLOAD_ON_USE=true
+MAC_MODEL_AUTO_DOWNLOAD_LIMIT=0
+
+# ── Docker Compose vLLM Tuning ────────────────────────────
+# Adjust these to match your GPU VRAM. 24GB GPU example:
+# Speed (7B) ≈ 5GB, Code (7B) ≈ 5GB, Reason (14B) ≈ 9GB → 19GB total
+VLLM_SPEED_MODEL=Qwen/Qwen2.5-7B-Instruct
+VLLM_SPEED_PORT=8001
+VLLM_SPEED_GPU_MEM=0.22
+VLLM_SPEED_MAX_LEN=8192
+
+VLLM_CODE_MODEL=Qwen/Qwen2.5-Coder-7B-Instruct
+VLLM_CODE_PORT=8002
+VLLM_CODE_GPU_MEM=0.22
+VLLM_CODE_MAX_LEN=8192
+
+VLLM_REASON_MODEL=deepseek-ai/DeepSeek-R1-Distill-Qwen-14B
+VLLM_REASON_PORT=8003
+VLLM_REASON_GPU_MEM=0.35
+VLLM_REASON_MAX_LEN=8192
+
+VLLM_DTYPE=auto # auto | float16 | bfloat16
+
+# Intelligence slot (uncomment vllm-intel in docker-compose.yml first)
+# VLLM_INTEL_MODEL=google/gemma-3-27b-it
+# VLLM_INTEL_PORT=8004
+# VLLM_INTEL_GPU_MEM=0.45
+# VLLM_INTEL_MAX_LEN=4096
+
+# ── Whisper / Speech-to-Text ─────────────────────────────
+# Uncomment the whisper service in docker-compose.yml first.
+# Uses OpenAI-compatible /v1/audio/transcriptions endpoint.
+WHISPER_URL=http://localhost:8005
+WHISPER_MODEL=Systran/faster-whisper-small
+WHISPER_TIMEOUT=300
+
+# ── Text-to-Speech ───────────────────────────────────────
+# Uncomment the tts service in docker-compose.yml first.
+# Uses OpenAI-compatible /v1/audio/speech endpoint.
+TTS_URL=http://localhost:8006
+TTS_MODEL=default
+TTS_TIMEOUT=120
+
+# ── Embeddings ────────────────────────────────────────────
+# Optional separate embedding server. Leave empty to use VLLM_BASE_URL.
+EMBEDDING_URL=
+EMBEDDING_MODEL=nomic-embed-text
+EMBEDDING_TIMEOUT=60
+
+# ── Rate Limits ───────────────────────────────────────────
+RATE_LIMIT_REQUESTS_PER_HOUR=100
+RATE_LIMIT_TOKENS_PER_DAY=50000
+
+# ── Qdrant (Vector DB for RAG) ───────────────────────────
+QDRANT_URL=http://localhost:6333
+
+# ── SearXNG (Web Search) ─────────────────────────────────
+SEARXNG_URL=http://localhost:8888
diff --git a/.gitattributes b/.gitattributes
index a6344aac8c09253b3b630fb776ae94478aa0275b..35108eecbba13a9548495fc93242d1ce55266ac4 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,35 +1,4 @@
-*.7z filter=lfs diff=lfs merge=lfs -text
-*.arrow filter=lfs diff=lfs merge=lfs -text
-*.bin filter=lfs diff=lfs merge=lfs -text
-*.bz2 filter=lfs diff=lfs merge=lfs -text
-*.ckpt filter=lfs diff=lfs merge=lfs -text
-*.ftz filter=lfs diff=lfs merge=lfs -text
-*.gz filter=lfs diff=lfs merge=lfs -text
-*.h5 filter=lfs diff=lfs merge=lfs -text
-*.joblib filter=lfs diff=lfs merge=lfs -text
-*.lfs.* filter=lfs diff=lfs merge=lfs -text
-*.mlmodel filter=lfs diff=lfs merge=lfs -text
-*.model filter=lfs diff=lfs merge=lfs -text
-*.msgpack filter=lfs diff=lfs merge=lfs -text
-*.npy filter=lfs diff=lfs merge=lfs -text
-*.npz filter=lfs diff=lfs merge=lfs -text
-*.onnx filter=lfs diff=lfs merge=lfs -text
-*.ot filter=lfs diff=lfs merge=lfs -text
-*.parquet filter=lfs diff=lfs merge=lfs -text
-*.pb filter=lfs diff=lfs merge=lfs -text
-*.pickle filter=lfs diff=lfs merge=lfs -text
-*.pkl filter=lfs diff=lfs merge=lfs -text
-*.pt filter=lfs diff=lfs merge=lfs -text
-*.pth filter=lfs diff=lfs merge=lfs -text
-*.rar filter=lfs diff=lfs merge=lfs -text
-*.safetensors filter=lfs diff=lfs merge=lfs -text
-saved_model/**/* filter=lfs diff=lfs merge=lfs -text
-*.tar.* filter=lfs diff=lfs merge=lfs -text
-*.tar filter=lfs diff=lfs merge=lfs -text
-*.tflite filter=lfs diff=lfs merge=lfs -text
-*.tgz filter=lfs diff=lfs merge=lfs -text
-*.wasm filter=lfs diff=lfs merge=lfs -text
-*.xz filter=lfs diff=lfs merge=lfs -text
-*.zip filter=lfs diff=lfs merge=lfs -text
-*.zst filter=lfs diff=lfs merge=lfs -text
-*tfevents* filter=lfs diff=lfs merge=lfs -text
+*.exe filter=lfs diff=lfs merge=lfs -text
+build/MAC-Installer/base_library.zip filter=lfs diff=lfs merge=lfs -text
+build/MAC-Installer/MAC-Installer.pkg filter=lfs diff=lfs merge=lfs -text
+build/MAC-Installer/PYZ-00.pyz filter=lfs diff=lfs merge=lfs -text
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..80dca517e6457881a52e8f76c15f22c023dab040
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,82 @@
+# Byte-compiled
+__pycache__/
+*.py[cod]
+*$py.class
+
+# Virtual environments
+venv/
+.venv/
+env/
+
+# Environment
+.env
+
+# Database
+*.db
+*.db-journal
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Docs build artifacts
+docs/*.pdf
+docs/*.docx
+
+# Frontend build artifacts
+frontend/node_modules/
+frontend/.svelte-kit/
+frontend/build/
+
+# Temp / scratch folders
+delete later/
+
+# PyInstaller build artifacts (keep dist/ for the released EXE)
+build/pyi/
+build/MAC-Installer/
+installer/__pycache__/
+
+# SSL certs (self-signed)
+nginx/ssl/
+
+# Logs
+*.log
+vllm-logs*.txt
+
+# Testing
+.pytest_cache/
+.coverage
+htmlcov/
+
+# Uploads (user content)
+uploads/*
+!uploads/.gitkeep
+
+# Logs
+logs/
+*.log
+
+# Docker volumes
+pgdata/
+redisdata/
+
+# Keys
+*.pem
+*.key
+
+# Local/generated build artifacts
+build/
+dist/*
+!dist/MAC-Installer.exe
+frontend/build/
+installer/build/
+installer/dist/
+
+# Local assistant config
+.claude/
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000000000000000000000000000000000000..d84f211717ec18865df0434dbd90365031421b96
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "python-envs.defaultEnvManager": "ms-python.python:system"
+}
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..e2ebe1060b912c3e70cd0f53e6996b4f8696bfed
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,27 @@
+FROM python:3.11-slim
+
+WORKDIR /app
+
+# Install system deps
+RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
+
+# Install Python deps
+COPY requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+
+# Copy application code
+COPY alembic.ini .
+COPY alembic/ alembic/
+COPY mac/ mac/
+COPY frontend/ frontend/
+
+# Don't run as root in production
+RUN useradd -m appuser && chown -R appuser:appuser /app
+USER appuser
+
+EXPOSE 8000
+
+HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
+ CMD curl -f http://localhost:8000/api/v1 || exit 1
+
+CMD sh -c "alembic upgrade head && uvicorn mac.main:app --host 0.0.0.0 --port 8000 --workers ${MAC_WORKERS:-4}"
diff --git a/README.md b/README.md
index 8d1116540628df26211029d1174f85cc3aad293a..ad1ca00f42a60e5c204a41ec54815f2aa89ad8f4 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,252 @@
----
-title: MAC
-emoji: 💻
-colorFrom: pink
-colorTo: indigo
-sdk: docker
-pinned: false
----
-
-Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
+---
+title: MAC - MBM AI Cloud
+emoji: 🤖
+colorFrom: red
+colorTo: blue
+sdk: docker
+pinned: true
+license: mit
+---
+
+
+
+
+
+
MAC — MBM AI Cloud
+
+
+ Self-hosted AI platform for MBM University Jodhpur.
+ Private ChatGPT-style chat, Jupyter-style notebooks, RAG over college documents,
+ face-based attendance, AI exam grading — all running on the college's own GPUs.
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dist/MAC-Installer.exe b/dist/MAC-Installer.exe
new file mode 100644
index 0000000000000000000000000000000000000000..2bfbf7eb582c910aeea512478f854c7a8a3a2bd3
--- /dev/null
+++ b/dist/MAC-Installer.exe
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c01d907d83babf33d5cf35d00b9359724b5edb688bd169bb5771df39eec313b4
+size 29829754
diff --git a/docker-compose.worker.yml b/docker-compose.worker.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c2683cbb5f3cee655912235e69619a21f6c88cc8
--- /dev/null
+++ b/docker-compose.worker.yml
@@ -0,0 +1,104 @@
+# ═══════════════════════════════════════════════════════════
+# MAC Worker Node — run this on each worker PC
+# Worker PCs run: vLLM (GPU inference) + optional Jupyter
+# PostgreSQL/Redis/Nginx stay on the master node only.
+#
+# Steps:
+# 1. Copy this file + worker_agent.py to the worker PC
+# 2. Create .env.worker with MAC_ENROLL_TOKEN and MAC_MASTER_URL
+# 3. docker compose -f docker-compose.worker.yml up -d
+# 4. Admin approves the node in the MAC cluster panel
+# ═══════════════════════════════════════════════════════════
+
+services:
+
+ # ── vLLM GPU Inference ─────────────────────────────────────
+ vllm:
+ image: vllm/vllm-openai:latest
+ container_name: mac-worker-vllm
+ ports:
+ - "${VLLM_PORT:-8001}:8001"
+ environment:
+ - HF_HOME=/root/.cache/huggingface
+ - HUGGING_FACE_HUB_TOKEN=${HF_TOKEN:-}
+ volumes:
+ - hf-cache:/root/.cache/huggingface
+ command: >
+ --model ${VLLM_MODEL:-Qwen/Qwen2.5-7B-Instruct-AWQ}
+ --port ${VLLM_PORT:-8001}
+ --gpu-memory-utilization ${VLLM_GPU_MEM:-0.85}
+ --max-model-len ${VLLM_MAX_LEN:-8192}
+ --trust-remote-code
+ --enforce-eager
+ --served-model-name ${VLLM_SERVED_NAME:-Qwen/Qwen2.5-7B-Instruct-AWQ}
+ deploy:
+ resources:
+ reservations:
+ devices:
+ - driver: nvidia
+ count: 1
+ capabilities: [gpu]
+ restart: unless-stopped
+ networks:
+ - worker-net
+ healthcheck:
+ test: ["CMD-SHELL", "curl -sf http://localhost:${VLLM_PORT:-8001}/health || exit 1"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 120s
+
+ # ── Jupyter Kernel Gateway (optional — for notebook offload) ──
+ # Enable by setting ENABLE_NOTEBOOK=1 in .env.worker
+ jupyter:
+ image: jupyter/scipy-notebook:latest
+ container_name: mac-worker-jupyter
+ ports:
+ - "${NOTEBOOK_PORT:-8888}:8888"
+ environment:
+ - JUPYTER_ENABLE_LAB=no
+ command: >
+ jupyter kernelgateway
+ --KernelGatewayApp.ip=0.0.0.0
+ --KernelGatewayApp.port=8888
+ --KernelGatewayApp.allow_origin=*
+ --KernelGatewayApp.auth_token=${JUPYTER_TOKEN:-mac-notebook-token}
+ volumes:
+ - notebooks:/home/jovyan/work
+ restart: unless-stopped
+ networks:
+ - worker-net
+ profiles:
+ - notebook # only starts with: docker compose --profile notebook up
+
+ # ── Worker Agent ───────────────────────────────────────────
+ worker-agent:
+ image: python:3.11-slim
+ container_name: mac-worker-agent
+ working_dir: /app
+ volumes:
+ - ./worker_agent.py:/app/worker_agent.py:ro
+ command: >
+ sh -c "pip install --quiet httpx psutil pynvml && python worker_agent.py"
+ environment:
+ - MAC_MASTER_URL=${MAC_MASTER_URL}
+ - MAC_ENROLL_TOKEN=${MAC_ENROLL_TOKEN:-}
+ - MAC_NODE_TOKEN=${MAC_NODE_TOKEN:-}
+ - MAC_WORKER_NAME=${MAC_WORKER_NAME:-Worker}
+ - MAC_VLLM_PORT=${VLLM_PORT:-8001}
+ - MAC_NOTEBOOK_PORT=${NOTEBOOK_PORT:-}
+ - MAC_TAGS=${MAC_TAGS:-llm}
+ - MAC_HEARTBEAT_SEC=${HEARTBEAT_SEC:-10}
+ network_mode: host # needs to see vLLM on localhost AND reach master
+ restart: unless-stopped
+ depends_on:
+ vllm:
+ condition: service_healthy
+
+volumes:
+ hf-cache:
+ notebooks:
+
+networks:
+ worker-net:
+ driver: bridge
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000000000000000000000000000000000000..652841b9aee8af1f9319efdcb19b2299ebad87aa
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,226 @@
+# ═══════════════════════════════════════════════════════════
+# MAC — MBM AI Cloud | Local Server Setup (12GB GPU)
+# ═══════════════════════════════════════════════════════════
+# RTX 3060 12GB VRAM — single model at a time strategy.
+# GPU: Qwen2.5-7B chat/code ~ 5GB (gpu_memory_utilization=0.45)
+# CPU: Whisper STT + Piper TTS ~ 1.5GB RAM (no VRAM)
+# Infra: PostgreSQL + Redis + Nginx + Qdrant + SearXNG
+# ═══════════════════════════════════════════════════════════
+
+services:
+
+ # ── MAC API Server ──────────────────────────────────────
+ mac:
+ build: .
+ container_name: mac-api
+ ports:
+ - "${APP_HOST:-0.0.0.0}:8001:8000"
+ env_file: .env
+ environment:
+ - DATABASE_URL=postgresql+asyncpg://mac:mac_password@postgres:5432/mac_db
+ - REDIS_URL=redis://redis:6379/0
+ - VLLM_BASE_URL=http://vllm-speed:8001
+ - VLLM_SPEED_URL=http://vllm-speed:8001
+ - VLLM_CODE_URL=http://vllm-speed:8001
+ - VLLM_REASONING_URL=http://vllm-speed:8001
+ - VLLM_INTELLIGENCE_URL=http://vllm-speed:8001
+ - WHISPER_URL=http://whisper:8000
+ - TTS_URL=http://tts:8000
+ - EMBEDDING_URL=http://vllm-speed:8001
+ - QDRANT_URL=http://qdrant:6333
+ - SEARXNG_URL=http://searxng:8080
+ - MAC_ENABLED_MODELS=qwen2.5:7b,whisper-small,tts-piper
+ depends_on:
+ postgres:
+ condition: service_healthy
+ redis:
+ condition: service_healthy
+ restart: unless-stopped
+ networks:
+ - mac-net
+
+ # ═══════════════════════════════════════════════════════
+ # vLLM GPU INFERENCE — Single model for 12GB GPU
+ # ═══════════════════════════════════════════════════════
+
+ # ── Speed Model: Qwen2.5-7B (handles ALL chat/code/general) ──
+ vllm-speed:
+ image: vllm/vllm-openai:latest
+ container_name: mac-vllm-speed
+ ports:
+ - "${VLLM_SPEED_PORT:-8001}:${VLLM_SPEED_PORT:-8001}"
+ environment:
+ - HF_HOME=/root/.cache/huggingface
+ volumes:
+ - hf-cache:/root/.cache/huggingface
+ command: >
+ --model ${VLLM_SPEED_MODEL:-Qwen/Qwen2.5-7B-Instruct-AWQ}
+ --port ${VLLM_SPEED_PORT:-8001}
+ --gpu-memory-utilization 0.85
+ --max-model-len 8192
+ --trust-remote-code
+ --enforce-eager
+ deploy:
+ resources:
+ reservations:
+ devices:
+ - driver: nvidia
+ count: 1
+ capabilities: [gpu]
+ restart: unless-stopped
+ networks:
+ - mac-net
+
+ # ── Code/Reasoning/Intelligence models DISABLED (12GB GPU) ──
+ # Uncomment when upgrading to 24GB+ GPU
+ # vllm-code:
+ # ...
+ # vllm-reason:
+ # ...
+ # vllm-intel:
+ # ...
+
+ # ═══════════════════════════════════════════════════════
+ # SPEECH & AUDIO SERVICES (CPU — saves GPU for LLM)
+ # ═══════════════════════════════════════════════════════
+
+ # ── Whisper — Speech-to-Text (CPU mode) ────────────────
+ whisper:
+ image: fedirz/faster-whisper-server:latest-cpu
+ container_name: mac-whisper
+ ports:
+ - "${WHISPER_PORT:-8005}:8000"
+ environment:
+ - WHISPER__MODEL=${WHISPER_MODEL:-Systran/faster-whisper-small}
+ - WHISPER__DEVICE=cpu
+ restart: unless-stopped
+ networks:
+ - mac-net
+
+ # ── Piper TTS — Text-to-Speech (CPU, lightweight) ─────
+ # TEMPORARILY DISABLED — image still downloading on slow WiFi
+ # tts:
+ # image: ghcr.io/matatonic/openedai-speech:latest
+ # container_name: mac-tts
+ # ports:
+ # - "${TTS_PORT:-8006}:8000"
+ # volumes:
+ # - tts-voices:/app/voices
+ # restart: unless-stopped
+ # networks:
+ # - mac-net
+
+ # ═══════════════════════════════════════════════════════
+ # INFRASTRUCTURE SERVICES
+ # ═══════════════════════════════════════════════════════
+
+ # ── PostgreSQL — Persistent data store ─────────────────
+ postgres:
+ image: postgres:16-alpine
+ container_name: mac-postgres
+ environment:
+ POSTGRES_USER: mac
+ POSTGRES_PASSWORD: mac_password
+ POSTGRES_DB: mac_db
+ ports:
+ - "5433:5432"
+ volumes:
+ - pgdata:/var/lib/postgresql/data
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U mac -d mac_db"]
+ interval: 5s
+ timeout: 5s
+ retries: 5
+ restart: unless-stopped
+ networks:
+ - mac-net
+
+ # ── pgAdmin — PostgreSQL admin UI (local-only by default) ──
+ pgadmin:
+ image: dpage/pgadmin4:8
+ container_name: mac-pgadmin
+ ports:
+ - "127.0.0.1:${PGADMIN_PORT:-5051}:80"
+ environment:
+ PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-admin@mbm.ac.in}
+ PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-ChangeThisStrongPassword!}
+ PGADMIN_CONFIG_ENHANCED_COOKIE_PROTECTION: "True"
+ depends_on:
+ postgres:
+ condition: service_healthy
+ volumes:
+ - pgadmin-data:/var/lib/pgadmin
+ restart: unless-stopped
+ networks:
+ - mac-net
+
+ # ── Redis — Rate limiting & caching ────────────────────
+ redis:
+ image: redis:7-alpine
+ container_name: mac-redis
+ ports:
+ - "6380:6379"
+ volumes:
+ - redisdata:/data
+ healthcheck:
+ test: ["CMD", "redis-cli", "ping"]
+ interval: 5s
+ timeout: 5s
+ retries: 5
+ restart: unless-stopped
+ networks:
+ - mac-net
+
+ # ── Nginx — Reverse proxy + SvelteKit frontend ─────────
+ nginx:
+ image: nginx:alpine
+ container_name: mac-nginx
+ ports:
+ - "${APP_HOST:-0.0.0.0}:${APP_PORT:-80}:80"
+ volumes:
+ - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
+ - ./frontend/build:/app:ro # SvelteKit static build output
+ depends_on:
+ - mac
+ restart: unless-stopped
+ networks:
+ - mac-net
+
+ # ── Qdrant — Vector DB for RAG ─────────────────────────
+ qdrant:
+ image: qdrant/qdrant:latest
+ container_name: mac-qdrant
+ ports:
+ - "6333:6333"
+ volumes:
+ - qdrantdata:/qdrant/storage
+ restart: unless-stopped
+ networks:
+ - mac-net
+
+ # ── SearXNG — Self-hosted web search ───────────────────
+ searxng:
+ image: searxng/searxng:latest
+ container_name: mac-searxng
+ ports:
+ - "8888:8080"
+ environment:
+ - SEARXNG_BASE_URL=http://localhost:8888/
+ volumes:
+ - searxngdata:/etc/searxng
+ restart: unless-stopped
+ networks:
+ - mac-net
+
+volumes:
+ pgdata:
+ pgadmin-data:
+ redisdata:
+ qdrantdata:
+ searxngdata:
+ hf-cache: # Shared HuggingFace model cache across all vLLM instances
+ tts-voices: # Persisted TTS voice models
+
+networks:
+ mac-net:
+ driver: bridge
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
new file mode 100644
index 0000000000000000000000000000000000000000..cdaa5220f8b5482687d0d189b66e39f8a6c15996
--- /dev/null
+++ b/docs/ARCHITECTURE.md
@@ -0,0 +1,536 @@
+# MAC — Architecture Reference
+
+> **Audience:** an AI coding agent (or new engineer) dropped into this repo with no prior context.
+> **Goal:** understand the system end-to-end — every subsystem, the data flow, where state lives, and how the pieces secure and observe each other.
+> Read [README.md](README.md) for the elevator pitch and [MAC-PROGRESS.md](MAC-PROGRESS.md) for the build log. This file is the *map*.
+
+---
+
+## 0. Identity in one paragraph
+
+MAC (MBM AI Cloud) is a **self-hosted, on-prem AI platform** for MBM University Jodhpur. It gives students/faculty a private ChatGPT-style chat, a notebook IDE, RAG over college docs, an attendance system using face capture, an exam copy-check workflow with AI vision + plagiarism detection, and an admin/cluster console — all powered by **open-source LLMs** running on the college's own GPUs. There are no external API calls; vLLM serves models locally, and worker GPUs are added by enrolling them into the cluster.
+
+---
+
+## 1. Top-level topology
+
+```
+┌────────────────────────────────────────────────────────────────┐
+│ CLIENTS │
+│ • Web (SvelteKit PWA, served by Nginx in prod) │
+│ • API consumers (curl / Python SDK / scripts) │
+└──────────────────────────┬─────────────────────────────────────┘
+ │ HTTPS
+ ▼
+ ┌──────────────────────┐
+ │ NGINX │ ← TLS, gzip, /api → mac, / → static
+ └──────────┬───────────┘
+ │
+ ┌──────────────────┴──────────────────┐
+ ▼ ▼
+┌─────────────────┐ ┌──────────────────────┐
+│ SvelteKit │ │ FastAPI (mac.main) │
+│ static build │ │ /api/v1/* │
+└─────────────────┘ └──────────┬───────────┘
+ │
+ ┌───────────────────────┬───────────────────┼─────────────────────────┐
+ ▼ ▼ ▼ ▼
+┌────────────┐ ┌────────────┐ ┌──────────────┐ ┌────────────────┐
+│ PostgreSQL │ │ Redis │ │ Qdrant │ │ SearXNG │
+│ (primary) │ │ cache / │ │ (RAG vec) │ │ (web search) │
+│ Alembic │ │ bl / rl │ └──────────────┘ └────────────────┘
+└────────────┘ └────────────┘
+
+ ▲ load_balancer.get_best_worker()
+ │
+ ┌───────────────────────┴───────────────────────────────────────────────┐
+ │ MAC CLUSTER (GPU workers, any LAN PC) │
+ │ ┌─────────────────┐ ┌─────────────────┐ ┌────────────────┐ │
+ │ │ vLLM (OpenAI │ │ Jupyter kernel │ │ worker_agent.py│ │
+ │ │ compatible) │ │ gateway (opt.) │ │ (heartbeat) │ │
+ │ └─────────────────┘ └─────────────────┘ └────────────────┘ │
+ └────────────────────────────────────────────────────────────────────────┘
+```
+
+- **Master node** runs FastAPI + Postgres + Redis + Nginx + Qdrant + SearXNG.
+- **Worker nodes** run vLLM + an optional Jupyter kernel gateway, plus [worker_agent.py](worker_agent.py) which self-registers via an enrollment token and sends a heartbeat every 10s (GPU util, VRAM, RAM, CPU).
+- **Routing** is master-side: every user request hits the master API, which uses [mac/services/load_balancer.py](mac/services/load_balancer.py) to score-pick the best worker for an LLM call or notebook kernel.
+
+---
+
+## 2. Repository map (what lives where)
+
+```
+mac/
+ main.py FastAPI app, lifespan (DB init, dev seeds, bg tasks),
+ router mounts under /api/v1, root SPA fallback.
+ config.py Pydantic Settings — every env var + .env loader.
+ database.py Async SQLAlchemy engine + session factory; `Base`.
+ utils/security.py JWT encode/decode + jti generation; password hash.
+ middleware/
+ auth_middleware.py Bearer extractor → JWT | legacy-key | scoped-key → User.
+ rate_limit.py Per-user req/hour + token/day; injects X-RateLimit-*.
+ feature_gate.py feature_required("ai_chat") dependency.
+ models/ SQLAlchemy ORM models (one file per domain).
+ schemas/ Pydantic request/response schemas.
+ services/ Pure business logic, no HTTP — called by routers.
+ routers/ FastAPI routers, thin: validate → call service → return.
+
+frontend/ SvelteKit 2 + Svelte 5 PWA.
+ src/routes/ File-system routing: login, setup, chat, dashboard,
+ admin, cluster, keys, settings, notifications, rag.
+ src/lib/api.js Single fetch wrapper; one export per backend domain.
+ src/lib/stores.js Svelte stores (auth, setup, features, chat, toast).
+ src/lib/i18n.js 19 Indian languages, lazy-loaded strings, RTL support.
+ static/manifest.json PWA manifest; static/sw.js is a no-cache worker.
+
+alembic/ Migration env + versioned revisions.
+nginx/ nginx.conf (HTTP) + nginx.https.conf (TLS).
+docker-compose.yml Master stack.
+docker-compose.worker.yml Worker stack (vLLM + worker_agent).
+worker_agent.py Enrollment + heartbeat agent for a GPU node.
+installer/ Windows installer (PyInstaller) + branding assets.
+tests/ pytest suite.
+```
+
+---
+
+## 3. Request lifecycle (the universal path)
+
+Every authenticated `/api/v1/*` request goes through these layers in order. Knowing this map means you can audit any new endpoint quickly.
+
+```
+HTTP request
+ │
+ ▼
+[1] CORS middleware (mac/main.py — allow_origins from settings)
+ │
+ ▼
+[2] Route handler (FastAPI) (mac/routers/*.py)
+ │ Depends(get_current_user)
+ ▼
+[3] Auth resolver (mac/middleware/auth_middleware.py)
+ │ Bearer token → branch:
+ │ • mac_sk_live_* → legacy API key (User.api_key)
+ │ • mac_sk_* → scoped API key (hashed, scopes, expiry, revocable)
+ │ • else → JWT (verify sig, check exp, check jti blacklist)
+ │ → returns User or raises 401
+ │
+ ▼
+[4] Role guard (optional) require_admin / require_faculty_or_admin
+ │
+ ▼
+[5] Feature gate (optional) feature_required("ai_chat")
+ │ → reads system_config / feature_flags table → 403 if disabled for role
+ │
+ ▼
+[6] Rate limit (optional) check_rate_limit
+ │ • requests/hour from usage_log (per-user)
+ │ • tokens/day from usage_log (per-user)
+ │ • injects X-RateLimit-* into request.state
+ │
+ ▼
+[7] Service layer mac/services/*.py
+ │ Business logic — never imports FastAPI; takes db: AsyncSession.
+ │
+ ▼
+[8] Response → HTTP middleware inject_rate_limit_headers reads request.state
+ and stamps headers onto the response
+```
+
+This separation is the single most important design rule:
+**routers do parsing + auth + I/O orchestration; services do business logic; models do persistence.** Anything calling FastAPI types from a service is a smell.
+
+---
+
+## 4. Identity & access — auth, sessions, keys
+
+There are **three** ways a request authenticates, all collapsed to a `User` by `get_current_user`:
+
+### 4.1 JWT (interactive users)
+- Login: `POST /api/v1/auth/login` with `{roll_number, password}` → `{access_token, refresh_token, user}`.
+- Access token lifetime: `JWT_ACCESS_TOKEN_EXPIRE_MINUTES` (default 1440 = 24h).
+- Every access token carries a `jti` (random UUID) baked into the JWT claims by [mac/utils/security.py](mac/utils/security.py).
+- `POST /api/v1/auth/logout` blacklists the current `jti` in Redis with a TTL equal to remaining token life ([token_blacklist_service.py](mac/services/token_blacklist_service.py)). Refresh tokens are also revoked. Falls back to an in-process set if Redis is unreachable (dev only).
+- The JWT signing secret is **not** read from env in production — it's stored in `system_config` and seeded on first boot by [setup_service.get_or_generate_jwt_secret](mac/services/setup_service.py). This means restarting the app does not invalidate everyone's sessions.
+
+### 4.2 Legacy API keys
+- Format: `mac_sk_live_<48 hex chars>`. Stored on `users.api_key`. One per user.
+- Use case: scripts that need a stable long-lived credential.
+- Resolved before JWT in `auth_middleware` because of the prefix check.
+
+### 4.3 Scoped API keys
+- Format: `mac_sk_`, hashed at rest. Created via `/api/v1/scoped-keys`.
+- Carry: scopes (list of allowed endpoints), optional expiry, label, revoke flag.
+- Resolved by [scoped_key_service.get_key_by_hash](mac/services/scoped_key_service.py).
+- Attached to `user._scoped_key` for downstream scope enforcement.
+
+### 4.4 Roles
+- `admin` | `faculty` | `student`. Enforced at the router layer via `require_admin` / `require_faculty_or_admin` dependencies.
+- Feature flags layer on top: a feature can be enabled globally but restricted to specific roles (see `feature_flags.roles`).
+
+### 4.5 First-run onboarding
+- `GET /api/v1/setup/status` → `{is_first_run, has_jwt_secret, version}`. Frontend uses this to decide whether to show the setup wizard or login.
+- `POST /api/v1/setup/create-admin` provisions the first admin and seals the system.
+
+---
+
+## 5. LLM serving & cluster routing
+
+### 5.1 Model registry — three layers of override
+`mac/services/llm_service.py::_BUILTIN_MODELS` holds the defaults (Qwen2.5 7B, Qwen2.5-Coder 7B/AWQ, DeepSeek-R1, etc.). Each entry knows its `served_name` (HF repo), `category` (`speed | code | reasoning | intelligence`), `capabilities`, and `url_key` pointing at one of `vllm_speed_url | vllm_code_url | …` in `Settings`.
+
+Override priority:
+1. `MAC_MODELS_JSON` env var (a full JSON array) — replaces the registry entirely.
+2. `MAC_ENABLED_MODELS` env var (comma-separated IDs) — filters which built-ins are exposed.
+3. `MAC_AUTO_FALLBACK` — what `model="auto"` resolves to.
+
+### 5.2 The system prompt is forced
+`_inject_system_prompt` in `llm_service` prepends a hard-coded MAC identity prompt to **every** chat completion. This prevents the underlying Qwen/DeepSeek model from claiming to be "Qwen made by Alibaba" — it always says it is MAC, built by MBM University. If the user supplied a system message, MAC's identity is concatenated in front of theirs.
+
+### 5.3 Routing decision (where does this call go?)
+```
+chat request
+ │
+ ▼
+llm_service._resolve_model_cluster(model_id)
+ │
+ ▼
+load_balancer.get_best_worker(db, model_id)
+ │ SELECT WorkerNode JOIN NodeModelDeployment
+ │ WHERE node.status='active' AND deployment.status='ready'
+ │ AND last_heartbeat within 30s
+ │ ORDER BY gpu_util*0.5 + (vram_used/total)*0.3
+ │
+ ├── candidate found → POST http://{node.ip}:{deployment.port}/v1/chat/completions
+ │
+ └── none → fall back to local config (settings.vllm__url)
+```
+
+vLLM speaks the **OpenAI-compatible** API, so the proxy is a near-pass-through with SSE streaming preserved end-to-end.
+
+### 5.4 Cluster lifecycle
+| Event | Endpoint | Auth | Effect |
+|---|---|---|---|
+| Admin mints token | `POST /cluster/enroll-token` | admin JWT | Single-use, expiring `EnrollmentToken` row |
+| Worker registers | `POST /cluster/register` | enroll token | Creates `WorkerNode` (status `pending`) + reports IP, GPU specs |
+| Admin approves | `POST /cluster/nodes/{id}/action {action:"approve"}` | admin | `status → active` |
+| Worker heartbeats | `POST /cluster/heartbeat` | node token | Updates `last_heartbeat`, GPU util, VRAM, CPU, RAM, queue depth — also append-only into `cluster_heartbeats` (time-series for charts) |
+| Worker reports models | (in heartbeat payload) | — | Upserts `NodeModelDeployment` rows |
+| Drain / remove | `POST /cluster/nodes/{id}/action` | admin | Stops new traffic; allows in-flight to finish |
+
+Workers older than 30s without a heartbeat are silently skipped by the balancer — no manual intervention needed if a worker dies.
+
+---
+
+## 6. Notebooks — multi-language code execution
+
+This is the most operationally complex subsystem. The design supports **two backends** and **distributed execution**.
+
+### 6.1 Architecture
+```
+Client (browser)
+ │ WebSocket /ws/notebook/{notebook_id}?token=JWT
+ ▼
+mac/routers/notebook_ws.py
+ │ • verifies JWT (decode_access_token, no DB hit on hot path)
+ │ • registers connection in _connections[notebook_id]
+ ▼
+kernel_manager (mac/services/kernel_manager.py)
+ │ Backend selection at startup:
+ │ _docker_available() → Docker mode
+ │ else → subprocess mode (dev)
+ │
+ ├── DOCKER MODE (production)
+ │ • spawns mac-kernel-{lang} container (image_prefix in config)
+ │ • applies memory + CPU limits from settings
+ │ • optionally attaches GPU (--gpus all) for ML kernels
+ │ • streams stdout/stderr back as JSONL events
+ │
+ ├── SUBPROCESS MODE (dev)
+ │ • runs the language interpreter directly on the host
+ │ • no isolation; only safe for trusted local dev
+ │
+ └── REMOTE WORKER MODE
+ • load_balancer.get_notebook_worker(db) picks a worker with notebook_port
+ • forwards the execute via the worker's Jupyter kernel gateway
+ • output streams back to the master, then to the client
+```
+
+### 6.2 WebSocket protocol
+Defined at the top of [notebook_ws.py](mac/routers/notebook_ws.py):
+
+| Direction | Type | Payload |
+|---|---|---|
+| C→S | `execute` | `{cell_id, code, language}` |
+| C→S | `interrupt` | `{kernel_id}` |
+| C→S | `ping` | — |
+| S→C | `status` | `{cell_id, execution_state: busy\|idle}` |
+| S→C | `stream` | `{cell_id, name: stdout\|stderr, text}` |
+| S→C | `error` | `{cell_id, ename, evalue, traceback[]}` |
+| S→C | `pong` | — |
+
+### 6.3 State & limits
+- `KernelInstance` per session: `id`, `language`, `node_id`, `container_id`, `status`, `last_activity`, `execution_count`.
+- Idle kernels are reaped after `kernel_timeout` seconds (default 120).
+- Max concurrent kernels per node: `kernel_max_per_node` (default 10).
+- Persistent notebook content: `notebooks` table; cells stored as JSON, ordered.
+
+### 6.4 Why a custom protocol and not raw Jupyter?
+Three reasons: (a) we need user-scoped auth via our JWT; (b) we need to fan-out execution across the cluster, not just one local kernel; (c) we want the option to swap kernels for sandboxed runners later without changing the wire format.
+
+---
+
+## 7. RAG — private document search
+
+Pipeline: **upload → chunk → embed → store → retrieve → augment**.
+
+```
+PDF/MD/TXT upload (POST /rag/upload)
+ │
+ ▼
+rag_service.ingest_document
+ │ • text extraction (pypdf for PDF, plain read otherwise)
+ │ • chunk_text(words=512, overlap=50) ← simple word-window
+ │ • for each chunk:
+ │ emb = await llm_service.embed(text) ← uses EMBEDDING_URL or vLLM
+ │ qdrant.upsert(point=(uuid, emb, payload))
+ │ • RAGDocument row in Postgres with chunk count & status
+ ▼
+QUERY TIME (chat with rag context)
+ │
+ ▼
+rag_service.query(question, top_k=5)
+ │ • emb_q = embed(question)
+ │ • qdrant.search(collection, emb_q, top_k)
+ │ • returns chunks + source metadata
+ ▼
+llm_service.chat with messages = [
+ {role:"system", content: MAC_PROMPT + "\n\nContext:\n" + chunks},
+ *user_messages,
+ ]
+```
+
+Collections (`RAGCollection`) namespace documents — e.g. one per subject. Documents (`RAGDocument`) track ownership and indexing status so the UI can show "Indexing 42/120 chunks…".
+
+---
+
+## 8. Attendance — face-based check-in
+
+### 8.1 Models
+- `FaceTemplate` — one per user, holds a face encoding (64-byte hash in dev; pluggable to `face_recognition`/`dlib` for production).
+- `AttendanceSession` — created by faculty: `{branch, section, subject, date, window_minutes}`.
+- `AttendanceRecord` — one per (session, student): `present | absent | late`, captured selfie hash, confidence, timestamp.
+
+### 8.2 Flow
+```
+1. Faculty: POST /attendance/sessions → creates session, returns join token + QR
+2. Student: GET /attendance/active → returns currently open sessions for them
+3. Student: POST /attendance/check-in → uploads base64 selfie
+ server:
+ • decodes image
+ • hashes (sha256) — dedupe replay
+ • computes encoding
+ • compares to stored FaceTemplate
+ • if (match && within window) → AttendanceRecord(present)
+ • else → 401 with reason
+4. Faculty: GET /attendance/sessions/{id}/report → CSV / PDF roster
+```
+
+### 8.3 Anti-cheat heuristics
+- Session has a strict `window_minutes` — late arrivals are recorded as `late`, not `present`.
+- Same selfie hash twice in a session → rejected (replay block).
+- One record per (session, student) — UPSERT prevents stuffing.
+- Production: swap `_compute_face_encoding` for the real `face_recognition.face_encodings()` (the call sites already accept it; only the function body changes).
+
+---
+
+## 9. Copy Check — exam paper evaluation
+
+A faculty workflow that grades scanned answer sheets using vision-capable LLMs and runs cross-paper plagiarism detection. Models in `mac/models/copy_check.py`:
+
+| Model | Role |
+|---|---|
+| `CopyCheckSession` | One exam: subject, class, total_marks, syllabus_text |
+| `CopyCheckSheet` | One student's submission: roll, scanned pages, AI score, feedback |
+| `CopyCheckPlagiarism` | Pairwise similarity between two sheets in the same session |
+
+### 9.1 Flow
+```
+Faculty creates session → uploads syllabus / answer key
+ │
+ ▼
+For each student answer sheet (PDF or image bundle):
+ • file saved under uploads/copy_check/{session_id}/{roll}/
+ • AI vision model reads each page (multimodal LLM)
+ • Service builds a structured prompt: syllabus + answer key + student answer
+ • LLM returns { per_question_marks, total, weakness_summary, suggestions }
+ • CopyCheckSheet upserted with score + JSON feedback
+ │
+ ▼
+Plagiarism pass:
+ • difflib.SequenceMatcher on extracted text per pair within session
+ • CopyCheckPlagiarism row written for (sheet_a, sheet_b, similarity, flagged_passages)
+ │
+ ▼
+Faculty reviews:
+ • per-student PDF report (fpdf2)
+ • plagiarism heatmap
+ • can override AI marks before "publish"
+```
+
+### 9.2 Why the AI doesn't have final authority
+The faculty UI explicitly requires a **"Reviewed & Approved"** flag before any score becomes visible to students. The AI is graded as a *recommendation* — the audit trail records both the AI suggestion and the faculty's override. This is the legal/academic-integrity boundary.
+
+---
+
+## 10. Other domain modules (one-paragraph each)
+
+- **Doubts forum** ([doubts.py](mac/routers/doubts.py)): students post questions; faculty/peers answer; AI generates a draft answer that the asker can accept or replace. Threaded, taggable.
+- **File sharing** ([file_share.py](mac/routers/file_share.py)): admin/faculty upload class materials; per-file access scoping; per-download analytics in `file_downloads`.
+- **Notifications** ([notifications.py](mac/routers/notifications.py)): in-app + Web Push (`pywebpush`); endpoints registered via VAPID; one row per user-notification with read/unread state.
+- **Academic** ([academic.py](mac/routers/academic.py)): branches & sections — used to scope attendance, file sharing, and admin lists.
+- **Doubt copy-check submissions** ([model_submission_service.py](mac/services/model_submission_service.py)): community-trained adapter / LoRA submissions queued for admin review before being published as model registry entries.
+- **Search** ([search.py](mac/routers/search.py) + SearXNG): private metasearch, no Google, no telemetry, returned to the chat as a tool result.
+- **Hardware / Network / System** ([hardware.py](mac/routers/hardware.py), [network.py](mac/routers/network.py), [system.py](mac/routers/system.py)): admin diagnostics — local CPU/GPU/RAM, recommended models for the detected GPU, LAN discovery (`mac/services/discovery.py` UDP broadcast on port 7700), version & update status (`mac/services/updater.py` polls GitHub releases).
+- **Quota** ([quota.py](mac/routers/quota.py)): per-user requests/hour and tokens/day; admin can override per user; default from `RATE_LIMIT_*` env.
+- **Guardrails** ([guardrails.py](mac/routers/guardrails.py) + `guardrail_service`): admin-editable ruleset (banned terms, forbidden topics) applied as a pre-check on chat input and a post-check on model output.
+
+---
+
+## 11. Cross-cutting concerns
+
+### 11.1 Configuration
+**One source of truth:** [mac/config.py](mac/config.py) `Settings(BaseSettings)`. Every value reads from env or `.env`. `_fix_database_url` auto-promotes `postgres://` and `postgresql://` to `postgresql+asyncpg://` and strips `sslmode=` (it's handled in `connect_args` separately for Neon/Supabase). Adding a new tunable means: add a field to `Settings`, document it in `.env.example`, use `settings.your_field` everywhere — never read `os.environ` directly.
+
+### 11.2 Migrations
+Alembic-managed. Two revisions today:
+- `20260426_0001_initial_schema.py` — full original schema.
+- `20260427_0002_session1_tables.py` — feature flags, system_config, branches, sections, cluster_heartbeats, shared_files, file_downloads, video_projects, video_jobs.
+
+In dev (`MAC_ENV=development`), `init_db()` in `lifespan` creates tables idempotently from `Base.metadata`. In prod, you **must** run `alembic upgrade head` before serving traffic; tables are not auto-created. Whenever you add a column to a model, write a new revision.
+
+### 11.3 Background tasks
+Started in `lifespan` and cancelled on shutdown:
+- [updater.background_check_loop](mac/services/updater.py) — polls GitHub for new releases every `MAC_UPDATE_CHECK_INTERVAL_HOURS`.
+- [discovery.start_discovery_server](mac/services/discovery.py) — UDP broadcast listener so worker PCs on the LAN can find the master without manual IP entry.
+
+### 11.4 Caching, blacklisting, rate limits
+All Redis-backed with **graceful in-process fallback**:
+- JWT blacklist → `mac:bl:{jti}` keys with TTL = remaining token life.
+- Rate-limit counters → derived from `usage_log` rows (no Redis needed for counts).
+- Session/feature caches → not implemented yet; designed to live under `mac:cache:*`.
+
+### 11.5 Observability
+Every chat call is logged to `usage_log`: user_id, model_id, tokens_in, tokens_out, latency_ms, status, request_id (`generate_request_id` in `utils/security`). The dashboard route reads these for per-user charts. Cluster heartbeats are append-only into `cluster_heartbeats` so node history charts are just `SELECT … ORDER BY ts`.
+
+---
+
+## 12. Frontend — SvelteKit PWA
+
+### 12.1 Stack
+SvelteKit 2 + Svelte 5 + Tailwind 3 + Vite 6. Built as a static site (`@sveltejs/adapter-static` with `fallback: 'index.html'`) and served by Nginx in production, by Vite dev server with `/api` proxy to the FastAPI port in development.
+
+### 12.2 SPA mode
+The root has `+layout.js` with `export const ssr = false; export const prerender = false;` so the entire app is rendered client-side. This is intentional — it sidesteps hydration issues, and there is no SEO need for an internal college tool.
+
+### 12.3 State
+[src/lib/stores.js](frontend/src/lib/stores.js) holds Svelte stores:
+- `authStore` — `{user, token, refreshToken}`, with `init()` that re-hydrates from `localStorage` and re-fetches `/auth/me`, plus `login`/`logout`.
+- `setupStore` — `is_first_run` flag.
+- `featureStore` — feature flag map for conditional UI.
+- `chatStore` — local conversation history (per-session, not yet server-persisted).
+- `toast` — single-message notifier.
+
+### 12.4 API client
+[src/lib/api.js](frontend/src/lib/api.js) is the *only* place that talks HTTP. One `headers()` helper attaches the bearer token from `localStorage`. Each backend domain (`auth`, `query`, `models`, `cluster`, `rag`, `files`, …) is its own export with named methods. Adding a new endpoint = add a method here, never `fetch()` from a component directly.
+
+### 12.5 Auth/setup gate
+[+layout.svelte](frontend/src/routes/+layout.svelte) boots the app on first paint:
+1. `initLocale()` — detect language from `localStorage` / browser.
+2. `authStore.init()` — restore session.
+3. `checkSetup()` — first-run check.
+4. `loadFeatures()` — fetch flags.
+5. Redirect: first-run → `/setup`, no user on protected route → `/login`, root → `/chat` or `/login`.
+6. Render either `Sidebar + slot` (logged in) or bare `slot` (login/setup).
+
+### 12.6 Internationalisation
+[src/lib/i18n.js](frontend/src/lib/i18n.js) ships **19 Indian languages** with lazy-loaded string maps and an `RTL_LOCALES` set (Urdu) that flips the layout direction. Adding a new locale = add to `SUPPORTED_LOCALES`, drop a translation map, no other file changes.
+
+### 12.7 PWA + service worker
+[static/manifest.json](frontend/static/manifest.json) declares the installable app + shortcuts. [static/sw.js](frontend/static/sw.js) is intentionally **caching-disabled** — every install/activate wipes all caches and there is no `fetch` handler. This was a deliberate decision: caching the SPA shell caused stale-build problems during rapid dev. Re-introduce caching only behind a versioned cache name with a clear invalidation strategy.
+
+---
+
+## 13. Deployment
+
+### 13.1 Master node (single command)
+```bash
+cd frontend && npm install && npm run build && cd ..
+cp .env.example .env # edit secrets
+docker compose up postgres -d
+docker compose run --rm mac alembic upgrade head
+docker compose up -d
+```
+Compose brings up: `mac` (FastAPI), `postgres`, `redis`, `qdrant`, `searxng`, `vllm-speed`, `nginx`. (Whisper/TTS commented out by default.)
+
+### 13.2 Adding a worker
+On master:
+```bash
+curl -X POST http://MASTER:8000/api/v1/cluster/enroll-token \
+ -H "Authorization: Bearer ADMIN_JWT" -d '{"label":"Lab PC 1","expires_hours":24}'
+```
+On the worker PC:
+```bash
+MAC_MASTER_URL=http://MASTER:8000 \
+MAC_ENROLL_TOKEN= \
+MAC_VLLM_PORT=8001 \
+docker compose -f docker-compose.worker.yml up -d
+```
+Then approve in admin → Cluster.
+
+### 13.3 HTTPS
+Drop certs into `nginx/ssl/`, swap the bind-mounted config to `nginx/nginx.https.conf` in `docker-compose.yml`, restart Nginx.
+
+### 13.4 Windows installer
+[installer/build_installer.ps1](installer/build_installer.ps1) builds a one-shot `dist/MAC-Installer.exe` (PyInstaller) that bootstraps Docker Desktop checks, clones/updates the repo, writes a sane `.env` with detected host IP, and starts the master stack. Branding assets are embedded base64 in [installer/embedded_assets.py](installer/embedded_assets.py) so the binary works even if image files are missing at runtime.
+
+---
+
+## 14. Security checklist (what every reviewer should verify)
+
+1. **No external API calls.** `grep -r "openai.com\|api.anthropic\|googleapis" mac/` should be empty. All inference is local.
+2. **JWT secret is not in env in production.** It's seeded in `system_config` on first boot and re-used across restarts.
+3. **JWT carries `jti`** and the auth middleware checks blacklist on every request.
+4. **Every router** requiring auth uses `Depends(get_current_user)` — search for any `@router.*` that doesn't and justify it.
+5. **Role guards** on admin-only operations: `Depends(require_admin)` on token mints, user list, cluster mutations, feature toggles, system restart.
+6. **Rate limits** on user-facing inference endpoints (`/query/*`, `/rag/query`).
+7. **Scoped keys** never logged in full; only the prefix is shown after creation.
+8. **Worker enrollment tokens** are single-use and time-limited (`expires_at` checked on register).
+9. **Heartbeats authenticate by `node_token`**, not by JWT — rotated on every approve/reactivate.
+10. **CORS:** `MAC_CORS_ORIGINS` defaults to `["*"]` for ease of dev; **set explicit origins in prod**.
+11. **Uploads:** `uploads/` is outside the static mount; copy-check sheets and RAG docs are served via authenticated endpoints, never directly.
+12. **WebSocket auth:** `notebook_ws` validates the JWT in the query string before `accept()`. Don't move the accept above the validation.
+
+---
+
+## 15. How to add a new feature (the recipe)
+
+1. **Model:** add a SQLAlchemy class in `mac/models/.py`, import it in `mac/main.py::lifespan` so `Base.metadata` knows.
+2. **Migration:** `alembic revision --autogenerate -m "add "` → review → commit.
+3. **Schema:** Pydantic request/response in `mac/schemas/.py`.
+4. **Service:** pure logic in `mac/services/_service.py`. Takes `db: AsyncSession` and primitive args. No FastAPI types.
+5. **Router:** thin handler in `mac/routers/.py`. Order of `Depends`: `get_db` → `get_current_user` → `require_*` → `feature_required("…")` → `check_rate_limit` (only if user-driven inference). Mount in `mac/main.py`.
+6. **Feature flag:** add a default to `feature_seeder.DEFAULT_FLAGS` so it can be toggled per role from admin.
+7. **API client:** add a method to `frontend/src/lib/api.js` under the matching export.
+8. **Store (if it has UI state):** add to `frontend/src/lib/stores.js`.
+9. **Route:** new directory under `frontend/src/routes//+page.svelte`.
+10. **Sidebar entry:** edit `frontend/src/lib/components/Sidebar.svelte`.
+11. **i18n:** add new strings to `BASE` in `frontend/src/lib/i18n.js`.
+12. **Test:** at least one happy-path + one auth-failure pytest in `tests/`.
+
+Follow this and the system stays consistent. Skip steps and you'll end up with a feature that's invisible to the admin, untranslated, untested, or worse — bypassing the auth chain.
+
+---
+
+*Last updated: 2026-04-27. If you change a subsystem and this file no longer matches reality, update it in the same PR.*
diff --git a/docs/MAC-CONTEXT.md b/docs/MAC-CONTEXT.md
new file mode 100644
index 0000000000000000000000000000000000000000..517e97873b3144aa1519494caf35e258dcde14d6
--- /dev/null
+++ b/docs/MAC-CONTEXT.md
@@ -0,0 +1,883 @@
+# MAC — Full Agent Context File
+> Generated: 2026-04-28
+> Sources: Claude (session knowledge), GitHub Copilot / Antigravity (VS Code agent), VS Code workspace
+
+---
+
+## 1. VS Code / Copilot Agent Session Info (Antigravity)
+
+| Variable | Value |
+|---|---|
+| `ANTIGRAVITY_AGENT` | `github.copilot-chat` |
+| `ANTIGRAVITY_EDITOR_APP_ROOT` | VS Code (Windows) |
+| `ANTIGRAVITY_TRAJECTORY_ID` | `e36c8c56-d0d8-4913-8da2-90176f0c34d3` |
+| `VSCODE_TARGET_SESSION_LOG` | `c:\Users\rampy\AppData\Roaming\Code\User\workspaceStorage\26393181f28fefe9ec94c456e08b07ec\GitHub.copilot-chat\debug-logs\e36c8c56-d0d8-4913-8da2-90176f0c34d3` |
+| `VSCODE_USER_PROMPTS_FOLDER` | `c:\Users\rampy\AppData\Roaming\Code\User\prompts` |
+| Workspace root | `D:\MAC` |
+| OS | Windows |
+| Date | 2026-04-28 |
+
+---
+
+## 2. Project Identity
+
+**MAC** = MBM AI Cloud
+**Owner:** MBM University Jodhpur (internal/institutional)
+**Purpose:** Self-hosted, on-prem AI platform — private ChatGPT-style chat, notebook IDE, RAG over college docs, attendance with face capture, exam copy-check with AI vision + plagiarism detection, and admin/cluster console — all running on the college's own GPUs via vLLM. **No external API calls.**
+
+---
+
+## 3. Stack
+
+| Layer | Technology |
+|---|---|
+| Backend API | FastAPI 0.115 (Python 3.11+) |
+| Database | PostgreSQL 16 + Alembic migrations |
+| Cache / Blacklist / RL | Redis |
+| Vector DB (RAG) | Qdrant |
+| Web search | SearXNG |
+| LLM inference | vLLM (OpenAI-compatible) |
+| Frontend | SvelteKit 2 + Svelte 5 + Tailwind 3 + Vite 6 (PWA) |
+| Reverse proxy | Nginx |
+| Containerisation | Docker Compose |
+| Installer | PyInstaller (Windows) |
+
+---
+
+## 4. Top-Level Topology
+
+```
+CLIENTS (Web PWA / API consumers)
+ │ HTTPS
+ ▼
+ NGINX ← TLS, gzip, /api → mac, / → static SPA
+ │
+ ├── SvelteKit static build
+ │
+ └── FastAPI /api/v1/*
+ │
+ ┌──────────┼──────────┬─────────────────┐
+ ▼ ▼ ▼ ▼
+PostgreSQL Redis Qdrant SearXNG
+(primary) (cache/bl/rl) (RAG vectors) (web search)
+ │
+ │ load_balancer.get_best_worker()
+ ▼
+ MAC CLUSTER (GPU worker nodes on LAN)
+ ├── vLLM (OpenAI-compatible inference)
+ ├── Jupyter kernel gateway (optional)
+ └── worker_agent.py (heartbeat every 10s)
+```
+
+- **Master node:** FastAPI + Postgres + Redis + Nginx + Qdrant + SearXNG
+- **Worker nodes:** vLLM + optional Jupyter gateway + `worker_agent.py`
+- **Routing:** master-side; `load_balancer.py` scores workers by `gpu_util×0.5 + vram_ratio×0.3`; stale threshold = 30 s
+
+---
+
+## 5. Repository Map
+
+```
+mac/
+ main.py FastAPI app, lifespan (DB init, dev seeds, bg tasks),
+ router mounts under /api/v1, root SPA fallback
+ config.py Pydantic Settings — every env var + .env loader
+ database.py Async SQLAlchemy engine + session factory; Base
+ utils/security.py JWT encode/decode + jti generation; password hash
+ middleware/
+ auth_middleware.py Bearer → JWT | legacy-key | scoped-key → User
+ rate_limit.py Per-user req/hour + token/day; X-RateLimit-* headers
+ feature_gate.py feature_required("ai_chat") dependency
+ models/ SQLAlchemy ORM models (one file per domain)
+ schemas/ Pydantic request/response schemas
+ services/ Pure business logic, no HTTP — called by routers
+ routers/ FastAPI routers: validate → call service → return
+
+frontend/
+ src/routes/ File-system routing: login, setup, chat, dashboard,
+ admin, cluster, keys, settings, notifications, rag
+ src/lib/api.js Single fetch wrapper; one export per backend domain
+ src/lib/stores.js Svelte stores (auth, setup, features, chat, toast)
+ src/lib/i18n.js 19 Indian languages, lazy-loaded strings, RTL support
+ static/manifest.json PWA manifest
+ static/sw.js No-cache service worker (intentional)
+
+alembic/ Migration env + versioned revisions
+nginx/ nginx.conf (HTTP) + nginx.https.conf (TLS)
+docker-compose.yml Master stack
+docker-compose.worker.yml Worker stack (vLLM + worker_agent)
+worker_agent.py Enrollment + heartbeat agent for GPU nodes
+installer/ Windows installer (PyInstaller) + branding
+tests/ pytest suite
+```
+
+---
+
+## 6. Request Lifecycle (every /api/v1/* call)
+
+```
+HTTP request
+ [1] CORS middleware
+ [2] FastAPI route handler
+ [3] Auth resolver (auth_middleware.py)
+ mac_sk_live_* → legacy API key
+ mac_sk_* → scoped API key (hashed, scopes, expiry)
+ else → JWT (verify sig, exp, jti blacklist)
+ [4] Role guard require_admin / require_faculty_or_admin
+ [5] Feature gate feature_required("ai_chat") — 403 if disabled
+ [6] Rate limit req/hour + tokens/day from usage_log
+ [7] Service layer business logic (no FastAPI types)
+ [8] Response inject_rate_limit_headers stamps X-RateLimit-*
+```
+
+**Design rule:** routers = parsing + auth + I/O orchestration; services = business logic; models = persistence.
+
+---
+
+## 7. Auth & Identity
+
+### Three auth paths (all collapse to a `User`):
+1. **JWT** — login → `{access_token (jti claim), refresh_token}`. Secret stored in `system_config` (not env). Logout blacklists `jti` in Redis with TTL = remaining life.
+2. **Legacy API key** — `mac_sk_live_<48 hex>`. One per user. Checked first by prefix.
+3. **Scoped API key** — `mac_sk_`, hashed at rest. Has scopes, optional expiry, label.
+
+### Roles: `admin | faculty | student`
+
+### First-run onboarding:
+- `GET /api/v1/setup/status` → `{is_first_run, has_jwt_secret, version}`
+- `POST /api/v1/setup/create-admin` → provisions first admin, seals system
+
+---
+
+## 8. LLM Serving & Cluster Routing
+
+### Model registry (three override layers):
+1. `MAC_MODELS_JSON` env → replaces entire registry
+2. `MAC_ENABLED_MODELS` env → filters built-ins
+3. `MAC_AUTO_FALLBACK` → what `model="auto"` resolves to
+
+Built-in models: Qwen2.5 7B, Qwen2.5-Coder 7B/AWQ, DeepSeek-R1, etc.
+Categories: `speed | code | reasoning | intelligence`
+
+### System prompt is forced:
+`_inject_system_prompt` prepends a hard-coded MAC identity to **every** completion. Model always presents itself as MAC by MBM University, never as Qwen/DeepSeek.
+
+### Routing flow:
+```
+chat request
+ → llm_service._resolve_model_cluster(model_id)
+ → load_balancer.get_best_worker(db, model_id)
+ SELECT WorkerNode JOIN NodeModelDeployment
+ WHERE status='active' AND last_heartbeat within 30s
+ ORDER BY gpu_util*0.5 + vram_ratio*0.3
+ → POST http://{node.ip}:{port}/v1/chat/completions (SSE passthrough)
+ → fallback to local vLLM if no workers
+```
+
+### Cluster lifecycle:
+| Event | Endpoint | Auth |
+|---|---|---|
+| Admin mints token | `POST /cluster/enroll-token` | admin JWT |
+| Worker registers | `POST /cluster/register` | enroll token |
+| Admin approves | `POST /cluster/nodes/{id}/action` | admin JWT |
+| Worker heartbeats | `POST /cluster/heartbeat` | node token |
+| Drain/remove | `POST /cluster/nodes/{id}/action` | admin JWT |
+
+Workers > 30 s without heartbeat are silently skipped by balancer.
+
+---
+
+## 9. Notebooks — Multi-language Code Execution
+
+### Architecture:
+```
+Browser WebSocket /ws/notebook/{id}?token=JWT
+ → notebook_ws.py (JWT verified before accept())
+ → kernel_manager.py
+ Docker mode (prod): mac-kernel-{lang} container, memory+CPU limits, optional GPU
+ Subprocess mode (dev): direct interpreter on host
+ Remote worker mode: forwards to Jupyter kernel gateway on worker node
+```
+
+### WebSocket protocol:
+| Direction | Type | Payload |
+|---|---|---|
+| C→S | `execute` | `{cell_id, code, language}` |
+| C→S | `interrupt` | `{kernel_id}` |
+| S→C | `stream` | `{cell_id, name: stdout\|stderr, text}` |
+| S→C | `error` | `{cell_id, ename, evalue, traceback[]}` |
+| S→C | `status` | `{cell_id, execution_state: busy\|idle}` |
+
+Idle kernels reaped after `kernel_timeout` s (default 120). Max 10 kernels/node.
+
+---
+
+## 10. RAG — Private Document Search
+
+```
+Upload (PDF/MD/TXT)
+ → text extraction (pypdf / plain read)
+ → chunk_text(words=512, overlap=50)
+ → embed each chunk via llm_service.embed()
+ → qdrant.upsert(point=(uuid, embedding, payload))
+ → RAGDocument row in Postgres
+
+Query time:
+ → embed(question) → qdrant.search(top_k=5)
+ → inject chunks as context into system message
+ → LLM responds with augmented answer
+```
+
+`RAGCollection` namespaces documents (e.g., one per subject). `RAGDocument` tracks chunk count + indexing status.
+
+---
+
+## 11. Attendance — Face-based Check-in
+
+1. Faculty creates `AttendanceSession` → `{branch, section, subject, date, window_minutes}`
+2. Student POSTs base64 selfie to `/attendance/check-in`
+3. Server: decodes → sha256 (replay block) → encoding → compare vs `FaceTemplate` → record `present/late`
+4. Faculty exports CSV/PDF roster
+
+Anti-cheat: strict window, replay hash block, one record per (session, student).
+
+---
+
+## 12. Copy Check — Exam Grading
+
+```
+Faculty creates session (syllabus + answer key)
+ → uploads student answer sheets (PDF / image)
+ → AI vision LLM reads each page
+ → returns {per_question_marks, total, feedback}
+ → difflib plagiarism pass across sheets in same session
+ → Faculty reviews, overrides if needed, approves before publish
+```
+
+Models: `CopyCheckSession`, `CopyCheckSheet`, `CopyCheckPlagiarism`
+AI score is a **recommendation** — requires faculty "Reviewed & Approved" flag before students see it.
+
+---
+
+## 13. Other Domain Modules
+
+| Module | Description |
+|---|---|
+| **Doubts forum** | Students post questions; AI drafts answer; faculty/peers reply |
+| **File sharing** | Admin/faculty upload class materials; per-download analytics |
+| **Notifications** | In-app + Web Push (VAPID/pywebpush); read/unread state |
+| **Academic** | Branches & sections — scopes attendance, file sharing, admin lists |
+| **Search** | SearXNG private metasearch; returned as tool result to chat |
+| **Hardware/Network/System** | Admin diagnostics, LAN discovery (UDP port 7700), version/update polling |
+| **Quota** | Per-user req/hour + tokens/day; admin override per user |
+| **Guardrails** | Admin-editable banned terms / forbidden topics; pre + post chat check |
+| **Video** | `VideoProject`/`VideoJob` models exist; router not yet built |
+
+---
+
+## 14. Frontend (SvelteKit PWA)
+
+- **Build:** `@sveltejs/adapter-static` → `fallback: 'index.html'` → pure CSR, no SSR
+- **State:** `authStore`, `chatStore`, `setupStore`, `featureStore`, `toast` in `stores.js`
+- **API client:** `src/lib/api.js` — only place that calls `fetch()`; one export per backend domain
+- **Auth gate:** `+layout.svelte` boots: `initLocale → authStore.init → checkSetup → loadFeatures → redirect`
+- **i18n:** 19 Indian languages, lazy-loaded, RTL support (Urdu)
+- **Service worker:** intentionally no caching — avoids stale-build problems during rapid dev
+
+---
+
+## 15. Alembic Migrations
+
+| Revision | Contents |
+|---|---|
+| `20260426_0001_initial_schema.py` | Full original schema |
+| `20260427_0002_session1_tables.py` | feature_flags, system_config, branches, sections, cluster_heartbeats, shared_files, file_downloads, video_projects, video_jobs |
+| `20260427_0003_file_share_node_columns.py` | node notebook_port and tags columns |
+
+Dev: `MAC_ENV=development` → `init_db()` auto-creates tables.
+Prod: **must** run `alembic upgrade head` before starting; no auto-create.
+
+---
+
+## 16. Configuration (mac/config.py)
+
+All env vars via Pydantic `Settings(BaseSettings)`. Never read `os.environ` directly.
+`_fix_database_url` auto-promotes `postgres://` → `postgresql+asyncpg://`.
+JWT secret: NOT from env in prod — generated once on first boot, stored in `system_config`.
+
+Key env vars:
+```
+DATABASE_URL, REDIS_URL, QDRANT_URL, SEARXNG_URL
+MAC_CORS_ORIGINS (default ["*"] — set explicit origins in prod!)
+MAC_MODELS_JSON, MAC_ENABLED_MODELS, MAC_AUTO_FALLBACK
+JWT_ACCESS_TOKEN_EXPIRE_MINUTES (default 1440 = 24h)
+RATE_LIMIT_REQUESTS_PER_HOUR, RATE_LIMIT_TOKENS_PER_DAY
+MAC_ENV (development | production)
+MAC_UPDATE_CHECK_INTERVAL_HOURS
+```
+
+---
+
+## 17. Security Checklist
+
+1. No external API calls — all inference is local vLLM
+2. JWT secret in `system_config`, not env in production
+3. Every JWT carries `jti`; middleware checks Redis blacklist on every request
+4. Every auth-required router has `Depends(get_current_user)`
+5. Role guards (`require_admin`) on token mints, cluster mutations, feature toggles, system restart
+6. Rate limits on `/query/*` and `/rag/query`
+7. Scoped keys never logged in full — only prefix shown post-creation
+8. Worker enrollment tokens: single-use + time-limited
+9. Heartbeats authenticate via `node_token` (not JWT), rotated on approve/reactivate
+10. CORS: set explicit origins in prod (`MAC_CORS_ORIGINS`)
+11. `uploads/` outside static mount; served via authenticated endpoints only
+12. WebSocket: JWT validated **before** `accept()` in `notebook_ws`
+
+---
+
+## 18. Cross-Cutting Concerns
+
+### Background tasks (started in lifespan):
+- `updater.background_check_loop` — polls GitHub for new releases every N hours
+- `discovery.start_discovery_server` — UDP broadcast on port 7700 for LAN worker discovery
+
+### Redis usage:
+- JWT blacklist: `mac:bl:{jti}` keys with TTL
+- Rate-limit counters: derived from `usage_log` rows
+- Graceful in-process fallback when Redis unreachable (dev only)
+
+### Observability:
+- Every chat call logged to `usage_log`: user_id, model_id, tokens_in, tokens_out, latency_ms, status, request_id
+- Cluster heartbeats append-only in `cluster_heartbeats` → used for node history charts
+
+---
+
+## 19. Deployment Quick-Start
+
+### Master node:
+```bash
+cd frontend && npm install && npm run build && cd ..
+cp .env.example .env # edit DB, Redis, model settings
+docker compose up postgres -d
+docker compose run --rm mac alembic upgrade head
+docker compose up -d
+```
+
+### Worker node:
+```bash
+# On master — mint enrollment token
+curl -X POST http://MASTER:8000/api/v1/cluster/enroll-token \
+ -H "Authorization: Bearer ADMIN_JWT" \
+ -d '{"label":"Lab PC 1","expires_hours":24}'
+
+# On worker PC
+MAC_MASTER_URL=http://MASTER:8000 \
+MAC_ENROLL_TOKEN= \
+MAC_VLLM_PORT=8001 \
+docker compose -f docker-compose.worker.yml up -d
+# Then: approve in MAC admin → Cluster tab
+```
+
+### HTTPS:
+Drop certs into `nginx/ssl/`, swap bind-mount to `nginx/nginx.https.conf`, restart Nginx.
+
+### Windows installer:
+```powershell
+powershell -ExecutionPolicy Bypass -File .\installer\build_installer.ps1
+# → dist/MAC-Installer.exe
+```
+
+### Tests:
+```bash
+pytest # full suite
+pytest -k "not gpu" # CPU-safe subset
+```
+
+---
+
+## 20. How to Add a New Feature (the Recipe)
+
+1. **Model:** SQLAlchemy class in `mac/models/.py`; import in `main.py::lifespan`
+2. **Migration:** `alembic revision --autogenerate -m "add "` → review → commit
+3. **Schema:** Pydantic in `mac/schemas/.py`
+4. **Service:** pure logic in `mac/services/_service.py`; takes `db: AsyncSession`
+5. **Router:** thin handler in `mac/routers/.py`; mount in `mac/main.py`
+6. **Feature flag:** add default to `feature_seeder.DEFAULT_FLAGS`
+7. **API client:** add method to `frontend/src/lib/api.js`
+8. **Store (if UI state):** add to `frontend/src/lib/stores.js`
+9. **Route:** `frontend/src/routes//+page.svelte`
+10. **Sidebar:** edit `frontend/src/lib/components/Sidebar.svelte`
+11. **i18n:** add strings to `BASE` in `frontend/src/lib/i18n.js`
+12. **Test:** happy-path + auth-failure pytest in `tests/`
+
+---
+
+## 21. Build Progress Summary (as of 2026-04-27)
+
+### Completed:
+- ✅ Session 1 — Full backend foundation (DB, migrations, auth, JWT blacklist, scoped keys, feature flags, system_config, setup wizard, cluster, file_share, academic, hardware, network, system routers + services)
+- ✅ Session 2 — Full SvelteKit PWA (all routes: login, setup, chat, dashboard, admin, cluster, keys, settings, notifications, rag), design system, API client, i18n (19 languages), PWA manifest + service worker, Nginx configs (HTTP + HTTPS), Docker Compose (master + worker), `worker_agent.py`
+
+### Remaining / Optional:
+| Item | Priority |
+|---|---|
+| Frontend PWA icons (icon-192.png, icon-512.png, favicon.ico) | Medium |
+| Frontend: silent JWT refresh token flow in `api.js` | Medium |
+| Multi-stage Dockerfile (node build + python + nginx) | Low |
+| Feature flag wiring on `/query/*` routes | Low |
+| HTTPS cert setup for production | Deployment |
+| Video generation router (models exist) | Future |
+
+---
+
+---
+
+## 22. Project Origin — Vision & Architecture Requirements
+
+The project was born from this exact goal:
+
+> "I am building a multi-node GPU cluster using standard PCs over a LAN (connected via Wi-Fi) to provide offline AI services. The goal is to make the entire cluster accessible through one single IP address that provides both an AI Chat interface (Svelte-based) and a Kaggle-like environment for Python notebooks. Additionally, I need to issue custom OpenAI-compatible API keys to students so they can access these models from anywhere in the world."
+
+### Original architecture requirements:
+- **Hardware:** Multiple PCs each with dedicated GPUs. Wi-Fi connected → each PC runs its own model instance (no network model-sharding — latency would kill it)
+- **One IP entry point:** Nginx reverse proxy on Master PC routing `/chat` → local vLLM, `/notebook` → JupyterHub/GPUSTACK on another PC
+- **API Management:** LiteLLM for student API key management, usage limits, OpenAI-compatible endpoint
+- **Global Access:** Cloudflare Tunnel to expose master IP publicly without opening router ports
+- **UI:** Svelte dashboard as primary interface
+
+### Evolution:
+MAC replaced LiteLLM + JupyterHub with a fully custom FastAPI + SvelteKit stack, giving complete control over auth, roles, feature flags, quota, and cluster routing — while preserving the OpenAI-compatible API surface via vLLM.
+
+---
+
+## 23. Session Work Log — What Claude Built (Session 2 Detailed)
+
+### Phase 1 — JWT Blacklist
+
+**Goal:** Prevent token reuse after logout.
+
+Steps taken:
+1. Added `jti` (UUID) claim to every access token in `mac/utils/security.py`
+2. Updated `auth_middleware.py` to check `mac:bl:{jti}` in Redis on every request
+3. Updated `mac/routers/auth.py` logout endpoint to:
+ - Blacklist current `jti` in Redis with TTL = remaining token life
+ - Revoke all refresh tokens for the user
+4. Fallback to in-process set if Redis unreachable (dev only)
+
+### Phase 2 — Distributed Computing Core
+
+**Foundation:** `WorkerNode`, `NodeModelDeployment`, `EnrollmentToken` models were already solid. Built on top.
+
+Steps taken:
+1. Added `notebook_port` and `tags` columns to `WorkerNode` model
+2. Created `mac/services/load_balancer.py` — score-based routing:
+ - `SELECT WorkerNode JOIN NodeModelDeployment WHERE status='active' AND last_heartbeat within 30s`
+ - `ORDER BY gpu_util*0.5 + vram_ratio*0.3`
+ - Returns best worker or `None` (triggers local vLLM fallback)
+3. Updated `mac/services/llm_service.py` → `_resolve_model_cluster` now calls `get_best_worker()` before falling back to local config
+4. Created full `mac/routers/cluster.py` with all endpoints (enroll-token, register, heartbeat, node CRUD, deploy, history)
+5. Created `worker_agent.py` — standalone Python script for worker PCs:
+ - Reads `MAC_MASTER_URL`, `MAC_ENROLL_TOKEN`, `MAC_VLLM_PORT` from env
+ - Self-registers on startup via enrollment token
+ - Sends heartbeats every 10s with GPU/CPU/RAM metrics (`pynvml` + `psutil`)
+ - Queries local vLLM `/v1/models` to report active models
+ - Handles stale/auth errors gracefully
+
+### Phase 3 — Academic + File Share Routers
+
+- Created `mac/routers/academic.py` — full CRUD for branches and sections
+- Created `mac/routers/file_share.py` — admin upload, user download, download stats, delete
+- Created `alembic/versions/20260427_0003_file_share_node_columns.py` — fixes mismatched column names between model and migration (display_name, storage_path, recipient_type, etc.) + adds node notebook_port/tags
+
+### Phase 4 — Migration 0002
+
+`20260427_0002_session1_tables.py` adds:
+- `feature_flags` table
+- `system_config` table
+- `branches`, `sections` tables
+- `cluster_heartbeats` table (time-series, append-only)
+- `shared_files`, `file_downloads` tables
+- `video_projects`, `video_jobs` tables
+- New user columns
+
+### Phase 5 — Frontend Pages
+
+**Checked existing routes, then added:**
+
+1. Updated `frontend/src/lib/components/Sidebar.svelte` — added 6 nav items: RAG, Notifications, API Keys, Settings, Cluster (admin-only)
+2. Updated `frontend/src/lib/api.js` — added `cluster`, `academic`, `files` API exports
+3. Created `frontend/src/routes/cluster/+page.svelte` — node list with live metrics, detail panel, approve/drain/remove, GPU history sparkline, enrollment token generation with setup instructions
+4. Created `frontend/src/routes/keys/+page.svelte` — generate, copy, revoke scoped API keys
+5. Created `frontend/src/routes/settings/+page.svelte` — profile, change password, language picker
+6. Created `frontend/src/routes/notifications/+page.svelte` — notification list with mark-read
+7. Created `frontend/src/routes/rag/+page.svelte` — drag-and-drop document upload + list
+
+### Phase 6 — Infrastructure
+
+- Created `docker-compose.worker.yml` — worker node compose: vLLM + optional Jupyter kernel gateway (`--profile notebook`) + worker-agent
+- Created `nginx/nginx.https.conf` — HTTPS with TLS, HSTS, WebSocket proxy for notebooks
+- Updated `MAC-PROGRESS.md`
+
+### Verification checks run:
+- `btn-secondary` CSS class existence confirmed in `app.css`
+- `__init__.py` in routers is empty → module imports work directly
+- `file_share.py` model vs migration column name mismatch found → fixed in 0003 migration
+- All new router imports in `main.py` verified against existing files
+
+---
+
+## 24. Final Tech Stack (Canonical Reference)
+
+### Backend
+| Package | Purpose |
+|---|---|
+| Python 3.11 | Core language |
+| FastAPI 0.115 | API server |
+| PostgreSQL 16 | Main database (master node only) |
+| Redis 7 | Cache, pub/sub, JWT blacklist |
+| vLLM | LLM inference (GPU workers, OpenAI-compatible) |
+| llama.cpp-python | CPU inference fallback |
+| faster-whisper | Speech-to-text (offline) |
+| piper-tts | Text-to-speech (offline) |
+| ffmpeg-python | Video/audio editing |
+| python-on-whales | Docker Engine API |
+| Alembic | Database migrations |
+| bcrypt | Password hashing |
+| python-jose | JWT encode/decode |
+| qdrant-client | Vector DB client for RAG |
+| httpx | Async HTTP client (LLM proxy, search) |
+| sse-starlette | SSE streaming to browser |
+| pywebpush | Web Push notifications (VAPID) |
+| psutil | CPU/RAM metrics |
+| pynvml (GPUtil) | GPU metrics on worker nodes |
+| fpdf2 | PDF report generation |
+| qrcode | QR code for attendance sessions |
+| py-cpuinfo | Hardware detection |
+
+### Frontend
+| Package | Purpose |
+|---|---|
+| SvelteKit 2 | Framework (compiles to vanilla JS) |
+| Svelte 5 | Compiler |
+| Vite 5 | Build tool |
+| TypeScript | Throughout |
+| TailwindCSS 3 | Styling (CSS variables only) |
+| svelte-i18n | 19 Indian languages offline |
+| CodeMirror 6 | MBM Book code editor |
+| Mermaid.js | Flowcharts in chat |
+| Chart.js | Admin dashboard charts |
+| Lucide Svelte | Icons |
+| marked + highlight.js | Markdown + syntax highlighting |
+| @fontsource/* | Geist + all Indic fonts (bundled) |
+
+### Infrastructure
+| Tool | Purpose |
+|---|---|
+| Docker Compose | All services containerised |
+| Nginx | Reverse proxy, SSL, ports 80/443 |
+| OpenSSL | Self-signed SSL for PWA (LAN only) |
+
+### Installer
+| Tool | Purpose |
+|---|---|
+| Inno Setup 6 | Windows .exe installer |
+| start-mac.bat | One-click server start |
+
+### DevOps
+| Tool | Purpose |
+|---|---|
+| GitHub | Source code + releases |
+| GitHub Actions | Auto-build .exe on version tag push |
+| `mac/VERSION` | Single source of truth for version |
+
+---
+
+## 25. Full Environment Variables Reference (.env.example)
+
+```env
+# App
+MAC_ENV=development
+MAC_HOST=0.0.0.0
+MAC_PORT=8000
+MAC_DEBUG=false
+MAC_SECRET_KEY=change-me
+MAC_CORS_ORIGINS=["*"] # ← set explicit origins in prod!
+MAC_WORKERS=4
+APP_HOST=0.0.0.0
+APP_PORT=80
+
+# Database
+DATABASE_URL=postgresql+asyncpg://mac:mac_password@localhost:5432/mac_db
+
+# Redis
+REDIS_URL=redis://localhost:6379/0
+
+# JWT
+JWT_SECRET_KEY=change-me # ← NOT used in prod; stored in system_config instead
+JWT_ALGORITHM=HS256
+JWT_ACCESS_TOKEN_EXPIRE_MINUTES=1440
+
+# vLLM endpoints
+VLLM_BASE_URL=http://localhost:8001
+VLLM_SPEED_URL=http://localhost:8001
+VLLM_CODE_URL=http://localhost:8002
+VLLM_REASONING_URL=http://localhost:8003
+VLLM_INTELLIGENCE_URL=http://localhost:8004
+VLLM_API_KEY=
+VLLM_TIMEOUT=120
+VLLM_HEALTH_TIMEOUT=5
+
+# Model registry overrides
+MAC_MODELS_JSON= # full JSON array → replaces built-ins
+MAC_ENABLED_MODELS= # comma-separated IDs → filters built-ins
+MAC_AUTO_FALLBACK= # model ID for model="auto"
+MAC_DEFAULT_MAX_TOKENS=2048
+
+# Model auto-download
+MAC_MODEL_AUTO_DOWNLOAD_ON_USE=true
+MAC_MODEL_AUTO_DOWNLOAD_LIMIT=0
+
+# vLLM tuning (per-model)
+VLLM_SPEED_MODEL=Qwen/Qwen2.5-7B-Instruct
+VLLM_SPEED_PORT=8001
+VLLM_SPEED_GPU_MEM=0.22
+VLLM_SPEED_MAX_LEN=8192
+
+VLLM_CODE_MODEL=Qwen/Qwen2.5-Coder-7B-Instruct
+VLLM_CODE_PORT=8002
+VLLM_CODE_GPU_MEM=0.22
+VLLM_CODE_MAX_LEN=8192
+
+VLLM_REASON_MODEL=deepseek-ai/DeepSeek-R1-Distill-Qwen-14B
+VLLM_REASON_PORT=8003
+VLLM_REASON_GPU_MEM=0.35
+VLLM_REASON_MAX_LEN=8192
+
+VLLM_DTYPE=auto
+```
+
+---
+
+## 26. requirements.txt (Pinned)
+
+```
+fastapi==0.115.6
+uvicorn[standard]==0.34.0
+pydantic==2.10.4
+pydantic-settings==2.7.1
+sqlalchemy[asyncio]==2.0.36
+asyncpg==0.30.0
+psycopg2-binary==2.9.10
+alembic==1.14.1
+aiosqlite==0.20.0
+python-jose[cryptography]==3.3.0
+bcrypt==4.2.1
+redis[hiredis]==5.2.1
+httpx==0.28.1
+sse-starlette==2.2.1
+qdrant-client==1.12.1
+huggingface-hub==0.31.2
+python-multipart==0.0.20
+aiofiles==24.1.0
+pywebpush==2.0.1
+psutil==6.1.1
+websockets>=12.0
+GPUtil>=1.4.0
+fpdf2==2.8.2
+py-cpuinfo>=9.0.0
+qrcode[pil]>=7.4.0
+aiohttp>=3.9.0
+cryptography>=42.0.0
+pytest==8.3.4
+pytest-asyncio==0.25.0
+pytest-httpx>=0.30.0
+```
+
+---
+
+## 27. UI Design System — Light Theme (Default)
+
+The app defaults to **light theme**. Dark mode available via toggle (bottom-right of landing page only).
+
+### Color tokens:
+```css
+--page-bg: #FAF9F7; /* warm off-white cream */
+--card-bg: #FFFFFF; /* pure white */
+--surface-2: #F5F4F0; /* slightly warm gray */
+--surface-3: #ECEAE4; /* warmer gray */
+--text-primary: #1A1A1A; /* near black, warm */
+--text-secondary: #666560; /* warm medium gray */
+--text-muted: #999791; /* warm light gray */
+--accent: #D97449; /* coral orange */
+--accent-hover: #C4623D; /* deeper coral */
+--border: rgba(0,0,0,0.12);
+--code-bg: #F0EDE8; /* warm parchment */
+```
+
+### Key UI features to implement / in progress:
+- **MAC title glitch effect** — vanilla JS CSS glitch animation (from previous pretext.js) ported to Svelte
+- **Background hover particle effect** — physics particle canvas (from previous UI), already in `ParticleCanvas.svelte`
+- **Extendable sidebar** — VS Code-style drag-to-resize sidebar panels
+- **Notebook UI** — Kaggle/Colab-style cells with CodeMirror 6
+- **Loader animation** — `delete later/Loader.svelte` — used on any delay (chat response, notebook execution, page load)
+- **MBM→MAC morph animation** — Devanagari letter morph (`delete later/MBM-MAC Globe.html`) — first-time landing page only
+- **Smooth light↔dark transition** — CSS variable swap with transition, toggle button bottom-right on landing only
+
+### Font:
+- Geist (Latin) + all Indic fonts via `@fontsource/*` — bundled offline, no CDN
+
+---
+
+## 28. Notebook Architecture Target
+
+Goal: **Kaggle/Colab-style notebook** that runs on the MAC cluster.
+
+```
+Browser (CodeMirror 6 cell editor)
+ │ WebSocket /ws/notebook/{id}?token=JWT
+ ▼
+notebook_ws.py → kernel_manager.py
+ ├── Docker mode (prod): mac-kernel-{lang} container
+ ├── Subprocess mode (dev): direct interpreter
+ └── Remote worker mode: Jupyter kernel gateway on GPU worker
+```
+
+Multi-language support: Python, JavaScript, SQL (at minimum).
+Kernel lifecycle: idle timeout 120s, max 10 per node.
+Persistent: cell content stored as JSON in `notebooks` table.
+
+---
+
+## 29. Global Access Strategy
+
+For students accessing from outside LAN:
+- **Cloudflare Tunnel** (`cloudflared`) on master PC → exposes local HTTPS to public domain
+- No router port-forwarding needed
+- Students use OpenAI-compatible API keys (`mac_sk_*`) against the public domain
+- Same keys work on LAN (direct) and WAN (via tunnel) — same auth chain
+
+---
+
+## 30. Source Files with No .claude or .vscode Config
+
+Checked: no `.claude/` directory, no `CLAUDE.md`, no `.vscode/settings.json` or `.vscode/extensions.json` exist in `D:\MAC`.
+All Claude session context lives in this file + `ARCHITECTURE.md` + `MAC-PROGRESS.md`.
+VS Code Copilot context: Antigravity trajectory `e36c8c56-d0d8-4913-8da2-90176f0c34d3`, session log at `c:\Users\rampy\AppData\Roaming\Code\User\workspaceStorage\26393181f28fefe9ec94c456e08b07ec\GitHub.copilot-chat\debug-logs\e36c8c56-d0d8-4913-8da2-90176f0c34d3`.
+
+---
+
+---
+
+## 31. Reference Codebases on Disk
+
+Three reference repos exist locally that informed MAC's design and are the source for UI features Claude was asked to port:
+
+### A. `D:\MBMmac\MAC\frontend` — Original Vanilla JS Frontend
+The **original MAC frontend** before the SvelteKit rewrite. This is the source of the UI features that must be ported to Svelte:
+- `frontend/app.js` — ~4782 lines, entire frontend in one file
+- `frontend/style.css` — black & white premium dark theme
+- `frontend/index.html` — SPA shell with Chart.js, highlight.js, Mermaid.js loaded from `/static/libs/`
+- `frontend/libs/` — bundled JS: chart.umd, highlight.min, mermaid.min, hljs language packs
+- **Has the MAC glitch text effect, background hover particle animation, and sidebar drag/expand** — must be ported to Svelte
+
+### B. `D:\MBMmac\Mbmbook\frontend` — MBMBook Notebook UI (React/TypeScript)
+The **notebook UI reference** — Kaggle/Colab-style built in React + Monaco Editor + TypeScript:
+```
+src/
+ App.tsx
+ components/
+ AnimatedTitle.tsx ← Animated MAC/MBM title
+ CellOutput.tsx ← Notebook cell output rendering
+ ClusterPanel.tsx ← GPU cluster management panel
+ NotebookCell.tsx ← Individual notebook cell (CodeMirror/Monaco)
+ NotebookView.tsx ← Full notebook layout
+ ResizeHandle.tsx ← VS Code-style drag-to-resize panels
+ Sidebar.tsx ← Navigation sidebar
+ ThemeToggle.tsx ← Light/dark toggle
+ Toolbar.tsx ← Notebook toolbar
+ services/
+ stores/
+ monaco-setup.ts
+```
+Stack: React + TypeScript + Vite + Tailwind + Monaco Editor
+**Port the notebook UI patterns (ResizeHandle, NotebookCell, CellOutput) to Svelte.**
+
+### C. `D:\MAC-ref` — Original Multi-Node Reference Repo
+The original MAC codebase from before `D:\mac2`. Has 5 docker-compose files for the multi-node cluster topology:
+- `docker-compose.control-node.yml`
+- `docker-compose.pc1-gpu.yml`
+- `docker-compose.pc2-app.yml`
+- `docker-compose.worker-node.yml`
+- `docker-compose.yml`
+Also has: `worker-agent.py`, `kernels/`, `examples/`, `test_students.csv/json`, `START-MAC.bat`, `START-WIFI.bat`, `setup-firewall.ps1`
+
+### D. MAC-PROJECT-SAMJHO.md
+Hinglish explanation doc at `D:\MAC-PROJECT-SAMJHO.md` — explains the full project in Hindi/English for onboarding. Describes the original 8-table schema (student_registry, users, refresh_tokens, usage_logs, guardrail_rules, quota_overrides, rag_collections, rag_documents).
+
+### E. GitHub Repos
+- **`https://github.com/mbmuniversity2026/MAC`** — target push repo (Claude attempted push on 2026-04-27, failed — not a git repo at `D:\mac2` at that time)
+- **`https://github.com/Mebeingmealways/MAC/tree/main/frontend`** — another reference frontend Claude was asked to draw inspiration from
+
+---
+
+## 32. Full Session Timeline (Claude Desktop Sessions)
+
+| Date | Session File | cwd | Key Work |
+|---|---|---|---|
+| 2026-04-26 | `D--mac2/4e4ab358` | `D:\mac2` | Session 1 plan: backend foundation, cleanup of old vanilla JS frontend, Alembic wiring, feature flags, hardware/network/system/setup services — see `gleaming-purring-mitten.md` plan |
+| 2026-04-26–27 | `D--mac2/833b6073` | `D:\mac2` | Session 1 execution continued; context ran out; push to GitHub attempted (failed — not a git repo) |
+| 2026-04-27 08:28 | `d--MAC/8b8be9aa` (5.7MB) | `D:\mac2` | Session 2: JWT blacklist, cluster, all frontend pages; blank screen bug appeared; SW cache issues; theme change requested; MBMBook reference fetched; model switched to claude-sonnet-4-6 |
+| 2026-04-27 14:43 | `d--MAC/969becc6` | `D:\mac2` | Short session, opened MAC-KNOWLEDGE-BASE.md |
+| 2026-04-27 16:52 | `d--MAC/91e087de` (1.5MB) | `D:\mac2` | Frontend blank screen still broken; user asked for ARCHITECTURE.md prompt; model switched to claude-opus-4-7 |
+| 2026-04-27 19:54 | `d--MAC/877da497` (1MB) | `D:\mac2` | Debugging frontend hosting: wrong IP served wrong code; LAN IP = `192.168.1.34`; login working |
+| 2026-04-27 20:12 | `d--MAC/8b8be9aa` (5.7MB) | `D:\mac2` | Continuation: blank screen fixed; CSS theme applied; frontend inspected from `D:\hey\MAC\frontend` (now `D:\MBMmac\MAC\frontend`) |
+| 2026-04-27 21:52 | `d--MAC/7b593b21` (1.5MB) | `D:\mac2` | Requested ARCHITECTURE.md write-up |
+| 2026-04-27 22:29 | `d--MAC/1fe8755d` | `D:\mac2` | Admin login working (`abhisek.cse@mbm.ac.in / Admin@1234`); asked to add more sidebar items and apply `D:\hey\MAC\frontend` glitch effects |
+| 2026-04-28 09:58 | `d--MAC/beae7ebc` (2.6MB) | `D:\MAC` | TODAY: Wrong code on LAN; language switcher broken; codebase in `D:\MAC` now; MAC-CONTEXT.md created |
+
+---
+
+## 33. Known Bugs Encountered & Status
+
+| Bug | Root Cause | Status |
+|---|---|---|
+| Blank screen on frontend load | SvelteKit SSR hydration issue + `export const ssr = false` not applied | Fixed: added `+layout.js` with `ssr=false, prerender=false` |
+| Stale build served after rebuild | Service worker caching old `index.html` and `_app/` chunks | Fixed: SW now wipes all caches on install/activate, no fetch handler |
+| Wrong code on LAN IP (`192.168.1.34`) | Docker serving a different container/port | Investigated; correct project is `D:\MAC` not `D:\mac2` |
+| Language switcher not working | i18n locale strings not loading / locale change not triggering reactive update | In progress |
+| Login page not advancing past splash | Particle canvas blocking click events or slow authStore.init() | Fixed: layout guard redirects on init completion |
+| `mac_sk_live_*` vs `mac_sk_*` prefix | Legacy key check order in auth_middleware | Resolved: legacy checked first |
+
+---
+
+## 34. Dev Credentials & Local Network
+
+| | Value |
+|---|---|
+| Admin email | `abhisek.cse@mbm.ac.in` |
+| Admin password | `Admin@1234` |
+| LAN IP | `192.168.1.34` |
+| Frontend URL | `http://192.168.1.34` (port 80 via Nginx) |
+| API URL | `http://192.168.1.34:8000` or `http://192.168.1.34/api/v1` |
+| API docs | `http://192.168.1.34:8000/docs` |
+
+Default seeded dev accounts (to create via setup or seed script):
+- Admin: roll `ADMIN001`, role `admin`
+- Faculty: roll `FAC001`, role `faculty`
+- Student: roll `STU001`, role `student`
+
+---
+
+## 35. Repo History — `D:\mac2` → `D:\MAC`
+
+The codebase started at `D:\mac2`. At some point it was copied/moved to `D:\MAC`. Both directories exist:
+- `D:\mac2` — old working directory (Claude sessions before 2026-04-28 used this)
+- `D:\MAC` — current canonical location (all work from 2026-04-28 onward)
+- `D:\MAC-ref` — older reference snapshot (pre-SvelteKit, vanilla JS frontend)
+- `D:\MBMmac\MAC` — another older snapshot (same as MAC-ref structure)
+
+When continuing work, always use `D:\MAC` as the project root.
+
+---
+
+*Context compiled from: ARCHITECTURE.md, MAC-PROGRESS.md, README.md, .env.example, requirements.txt, workspace structure, full Claude session timeline (all 11 sessions), plan file `gleaming-purring-mitten.md`, MAC-PROJECT-SAMJHO.md, reference codebases at D:\MBMmac\MAC, D:\MBMmac\Mbmbook, D:\MAC-ref, VS Code Copilot session (Antigravity trajectory `e36c8c56-d0d8-4913-8da2-90176f0c34d3`)*
diff --git a/docs/MAC-PROGRESS.md b/docs/MAC-PROGRESS.md
new file mode 100644
index 0000000000000000000000000000000000000000..ab83e943a4193e1c599e68fece094e6a4d05c43d
--- /dev/null
+++ b/docs/MAC-PROGRESS.md
@@ -0,0 +1,202 @@
+# MAC — MBM AI Cloud · Build Progress
+
+**Project:** Self-hosted AI inference platform for MBM University Jodhpur
+**Stack:** FastAPI · SvelteKit · PostgreSQL · Redis · vLLM · Nginx · Docker
+**Repo:** `D:\mac2` (push to `github.com/mbmuniversity2026/MAC`)
+
+---
+
+## ✅ Completed
+
+### Session 1 — Backend Foundation
+
+#### Database / Migrations
+- Alembic wired — `alembic/env.py` imports all models
+- `20260426_0001_initial_schema.py` — full initial schema capture
+- `20260427_0002_session1_tables.py` — feature_flags, system_config, branches, sections, cluster_heartbeats, shared_files, file_downloads, video_projects, video_jobs + user columns
+
+#### New Models (`mac/models/`)
+| File | Tables |
+|------|--------|
+| `feature_flag.py` | `FeatureFlag` |
+| `academic.py` | `Branch`, `Section` |
+| `cluster.py` | `ClusterNode`, `ClusterHeartbeat` |
+| `file_share.py` | `SharedFile`, `FileDownload` |
+| `video.py` | `VideoProject`, `VideoJob` |
+| `system_config.py` | `SystemConfig` |
+
+#### New Routers (`mac/routers/`)
+| File | Endpoints |
+|------|-----------|
+| `features.py` | GET /features/status, PATCH /admin/features/{key} |
+| `hardware.py` | GET /hardware/local, /hardware/recommendations |
+| `network.py` | GET /network/local-ip, /network/discover |
+| `system.py` | GET /system/version, /system/update-status, POST /admin/system/restart |
+| `setup.py` | GET /setup/status, POST /setup/create-admin, GET /setup/recovery |
+
+#### New Services (`mac/services/`)
+- `feature_seeder.py` — seeds default flags on startup
+- `setup_service.py` — JWT secret management via system_config
+- `token_blacklist_service.py` — JWT blacklist via Redis (TTL-matched)
+
+#### Security Improvements
+- JWT `jti` claim added to all access tokens (`mac/utils/security.py`)
+- `auth_middleware.py` checks blacklist on every request
+- Logout blacklists current access token + revokes refresh tokens
+
+---
+
+### Session 2 — SvelteKit Frontend
+
+**Full PWA frontend at `frontend/`:**
+
+| File | Description |
+|------|-------------|
+| `src/app.html` | PWA shell, Google Fonts, SW registration |
+| `src/app.css` | Full design system — dark theme, mac-blue palette, all component classes |
+| `src/lib/api.js` | Complete API client (auth, query, models, usage, quota, keys, features, hardware, network, system, users, guardrails, rag, notifications, cluster, academic, files) |
+| `src/lib/stores.js` | authStore, chatStore, setupStore, featureStore, toast, sidebarOpen |
+| `src/lib/i18n.js` | 19 Indian languages with lazy loading + RTL support |
+| `src/lib/components/ParticleCanvas.svelte` | Physics particle animation (login/splash) |
+| `src/lib/components/Sidebar.svelte` | Navigation sidebar with all routes |
+| `src/lib/components/Toast.svelte` | Toast notification component |
+| `src/lib/components/ChatMessage.svelte` | Chat bubble with markdown rendering |
+| `src/routes/+layout.svelte` | Auth guard, setup check, shell layout |
+| `src/routes/+page.svelte` | Landing / redirect |
+| `src/routes/login/+page.svelte` | Animated login page with particle canvas |
+| `src/routes/setup/+page.svelte` | First-run admin setup wizard |
+| `src/routes/chat/+page.svelte` | SSE streaming chat with model picker |
+| `src/routes/dashboard/+page.svelte` | Activity heatmap, quota rings, model distribution |
+| `src/routes/admin/+page.svelte` | Admin panel: Users, Models, Features, Hardware, System tabs |
+| `src/routes/cluster/+page.svelte` | Cluster management: node list, detail, actions, history chart, enrollment tokens |
+| `src/routes/keys/+page.svelte` | API key management (generate, copy, revoke) |
+| `src/routes/settings/+page.svelte` | Profile, change password, language picker |
+| `src/routes/notifications/+page.svelte` | Notification list with mark-read |
+| `src/routes/rag/+page.svelte` | RAG document upload (drag-and-drop) + list |
+
+**Static assets:**
+- `static/manifest.json` — PWA manifest with shortcuts
+- `static/sw.js` — Service worker (cache-first shell, network-first API, SSE passthrough)
+
+**Infrastructure:**
+- `nginx/nginx.conf` — HTTP server (production)
+- `nginx/nginx.https.conf` — HTTPS server with TLS, HSTS, WebSocket proxy
+- `docker-compose.yml` — Master node: MAC API + vLLM + Postgres + Redis + Nginx + Qdrant + SearXNG
+- `docker-compose.worker.yml` — Worker node: vLLM + optional Jupyter + worker-agent
+
+---
+
+### Session 2 — Distributed Cluster Backend
+
+#### Cluster Architecture
+```
+Master node (this machine)
+ ├── MAC API (FastAPI) — receives all user requests
+ ├── PostgreSQL — DB (master-only)
+ ├── Redis — cache, rate limiting, JWT blacklist
+ ├── Nginx — reverse proxy + frontend
+ ├── Qdrant — vector DB for RAG
+ └── SearXNG — web search
+
+Worker nodes (any PC on same network)
+ ├── vLLM — GPU inference (OpenAI-compatible)
+ ├── Jupyter kernel gateway — notebook execution (optional)
+ └── worker_agent.py — heartbeat + registration agent
+```
+
+#### Cluster Services
+- `mac/services/load_balancer.py` — score-based routing: `gpu_util×0.5 + vram_ratio×0.3`, 30s stale threshold
+- `mac/services/llm_service.py` — updated `_resolve_model_cluster` to use load balancer before local vLLM
+- `mac/models/node.py` — `WorkerNode` + `NodeModelDeployment` + `EnrollmentToken`; added `notebook_port`, `tags`
+- `mac/models/cluster.py` — `ClusterHeartbeat` time-series
+
+#### Cluster Router (`mac/routers/cluster.py`)
+| Endpoint | Description |
+|----------|-------------|
+| `POST /cluster/enroll-token` | Admin generates one-time enrollment token |
+| `GET /cluster/enroll-tokens` | List all tokens |
+| `POST /cluster/register` | Worker self-registers (no JWT — uses enrollment token) |
+| `POST /cluster/heartbeat` | Worker sends heartbeat every 10s |
+| `GET /cluster/nodes` | List all nodes with live health |
+| `GET /cluster/nodes/{id}` | Node detail with deployments |
+| `POST /cluster/nodes/{id}/action` | approve / drain / reactivate / remove |
+| `POST /cluster/nodes/{id}/deploy` | Register vLLM deployment on node |
+| `DELETE /cluster/nodes/{id}/deploy/{dep_id}` | Remove deployment |
+| `GET /cluster/nodes/{id}/history` | Heartbeat time-series (for charts) |
+
+#### Worker Agent (`worker_agent.py`)
+Standalone Python script for worker PCs:
+- Reads `MAC_MASTER_URL`, `MAC_ENROLL_TOKEN`, `MAC_VLLM_PORT`, etc. from env
+- Self-registers on startup via enrollment token
+- Sends heartbeats every 10s with GPU/CPU/RAM metrics (via `pynvml` + `psutil`)
+- Queries local vLLM `/v1/models` to report active models
+- Handles stale/auth errors gracefully
+
+#### Other New Routers
+| File | Endpoints |
+|------|-----------|
+| `mac/routers/academic.py` | CRUD for branches and sections |
+| `mac/routers/file_share.py` | Admin upload, user download, stats |
+
+---
+
+## 🔲 Remaining / Optional
+
+| Item | Priority | Notes |
+|------|----------|-------|
+| Frontend PWA icons | Medium | `static/icon-192.png`, `static/icon-512.png`, `static/favicon.ico` — need actual PNG files |
+| Frontend: refresh token flow | Medium | Silent JWT refresh in `api.js` before expiry |
+| Multi-stage Dockerfile | Low | Stage 1: node build frontend; Stage 2: python + nginx |
+| Feature flag wiring | Low | `feature_required("ai_chat")` on `/query/*`, etc. |
+| HTTPS cert setup | Deployment | Use `nginx.https.conf` + Let's Encrypt / self-signed |
+| `alembic/versions/0003` | When schema changes | Node notebook_port and tags columns |
+| Video generation service | Future | `VideoProject` / `VideoJob` models exist, router not yet created |
+
+---
+
+## Deployment Quick-Start
+
+### Master node
+```bash
+# 1. Build frontend
+cd frontend && npm install && npm run build && cd ..
+
+# 2. Configure environment
+cp .env.example .env # edit DB, Redis, model settings
+
+# 3. Run DB migrations
+docker compose up postgres -d
+docker compose run --rm mac alembic upgrade head
+
+# 4. Start all services
+docker compose up -d
+```
+
+### Adding a worker node
+```bash
+# On the master — generate enrollment token
+curl -X POST http://MASTER_IP:8000/api/v1/cluster/enroll-token \
+ -H "Authorization: Bearer ADMIN_JWT" \
+ -d '{"label":"Lab PC 1","expires_hours":24}'
+
+# On the worker PC
+MAC_MASTER_URL=http://MASTER_IP:8000 \
+MAC_ENROLL_TOKEN= \
+MAC_VLLM_PORT=8001 \
+docker compose -f docker-compose.worker.yml up -d
+
+# Then approve the node in MAC admin panel → Cluster tab
+```
+
+### HTTPS (production)
+```bash
+# Place certs in nginx/ssl/
+# Swap nginx config:
+# In docker-compose.yml, change:
+# volumes: ./nginx/nginx.conf → ./nginx/nginx.https.conf
+# Then restart nginx
+```
+
+---
+
+*Last updated: 2026-04-27 — Session 2 complete*
diff --git a/frontend/build.sh b/frontend/build.sh
new file mode 100644
index 0000000000000000000000000000000000000000..d24ed52ccd4d172f19b3691be55a3d06371c52c3
--- /dev/null
+++ b/frontend/build.sh
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+# Build SvelteKit frontend for production.
+# Output goes to frontend/build/ — Nginx mounts this directory.
+set -e
+
+cd "$(dirname "$0")"
+
+echo "Installing dependencies…"
+npm install
+
+echo "Building SvelteKit app…"
+npm run build
+
+echo "Build complete → frontend/build/"
+ls -lh build/
diff --git a/frontend/build/_app/env.js b/frontend/build/_app/env.js
new file mode 100644
index 0000000000000000000000000000000000000000..f5427da6b8aff07b5685528dab1d03caec5e682f
--- /dev/null
+++ b/frontend/build/_app/env.js
@@ -0,0 +1 @@
+export const env={}
\ No newline at end of file
diff --git a/frontend/build/_app/immutable/assets/0.DBvVKUFC.css b/frontend/build/_app/immutable/assets/0.DBvVKUFC.css
new file mode 100644
index 0000000000000000000000000000000000000000..c88317aa9886b64266835b04b06e3db90b494ce6
--- /dev/null
+++ b/frontend/build/_app/immutable/assets/0.DBvVKUFC.css
@@ -0,0 +1 @@
+*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Inter,system-ui,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:Fira Code,JetBrains Mono,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--bg: #FAF9F7;--surface: #FFFFFF;--surface2: #F5F4F0;--surface3: #ECEAE4;--border: rgba(0,0,0,.12);--border-strong: rgba(0,0,0,.22);--text: #1A1A1A;--text2: #666560;--text3: #999791;--accent: #D97449;--accent-hover: #C4623D;--accent-text: #FFFFFF;--success: #2D7D52;--success-bg: #F0FAF4;--warning: #B5620A;--warning-bg: #FFF8F0;--error: #C0392B;--error-bg: #FFF0EE;--code-bg: #F0EDE8;--shadow: 0 1px 3px rgba(0,0,0,.08)}[data-theme=dark]{--bg: #1A1917;--surface: #242220;--surface2: #2E2C29;--surface3: #393733;--border: rgba(255,255,255,.1);--border-strong: rgba(255,255,255,.18);--text: #E8E6E1;--text2: #9B9891;--text3: #6B6965;--accent: #E8855A;--accent-hover: #D9724A;--accent-text: #FFFFFF;--success: #4CAF80;--success-bg: #0F2D1E;--warning: #E8A030;--warning-bg: #2A1F0A;--error: #E05555;--error-bg: #2A0F0F;--code-bg: #161513;--shadow: 0 1px 3px rgba(0,0,0,.3)}*{box-sizing:border-box}html{scroll-behavior:smooth}body{min-height:100vh;background-color:var(--bg);color:var(--text);font-family:Inter,system-ui,sans-serif;transition:background-color .35s ease,color .35s ease}*{transition:background-color .35s ease,color .2s ease,border-color .3s ease,box-shadow .3s ease}input,textarea,button,a,[class*=nav-],[class*=btn-],canvas{transition:none}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:var(--surface2)}::-webkit-scrollbar-thumb{background:var(--surface3);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:var(--accent)}.btn-primary{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:8px 16px;background:var(--accent);color:var(--accent-text);font-size:14px;font-weight:600;border-radius:8px;border:none;cursor:pointer;transition:background .15s ease,transform .1s ease,box-shadow .15s ease;box-shadow:0 1px 3px #00000026;text-decoration:none}.btn-primary:hover:not(:disabled){background:var(--accent-hover);box-shadow:0 2px 6px #0003}.btn-primary:active:not(:disabled){transform:scale(.98)}.btn-primary:disabled{opacity:.5;cursor:not-allowed}.btn-secondary{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:8px 16px;background:var(--surface2);color:var(--text);font-size:14px;font-weight:500;border-radius:8px;border:1px solid var(--border);cursor:pointer;transition:background .15s ease,border-color .15s ease;text-decoration:none}.btn-secondary:hover:not(:disabled){background:var(--surface3);border-color:var(--border-strong)}.btn-secondary:disabled{opacity:.5;cursor:not-allowed}.btn-danger{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:8px 16px;background:var(--error-bg);color:var(--error);font-size:14px;font-weight:600;border-radius:8px;border:1px solid var(--error);cursor:pointer;transition:background .15s ease}.btn-danger:hover:not(:disabled){background:var(--error);color:#fff}.card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:16px;box-shadow:var(--shadow)}.input-field{width:100%;background:var(--surface2);border:1px solid var(--border);border-radius:8px;padding:8px 12px;color:var(--text);font-size:14px;outline:none;transition:border-color .15s ease,box-shadow .15s ease}.input-field::-moz-placeholder{color:var(--text3)}.input-field::placeholder{color:var(--text3)}.input-field:focus{border-color:var(--accent);box-shadow:0 0 0 3px #d9744926}.label{display:block;font-size:13px;font-weight:500;color:var(--text2);margin-bottom:4px}.badge{display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:600;letter-spacing:.02em}.badge-green{background:var(--success-bg);color:var(--success)}.badge-yellow{background:var(--warning-bg);color:var(--warning)}.badge-gray{background:var(--surface3);color:var(--text2)}.nav-link{display:flex;align-items:center;gap:10px;padding:7px 10px;border-radius:8px;color:var(--text2);font-size:13px;font-weight:500;text-decoration:none;cursor:pointer;background:none;border:none;transition:background .12s ease,color .12s ease;white-space:nowrap}.nav-link:hover{background:var(--surface2);color:var(--text)}.nav-link.active{background:#d974491f;color:var(--accent)}.nav-link.\!active{background:#d974491f!important;color:var(--accent)!important}.stat-card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:16px;display:flex;flex-direction:column;gap:4px;box-shadow:var(--shadow)}.typing-dot{width:6px;height:6px;border-radius:50%;background:var(--accent);animation:bounce 1s infinite}.message-user{background:#d974491a;border:1px solid rgba(217,116,73,.2);border-radius:16px 4px 16px 16px;padding:12px 16px;max-width:80%;margin-left:auto;color:var(--text)}.message-assistant{background:var(--surface2);border:1px solid var(--border);border-radius:4px 16px 16px;padding:12px 16px;max-width:90%;color:var(--text)}pre code{display:block;background:var(--code-bg);border:1px solid var(--border);border-radius:8px;padding:16px;font-family:Fira Code,JetBrains Mono,monospace;font-size:13px;overflow-x:auto;color:var(--text)}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.collapse{visibility:collapse}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.bottom-6{bottom:1.5rem}.right-6{right:1.5rem}.top-1{top:.25rem}.z-10{z-index:10}.z-50{z-index:50}.mx-auto{margin-left:auto;margin-right:auto}.my-2{margin-top:.5rem;margin-bottom:.5rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.ml-4{margin-left:1rem}.ml-auto{margin-left:auto}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.flex{display:flex}.table{display:table}.grid{display:grid}.hidden{display:none}.aspect-video{aspect-ratio:16 / 9}.h-1{height:.25rem}.h-16{height:4rem}.h-2{height:.5rem}.h-20{height:5rem}.h-4{height:1rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-full{height:100%}.min-h-0{min-height:0px}.min-h-20{min-height:5rem}.min-h-24{min-height:6rem}.min-h-28{min-height:7rem}.min-h-\[520px\]{min-height:520px}.min-h-screen{min-height:100vh}.w-11{width:2.75rem}.w-16{width:4rem}.w-2{width:.5rem}.w-20{width:5rem}.w-36{width:9rem}.w-4{width:1rem}.w-40{width:10rem}.w-7{width:1.75rem}.w-72{width:18rem}.w-8{width:2rem}.w-full{width:100%}.min-w-0{min-width:0px}.min-w-40{min-width:10rem}.min-w-44{min-width:11rem}.min-w-48{min-width:12rem}.min-w-56{min-width:14rem}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-72{max-width:18rem}.max-w-7xl{max-width:80rem}.max-w-\[85\%\]{max-width:85%}.max-w-lg{max-width:32rem}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.flex-shrink{flex-shrink:1}.flex-shrink-0{flex-shrink:0}.border-collapse{border-collapse:collapse}.translate-x-1{--tw-translate-x: .25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-5{--tw-translate-x: 1.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.animate-fade-in{animation:fadeIn .3s ease-in-out}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes slideUp{0%{transform:translateY(10px);opacity:0}to{transform:translateY(0);opacity:1}}.animate-slide-up{animation:slideUp .3s ease-out}.cursor-pointer{cursor:pointer}.resize{resize:both}.list-inside{list-style-position:inside}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-dark-600>:not([hidden])~:not([hidden]){border-color:var(--surface3)}.self-end{align-self:flex-end}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-3xl{border-radius:1.5rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-sm{border-radius:.125rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-dashed{border-style:dashed}.border-blue-800\/40{border-color:#1e40af66}.border-dark-500{border-color:var(--border-strong)}.border-dark-600{border-color:var(--surface3)}.border-green-500{--tw-border-opacity: 1;border-color:rgb(34 197 94 / var(--tw-border-opacity, 1))}.border-green-800\/40{border-color:#16653466}.border-mac-500,.border-mac-600{border-color:var(--accent)}.border-mac-700{border-color:var(--accent-hover)}.border-orange-800\/40{border-color:#9a341266}.border-red-500{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity, 1))}.border-red-800{--tw-border-opacity: 1;border-color:rgb(153 27 27 / var(--tw-border-opacity, 1))}.border-red-800\/40{border-color:#991b1b66}.border-transparent{border-color:transparent}.border-yellow-500{--tw-border-opacity: 1;border-color:rgb(234 179 8 / var(--tw-border-opacity, 1))}.border-yellow-800\/40{border-color:#854d0e66}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.bg-blue-900\/30{background-color:#1e3a8a4d}.bg-blue-900\/40{background-color:#1e3a8a66}.bg-dark-500{background-color:var(--border-strong)}.bg-dark-600{background-color:var(--surface3)}.bg-dark-700{background-color:var(--surface2)}.bg-dark-900{background-color:var(--bg)}.bg-gray-900\/40{background-color:#11182766}.bg-green-700\/30{background-color:#15803d4d}.bg-green-900\/30{background-color:#14532d4d}.bg-green-900\/40{background-color:#14532d66}.bg-green-900\/50{background-color:#14532d80}.bg-green-950\/40{background-color:#052e1666}.bg-mac-400,.bg-mac-500,.bg-mac-600{background-color:var(--accent)}.bg-mac-700{background-color:var(--accent-hover)}.bg-mac-800{background-color:var(--surface3)}.bg-orange-900\/30{background-color:#7c2d124d}.bg-orange-900\/40{background-color:#7c2d1266}.bg-red-900\/30{background-color:#7f1d1d4d}.bg-red-900\/40{background-color:#7f1d1d66}.bg-red-950\/50{background-color:#450a0a80}.bg-surface2{background-color:var(--surface2)}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-yellow-900\/30{background-color:#713f124d}.bg-yellow-900\/40{background-color:#713f1266}.bg-gradient-radial{background-image:radial-gradient(var(--tw-gradient-stops))}.to-dark-900{--tw-gradient-to: var(--bg) var(--tw-gradient-to-position)}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-2{padding:.5rem}.p-2\.5{padding:.625rem}.p-3{padding:.75rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-10{padding-top:2.5rem;padding-bottom:2.5rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-16{padding-top:4rem;padding-bottom:4rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-4{padding-bottom:1rem}.pt-2{padding-top:.5rem}.pt-4{padding-top:1rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:Fira Code,JetBrains Mono,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.italic{font-style:italic}.leading-relaxed{line-height:1.625}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-gray-100{--tw-text-opacity: 1;color:rgb(243 244 246 / var(--tw-text-opacity, 1))}.text-gray-200{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-green-300{--tw-text-opacity: 1;color:rgb(134 239 172 / var(--tw-text-opacity, 1))}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-mac-300,.text-mac-400{color:var(--accent)}.text-orange-400{--tw-text-opacity: 1;color:rgb(251 146 60 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-yellow-400{--tw-text-opacity: 1;color:rgb(250 204 21 / var(--tw-text-opacity, 1))}.opacity-0{opacity:0}.opacity-60{opacity:.6}.opacity-80{opacity:.8}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-black\/50{--tw-shadow-color: rgb(0 0 0 / .5);--tw-shadow: var(--tw-shadow-colored)}.shadow-black\/60{--tw-shadow-color: rgb(0 0 0 / .6);--tw-shadow: var(--tw-shadow-colored)}.outline{outline-style:solid}.ring{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-1{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-mac-500{--tw-ring-color: var(--accent)}.blur{--tw-blur: blur(8px);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-xl{--tw-backdrop-blur: blur(24px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-filter{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.mac-glitch{position:relative;display:inline-block;font-family:Fira Code,JetBrains Mono,monospace;font-weight:900;letter-spacing:.12em;color:var(--text)}.mac-glitch:before,.mac-glitch:after{content:attr(data-text);position:absolute;top:0;right:0;bottom:0;left:0;pointer-events:none;opacity:.65}.mac-glitch:before{color:var(--accent);clip-path:inset(0 0 62% 0);text-shadow:-2px 0 rgba(224,85,85,.42);animation:macGlitchA 3.2s infinite linear alternate-reverse}.mac-glitch:after{color:var(--success);clip-path:inset(58% 0 0 0);text-shadow:2px 0 rgba(58,125,68,.38);animation:macGlitchB 2.7s infinite linear alternate-reverse}@keyframes macGlitchA{0%,to{transform:translate(0);clip-path:inset(0 0 62% 0)}25%{transform:translate(-2px,1px);clip-path:inset(12% 0 50% 0)}50%{transform:translate(2px,-1px);clip-path:inset(32% 0 38% 0)}75%{transform:translate(-1px,2px);clip-path:inset(5% 0 70% 0)}}@keyframes macGlitchB{0%,to{transform:translate(0);clip-path:inset(58% 0 0 0)}30%{transform:translate(2px,-2px);clip-path:inset(48% 0 16% 0)}60%{transform:translate(-2px,1px);clip-path:inset(68% 0 6% 0)}}.glitch{position:relative;display:inline-block;font-family:Courier New,monospace;font-weight:900;letter-spacing:.15em;color:var(--text, var(--fg))}.glitch:before,.glitch:after{content:attr(data-text);position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}.glitch:before{color:var(--text, var(--fg));animation:glitch-1 3s infinite linear alternate-reverse;clip-path:inset(0 0 65% 0);text-shadow:-2px 0 rgba(255,0,0,.35)}.glitch:after{color:var(--text, var(--fg));animation:glitch-2 2.5s infinite linear alternate-reverse;clip-path:inset(65% 0 0 0);text-shadow:2px 0 rgba(0,0,255,.35)}@keyframes glitch-1{0%,to{clip-path:inset(0 0 65% 0);transform:translate(0)}20%{clip-path:inset(10% 0 55% 0);transform:translate(-3px,1px)}40%{clip-path:inset(30% 0 40% 0);transform:translate(2px,-1px)}60%{clip-path:inset(5% 0 70% 0);transform:translate(-1px,2px)}80%{clip-path:inset(20% 0 50% 0);transform:translate(3px)}}@keyframes glitch-2{0%,to{clip-path:inset(65% 0 0 0);transform:translate(0)}25%{clip-path:inset(50% 0 10% 0);transform:translate(2px,-2px)}50%{clip-path:inset(70% 0 5% 0);transform:translate(-3px,1px)}75%{clip-path:inset(60% 0 15% 0);transform:translate(1px,2px)}}.hover\:border-dark-500:hover{border-color:var(--border-strong)}.hover\:border-mac-500:hover{border-color:var(--accent)}.hover\:bg-dark-500:hover{background-color:var(--border-strong)}.hover\:bg-green-900\/70:hover{background-color:#14532db3}.hover\:bg-orange-900\/70:hover{background-color:#7c2d12b3}.hover\:bg-red-900\/70:hover{background-color:#7f1d1db3}.hover\:text-gray-200:hover{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.hover\:text-gray-300:hover{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.hover\:opacity-100:hover,.group:hover .group-hover\:opacity-100{opacity:1}@media(min-width:640px){.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media(min-width:768px){.md\:col-span-2{grid-column:span 2 / span 2}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media(min-width:1024px){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-\[340px_1fr\]{grid-template-columns:340px 1fr}.lg\:grid-cols-\[360px_1fr\]{grid-template-columns:360px 1fr}}.\[\&_code\]\:text-mac-300 code{color:var(--accent)}.\[\&_h1\]\:text-gray-100 h1,.\[\&_h2\]\:text-gray-100 h2{--tw-text-opacity: 1;color:rgb(243 244 246 / var(--tw-text-opacity, 1))}.\[\&_h3\]\:text-gray-200 h3{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.\[\&_ol\]\:list-decimal ol{list-style-type:decimal}.\[\&_ol\]\:pl-4 ol{padding-left:1rem}.\[\&_pre\]\:mt-2 pre{margin-top:.5rem}.\[\&_pre_code\]\:text-gray-200 pre code{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.\[\&_strong\]\:text-gray-100 strong{--tw-text-opacity: 1;color:rgb(243 244 246 / var(--tw-text-opacity, 1))}.\[\&_ul\]\:list-disc ul{list-style-type:disc}.\[\&_ul\]\:pl-4 ul{padding-left:1rem}.sidebar.svelte-129hoe0{position:relative;z-index:2;display:flex;flex-direction:column;height:100vh;background:var(--surface);border-right:1px solid var(--border);flex-shrink:0;transition:width .15s ease;overflow:hidden;-webkit-user-select:none;-moz-user-select:none;user-select:none}.sidebar.dragging.svelte-129hoe0{transition:none}.logo-area.svelte-129hoe0{display:flex;align-items:center;gap:10px;padding:14px;border-bottom:1px solid var(--border);flex-shrink:0;overflow:hidden}.sidebar-mac.svelte-129hoe0{font-size:16px;flex-shrink:0}.logo-sub.svelte-129hoe0{font-size:10px;color:var(--text3);white-space:nowrap;margin-top:1px}.nav-section.svelte-129hoe0{flex:1;padding:8px 6px;overflow-y:auto;overflow-x:hidden;display:flex;flex-direction:column;gap:2px}.nav-section.svelte-129hoe0::-webkit-scrollbar{width:4px}.nav-section.svelte-129hoe0::-webkit-scrollbar-track{background:transparent}.nav-section.svelte-129hoe0::-webkit-scrollbar-thumb{background:var(--surface3);border-radius:2px}.nav-link.svelte-129hoe0{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:8px;color:var(--text2);font-size:13px;font-weight:500;text-decoration:none;cursor:pointer;background:none;border:none;width:100%;transition:background .15s,color .15s;white-space:nowrap;overflow:hidden}.nav-link.svelte-129hoe0:hover{background:var(--surface2);color:var(--text)}.nav-link.active.svelte-129hoe0{background:#d974491a;color:var(--accent)}.nav-link.active.svelte-129hoe0 .nav-icon:where(.svelte-129hoe0) svg{stroke:var(--accent)}.nav-icon.svelte-129hoe0{width:18px;height:18px;display:flex;align-items:center;justify-content:center;flex-shrink:0}.nav-icon.svelte-129hoe0 svg{width:18px;height:18px;stroke:currentColor;flex-shrink:0}.nav-label.svelte-129hoe0{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.section-header.svelte-129hoe0{padding:10px 12px 4px;font-size:10px;color:var(--text3);text-transform:uppercase;letter-spacing:.12em;font-weight:600}.section-divider.svelte-129hoe0{height:1px;background:var(--border);margin:6px 8px}.compact.svelte-129hoe0 .nav-link:where(.svelte-129hoe0){justify-content:center;padding:10px;gap:0}.compact.svelte-129hoe0 .logo-area:where(.svelte-129hoe0){justify-content:center;padding:14px 10px}.user-area.svelte-129hoe0{border-top:1px solid var(--border);padding:8px 6px;flex-shrink:0;display:flex;flex-direction:column;gap:2px}.user-info.svelte-129hoe0{display:flex;align-items:center;gap:10px;padding:6px 10px 4px;overflow:hidden}.user-info.compact-user.svelte-129hoe0{justify-content:center;padding:6px 10px}.avatar.svelte-129hoe0{width:28px;height:28px;border-radius:50%;background:#d9744926;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:var(--accent);flex-shrink:0}.user-text.svelte-129hoe0{display:flex;flex-direction:column;min-width:0;overflow:hidden}.user-name.svelte-129hoe0{font-size:12px;font-weight:600;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.user-role.svelte-129hoe0{font-size:10px;color:var(--text3);text-transform:capitalize;white-space:nowrap}.logout-btn.svelte-129hoe0{color:var(--text3)}.logout-btn.svelte-129hoe0:hover{background:var(--error-bg);color:var(--error)}.resizer.svelte-129hoe0{position:absolute;top:0;right:0;width:8px;height:100%;cursor:col-resize;z-index:10;display:flex;align-items:stretch;justify-content:flex-end}.resizer-line.svelte-129hoe0{width:2px;background:transparent;transition:background .2s;border-radius:1px;margin-right:1px}.resizer.svelte-129hoe0:hover .resizer-line:where(.svelte-129hoe0),.dragging.svelte-129hoe0 .resizer:where(.svelte-129hoe0) .resizer-line:where(.svelte-129hoe0){background:var(--accent)}.mac-backdrop.svelte-1qbfbt1{position:fixed;top:0;right:0;bottom:0;left:0;z-index:0;pointer-events:none;overflow:hidden;opacity:.72}.word.svelte-1qbfbt1{position:absolute;color:var(--accent);opacity:.045;font-weight:800;letter-spacing:.12em;font-family:Inter,system-ui,sans-serif;animation:svelte-1qbfbt1-drift 10s ease-in-out infinite alternate}.scanlines.svelte-1qbfbt1{position:absolute;top:0;right:0;bottom:0;left:0;background:repeating-linear-gradient(to bottom,transparent 0,transparent 5px,rgba(217,116,73,.025) 6px);-webkit-mask-image:linear-gradient(to bottom,transparent,#000 18%,#000 80%,transparent);mask-image:linear-gradient(to bottom,transparent,#000 18%,#000 80%,transparent)}@keyframes svelte-1qbfbt1-drift{0%{translate:0 0}to{translate:10px -8px}}@media(prefers-reduced-motion:reduce){.word.svelte-1qbfbt1{animation:none}}.pretext-bg.svelte-1w4fu1y{position:fixed;top:0;right:0;bottom:0;left:0;z-index:0;pointer-events:auto;overflow:hidden;-webkit-user-select:none;-moz-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent}.pt-scanlines.svelte-1w4fu1y{position:absolute;top:0;right:0;bottom:0;left:0;background:repeating-linear-gradient(to bottom,transparent 0,transparent 5px,rgba(217,116,73,.018) 6px);-webkit-mask-image:linear-gradient(to bottom,transparent,#000 18%,#000 80%,transparent);mask-image:linear-gradient(to bottom,transparent,#000 18%,#000 80%,transparent);pointer-events:none}.pt-word.svelte-1w4fu1y{position:absolute;color:var(--accent, #D97449);font-family:Courier New,monospace;letter-spacing:.15em;pointer-events:none;will-change:transform,opacity;transition:opacity .3s ease;text-transform:uppercase}@media(prefers-reduced-motion:reduce){.pt-word.svelte-1w4fu1y{transition:none!important}}.loading-overlay.svelte-1qpkoic{position:fixed;top:0;right:0;bottom:0;left:0;z-index:9999;display:flex;align-items:center;justify-content:center;background:#0000002e;backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);animation:svelte-1qpkoic-fadeIn .25s ease}.loading-content.svelte-1qpkoic{display:flex;flex-direction:column;align-items:center;gap:18px}.loading-msg.svelte-1qpkoic{font-size:14px;font-weight:500;color:var(--text, #1A1A1A);letter-spacing:.03em;opacity:.85;margin:0;text-align:center;max-width:280px}@keyframes svelte-1qpkoic-fadeIn{0%{opacity:0}to{opacity:1}}.app-shell.svelte-12qhfyh{display:flex;height:100vh;overflow:hidden;background:var(--bg)}.app-main.svelte-12qhfyh{flex:1;overflow:auto;min-width:0;background:var(--bg);color:var(--text);position:relative;z-index:1}
diff --git a/frontend/build/_app/immutable/assets/12.BQVrdhQn.css b/frontend/build/_app/immutable/assets/12.BQVrdhQn.css
new file mode 100644
index 0000000000000000000000000000000000000000..609158ecae412399b28adc21116bc27f3235f6a3
--- /dev/null
+++ b/frontend/build/_app/immutable/assets/12.BQVrdhQn.css
@@ -0,0 +1 @@
+.theme-toggle.svelte-1cmi4dh{position:fixed;bottom:24px;right:24px;z-index:100;width:44px;height:44px;border:1px solid var(--border, rgba(0,0,0,.12));border-radius:50%;background:var(--surface, #fff);box-shadow:0 2px 12px #0000001f;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:transform .2s ease,box-shadow .2s ease,background-color .35s ease;outline:none;padding:0}.theme-toggle.svelte-1cmi4dh:hover{transform:scale(1.08);box-shadow:0 4px 20px #d9744940}.theme-toggle.svelte-1cmi4dh:active{transform:scale(.95)}.icon-wrap.svelte-1cmi4dh{position:relative;width:18px;height:18px}.sun.svelte-1cmi4dh,.moon.svelte-1cmi4dh{position:absolute;top:0;right:0;bottom:0;left:0;transition:opacity .35s ease,transform .35s ease}.sun.svelte-1cmi4dh{opacity:1;transform:rotate(0) scale(1);color:var(--accent, #D97449)}.moon.svelte-1cmi4dh{opacity:0;transform:rotate(-90deg) scale(.6);color:var(--accent, #D97449)}.icon-wrap.dark.svelte-1cmi4dh .sun:where(.svelte-1cmi4dh){opacity:0;transform:rotate(90deg) scale(.6)}.icon-wrap.dark.svelte-1cmi4dh .moon:where(.svelte-1cmi4dh){opacity:1;transform:rotate(0) scale(1)}.login-root.svelte-1x05zx6{position:fixed;top:0;right:0;bottom:0;left:0;background:var(--bg);display:flex;align-items:center;justify-content:center;overflow:hidden;font-family:Inter,system-ui,sans-serif;-webkit-tap-highlight-color:transparent}.orb.svelte-1x05zx6{position:absolute;border-radius:50%;filter:blur(80px);pointer-events:none;animation:svelte-1x05zx6-orbFloat 8s ease-in-out infinite alternate}.orb-1.svelte-1x05zx6{width:360px;height:360px;background:radial-gradient(circle,rgba(217,116,73,.14) 0%,transparent 70%);top:-80px;left:-80px}.orb-2.svelte-1x05zx6{width:280px;height:280px;background:radial-gradient(circle,rgba(196,98,61,.1) 0%,transparent 70%);bottom:-60px;right:-60px;animation-delay:-4s}@keyframes svelte-1x05zx6-orbFloat{0%{transform:translate(0) scale(1)}to{transform:translate(30px,20px) scale(1.08)}}.words-layer.svelte-1x05zx6{position:absolute;top:0;right:0;bottom:0;left:0;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;overflow:hidden}.wm-word.svelte-1x05zx6{position:absolute;font-weight:900;letter-spacing:.15em;color:var(--accent);font-family:Courier New,monospace;pointer-events:none}.card-wrap.svelte-1x05zx6{position:relative;z-index:10;width:100%;max-width:410px;margin:0 16px;background:var(--surface);border:1px solid var(--border-strong);border-radius:20px;padding:36px 32px 28px;box-shadow:0 8px 40px #0000001f,var(--shadow);animation:svelte-1x05zx6-cardEnter .45s cubic-bezier(.16,1,.3,1) both}@keyframes svelte-1x05zx6-cardEnter{0%{transform:translateY(20px);opacity:.4}to{opacity:1;transform:translateY(0)}}.card-wrap.shake{animation:svelte-1x05zx6-shake .52s cubic-bezier(.36,.07,.19,.97) both!important}@keyframes svelte-1x05zx6-shake{10%,90%{transform:translate(-3px)}20%,80%{transform:translate(4px)}30%,50%,70%{transform:translate(-5px)}40%,60%{transform:translate(5px)}}.card-header.svelte-1x05zx6{text-align:center;margin-bottom:20px}.mac-title.svelte-1x05zx6{font-size:clamp(2.5rem,6vw,4rem);margin:0 0 6px;animation:svelte-1x05zx6-cardEnter .55s .05s cubic-bezier(.16,1,.3,1) both}.sub.svelte-1x05zx6{font-size:12px;color:var(--text3);letter-spacing:.02em;margin:0;animation:svelte-1x05zx6-cardEnter .55s .12s cubic-bezier(.16,1,.3,1) both}.view-hint.svelte-1x05zx6{font-size:13px;color:var(--text2);text-align:center;margin:0 0 18px;line-height:1.5}.back-btn.svelte-1x05zx6{display:inline-flex;align-items:center;gap:5px;background:none;border:none;cursor:pointer;color:var(--text3);font-size:12px;font-family:inherit;padding:4px 0;margin-bottom:12px;transition:color .15s}.back-btn.svelte-1x05zx6:hover{color:var(--text2)}.form.svelte-1x05zx6{display:flex;flex-direction:column;gap:14px}.field-wrap.svelte-1x05zx6{position:relative}.float-label.svelte-1x05zx6{position:absolute;top:50%;left:40px;transform:translateY(-50%);font-size:13px;color:var(--text3);pointer-events:none;transition:all .2s cubic-bezier(.16,1,.3,1);z-index:2}.field-wrap.focused.svelte-1x05zx6 .float-label:where(.svelte-1x05zx6),.field-wrap.filled.svelte-1x05zx6 .float-label:where(.svelte-1x05zx6){top:-8px;left:10px;font-size:10px;letter-spacing:.06em;color:var(--accent);background:var(--surface);padding:0 5px;border-radius:3px;text-transform:uppercase;font-weight:600}.field-inner.svelte-1x05zx6{position:relative;display:flex;align-items:center}.field-icon.svelte-1x05zx6{position:absolute;left:13px;color:var(--text3);transition:color .2s;pointer-events:none;z-index:1}.field-wrap.focused.svelte-1x05zx6 .field-icon:where(.svelte-1x05zx6){color:var(--accent)}.field-input.svelte-1x05zx6{width:100%;background:var(--surface2);border:1px solid var(--border);border-radius:11px;padding:13px 13px 13px 38px;font-size:14px;color:var(--text);outline:none;font-family:inherit;transition:border-color .2s,box-shadow .2s}.field-input.svelte-1x05zx6::-moz-placeholder{color:transparent}.field-input.svelte-1x05zx6::placeholder{color:transparent}.field-wrap.focused.svelte-1x05zx6 .field-input:where(.svelte-1x05zx6){border-color:var(--accent);box-shadow:0 0 0 3px #d9744926;background:var(--surface)}.field-input.mismatch.svelte-1x05zx6{border-color:var(--error)}.field-hint.svelte-1x05zx6{display:block;font-size:11px;color:var(--text3);margin-top:4px;padding-left:4px}.eye-btn.svelte-1x05zx6{position:absolute;right:11px;background:none;border:none;cursor:pointer;color:var(--text3);padding:4px;display:flex;align-items:center;border-radius:5px;outline:none;transition:color .2s}.eye-btn.svelte-1x05zx6:hover{color:var(--text2)}.pw-strength.svelte-1x05zx6{display:flex;align-items:center;gap:8px}.pw-bar.svelte-1x05zx6{flex:1;height:4px;background:var(--surface3);border-radius:2px;overflow:hidden}.pw-fill.svelte-1x05zx6{height:100%;border-radius:2px;transition:width .3s,background .3s}.pw-label.svelte-1x05zx6{font-size:11px;font-weight:600;width:40px;text-align:right}.err-msg.svelte-1x05zx6{display:flex;align-items:center;gap:8px;padding:10px 13px;border-radius:10px;background:var(--error-bg);border:1px solid var(--error);color:var(--error);font-size:13px}.sign-btn.svelte-1x05zx6{width:100%;display:flex;align-items:center;justify-content:center;gap:8px;padding:13px 20px;background:var(--accent);border:none;border-radius:11px;color:#fff;font-size:14px;font-weight:600;cursor:pointer;letter-spacing:.03em;box-shadow:0 4px 18px #d974494d;font-family:inherit;outline:none;transition:background .2s,box-shadow .2s}.sign-btn.svelte-1x05zx6:hover:not(:disabled){background:var(--accent-hover);box-shadow:0 6px 26px #d9744973}.sign-btn.svelte-1x05zx6:disabled{opacity:.4;cursor:not-allowed;box-shadow:none}.spinner.svelte-1x05zx6{width:15px;height:15px;border:2px solid rgba(255,255,255,.3);border-top-color:#fff;border-radius:50%;animation:svelte-1x05zx6-spin .6s linear infinite}@keyframes svelte-1x05zx6-spin{to{transform:rotate(360deg)}}.divider.svelte-1x05zx6{display:flex;align-items:center;gap:10px;margin:14px 0 10px}.divider.svelte-1x05zx6:before,.divider.svelte-1x05zx6:after{content:"";flex:1;height:1px;background:var(--border)}.divider.svelte-1x05zx6 span:where(.svelte-1x05zx6){font-size:11px;color:var(--text3)}.alt-btn.svelte-1x05zx6{display:flex;align-items:center;justify-content:center;gap:7px;width:100%;padding:11px 16px;background:none;border:1px solid var(--border);border-radius:11px;cursor:pointer;font-size:13px;font-weight:500;color:var(--text2);font-family:inherit;transition:border-color .2s,color .2s,background .2s}.alt-btn.svelte-1x05zx6:hover{border-color:var(--accent);color:var(--accent);background:#d974490a}.card-footer.svelte-1x05zx6{display:flex;align-items:center;justify-content:space-between;margin-top:20px;padding-top:14px;border-top:1px solid var(--border)}.version.svelte-1x05zx6{font-size:11px;color:var(--text3);font-family:Courier New,monospace}.locale-wrap.svelte-1x05zx6{position:relative}.locale-btn.svelte-1x05zx6{display:flex;align-items:center;gap:5px;background:none;border:none;cursor:pointer;color:var(--text3);font-size:12px;padding:4px 7px;border-radius:6px;font-family:inherit;transition:color .2s,background .2s}.locale-btn.svelte-1x05zx6:hover{color:var(--text2);background:var(--surface2)}.locale-dropdown.svelte-1x05zx6{position:absolute;bottom:calc(100% + 6px);right:0;background:var(--surface);border:1px solid var(--border-strong);border-radius:10px;box-shadow:0 8px 32px #00000026;z-index:50;width:180px;max-height:300px;overflow-y:auto;overscroll-behavior:contain;padding:4px;-webkit-overflow-scrolling:touch}.locale-dropdown.svelte-1x05zx6::-webkit-scrollbar{width:5px}.locale-dropdown.svelte-1x05zx6::-webkit-scrollbar-thumb{background:var(--accent);border-radius:6px}.locale-item.svelte-1x05zx6{display:block;width:100%;text-align:left;background:none;border:none;padding:8px 12px;font-size:13px;color:var(--text2);cursor:pointer;border-radius:7px;font-family:inherit;transition:background .15s,color .15s}.locale-item.svelte-1x05zx6:hover{background:var(--surface2);color:var(--text)}.locale-item.active.svelte-1x05zx6{color:var(--accent);font-weight:600;background:var(--surface2)}.locale-overlay.svelte-1x05zx6{position:fixed;top:0;right:0;bottom:0;left:0;z-index:40}@media(max-width:480px){.card-wrap.svelte-1x05zx6{padding:28px 20px 22px;border-radius:16px}}
diff --git a/frontend/build/_app/immutable/assets/13.M6eN8M_c.css b/frontend/build/_app/immutable/assets/13.M6eN8M_c.css
new file mode 100644
index 0000000000000000000000000000000000000000..7ac861e6bcaa8209ccc84c98fb15604b9a27a007
--- /dev/null
+++ b/frontend/build/_app/immutable/assets/13.M6eN8M_c.css
@@ -0,0 +1 @@
+.nb-root.svelte-t5mrr1{display:flex;height:100%;background:var(--bg);overflow:hidden}.nb-root.sb-dragging.svelte-t5mrr1{cursor:col-resize;-webkit-user-select:none;-moz-user-select:none;user-select:none}.nb-sidebar.svelte-t5mrr1{position:relative;display:flex;flex-direction:column;background:var(--surface);border-right:1px solid var(--border);flex-shrink:0;height:100%;overflow:hidden;transition:none}.nb-sb-header.svelte-t5mrr1{display:flex;align-items:center;padding:12px 14px;border-bottom:1px solid var(--border);flex-shrink:0}.nb-sb-label.svelte-t5mrr1{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.12em;color:var(--text3)}.nb-sb-body.svelte-t5mrr1{flex:1;overflow-y:auto;padding:10px 8px}.nb-sb-body.svelte-t5mrr1::-webkit-scrollbar{width:4px}.nb-sb-body.svelte-t5mrr1::-webkit-scrollbar-thumb{background:var(--surface3);border-radius:2px}.nb-new-form.svelte-t5mrr1{display:flex;flex-direction:column;gap:6px;padding:2px 4px 12px}.nb-input.svelte-t5mrr1{background:var(--surface2);border:1px solid var(--border);border-radius:6px;padding:6px 10px;font-size:12px;color:var(--text);outline:none;width:100%;font-family:inherit}.nb-input.svelte-t5mrr1:focus{border-color:var(--accent)}.nb-select.svelte-t5mrr1{background:var(--surface2);border:1px solid var(--border);border-radius:6px;padding:5px 8px;font-size:12px;color:var(--text);outline:none;cursor:pointer;width:100%;font-family:inherit}.nb-btn-accent.svelte-t5mrr1{display:flex;align-items:center;justify-content:center;gap:5px;background:var(--accent);color:#fff;border:none;border-radius:7px;padding:7px 12px;font-size:12px;font-weight:600;cursor:pointer;width:100%;font-family:inherit}.nb-btn-accent.svelte-t5mrr1:hover{background:var(--accent-hover)}.nb-section-label.svelte-t5mrr1{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:var(--text3);padding:2px 6px 6px}.nb-loader-row.svelte-t5mrr1{display:flex;justify-content:center;padding:16px 0}.nb-empty-list.svelte-t5mrr1{font-size:12px;color:var(--text3);padding:4px 6px}.nb-list-item.svelte-t5mrr1{display:flex;align-items:center;gap:8px;width:100%;padding:7px 8px;border-radius:7px;border:1px solid transparent;cursor:pointer;background:none;text-align:left;color:var(--text2);margin-bottom:2px;font-family:inherit}.nb-list-item.svelte-t5mrr1:hover{background:var(--surface2);color:var(--text)}.nb-list-item.active.svelte-t5mrr1{background:#d9744914;border-color:#d974492e;color:var(--accent)}.nb-list-icon.svelte-t5mrr1{width:14px;height:14px;flex-shrink:0}.nb-list-text.svelte-t5mrr1{display:flex;flex-direction:column;min-width:0}.nb-list-title.svelte-t5mrr1{font-size:12px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.nb-list-meta.svelte-t5mrr1{font-size:10px;color:var(--text3);margin-top:2px;display:flex;align-items:center;gap:4px}.nb-list-dot.svelte-t5mrr1{width:6px;height:6px;border-radius:50%;flex-shrink:0}.nb-sb-resizer.svelte-t5mrr1{position:absolute;top:0;right:0;width:8px;height:100%;cursor:col-resize;z-index:10;display:flex;align-items:center;justify-content:flex-end}.nb-sb-resizer-pill.svelte-t5mrr1{width:3px;height:40px;border-radius:2px;background:transparent;transition:background .2s;margin-right:1px}.nb-sb-resizer.svelte-t5mrr1:hover .nb-sb-resizer-pill:where(.svelte-t5mrr1),.nb-sb-resizer.active.svelte-t5mrr1 .nb-sb-resizer-pill:where(.svelte-t5mrr1){background:var(--accent)}.nb-main.svelte-t5mrr1{flex:1;min-width:0;overflow-y:auto;background:var(--bg);display:flex;flex-direction:column}.nb-empty-state.svelte-t5mrr1{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;color:var(--text3)}.nb-empty-state.svelte-t5mrr1 p:where(.svelte-t5mrr1){font-size:13px}.nb-toolbar.svelte-t5mrr1{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 24px;border-bottom:1px solid var(--border);background:var(--surface);position:sticky;top:0;z-index:5;flex-shrink:0}.nb-toolbar-left.svelte-t5mrr1{display:flex;align-items:center;gap:10px}.nb-toolbar-right.svelte-t5mrr1{display:flex;align-items:center;gap:8px}.nb-nb-title.svelte-t5mrr1{font-size:14px;font-weight:600;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:280px}.nb-lang-badge.svelte-t5mrr1{display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:500;border:1px solid;border-radius:20px;padding:2px 9px;opacity:.85}.nb-lang-dot.svelte-t5mrr1{width:7px;height:7px;border-radius:50%;flex-shrink:0}.nb-btn-ghost.svelte-t5mrr1{display:flex;align-items:center;gap:5px;background:var(--surface2);border:1px solid var(--border);border-radius:6px;padding:5px 12px;font-size:12px;font-weight:500;color:var(--text2);cursor:pointer;font-family:inherit}.nb-btn-ghost.svelte-t5mrr1:hover{background:var(--surface3);color:var(--text)}.nb-cells.svelte-t5mrr1{padding:16px 24px 48px;max-width:880px;width:100%;margin:0 auto;display:flex;flex-direction:column;gap:6px}.nb-no-cells.svelte-t5mrr1{text-align:center;color:var(--text3);font-size:13px;padding:28px;background:var(--surface);border:1px dashed var(--border);border-radius:8px;margin-bottom:6px}.nb-cell.svelte-t5mrr1{border:1px solid transparent;border-radius:8px;overflow:hidden;background:var(--surface);cursor:text}.nb-cell.svelte-t5mrr1:hover{border-color:var(--border)}.nb-cell.nb-cell-active.svelte-t5mrr1{border-color:#d9744973}.nb-cell.nb-cell-running.svelte-t5mrr1{border-color:#d97449a6;animation:svelte-t5mrr1-cell-pulse 1.4s ease-in-out infinite}@keyframes svelte-t5mrr1-cell-pulse{0%,to{box-shadow:0 0 #d9744900}50%{box-shadow:0 0 0 4px #d974491f}}.nb-cell-hdr.svelte-t5mrr1{display:flex;align-items:center;gap:5px;padding:5px 10px;border-bottom:1px solid var(--border);background:var(--surface2);min-height:32px}.nb-grip.svelte-t5mrr1{color:var(--text3);cursor:grab;flex-shrink:0;opacity:.45}.nb-exec.svelte-t5mrr1{font-size:10px;font-family:Courier New,monospace;color:var(--text3);width:32px;text-align:right;flex-shrink:0}.nb-exec-spin.svelte-t5mrr1{animation:svelte-t5mrr1-spin-blink .6s step-end infinite}@keyframes svelte-t5mrr1-spin-blink{0%,to{opacity:1}50%{opacity:0}}.nb-lang-sel.svelte-t5mrr1{background:var(--surface3);border:1px solid var(--border);border-radius:4px;padding:2px 6px;font-size:11px;color:var(--text2);outline:none;cursor:pointer;font-family:inherit}.nb-lang-pip.svelte-t5mrr1{width:8px;height:8px;border-radius:50%;flex-shrink:0}.nb-spacer.svelte-t5mrr1{flex:1}.nb-cell-actions.svelte-t5mrr1{display:flex;align-items:center;gap:1px;opacity:0;transition:opacity .15s}.nb-cell.svelte-t5mrr1:hover .nb-cell-actions:where(.svelte-t5mrr1),.nb-cell.nb-cell-active.svelte-t5mrr1 .nb-cell-actions:where(.svelte-t5mrr1){opacity:1}.nb-act.svelte-t5mrr1{width:26px;height:26px;border:none;background:none;border-radius:5px;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--text3);padding:0}.nb-act.svelte-t5mrr1:hover{background:var(--surface3);color:var(--text)}.nb-act.svelte-t5mrr1:disabled{opacity:.35;cursor:not-allowed}.nb-run.svelte-t5mrr1{color:var(--success)}.nb-run.svelte-t5mrr1:hover{background:var(--success-bg);color:var(--success)}.nb-del.svelte-t5mrr1:hover{background:var(--error-bg);color:var(--error)}.nb-editor.svelte-t5mrr1{width:100%;background:transparent;border:none;outline:none;resize:none;font-size:13px;line-height:1.65;color:var(--text);padding:12px 16px;min-height:80px;display:block;overflow:hidden;font-family:inherit}.nb-code-editor.svelte-t5mrr1{background:var(--code-bg);font-family:Courier New,JetBrains Mono,Fira Code,monospace;font-size:13px;-moz-tab-size:4;-o-tab-size:4;tab-size:4;caret-color:var(--accent)}.nb-md-editor.svelte-t5mrr1{background:var(--surface2);font-family:inherit}.nb-md-preview.svelte-t5mrr1{padding:12px 20px;cursor:pointer;min-height:44px;font-size:14px;line-height:1.75;color:var(--text2)}.nb-md-preview.svelte-t5mrr1:hover{background:#00000004}.nb-md-preview p{margin:.3em 0}.nb-md-preview .md-h1{font-size:1.5em;font-weight:700;color:var(--text);margin:.6em 0 .3em}.nb-md-preview .md-h2{font-size:1.25em;font-weight:600;color:var(--text);margin:.5em 0 .25em}.nb-md-preview .md-h3{font-size:1.1em;font-weight:600;color:var(--text);margin:.4em 0 .2em}.nb-md-preview .md-pre{background:var(--code-bg);padding:10px 14px;border-radius:6px;overflow-x:auto;margin:.5em 0;font-family:Courier New,monospace;font-size:12.5px}.nb-md-preview .md-code{background:var(--code-bg);padding:1px 5px;border-radius:3px;font-family:Courier New,monospace;font-size:.88em}.nb-md-preview .md-li{margin-left:1.5em;display:list-item;list-style-type:disc}.nb-md-preview .md-oli{list-style-type:decimal}.nb-output.svelte-t5mrr1{border-top:1px solid var(--border);padding:10px 16px;background:var(--bg);display:flex;flex-direction:column;gap:4px}.nb-out.svelte-t5mrr1{font-family:Courier New,monospace;font-size:12.5px;line-height:1.55;white-space:pre-wrap;word-break:break-all;margin:0}.nb-out-stream.svelte-t5mrr1{color:var(--text)}.nb-out-err.svelte-t5mrr1{color:var(--error)}.nb-out-result.svelte-t5mrr1{color:var(--text2)}.nb-out-error-block.svelte-t5mrr1{display:flex;flex-direction:column;gap:2px}.nb-err-name.svelte-t5mrr1{font-size:11px;font-weight:700;color:var(--error);font-family:Courier New,monospace}.nb-add-row.svelte-t5mrr1{display:flex;justify-content:center;gap:8px;padding:16px 0 4px}.nb-add-btn.svelte-t5mrr1{display:inline-flex;align-items:center;gap:5px;padding:6px 16px;border:1px dashed var(--border);background:none;border-radius:7px;font-size:12px;font-weight:500;color:var(--text3);cursor:pointer;font-family:inherit}.nb-add-btn.svelte-t5mrr1:hover{border-color:var(--accent);color:var(--accent)}.nb-add-md.svelte-t5mrr1:hover{border-color:#7c6ff7;color:#7c6ff7}
diff --git a/frontend/build/_app/immutable/assets/2.DfxUCL9T.css b/frontend/build/_app/immutable/assets/2.DfxUCL9T.css
new file mode 100644
index 0000000000000000000000000000000000000000..65eedc7c3aa39f6ae5cb2e6f694a52546f7d6b50
--- /dev/null
+++ b/frontend/build/_app/immutable/assets/2.DfxUCL9T.css
@@ -0,0 +1 @@
+.morph-overlay.svelte-5m2nwc{position:fixed;top:0;right:0;bottom:0;left:0;z-index:10000;background:#0e0d0c;display:flex;align-items:center;justify-content:center;transition:opacity .4s ease}.morph-overlay.hidden.svelte-5m2nwc{pointer-events:none}.morph-canvas.svelte-5m2nwc{width:100%;height:100%;position:absolute;top:0;right:0;bottom:0;left:0}.morph-loader.svelte-5m2nwc{position:absolute;z-index:2;display:flex;align-items:center;justify-content:center}.morph-skip.svelte-5m2nwc{position:absolute;bottom:5%;right:4%;z-index:3;font-size:12px;color:#fff3;background:none;border:none;cursor:pointer;letter-spacing:.08em;font-family:inherit;padding:4px 8px;border-radius:4px;transition:color .2s,background .2s}.morph-skip.svelte-5m2nwc:hover{color:#ffffff80;background:#ffffff0d}
diff --git a/frontend/build/_app/immutable/assets/5.Bb_sFVPM.css b/frontend/build/_app/immutable/assets/5.Bb_sFVPM.css
new file mode 100644
index 0000000000000000000000000000000000000000..b463b39e93f6603e496352afe09f92af11dfe0db
--- /dev/null
+++ b/frontend/build/_app/immutable/assets/5.Bb_sFVPM.css
@@ -0,0 +1 @@
+.chat-root.svelte-23dtxz{display:flex;height:100%;background:var(--bg);overflow:hidden}.chat-sidebar.svelte-23dtxz{width:220px;flex-shrink:0;background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;height:100%}.chat-sb-top.svelte-23dtxz{padding:12px;border-bottom:1px solid var(--border);flex-shrink:0}.chat-new-btn.svelte-23dtxz{display:flex;align-items:center;justify-content:center;gap:6px;width:100%;background:var(--accent);color:#fff;border:none;border-radius:7px;padding:8px 14px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit}.chat-new-btn.svelte-23dtxz:hover{background:var(--accent-hover)}.chat-conv-list.svelte-23dtxz{flex:1;overflow-y:auto;padding:8px;display:flex;flex-direction:column;gap:2px}.chat-conv-list.svelte-23dtxz::-webkit-scrollbar{width:4px}.chat-conv-list.svelte-23dtxz::-webkit-scrollbar-thumb{background:var(--surface3);border-radius:2px}.chat-conv-item.svelte-23dtxz{width:100%;text-align:left;padding:8px 10px;border-radius:7px;border:none;cursor:pointer;background:none;font-size:12px;font-weight:500;color:var(--text2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:inherit}.chat-conv-item.svelte-23dtxz:hover{background:var(--surface2);color:var(--text)}.chat-conv-item.active.svelte-23dtxz{background:#d974491a;color:var(--accent);border:1px solid rgba(217,116,73,.18)}.chat-conv-empty.svelte-23dtxz{font-size:11px;color:var(--text3);text-align:center;padding:16px 8px}.chat-main.svelte-23dtxz{flex:1;min-width:0;display:flex;flex-direction:column;height:100%}.chat-topbar.svelte-23dtxz{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 20px;border-bottom:1px solid var(--border);background:var(--surface);flex-shrink:0}.chat-conv-title.svelte-23dtxz{font-size:13px;font-weight:600;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.chat-model-row.svelte-23dtxz{display:flex;align-items:center;gap:6px}.chat-model-label.svelte-23dtxz{font-size:11px;color:var(--text3)}.chat-model-sel.svelte-23dtxz{background:var(--surface2);border:1px solid var(--border);border-radius:6px;padding:4px 8px;font-size:11px;color:var(--text);outline:none;cursor:pointer;font-family:inherit}.chat-messages.svelte-23dtxz{flex:1;overflow-y:auto;padding:20px 24px;display:flex;flex-direction:column;gap:16px}.chat-messages.svelte-23dtxz::-webkit-scrollbar{width:5px}.chat-messages.svelte-23dtxz::-webkit-scrollbar-thumb{background:var(--surface3);border-radius:3px}.chat-empty.svelte-23dtxz{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;text-align:center;padding:32px;margin:auto}.chat-empty-icon.svelte-23dtxz{width:56px;height:56px;border-radius:16px;background:var(--surface2);display:flex;align-items:center;justify-content:center;color:var(--text3);margin-bottom:4px}.chat-empty-title.svelte-23dtxz{font-size:16px;font-weight:600;color:var(--text)}.chat-empty-hint.svelte-23dtxz{font-size:13px;color:var(--text3);max-width:340px}.chat-suggestions.svelte-23dtxz{display:grid;grid-template-columns:1fr 1fr;gap:8px;width:100%;max-width:480px;margin-top:8px}.chat-suggest-btn.svelte-23dtxz{text-align:left;padding:10px 12px;border-radius:10px;border:1px solid var(--border);background:var(--surface);font-size:12px;color:var(--text2);cursor:pointer;line-height:1.4;font-family:inherit}.chat-suggest-btn.svelte-23dtxz:hover{border-color:var(--accent);color:var(--text);background:var(--surface2)}.chat-input-area.svelte-23dtxz{border-top:1px solid var(--border);background:var(--surface);padding:14px 20px;flex-shrink:0}.chat-input-box.svelte-23dtxz{display:flex;align-items:flex-end;gap:10px;background:var(--surface2);border:1px solid var(--border);border-radius:12px;padding:10px 12px 10px 16px}.chat-input-box.svelte-23dtxz:focus-within{border-color:var(--accent)}.chat-textarea.svelte-23dtxz{flex:1;background:transparent;border:none;outline:none;resize:none;font-size:14px;line-height:1.5;color:var(--text);min-height:1.5rem;max-height:200px;font-family:inherit}.chat-textarea.svelte-23dtxz::-moz-placeholder{color:var(--text3)}.chat-textarea.svelte-23dtxz::placeholder{color:var(--text3)}.chat-send-btn.svelte-23dtxz{flex-shrink:0;width:34px;height:34px;border-radius:8px;border:none;background:var(--accent);color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center}.chat-send-btn.svelte-23dtxz:hover:not(:disabled){background:var(--accent-hover)}.chat-send-btn.svelte-23dtxz:disabled{opacity:.4;cursor:not-allowed}.chat-footer-note.svelte-23dtxz{font-size:11px;color:var(--text3);text-align:center;margin-top:8px}
diff --git a/frontend/build/_app/immutable/assets/8.D2JiE0Gd.css b/frontend/build/_app/immutable/assets/8.D2JiE0Gd.css
new file mode 100644
index 0000000000000000000000000000000000000000..91e961f3b244d04ceebecd071742df4f36dc0ab6
--- /dev/null
+++ b/frontend/build/_app/immutable/assets/8.D2JiE0Gd.css
@@ -0,0 +1 @@
+.dash.svelte-x1i5gj{padding:24px;max-width:1100px;margin:0 auto;display:flex;flex-direction:column;gap:20px}.dash-header.svelte-x1i5gj{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}.dash-title.svelte-x1i5gj{font-size:22px;font-weight:700;color:var(--text);line-height:1.2}.dash-sub.svelte-x1i5gj{font-size:13px;color:var(--text3);margin-top:4px}.dash-sub.svelte-x1i5gj strong:where(.svelte-x1i5gj){color:var(--text2);font-weight:600}.role-badge.svelte-x1i5gj{display:inline-block;padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;text-transform:capitalize;letter-spacing:.02em}.role-admin.svelte-x1i5gj{background:var(--error-bg);color:var(--error)}.role-faculty.svelte-x1i5gj{background:var(--warning-bg);color:var(--warning)}.role-student.svelte-x1i5gj{background:#d974491f;color:var(--accent)}.dash-header-actions.svelte-x1i5gj{display:flex;align-items:center;gap:12px;flex-shrink:0}.dash-date.svelte-x1i5gj{display:flex;align-items:center;gap:5px;font-size:12px;color:var(--text3);background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:6px 10px;white-space:nowrap}.dash-loading.svelte-x1i5gj{display:flex;flex-direction:column;gap:20px}.skeleton.svelte-x1i5gj{height:96px;background:var(--surface2)!important;animation:svelte-x1i5gj-pulse 1.5s ease-in-out infinite}@keyframes svelte-x1i5gj-pulse{0%,to{opacity:1}50%{opacity:.5}}.dash-error.svelte-x1i5gj{display:flex;align-items:center;gap:8px;padding:14px 16px;background:var(--error-bg);border:1px solid var(--error);border-radius:10px;color:var(--error);font-size:13px}.stat-grid.svelte-x1i5gj{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}@media(max-width:900px){.stat-grid.svelte-x1i5gj{grid-template-columns:repeat(2,1fr)}}@media(max-width:500px){.stat-grid.svelte-x1i5gj{grid-template-columns:1fr}}.stat-card.svelte-x1i5gj{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:16px;display:flex;align-items:flex-start;gap:12px;box-shadow:var(--shadow)}.stat-icon-wrap.svelte-x1i5gj{width:38px;height:38px;border-radius:10px;background:var(--surface2);border:1px solid var(--border);display:flex;align-items:center;justify-content:center;color:var(--text3);flex-shrink:0}.accent-icon.svelte-x1i5gj{background:#d974491f;border-color:#d9744933;color:var(--accent)}.stat-body.svelte-x1i5gj{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.stat-label.svelte-x1i5gj{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text3)}.stat-value.svelte-x1i5gj{font-size:24px;font-weight:700;color:var(--text);line-height:1.1}.stat-bar-track.svelte-x1i5gj{height:3px;background:var(--surface3);border-radius:2px;overflow:hidden;margin-top:6px}.stat-bar-fill.svelte-x1i5gj{height:100%;border-radius:2px;transition:width .6s ease}.stat-sub.svelte-x1i5gj{font-size:11px;color:var(--text3);margin-top:2px}.section-card.svelte-x1i5gj{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:18px 20px;box-shadow:var(--shadow)}.section-header.svelte-x1i5gj{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;gap:8px}.section-title.svelte-x1i5gj{display:flex;align-items:center;gap:6px;font-size:13px;font-weight:600;color:var(--text)}.section-sub.svelte-x1i5gj{font-size:11px;color:var(--text3)}.quota-rings.svelte-x1i5gj{display:flex;gap:32px;flex-wrap:wrap}.quota-ring-item.svelte-x1i5gj{display:flex;align-items:center;gap:14px}.quota-info.svelte-x1i5gj{display:flex;flex-direction:column;gap:3px}.quota-label.svelte-x1i5gj{font-size:13px;font-weight:500;color:var(--text2)}.quota-used.svelte-x1i5gj{font-size:20px;font-weight:700;color:var(--text)}.quota-limit.svelte-x1i5gj{font-size:12px;font-weight:400;color:var(--text3)}.two-col.svelte-x1i5gj{display:grid;grid-template-columns:1fr 1fr;gap:14px}@media(max-width:780px){.two-col.svelte-x1i5gj{grid-template-columns:1fr}}.heatmap-wrap.svelte-x1i5gj{display:flex;flex-direction:column;gap:8px}.heatmap-grid.svelte-x1i5gj{display:grid;grid-template-columns:repeat(26,1fr);grid-template-rows:repeat(7,1fr);gap:2px}.hm-cell.svelte-x1i5gj{width:100%;padding-bottom:100%;border-radius:2px;cursor:default;min-width:8px;min-height:8px}.heatmap-legend.svelte-x1i5gj{display:flex;align-items:center;gap:3px}.legend-label.svelte-x1i5gj{font-size:10px;color:var(--text3)}.heatmap-empty.svelte-x1i5gj{display:flex;flex-direction:column;align-items:center;gap:4px;padding:16px;color:var(--text3);font-size:12px;text-align:center}.heatmap-empty.svelte-x1i5gj p:where(.svelte-x1i5gj){font-weight:500;color:var(--text2);margin:0}.heatmap-empty.svelte-x1i5gj span:where(.svelte-x1i5gj){font-size:11px}.model-dist.svelte-x1i5gj{display:flex;flex-direction:column;gap:12px}.model-row.svelte-x1i5gj{display:flex;flex-direction:column;gap:4px}.model-row-top.svelte-x1i5gj{display:flex;justify-content:space-between;align-items:center}.model-name.svelte-x1i5gj{font-size:12px;color:var(--text2);font-family:Fira Code,JetBrains Mono,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:70%}.model-pct.svelte-x1i5gj{font-size:11px;color:var(--text3);flex-shrink:0}.model-bar-track.svelte-x1i5gj{height:5px;background:var(--surface3);border-radius:3px;overflow:hidden}.model-bar-fill.svelte-x1i5gj{height:100%;border-radius:3px;transition:width .6s ease}.table-wrap.svelte-x1i5gj{overflow-x:auto}.activity-table.svelte-x1i5gj{width:100%;border-collapse:collapse;font-size:13px}.activity-table.svelte-x1i5gj thead:where(.svelte-x1i5gj) tr:where(.svelte-x1i5gj){border-bottom:1px solid var(--border)}.activity-table.svelte-x1i5gj th:where(.svelte-x1i5gj){text-align:left;padding:6px 12px 8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text3);white-space:nowrap}.activity-table.svelte-x1i5gj th.num:where(.svelte-x1i5gj){text-align:right}.activity-table.svelte-x1i5gj tbody:where(.svelte-x1i5gj) tr:where(.svelte-x1i5gj){border-bottom:1px solid var(--border)}.activity-table.svelte-x1i5gj tbody:where(.svelte-x1i5gj) tr:where(.svelte-x1i5gj):last-child{border-bottom:none}.activity-table.svelte-x1i5gj tbody:where(.svelte-x1i5gj) tr:where(.svelte-x1i5gj):hover{background:var(--surface2)}.activity-table.svelte-x1i5gj td:where(.svelte-x1i5gj){padding:9px 12px;color:var(--text2);white-space:nowrap}.activity-table.svelte-x1i5gj td.num:where(.svelte-x1i5gj){text-align:right;color:var(--text3)}.activity-table.svelte-x1i5gj td.muted:where(.svelte-x1i5gj){color:var(--text3);font-size:12px}.model-tag.svelte-x1i5gj{display:inline-block;font-family:Fira Code,JetBrains Mono,monospace;font-size:11px;background:var(--surface2);border:1px solid var(--border);border-radius:5px;padding:2px 6px;color:var(--text2);max-width:200px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.empty-hint.svelte-x1i5gj{font-size:13px;color:var(--text3);text-align:center;padding:24px 0 8px}
diff --git a/frontend/build/_app/immutable/assets/Loader.CSywfDIO.css b/frontend/build/_app/immutable/assets/Loader.CSywfDIO.css
new file mode 100644
index 0000000000000000000000000000000000000000..375a1e763e57baf1ec56d90d1f96bffc71176b76
--- /dev/null
+++ b/frontend/build/_app/immutable/assets/Loader.CSywfDIO.css
@@ -0,0 +1 @@
+.mac-loader.svelte-v1tg6x{display:inline-flex;align-items:center;justify-content:center}.hex-sweep.svelte-v1tg6x{transform-origin:50% 50%;animation:svelte-v1tg6x-hexSweep 2.4s linear infinite}@keyframes svelte-v1tg6x-hexSweep{0%{stroke-dashoffset:0}to{stroke-dashoffset:calc(var(--perim) * -1px)}}.spoke-pulse.svelte-v1tg6x{animation:svelte-v1tg6x-spokePulse 1.8s ease-in-out infinite}@keyframes svelte-v1tg6x-spokePulse{0%{stroke-dashoffset:0;stroke-opacity:0}10%{stroke-opacity:.9}80%{stroke-dashoffset:calc(var(--spoke-len) * -1px);stroke-opacity:0}to{stroke-dashoffset:calc(var(--spoke-len) * -1px);stroke-opacity:0}}.node-glow.svelte-v1tg6x{animation:svelte-v1tg6x-nodeGlow 1.8s ease-in-out infinite}@keyframes svelte-v1tg6x-nodeGlow{0%,to{fill-opacity:0;r:0}40%{fill-opacity:.18}60%{fill-opacity:.08}}.node-dot.svelte-v1tg6x{animation:svelte-v1tg6x-nodeDot 1.8s ease-in-out infinite}@keyframes svelte-v1tg6x-nodeDot{0%,to{fill-opacity:.25}50%{fill-opacity:1}}.center-arc.svelte-v1tg6x{transform-origin:50% 50%;animation:svelte-v1tg6x-centerSpin 1.2s linear infinite}@keyframes svelte-v1tg6x-centerSpin{0%{transform:rotate(-90deg)}to{transform:rotate(270deg)}}.center-core.svelte-v1tg6x{animation:svelte-v1tg6x-corePulse 1.8s ease-in-out infinite}@keyframes svelte-v1tg6x-corePulse{0%,to{fill-opacity:.7;transform:scale(1)}50%{fill-opacity:1;transform:scale(1.15)}}
diff --git a/frontend/build/_app/immutable/chunks/B8pdRQVM.js b/frontend/build/_app/immutable/chunks/B8pdRQVM.js
new file mode 100644
index 0000000000000000000000000000000000000000..418c71cc16910d9382aefee47be1b5b88a90c92d
--- /dev/null
+++ b/frontend/build/_app/immutable/chunks/B8pdRQVM.js
@@ -0,0 +1 @@
+import{U as l,aq as c,ae as f,m as b,K as o,ar as d,as as p,g,n as _}from"./CPYeCQyA.js";let s=!1,i=Symbol();function y(e,n,r){const u=r[n]??(r[n]={store:null,source:b(void 0),unsubscribe:f});if(o&&(u.source.label=n),u.store!==e&&!(i in r))if(u.unsubscribe(),u.store=e??null,e==null)u.source.v=void 0,u.unsubscribe=f;else{var a=!0;u.unsubscribe=d(e,t=>{a?u.source.v=t:_(u.source,t)}),a=!1}return e&&i in r?p(e):g(u.source)}function U(){const e={};function n(){l(()=>{for(var r in e)e[r].unsubscribe();c(e,i,{enumerable:!1,value:!0})})}return[e,n]}function D(e){var n=s;try{return s=!1,[e(),s]}finally{s=n}}export{y as a,D as c,U as s};
diff --git a/frontend/build/_app/immutable/chunks/BIHI7g3E.js b/frontend/build/_app/immutable/chunks/BIHI7g3E.js
new file mode 100644
index 0000000000000000000000000000000000000000..b480ffe6ce7040f68468da9d453d371cbc190177
--- /dev/null
+++ b/frontend/build/_app/immutable/chunks/BIHI7g3E.js
@@ -0,0 +1 @@
+const e={};export{e as default};
diff --git a/frontend/build/_app/immutable/chunks/BOGzIfIj.js b/frontend/build/_app/immutable/chunks/BOGzIfIj.js
new file mode 100644
index 0000000000000000000000000000000000000000..4b875ad8664ed3c1826d415b3f8e30de1afb783d
--- /dev/null
+++ b/frontend/build/_app/immutable/chunks/BOGzIfIj.js
@@ -0,0 +1,11 @@
+var at=e=>{throw TypeError(e)};var Ht=(e,t,n)=>t.has(e)||at("Cannot "+n);var y=(e,t,n)=>(Ht(e,t,"read from private field"),n?n.call(e):t.get(e)),L=(e,t,n)=>t.has(e)?at("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,n);import{b as j,_ as Ft}from"./DmxDsgrj.js";import{K as S,ap as He,ae as ot,bm as U,g as A,n as P}from"./CPYeCQyA.js";import{t as we,s as Mt}from"./BprE2qdV.js";class Fe{constructor(t,n){this.status=t,typeof n=="string"?this.body={message:n}:n?this.body=n:this.body={message:`Error: ${t}`}}toString(){return JSON.stringify(this.body)}}class Me{constructor(t,n){try{new Headers({location:n})}catch{throw new Error(`Invalid redirect location ${JSON.stringify(n)}: this string contains characters that cannot be used in HTTP headers`)}this.status=t,this.location=n}}class ve extends Error{constructor(t,n,r){super(r),this.status=t,this.text=n}}new URL("sveltekit-internal://");function Wt(e,t){return e==="/"||t==="ignore"?e:t==="never"?e.endsWith("/")?e.slice(0,-1):e:t==="always"&&!e.endsWith("/")?e+"/":e}function Jt(e){return e.split("%25").map(decodeURI).join("%25")}function Yt(e){for(const t in e)e[t]=decodeURIComponent(e[t]);return e}function je({href:e}){return e.split("#")[0]}function I(){}function zt(...e){let t=5381;for(const n of e)if(typeof n=="string"){let r=n.length;for(;r;)t=t*33^n.charCodeAt(--r)}else if(ArrayBuffer.isView(n)){const r=new Uint8Array(n.buffer,n.byteOffset,n.byteLength);let a=r.length;for(;a;)t=t*33^r[--a]}else throw new TypeError("value must be a string or TypedArray");return(t>>>0).toString(36)}new TextEncoder;new TextDecoder;function Xt(e){if(!j&&globalThis.Buffer){const r=globalThis.Buffer.from(e,"base64");return new Uint8Array(r)}const t=atob(e),n=new Uint8Array(t.length);for(let r=0;r{e=new Error().stack.includes("check_stack_trace")})(),window.fetch=(n,r)=>{const a=n instanceof Request?n.url:n.toString(),o=new Error().stack.split(`
+`),s=o.findIndex(f=>f.includes("load@")||f.includes("at load")),i=o.slice(0,s+2).join(`
+`),l=e?i.includes("src/runtime/client/client.js"):Qt,c=r==null?void 0:r.__sveltekit_fetch__;return l&&!c&&console.warn(`Loading ${a} using \`window.fetch\`. For best results, use the \`fetch\` that is passed to your \`load\` function: https://svelte.dev/docs/kit/load#making-fetch-requests`),(n instanceof Request?n.method:(r==null?void 0:r.method)||"GET")!=="GET"&&M.delete(ye(n)),st(n,r)}}else j&&(window.fetch=(e,t)=>((e instanceof Request?e.method:(t==null?void 0:t.method)||"GET")!=="GET"&&M.delete(ye(e)),st(e,t)));const M=new Map;function Zt(e,t){const n=ye(e,t),r=document.querySelector(n);if(r!=null&&r.textContent){r.remove();let{body:a,...o}=JSON.parse(r.textContent);const s=r.getAttribute("data-ttl");return s&&M.set(n,{body:a,init:o,ttl:1e3*Number(s)}),r.getAttribute("data-b64")!==null&&(a=Xt(a)),Promise.resolve(new Response(a,o))}return S?kt(e,t):window.fetch(e,t)}function en(e,t,n){if(M.size>0){const r=ye(e,n),a=M.get(r);if(a){if(performance.now(){const a=/^\[\.\.\.(\w+)(?:=(\w+))?\]$/.exec(r);if(a)return t.push({name:a[1],matcher:a[2],optional:!1,rest:!0,chained:!0}),"(?:/([^]*))?";const o=/^\[\[(\w+)(?:=(\w+))?\]\]$/.exec(r);if(o)return t.push({name:o[1],matcher:o[2],optional:!0,rest:!1,chained:!0}),"(?:/([^/]+))?";if(!r)return;const s=r.split(/\[(.+?)\](?!\])/);return"/"+s.map((l,c)=>{if(c%2){if(l.startsWith("x+"))return Ce(String.fromCharCode(parseInt(l.slice(2),16)));if(l.startsWith("u+"))return Ce(String.fromCharCode(...l.slice(2).split("-").map(m=>parseInt(m,16))));const d=tn.exec(l);if(!j&&!d)throw new Error(`Invalid param: ${l}. Params and matcher names can only have underscores and alphanumeric characters.`);const[,f,_,g,u]=d;return t.push({name:g,matcher:u,optional:!!f,rest:!!_,chained:_?c===1&&s[0]==="":!1}),_?"([^]*?)":f?"([^/]*)?":"([^/]+?)"}return Ce(l)}).join("")}).join("")}/?$`),params:t}}function rn(e){return e!==""&&!/^\([^)]+\)$/.test(e)}function an(e){return e.slice(1).split("/").filter(rn)}function on(e,t,n){const r={},a=e.slice(1),o=a.filter(i=>i!==void 0);let s=0;for(let i=0;id).join("/"),s=0),c===void 0)if(l.rest)c="";else continue;if(!l.matcher||n[l.matcher](c)){r[l.name]=c;const d=t[i+1],f=a[i+1];d&&!d.rest&&d.optional&&f&&l.chained&&(s=0),!d&&!f&&Object.keys(r).length===o.length&&(s=0);continue}if(l.optional&&l.chained){s++;continue}return}if(!s)return r}function Ce(e){return e.normalize().replace(/[[\]]/g,"\\$&").replace(/%/g,"%25").replace(/\//g,"%2[Ff]").replace(/\?/g,"%3[Ff]").replace(/#/g,"%23").replace(/[.*+?^${}()|\\]/g,"\\$&")}function sn({nodes:e,server_loads:t,dictionary:n,matchers:r}){const a=new Set(t);return Object.entries(n).map(([i,[l,c,d]])=>{const{pattern:f,params:_}=nn(i),g={id:i,exec:u=>{const m=f.exec(u);if(m)return on(m,_,r)},errors:[1,...d||[]].map(u=>e[u]),layouts:[0,...c||[]].map(s),leaf:o(l)};return g.errors.length=g.layouts.length=Math.max(g.errors.length,g.layouts.length),g});function o(i){const l=i<0;return l&&(i=~i),[l,e[i]]}function s(i){return i===void 0?i:[a.has(i),e[i]]}}function Et(e,t=JSON.parse){try{return t(sessionStorage[e])}catch{}}function it(e,t,n=JSON.stringify){const r=n(t);try{sessionStorage[e]=r}catch{}}let D="",ln=D;var mt,wt;(wt=(mt=globalThis.process)==null?void 0:mt.versions)!=null&&wt.webcontainer;Ft(()=>import("./BIHI7g3E.js"),[],import.meta.url).then(e=>new e.AsyncLocalStorage).catch(()=>{});const cn="1777350896255",St="sveltekit:snapshot",Rt="sveltekit:scroll",xt="sveltekit:states",fn="sveltekit:pageurl",W="sveltekit:history",te="sveltekit:navigation",B={tap:1,hover:2,viewport:3,eager:4,off:-1,false:-1},Ue=j?location.origin:"";function We(e){if(e instanceof URL)return e;let t=document.baseURI;if(!t){const n=document.getElementsByTagName("base");t=n.length?n[0].href:document.URL}return new URL(e,t)}function G(){return{x:pageXOffset,y:pageYOffset}}const lt=new WeakSet,ct={"preload-code":["","off","false","tap","hover","viewport","eager"],"preload-data":["","off","false","tap","hover"],keepfocus:["","true","off","false"],noscroll:["","true","off","false"],reload:["","true","off","false"],replacestate:["","true","off","false"]};function F(e,t){const n=e.getAttribute(`data-sveltekit-${t}`);return S&&un(e,t,n),n}function un(e,t,n){n!==null&&!lt.has(e)&&!ct[t].includes(n)&&(console.error(`Unexpected value for ${t} — should be one of ${ct[t].map(r=>JSON.stringify(r)).join(", ")}`,e),lt.add(e))}const ft={...B,"":B.hover};function Tt(e){let t=e.assignedSlot??e.parentNode;return(t==null?void 0:t.nodeType)===11&&(t=t.host),t}function $t(e,t){for(;e&&e!==t;){if(e.nodeName.toUpperCase()==="A"&&e.hasAttribute("href"))return e;e=Tt(e)}}function qe(e,t,n){let r;try{if(r=new URL(e instanceof SVGAElement?e.href.baseVal:e.href,document.baseURI),n&&r.hash.match(/^#[^/]/)){const i=location.hash.split("#")[1]||"/";r.hash=`#${i}${r.hash}`}}catch{}const a=e instanceof SVGAElement?e.target.baseVal:e.target,o=!r||!!a||Ae(r,t,n)||(e.getAttribute("rel")||"").split(/\s+/).includes("external"),s=(r==null?void 0:r.origin)===Ue&&e.hasAttribute("download");return{url:r,external:o,target:a,download:s}}function be(e){let t=null,n=null,r=null,a=null,o=null,s=null,i=e;for(;i&&i!==document.documentElement;)r===null&&(r=F(i,"preload-code")),a===null&&(a=F(i,"preload-data")),t===null&&(t=F(i,"keepfocus")),n===null&&(n=F(i,"noscroll")),o===null&&(o=F(i,"reload")),s===null&&(s=F(i,"replacestate")),i=Tt(i);function l(c){switch(c){case"":case"true":return!0;case"off":case"false":return!1;default:return}}return{preload_code:ft[r??"off"],preload_data:ft[a??"off"],keepfocus:l(t),noscroll:l(n),reload:l(o),replace_state:l(s)}}function ut(e){const t=He(e);let n=!0;function r(){n=!0,t.update(s=>s)}function a(s){n=!1,t.set(s)}function o(s){let i;return t.subscribe(l=>{(i===void 0||n&&l!==i)&&s(i=l)})}return{notify:r,set:a,subscribe:o}}const Lt={v:I};function dn(){const{set:e,subscribe:t}=He(!1);if(S||!j)return{subscribe:t,check:async()=>!1};let n;async function r(){clearTimeout(n);try{const a=await fetch(`${ln}/_app/version.json`,{headers:{pragma:"no-cache","cache-control":"no-cache"}});if(!a.ok)return!1;const s=(await a.json()).version!==cn;return s&&(e(!0),Lt.v(),clearTimeout(n)),s}catch{return!1}}return{subscribe:t,check:r}}function Ae(e,t,n){return e.origin!==Ue||!e.pathname.startsWith(t)?!0:n?e.pathname!==location.pathname:!1}function Hn(e){}function hn(e){function t(n,r){if(n)for(const a in n){if(a[0]==="_"||e.has(a))continue;const o=[...e.values()],s=pn(a,r==null?void 0:r.slice(r.lastIndexOf(".")))??`valid exports are ${o.join(", ")}, or anything with a '_' prefix`;throw new Error(`Invalid export '${a}'${r?` in ${r}`:""} (${s})`)}}return t}function pn(e,t=".js"){const n=[];if(Je.has(e)&&n.push(`+layout${t}`),Ut.has(e)&&n.push(`+page${t}`),At.has(e)&&n.push(`+layout.server${t}`),gn.has(e)&&n.push(`+page.server${t}`),_n.has(e)&&n.push(`+server${t}`),n.length>0)return`'${e}' is a valid export in ${n.slice(0,-1).join(", ")}${n.length>1?" or ":""}${n.at(-1)}`}const Je=new Set(["load","prerender","csr","ssr","trailingSlash","config"]),Ut=new Set([...Je,"entries"]),At=new Set([...Je]),gn=new Set([...At,"actions","entries"]),_n=new Set(["GET","POST","PATCH","PUT","DELETE","OPTIONS","HEAD","fallback","prerender","trailingSlash","config","entries"]),mn=hn(Ut);function wn(e){return e.filter(t=>t!=null)}function Ye(e){return e instanceof Fe||e instanceof ve?e.status:500}function vn(e){return e instanceof ve?e.text:"Internal Error"}let x,ne,Ne;const yn=ot.toString().includes("$$")||/function \w+\(\) \{\}/.test(ot.toString()),dt="a:";var se,ie,le,ce,fe,ue,de,he,vt,pe,yt,ge,bt;yn?(x={data:{},form:null,error:null,params:{},route:{id:null},state:{},status:-1,url:new URL(dt)},ne={current:null},Ne={current:!1}):(x=new(vt=class{constructor(){L(this,se,U({}));L(this,ie,U(null));L(this,le,U(null));L(this,ce,U({}));L(this,fe,U({id:null}));L(this,ue,U({}));L(this,de,U(-1));L(this,he,U(new URL(dt)))}get data(){return A(y(this,se))}set data(t){P(y(this,se),t)}get form(){return A(y(this,ie))}set form(t){P(y(this,ie),t)}get error(){return A(y(this,le))}set error(t){P(y(this,le),t)}get params(){return A(y(this,ce))}set params(t){P(y(this,ce),t)}get route(){return A(y(this,fe))}set route(t){P(y(this,fe),t)}get state(){return A(y(this,ue))}set state(t){P(y(this,ue),t)}get status(){return A(y(this,de))}set status(t){P(y(this,de),t)}get url(){return A(y(this,he))}set url(t){P(y(this,he),t)}},se=new WeakMap,ie=new WeakMap,le=new WeakMap,ce=new WeakMap,fe=new WeakMap,ue=new WeakMap,de=new WeakMap,he=new WeakMap,vt),ne=new(yt=class{constructor(){L(this,pe,U(null))}get current(){return A(y(this,pe))}set current(t){P(y(this,pe),t)}},pe=new WeakMap,yt),Ne=new(bt=class{constructor(){L(this,ge,U(!1))}get current(){return A(y(this,ge))}set current(t){P(y(this,ge),t)}},ge=new WeakMap,bt),Lt.v=()=>Ne.current=!0);function bn(e){Object.assign(x,e)}const kn=new Set(["icon","shortcut icon","apple-touch-icon"]);let Q=null;const q=Et(Rt)??{},re=Et(St)??{};if(S&&j){let e=!1;const t=import.meta.url.split("?")[0],n=()=>{var s,i;if(e)return;let o=(s=new Error().stack)==null?void 0:s.split(`
+`);o&&(!o[0].includes("https:")&&!o[0].includes("http:")&&(o=o.slice(1)),o=o.slice(2),!((i=o[0])!=null&&i.includes(t))&&(e=!0,console.warn("Avoid using `history.pushState(...)` and `history.replaceState(...)` as these will conflict with SvelteKit's router. Use the `pushState` and `replaceState` imports from `$app/navigation` instead.")))},r=history.pushState;history.pushState=(...o)=>(n(),r.apply(history,o));const a=history.replaceState;history.replaceState=(...o)=>(n(),a.apply(history,o))}const N={url:ut({}),page:ut({}),navigating:He(null),updated:dn()};function ze(e){q[e]=G()}function En(e,t){let n=e+1;for(;q[n];)delete q[n],n+=1;for(n=t+1;re[n];)delete re[n],n+=1}function ae(e,t=!1){return t?location.replace(e.href):location.href=e.href,new Promise(I)}async function Pt(){if("serviceWorker"in navigator){const e=await navigator.serviceWorker.getRegistration("/");e&&await e.update()}}let Xe,Ve,ke,O,Be,k;const Ee=[],Se=[];let v=null;function Re(){var e;(e=v==null?void 0:v.fork)==null||e.then(t=>t==null?void 0:t.discard()),v=null}const me=new Map,Ot=new Set,Sn=new Set,ee=new Set;let w={branch:[],error:null,url:null},It=!1,xe=!1,ht=!0,oe=!1,Z=!1,jt=!1,Qe=!1,Ct,E,$,K;const Te=new Set,pt=new Map;async function Jn(e,t,n){var o,s,i,l;S&&t===document.body&&console.warn(`Placing %sveltekit.body% directly inside is not recommended, as your app may break for users who have certain browser extensions installed.
+
+Consider wrapping it in an element:
+
+
+ %sveltekit.body%
+
`),globalThis.__sveltekit_10sd49c&&(globalThis.__sveltekit_10sd49c.query,globalThis.__sveltekit_10sd49c.prerender),document.URL!==location.href&&(location.href=location.href),k=e,await((s=(o=e.hooks).init)==null?void 0:s.call(o)),Xe=sn(e),O=document.documentElement,Be=t,Ve=e.nodes[0],ke=e.nodes[1],Ve(),ke(),E=(i=history.state)==null?void 0:i[W],$=(l=history.state)==null?void 0:l[te],E||(E=$=Date.now(),history.replaceState({...history.state,[W]:E,[te]:$},""));const r=q[E];function a(){r&&(history.scrollRestoration="manual",scrollTo(r.x,r.y))}n?(a(),await Cn(Be,n)):(await J({type:"enter",url:We(k.hash?qn(new URL(location.href)):location.href),replace_state:!0}),a()),jn()}function Rn(){Ee.length=0,Qe=!1}function Nt(e){Se.some(t=>t==null?void 0:t.snapshot)&&(re[e]=Se.map(t=>{var n;return(n=t==null?void 0:t.snapshot)==null?void 0:n.capture()}))}function Dt(e){var t;(t=re[e])==null||t.forEach((n,r)=>{var a,o;(o=(a=Se[r])==null?void 0:a.snapshot)==null||o.restore(n)})}function gt(){ze(E),it(Rt,q),Nt($),it(St,re)}async function qt(e,t,n,r){let a;t.invalidateAll&&Re(),await J({type:"goto",url:We(e),keepfocus:t.keepFocus,noscroll:t.noScroll,replace_state:t.replaceState,state:t.state,redirect_count:n,nav_token:r,accept:()=>{t.invalidateAll&&(Qe=!0,a=[],pt.forEach((o,s)=>{for(const i of o.keys())a.push(s+"/"+i)})),t.invalidate&&t.invalidate.forEach(In)}}),t.invalidateAll&&we().then(we).then(()=>{pt.forEach((o,s)=>{o.forEach(({resource:i},l)=>{var c;a!=null&&a.includes(s+"/"+l)&&((c=i.refresh)==null||c.call(i))})})})}async function _t(e){if(e.id!==(v==null?void 0:v.id)){Re();const t={};Te.add(t),v={id:e.id,token:t,promise:Bt({...e,preload:t}).then(n=>(Te.delete(t),n.type==="loaded"&&n.state.error&&Re(),n)),fork:null}}return v.promise}async function De(e){var n;const t=(n=await Pe(e,!1))==null?void 0:n.route;t&&await Promise.all([...t.layouts,t.leaf].filter(Boolean).map(r=>r[1]()))}async function Vt(e,t,n){var o;if(S&&e.state.error&&document.querySelector("vite-error-overlay"))return;const r={params:w.params,route:{id:((o=w.route)==null?void 0:o.id)??null},url:new URL(location.href)};w={...e.state,nav:r};const a=document.querySelector("style[data-sveltekit]");if(a&&a.remove(),Object.assign(x,e.props.page),Ct=new k.root({target:t,props:{...e.props,stores:N,components:Se},hydrate:n,sync:!1,transformError:void 0}),await Promise.resolve(),Dt($),n){const s={from:null,to:{...r,scroll:q[E]??G()},willUnload:!1,type:"enter",complete:Promise.resolve()};ee.forEach(i=>i(s))}xe=!0}async function $e({url:e,params:t,branch:n,errors:r,status:a,error:o,route:s,form:i}){let l="never";for(const u of n)(u==null?void 0:u.slash)!==void 0&&(l=u.slash);e.pathname=Wt(e.pathname,l),e.search=e.search;const c={type:"loaded",state:{url:e,params:t,branch:n,error:o,route:s},props:{constructors:wn(n).map(u=>u.node.component),page:rt(x)}};i!==void 0&&(c.props.form=i);let d={},f=!x,_=0;for(let u=0;u_!=="load");if(f.length>0)throw new Error(`Page options are ignored when \`router.type === 'hash'\` (${a.id} has ${f.filter(_=>_!=="load").map(_=>`'${_}'`).join(", ")})`)}return{node:l,loader:e,server:o,universal:(c=l.universal)!=null&&c.load?{type:"data",data:s,uses:i}:null,data:s??(o==null?void 0:o.data)??null,slash:((d=l.universal)==null?void 0:d.trailingSlash)??(o==null?void 0:o.slash)}}function xn(e,t,n){let r=e instanceof Request?e.url:e;const a=new URL(r,n);a.origin===n.origin&&(r=a.href.slice(n.origin.length));const o=xe?en(r,a.href,t):Zt(r,t);return{resolved:a,promise:o}}function Tn(e,t,n,r,a,o){if(Qe)return!0;if(!a)return!1;if(a.parent&&e||a.route&&t||a.url&&n)return!0;for(const s of a.search_params)if(r.has(s))return!0;for(const s of a.params)if(o[s]!==w.params[s])return!0;for(const s of a.dependencies)if(Ee.some(i=>i(new URL(s))))return!0;return!1}function et(e,t){return(e==null?void 0:e.type)==="data"?e:(e==null?void 0:e.type)==="skip"?t??null:null}function $n(e,t){if(!e)return new Set(t.searchParams.keys());const n=new Set([...e.searchParams.keys(),...t.searchParams.keys()]);for(const r of n){const a=e.searchParams.getAll(r),o=t.searchParams.getAll(r);a.every(s=>o.includes(s))&&o.every(s=>a.includes(s))&&n.delete(r)}return n}function Ln({error:e,url:t,route:n,params:r}){return{type:"loaded",state:{error:e,url:t,route:n,params:r,branch:[]},props:{page:rt(x),constructors:[]}}}async function Bt({id:e,invalidating:t,url:n,params:r,route:a,preload:o}){if((v==null?void 0:v.id)===e)return Te.delete(v.token),v.promise;const{errors:s,layouts:i,leaf:l}=a,c=[...i,l];s.forEach(p=>p==null?void 0:p().catch(I)),c.forEach(p=>p==null?void 0:p[1]().catch(I));const d=w.url?e!==Le(w.url):!1,f=w.route?a.id!==w.route.id:!1,_=$n(w.url,n);let g=!1;const u=c.map(async(p,h)=>{var C;if(!p)return;const b=w.branch[h];return p[1]===(b==null?void 0:b.loader)&&!Tn(g,f,d,_,(C=b.universal)==null?void 0:C.uses,r)?b:(g=!0,Ze({loader:p[1],url:n,params:r,route:a,parent:async()=>{var _e;const V={};for(let H=0;HPromise.resolve({}),server_data_node:et(o)}),i={node:await ke(),loader:ke,universal:null,server:null,data:null};return $e({url:n,params:a,branch:[s,i],status:e,error:t,errors:[],route:null})}catch(s){if(s instanceof Me)return qt(new URL(s.location,location.href),{},0);throw s}}async function An(e){const t=e.href;if(me.has(t))return me.get(t);let n;try{const r=(async()=>{let a=await k.hooks.reroute({url:new URL(e),fetch:async(o,s)=>xn(o,s,e).promise})??e;if(typeof a=="string"){const o=new URL(e);k.hash?o.hash=a:o.pathname=a,a=o}return a})();me.set(t,r),n=await r}catch(r){if(me.delete(t),S){console.error(r);debugger}return}return n}async function Pe(e,t){if(e&&!Ae(e,D,k.hash)){const n=await An(e);if(!n)return;const r=Pn(n);for(const a of Xe){const o=a.exec(r);if(o)return{id:Le(e),invalidating:t,route:a,params:Yt(o),url:e}}}}function Pn(e){return Jt(k.hash?e.hash.replace(/^#/,"").replace(/[?#].+/,""):e.pathname.slice(D.length))||"/"}function Le(e){return(k.hash?e.hash.replace(/^#/,""):e.pathname)+e.search}function Kt({url:e,type:t,intent:n,delta:r,event:a,scroll:o}){let s=!1;const i=nt(w,n,e,t,o??null);r!==void 0&&(i.navigation.delta=r),a!==void 0&&(i.navigation.event=a);const l={...i.navigation,cancel:()=>{s=!0,i.reject(new Error("navigation cancelled"))}};return oe||Ot.forEach(c=>c(l)),s?null:i}async function J({type:e,url:t,popped:n,keepfocus:r,noscroll:a,replace_state:o,state:s={},redirect_count:i=0,nav_token:l={},accept:c=I,block:d=I,event:f}){var H;const _=K;K=l;const g=await Pe(t,!1),u=e==="enter"?nt(w,g,t,e):Kt({url:t,type:e,delta:n==null?void 0:n.delta,intent:g,scroll:n==null?void 0:n.scroll,event:f});if(!u){d(),K===l&&(K=_);return}const m=E,p=$;c(),oe=!0,xe&&u.navigation.type!=="enter"&&N.navigating.set(ne.current=u.navigation);let h=g&&await Bt(g);if(!h)if(Ae(t,D,k.hash))if(S&&k.hash)h=await Ke(t,{id:null},await Y(new ve(404,"Not Found",`Not found: ${t.pathname} (did you forget the hash?)`),{url:t,params:{},route:{id:null}}),404,o);else return await ae(t,o);else h=await Ke(t,{id:null},await Y(new ve(404,"Not Found",`Not found: ${t.pathname}`),{url:t,params:{},route:{id:null}}),404,o);if(t=(g==null?void 0:g.url)||t,K!==l)return u.reject(new Error("navigation aborted")),!1;if(h.type==="redirect"){if(i<20){await J({type:e,url:new URL(h.location,t),popped:n,keepfocus:r,noscroll:a,replace_state:o,state:s,redirect_count:i+1,nav_token:l}),u.fulfil(void 0);return}h=await tt({status:500,error:await Y(new Error("Redirect loop"),{url:t,params:{},route:{id:null}}),url:t,route:{id:null}})}else h.props.page.status>=400&&await N.updated.check()&&(await Pt(),await ae(t,o));if(Rn(),ze(m),Nt(p),h.props.page.url.pathname!==t.pathname&&(t.pathname=h.props.page.url.pathname),s=n?n.state:s,!n){const R=o?0:1,z={[W]:E+=R,[te]:$+=R,[xt]:s};(o?history.replaceState:history.pushState).call(history,z,"",t),o||En(E,$)}const b=g&&(v==null?void 0:v.id)===g.id?v.fork:null;v!=null&&v.fork&&!b&&Re(),v=null,h.props.page.state=s;let T;if(xe){const R=(await Promise.all(Array.from(Sn,X=>X(u.navigation)))).filter(X=>typeof X=="function");if(R.length>0){let X=function(){R.forEach(Ie=>{ee.delete(Ie)})};R.push(X),R.forEach(Ie=>{ee.add(Ie)})}const z=u.navigation.to;w={...h.state,nav:{params:z.params,route:z.route,url:z.url}},h.props.page&&(h.props.page.url=t);const Oe=b&&await b;Oe?T=Oe.commit():(Q=null,Ct.$set(h.props),Q&&Object.assign(h.props.page,Q),bn(h.props.page),T=(H=Mt)==null?void 0:H()),jt=!0}else await Vt(h,Be,!1);const{activeElement:C}=document;await T,await we(),await we();let V=null;if(ht){const R=n?n.scroll:a?G():null;R?scrollTo(R.x,R.y):(V=t.hash&&document.getElementById(Gt(t)))?V.scrollIntoView():scrollTo(0,0)}const _e=document.activeElement!==C&&document.activeElement!==document.body;!r&&!_e&&Dn(t,!V),ht=!0,h.props.page&&(Q&&Object.assign(h.props.page,Q),Object.assign(x,h.props.page)),oe=!1,e==="popstate"&&Dt($),u.fulfil(void 0),u.navigation.to&&(u.navigation.to.scroll=G()),ee.forEach(R=>R(u.navigation)),N.navigating.set(ne.current=null)}async function Ke(e,t,n,r,a){if(e.origin===Ue&&e.pathname===location.pathname&&!It)return await tt({status:r,error:n,url:e,route:t});if(S&&r!==404){console.error("An error occurred while loading the page. This will cause a full page reload. (This message will only appear during development.)");debugger}return await ae(e,a)}function On(){let e,t={element:void 0,href:void 0},n;O.addEventListener("mousemove",i=>{const l=i.target;clearTimeout(e),e=setTimeout(()=>{o(l,B.hover)},20)});function r(i){i.defaultPrevented||o(i.composedPath()[0],B.tap)}O.addEventListener("mousedown",r),O.addEventListener("touchstart",r,{passive:!0});const a=new IntersectionObserver(i=>{for(const l of i)l.isIntersecting&&(De(new URL(l.target.href)),a.unobserve(l.target))},{threshold:0});async function o(i,l){const c=$t(i,O),d=c===t.element&&(c==null?void 0:c.href)===t.href&&l>=n;if(!c||d)return;const{url:f,external:_,download:g}=qe(c,D,k.hash);if(_||g)return;const u=be(c),m=f&&Le(w.url)===Le(f);if(!(u.reload||m))if(l<=u.preload_data){t={element:c,href:c.href},n=B.tap;const p=await Pe(f,!1);if(!p)return;S?_t(p).then(h=>{h.type==="loaded"&&h.state.error&&console.warn(`Preloading data for ${p.url.pathname} failed with the following error: ${h.state.error.message}
+If this error is transient, you can ignore it. Otherwise, consider disabling preloading for this route. This route was preloaded due to a data-sveltekit-preload-data attribute. See https://svelte.dev/docs/kit/link-options for more info`)}):_t(p)}else l<=u.preload_code&&(t={element:c,href:c.href},n=l,De(f))}function s(){a.disconnect();for(const i of O.querySelectorAll("a")){const{url:l,external:c,download:d}=qe(i,D,k.hash);if(c||d)continue;const f=be(i);f.reload||(f.preload_code===B.viewport&&a.observe(i),f.preload_code===B.eager&&De(l))}}ee.add(s),s()}function Y(e,t){if(e instanceof Fe)return e.body;S&&console.warn("The next HMR update will cause the page to reload");const n=Ye(e),r=vn(e);return k.hooks.handleError({error:e,event:t,status:n,message:r})??{message:r}}function Yn(e,t={}){if(!j)throw new Error("Cannot call goto(...) on the server");return e=new URL(We(e)),e.origin!==Ue?Promise.reject(new Error(S?`Cannot use \`goto\` with an external URL. Use \`window.location = "${e}"\` instead`:"goto: invalid URL")):qt(e,t,0)}function In(e){if(typeof e=="function")Ee.push(e);else{const{href:t}=new URL(e,location.href);Ee.push(n=>n.href===t)}}function jn(){var t;history.scrollRestoration="manual",addEventListener("beforeunload",n=>{let r=!1;if(gt(),!oe){const a=nt(w,void 0,null,"leave"),o={...a.navigation,cancel:()=>{r=!0,a.reject(new Error("navigation cancelled"))}};Ot.forEach(s=>s(o))}r?(n.preventDefault(),n.returnValue=""):history.scrollRestoration="auto"}),addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&>()}),(t=navigator.connection)!=null&&t.saveData||On(),O.addEventListener("click",async n=>{if(n.button||n.which!==1||n.metaKey||n.ctrlKey||n.shiftKey||n.altKey||n.defaultPrevented)return;const r=$t(n.composedPath()[0],O);if(!r)return;const{url:a,external:o,target:s,download:i}=qe(r,D,k.hash);if(!a)return;if(s==="_parent"||s==="_top"){if(window.parent!==window)return}else if(s&&s!=="_self")return;const l=be(r);if(!(r instanceof SVGAElement)&&a.protocol!==location.protocol&&!(a.protocol==="https:"||a.protocol==="http:")||i)return;const[d,f]=(k.hash?a.hash.replace(/^#/,""):a.href).split("#"),_=d===je(location);if(o||l.reload&&(!_||!f)){Kt({url:a,type:"link",event:n})?oe=!0:n.preventDefault();return}if(f!==void 0&&_){const[,g]=w.url.href.split("#");if(g===f){if(n.preventDefault(),f===""||f==="top"&&r.ownerDocument.getElementById("top")===null)scrollTo({top:0});else{const u=r.ownerDocument.getElementById(decodeURIComponent(f));u&&(u.scrollIntoView(),u.focus())}return}if(Z=!0,ze(E),e(a),!l.replace_state)return;Z=!1}n.preventDefault(),await new Promise(g=>{requestAnimationFrame(()=>{setTimeout(g,0)}),setTimeout(g,100)}),await J({type:"link",url:a,keepfocus:l.keepfocus,noscroll:l.noscroll,replace_state:l.replace_state??a.href===location.href,event:n})}),O.addEventListener("submit",n=>{if(n.defaultPrevented)return;const r=HTMLFormElement.prototype.cloneNode.call(n.target),a=n.submitter;if(((a==null?void 0:a.formTarget)||r.target)==="_blank"||((a==null?void 0:a.formMethod)||r.method)!=="get")return;const i=new URL((a==null?void 0:a.hasAttribute("formaction"))&&(a==null?void 0:a.formAction)||r.action);if(Ae(i,D,!1))return;const l=n.target,c=be(l);if(c.reload)return;n.preventDefault(),n.stopPropagation();const d=new FormData(l,a);i.search=new URLSearchParams(d).toString(),J({type:"form",url:i,keepfocus:c.keepfocus,noscroll:c.noscroll,replace_state:c.replace_state??i.href===location.href,event:n})}),addEventListener("popstate",async n=>{var r;if(!Ge){if((r=n.state)!=null&&r[W]){const a=n.state[W];if(K={},a===E)return;const o=q[a],s=n.state[xt]??{},i=new URL(n.state[fn]??location.href),l=n.state[te],c=w.url?je(location)===je(w.url):!1;if(l===$&&(jt||c)){s!==x.state&&(x.state=s),e(i),q[E]=G(),o&&scrollTo(o.x,o.y),E=a;return}const f=a-E;await J({type:"popstate",url:i,popped:{state:s,scroll:o,delta:f},accept:()=>{E=a,$=l},block:()=>{history.go(-f)},nav_token:K,event:n})}else if(!Z){const a=new URL(location.href);e(a),k.hash&&location.reload()}}}),addEventListener("hashchange",()=>{Z&&(Z=!1,history.replaceState({...history.state,[W]:++E,[te]:$},"",location.href))});for(const n of document.querySelectorAll("link"))kn.has(n.rel)&&(n.href=n.href);addEventListener("pageshow",n=>{n.persisted&&N.navigating.set(ne.current=null)});function e(n){w.url=x.url=n,N.page.set(rt(x)),N.page.notify()}}async function Cn(e,{status:t=200,error:n,node_ids:r,params:a,route:o,server_route:s,data:i,form:l}){It=!0;const c=new URL(location.href);let d;({params:a={},route:o={id:null}}=await Pe(c,!1)||{}),d=Xe.find(({id:g})=>g===o.id);let f,_=!0;try{const g=r.map(async(m,p)=>{const h=i[p];return h!=null&&h.uses&&(h.uses=Nn(h.uses)),Ze({loader:k.nodes[m],url:c,params:a,route:o,parent:async()=>{const b={};for(let T=0;T
{const i=history.state;Ge=!0,location.replace(new URL(`#${r}`,location.href)),history.replaceState(i,"",e),t&&scrollTo(o,s),Ge=!1})}else{const o=document.body,s=o.getAttribute("tabindex");o.tabIndex=-1,o.focus({preventScroll:!0,focusVisible:!1}),s!==null?o.setAttribute("tabindex",s):o.removeAttribute("tabindex")}const a=getSelection();if(a&&a.type!=="None"){const o=[];for(let s=0;s{if(a.rangeCount===o.length){for(let s=0;s{o=f,s=_});return i.catch(I),{navigation:{from:{params:e.params,route:{id:((c=e.route)==null?void 0:c.id)??null},url:e.url,scroll:G()},to:n&&{params:(t==null?void 0:t.params)??null,route:{id:((d=t==null?void 0:t.route)==null?void 0:d.id)??null},url:n,scroll:a},willUnload:!t,type:r,complete:i},fulfil:o,reject:s}}function rt(e){return{data:e.data,error:e.error,form:e.form,params:e.params,route:e.route,state:e.state,status:e.status,url:e.url}}function qn(e){const t=new URL(e);return t.hash=decodeURIComponent(e.hash),t}function Gt(e){let t;if(k.hash){const[,,n]=e.hash.split("#",3);t=n??""}else t=e.hash.slice(1);return decodeURIComponent(t)}if(S){const e=console.warn;console.warn=function(...n){n.length===1&&/<(Layout|Page|Error)(_[\w$]+)?> was created (with unknown|without expected) prop '(data|form)'/.test(n[0])||e(...n)}}export{Jn as a,Yn as g,Hn as l,x as p,N as s};
diff --git a/frontend/build/_app/immutable/chunks/BRcwu1Xf.js b/frontend/build/_app/immutable/chunks/BRcwu1Xf.js
new file mode 100644
index 0000000000000000000000000000000000000000..ef5877913dac0947394ef9c80c7c004ba0de1b42
--- /dev/null
+++ b/frontend/build/_app/immutable/chunks/BRcwu1Xf.js
@@ -0,0 +1 @@
+import{ao as h,ap as d}from"./CPYeCQyA.js";const l=[{code:"en",name:"English",nativeName:"English"},{code:"hi",name:"Hindi",nativeName:"हिन्दी"},{code:"raj",name:"Rajasthani",nativeName:"राजस्थानी"},{code:"gu",name:"Gujarati",nativeName:"ગુજરાતી"},{code:"mr",name:"Marathi",nativeName:"मराठी"},{code:"pa",name:"Punjabi",nativeName:"ਪੰਜਾਬੀ"},{code:"bn",name:"Bengali",nativeName:"বাংলা"},{code:"ta",name:"Tamil",nativeName:"தமிழ்"},{code:"te",name:"Telugu",nativeName:"తెలుగు"},{code:"kn",name:"Kannada",nativeName:"ಕನ್ನಡ"},{code:"ml",name:"Malayalam",nativeName:"മലയാളം"},{code:"or",name:"Odia",nativeName:"ଓଡ଼ିଆ"},{code:"as",name:"Assamese",nativeName:"অসমীয়া"},{code:"ur",name:"Urdu",nativeName:"اردو"},{code:"ne",name:"Nepali",nativeName:"नेपाली"},{code:"si",name:"Sinhala",nativeName:"සිංහල"},{code:"kok",name:"Konkani",nativeName:"कोंकणी"},{code:"mai",name:"Maithili",nativeName:"मैथिली"},{code:"bho",name:"Bhojpuri",nativeName:"भोजपुरी"}],r=new Set(["ur"]),a={"nav.chat":"Chat","nav.dashboard":"Dashboard","nav.notebooks":"Notebooks","nav.admin":"Admin","nav.cluster":"Cluster","nav.rag":"Knowledge Base","nav.doubts":"Doubts","nav.attendance":"Attendance","nav.copycheck":"Copy Check","nav.files":"Files","nav.notifications":"Notifications","nav.keys":"API Keys","nav.settings":"Settings","nav.logout":"Log out","nav.profile":"Profile","auth.login":"Sign in to MAC","auth.email":"Email address","auth.password":"Password","auth.signin":"Sign in","auth.signing_in":"Signing in…","auth.forgot":"Forgot password?","auth.error":"Invalid credentials","setup.title":"Welcome to MAC","setup.subtitle":"MBM AI Cloud — First-time setup","setup.name":"Your name","setup.email":"Admin email","setup.password":"Password (min 8 chars)","setup.create":"Create admin account","setup.creating":"Creating account…","setup.success":"Account created! Redirecting…","chat.placeholder":"Ask anything… (Shift+Enter for new line)","chat.send":"Send","chat.new":"New chat","chat.model":"Model","chat.auto":"Auto (smart routing)","chat.thinking":"Thinking…","chat.error":"Something went wrong. Please try again.","chat.empty":"Start a conversation","chat.empty_hint":"Ask MAC anything — code, math, essays, or general questions.","dash.title":"Dashboard","dash.requests":"Total Requests","dash.tokens":"Tokens Used","dash.models":"Active Models","dash.days":"Days Active","dash.heatmap":"Activity (last 26 weeks)","dash.distribution":"Model Distribution","dash.hourly":"Hourly Usage","dash.quota":"Quota Status","dash.recent":"Recent Activity","dash.no_data":"No activity yet","admin.users":"Users","admin.models":"Models","admin.features":"Features","admin.hardware":"Hardware","admin.system":"System","admin.guardrails":"Guardrails","admin.rag":"Knowledge Base","common.save":"Save","common.cancel":"Cancel","common.delete":"Delete","common.edit":"Edit","common.loading":"Loading…","common.error":"Error","common.success":"Success","common.search":"Search","common.refresh":"Refresh","common.enabled":"Enabled","common.disabled":"Disabled","common.yes":"Yes","common.no":"No","common.unknown":"Unknown","common.copy":"Copy","common.copied":"Copied!","common.close":"Close","common.back":"Back","common.next":"Next","common.submit":"Submit"},u={"nav.chat":"चैट","nav.dashboard":"डैशबोर्ड","nav.notebooks":"नोटबुक","nav.admin":"प्रशासक","nav.cluster":"क्लस्टर","nav.rag":"ज्ञान आधार","nav.doubts":"शंका","nav.attendance":"उपस्थिति","nav.copycheck":"नकल जाँच","nav.files":"फ़ाइलें","nav.notifications":"सूचनाएं","nav.keys":"API कुंजी","nav.settings":"सेटिंग","nav.logout":"लॉग आउट","nav.profile":"प्रोफाइल","auth.login":"MAC में साइन इन करें","auth.email":"ईमेल पता","auth.password":"पासवर्ड","auth.signin":"साइन इन","auth.signing_in":"साइन इन हो रहा है…","auth.forgot":"पासवर्ड भूल गए?","auth.error":"गलत क्रेडेंशियल","setup.title":"MAC में आपका स्वागत है","setup.subtitle":"MBM AI Cloud — पहली बार सेटअप","setup.name":"आपका नाम","setup.email":"प्रशासक ईमेल","setup.password":"पासवर्ड (न्यूनतम 8 अक्षर)","setup.create":"प्रशासक खाता बनाएं","setup.creating":"खाता बन रहा है…","setup.success":"खाता बन गया! पुनर्निर्देशित हो रहे हैं…","chat.placeholder":"कुछ भी पूछें… (नई लाइन के लिए Shift+Enter)","chat.send":"भेजें","chat.new":"नई चैट","chat.model":"मॉडल","chat.auto":"स्वत: (स्मार्ट रूटिंग)","chat.thinking":"सोच रहा है…","chat.error":"कुछ गलत हुआ। कृपया पुनः प्रयास करें।","chat.empty":"बातचीत शुरू करें","chat.empty_hint":"MAC से कुछ भी पूछें — कोड, गणित, निबंध, या सामान्य प्रश्न।","dash.title":"डैशबोर्ड","dash.requests":"कुल अनुरोध","dash.tokens":"उपयोग किए गए टोकन","dash.models":"सक्रिय मॉडल","dash.days":"सक्रिय दिन","dash.heatmap":"गतिविधि (पिछले 26 सप्ताह)","dash.distribution":"मॉडल वितरण","dash.hourly":"प्रति घंटा उपयोग","dash.quota":"कोटा स्थिति","dash.recent":"हालिया गतिविधि","dash.no_data":"अभी तक कोई गतिविधि नहीं","admin.users":"उपयोगकर्ता","admin.models":"मॉडल","admin.features":"सुविधाएं","admin.hardware":"हार्डवेयर","admin.system":"सिस्टम","admin.guardrails":"सुरक्षा नियम","admin.rag":"ज्ञान आधार","common.save":"सहेजें","common.cancel":"रद्द करें","common.delete":"हटाएं","common.edit":"संपादित करें","common.loading":"लोड हो रहा है…","common.error":"त्रुटि","common.success":"सफलता","common.search":"खोजें","common.refresh":"ताज़ा करें","common.enabled":"सक्षम","common.disabled":"अक्षम","common.yes":"हाँ","common.no":"नहीं","common.unknown":"अज्ञात","common.copy":"कॉपी","common.copied":"कॉपी हो गया!","common.close":"बंद करें","common.back":"वापस","common.next":"अगला","common.submit":"जमा करें"},g={"nav.chat":"बात","nav.dashboard":"मुख पानो","nav.notebooks":"नोटबुक","nav.logout":"लॉग आउट","nav.settings":"सेटिंग","auth.login":"MAC में प्रवेश करो","auth.signin":"प्रवेश","auth.signing_in":"प्रवेश हो रह्यो है…","auth.password":"पासवर्ड","chat.placeholder":"कुछ भी पूछो… (नई लाइन खातर Shift+Enter)","chat.send":"भेजो","chat.new":"नई बात","chat.empty":"बात चालू करो","chat.empty_hint":"MAC सूं कुछ भी पूछो — कोड, गणित, निबंध।","dash.title":"मुख पानो","common.save":"संग्रह करो","common.cancel":"रद्द","common.loading":"लोड हो रह्यो है…","common.search":"ढूंढो","common.back":"पाछो"},v={"nav.chat":"ચેટ","nav.dashboard":"ડેશબોર્ડ","nav.notebooks":"નોટબુક","nav.logout":"લૉગ આઉટ","nav.settings":"સેટિંગ","nav.files":"ફ઼ાઇલો","nav.notifications":"સૂચનાઓ","auth.login":"MAC માં સાઇન ઇન કરો","auth.email":"ઇમેઇલ સરનામું","auth.password":"પાસવર્ડ","auth.signin":"સાઇન ઇન","auth.signing_in":"સાઇન ઇન થઈ રહ્યું છે…","auth.error":"ખોટી ઓળખ","chat.placeholder":"કંઈ પણ પૂછો… (નવી લીટી માટે Shift+Enter)","chat.send":"મોકલો","chat.new":"નવી ચેટ","chat.empty":"વાતચીત શરૂ કરો","chat.empty_hint":"MAC ને કોઈ પણ વિષે પૂછો — કોડ, ગણિત, નિબંધ.","dash.title":"ડેશબોર્ડ","dash.recent":"તાજેતરની પ્રવૃત્તિ","dash.no_data":"હજી કોઈ પ્રવૃત્તિ નથી","common.save":"સાચવો","common.cancel":"રદ કરો","common.loading":"લોડ થઈ રહ્યું છે…","common.search":"શોધો","common.back":"પાછળ","common.close":"બંધ"},p={"nav.chat":"चॅट","nav.dashboard":"डॅशबोर्ड","nav.notebooks":"नोटबुक","nav.logout":"लॉग आउट","nav.settings":"सेटिंग्ज","nav.attendance":"हजेरी","nav.doubts":"शंका","nav.files":"फाइल्स","nav.notifications":"सूचना","auth.login":"MAC मध्ये साइन इन करा","auth.email":"ईमेल पत्ता","auth.password":"पासवर्ड","auth.signin":"साइन इन","auth.signing_in":"साइन इन होत आहे…","auth.error":"चुकीची ओळख","chat.placeholder":"काहीही विचारा… (नवी ओळ साठी Shift+Enter)","chat.send":"पाठवा","chat.new":"नवीन चॅट","chat.empty":"संभाषण सुरू करा","chat.empty_hint":"MAC ला काहीही विचारा — कोड, गणित, निबंध.","dash.title":"डॅशबोर्ड","dash.recent":"अलीकडील क्रियाकलाप","dash.no_data":"अद्याप कोणतीही क्रियाकलाप नाही","common.save":"जतन करा","common.cancel":"रद्द करा","common.loading":"लोड होत आहे…","common.search":"शोधा","common.back":"मागे","common.close":"बंद करा"},b={"nav.chat":"ਚੈਟ","nav.dashboard":"ਡੈਸ਼ਬੋਰਡ","nav.notebooks":"ਨੋਟਬੁੱਕ","nav.logout":"ਲੌਗ ਆਉਟ","nav.settings":"ਸੈਟਿੰਗਜ਼","nav.files":"ਫਾਈਲਾਂ","nav.notifications":"ਸੂਚਨਾਵਾਂ","auth.login":"MAC ਵਿੱਚ ਸਾਈਨ ਇਨ ਕਰੋ","auth.email":"ਈਮੇਲ ਪਤਾ","auth.password":"ਪਾਸਵਰਡ","auth.signin":"ਸਾਈਨ ਇਨ","auth.signing_in":"ਸਾਈਨ ਇਨ ਹੋ ਰਿਹਾ ਹੈ…","auth.error":"ਗਲਤ ਜਾਣਕਾਰੀ","chat.placeholder":"ਕੁਝ ਵੀ ਪੁੱਛੋ… (ਨਵੀਂ ਲਾਈਨ ਲਈ Shift+Enter)","chat.send":"ਭੇਜੋ","chat.new":"ਨਵੀਂ ਚੈਟ","chat.empty":"ਗੱਲਬਾਤ ਸ਼ੁਰੂ ਕਰੋ","chat.empty_hint":"MAC ਨੂੰ ਕੁਝ ਵੀ ਪੁੱਛੋ — ਕੋਡ, ਗਣਿਤ, ਲੇਖ.","dash.title":"ਡੈਸ਼ਬੋਰਡ","common.save":"ਸੁਰੱਖਿਅਤ ਕਰੋ","common.cancel":"ਰੱਦ ਕਰੋ","common.loading":"ਲੋਡ ਹੋ ਰਿਹਾ ਹੈ…","common.search":"ਖੋਜੋ","common.back":"ਵਾਪਸ"},w={"nav.chat":"চ্যাট","nav.dashboard":"ড্যাশবোর্ড","nav.notebooks":"নোটবুক","nav.logout":"লগ আউট","nav.settings":"সেটিংস","nav.files":"ফাইল","nav.notifications":"বিজ্ঞপ্তি","auth.login":"MAC-এ সাইন ইন করুন","auth.email":"ইমেল ঠিকানা","auth.password":"পাসওয়ার্ড","auth.signin":"সাইন ইন","auth.signing_in":"সাইন ইন হচ্ছে…","auth.error":"ভুল পরিচয়পত্র","chat.placeholder":"যেকোনো কিছু জিজ্ঞেস করুন… (নতুন লাইনের জন্য Shift+Enter)","chat.send":"পাঠান","chat.new":"নতুন চ্যাট","chat.empty":"কথোপকথন শুরু করুন","chat.empty_hint":"MAC-কে যেকোনো কিছু জিজ্ঞেস করুন — কোড, গণিত, প্রবন্ধ।","dash.title":"ড্যাশবোর্ড","dash.recent":"সাম্প্রতিক কার্যক্রম","dash.no_data":"এখনও কোনো কার্যক্রম নেই","common.save":"সংরক্ষণ করুন","common.cancel":"বাতিল করুন","common.loading":"লোড হচ্ছে…","common.search":"খুঁজুন","common.back":"ফিরে যান"},A={"nav.chat":"அரட்டை","nav.dashboard":"டாஷ்போர்டு","nav.notebooks":"நோட்புக்","nav.logout":"வெளியேறு","nav.settings":"அமைப்புகள்","auth.login":"MAC-ல் உள்நுழையவும்","auth.email":"மின்னஞ்சல் முகவரி","auth.password":"கடவுச்சொல்","auth.signin":"உள்நுழை","auth.signing_in":"உள்நுழைகிறது…","auth.error":"தவறான சான்றுகள்","chat.placeholder":"எதையும் கேளுங்கள்…","chat.send":"அனுப்பு","chat.new":"புதிய அரட்டை","chat.empty":"உரையாடலை தொடங்குங்கள்","chat.empty_hint":"MAC-ஐ எதையும் கேளுங்கள் — குறியீடு, கணிதம், கட்டுரை.","dash.title":"டாஷ்போர்டு","common.save":"சேமி","common.cancel":"ரத்து செய்","common.loading":"ஏற்றுகிறது…","common.search":"தேடு","common.back":"திரும்பு"},y={"nav.chat":"చాట్","nav.dashboard":"డాష్బోర్డ్","nav.notebooks":"నోట్బుక్","nav.logout":"లాగ్ అవుట్","nav.settings":"సెట్టింగ్లు","auth.login":"MAC లో సైన్ ఇన్ చేయండి","auth.password":"పాస్వర్డ్","auth.signin":"సైన్ ఇన్","auth.signing_in":"సైన్ ఇన్ అవుతోంది…","auth.error":"తప్పు ఆధారాలు","chat.placeholder":"ఏదైనా అడగండి…","chat.send":"పంపు","chat.new":"కొత్త చాట్","chat.empty":"సంభాషణ ప్రారంభించండి","dash.title":"డాష్బోర్డ్","common.save":"సేవ్ చేయి","common.cancel":"రద్దు చేయి","common.loading":"లోడ్ అవుతోంది…","common.search":"వెతకండి","common.back":"వెనుకకు"},f={"nav.chat":"ಚಾಟ್","nav.dashboard":"ಡ್ಯಾಶ್ಬೋರ್ಡ್","nav.logout":"ಲಾಗ್ ಔಟ್","nav.settings":"ಸೆಟ್ಟಿಂಗ್ಗಳು","auth.login":"MAC ಗೆ ಸೈನ್ ಇನ್ ಮಾಡಿ","auth.password":"ಪಾಸ್ವರ್ಡ್","auth.signin":"ಸೈನ್ ಇನ್","auth.signing_in":"ಸೈನ್ ಇನ್ ಆಗುತ್ತಿದೆ…","chat.placeholder":"ಏನಾದರೂ ಕೇಳಿ…","chat.send":"ಕಳಿಸಿ","chat.new":"ಹೊಸ ಚಾಟ್","chat.empty":"ಸಂಭಾಷಣೆ ಪ್ರಾರಂಭಿಸಿ","dash.title":"ಡ್ಯಾಶ್ಬೋರ್ಡ್","common.save":"ಉಳಿಸಿ","common.cancel":"ರದ್ದು ಮಾಡಿ","common.loading":"ಲೋಡ್ ಆಗುತ್ತಿದೆ…","common.search":"ಹುಡುಕಿ"},k={"nav.chat":"ചാറ്റ്","nav.dashboard":"ഡാഷ്ബോർഡ്","nav.logout":"ലോഗ് ഔട്ട്","nav.settings":"ക്രമീകരണങ്ങൾ","auth.login":"MAC-ൽ സൈൻ ഇൻ ചെയ്യുക","auth.password":"പാസ്വേഡ്","auth.signin":"സൈൻ ഇൻ","auth.signing_in":"സൈൻ ഇൻ ചെയ്യുന്നു…","chat.placeholder":"എന്തും ചോദിക്കൂ…","chat.send":"അയക്കുക","chat.new":"പുതിയ ചാറ്റ്","chat.empty":"സംഭാഷണം ആരംഭിക്കുക","dash.title":"ഡാഷ്ബോർഡ്","common.save":"സേവ് ചെയ്യുക","common.cancel":"റദ്ദാക്കുക","common.loading":"ലോഡ് ചെയ്യുന്നു…","common.search":"തിരയുക"},C={"nav.chat":"ଚ୍ୟାଟ୍","nav.dashboard":"ଡ୍ୟାଶ୍ବୋର୍ଡ","nav.logout":"ଲଗ ଆଉଟ","auth.login":"MAC ରେ ସାଇନ ଇନ କରନ୍ତୁ","auth.password":"ପାସୱାର୍ଡ","auth.signin":"ସାଇନ ଇନ","chat.send":"ପଠାନ୍ତୁ","chat.new":"ନୂଆ ଚ୍ୟାଟ","dash.title":"ଡ୍ୟାଶ୍ବୋର୍ଡ","common.save":"ସଂରକ୍ଷଣ","common.cancel":"ବାତିଲ","common.loading":"ଲୋଡ ହେଉଛି…","common.search":"ଖୋଜ"},M={"nav.chat":"চেট","nav.dashboard":"ডেশ্ববৰ্ড","nav.logout":"লগ আউট","auth.login":"MAC ত চাইন ইন কৰক","auth.password":"পাছৱৰ্ড","auth.signin":"চাইন ইন","chat.send":"পঠাওক","chat.new":"নতুন চেট","dash.title":"ডেশ্ববৰ্ড","common.save":"সংৰক্ষণ","common.loading":"লোড হৈছে…"},S={"nav.chat":"چیٹ","nav.dashboard":"ڈیش بورڈ","nav.notebooks":"نوٹ بکس","nav.logout":"لاگ آؤٹ","nav.settings":"ترتیبات","auth.login":"MAC میں سائن ان کریں","auth.email":"ای میل پتہ","auth.password":"پاس ورڈ","auth.signin":"سائن ان","auth.signing_in":"سائن ان ہو رہا ہے…","auth.error":"غلط اعتماد نامہ","chat.placeholder":"کچھ بھی پوچھیں…","chat.send":"بھیجیں","chat.new":"نئی چیٹ","chat.empty":"گفتگو شروع کریں","chat.empty_hint":"MAC سے کچھ بھی پوچھیں — کوڈ، ریاضی، مضمون۔","dash.title":"ڈیش بورڈ","common.save":"محفوظ کریں","common.cancel":"منسوخ","common.loading":"لوڈ ہو رہا ہے…","common.search":"تلاش","common.back":"واپس"},_={"nav.chat":"च्याट","nav.dashboard":"ड्यासबोर्ड","nav.logout":"लग आउट","nav.settings":"सेटिङ","auth.login":"MAC मा साइन इन गर्नुहोस्","auth.password":"पासवर्ड","auth.signin":"साइन इन","auth.signing_in":"साइन इन भइरहेको छ…","chat.placeholder":"केही पनि सोध्नुहोस्…","chat.send":"पठाउनुहोस्","chat.new":"नयाँ च्याट","chat.empty":"कुराकानी सुरु गर्नुहोस्","dash.title":"ड्यासबोर्ड","common.save":"सुरक्षित गर्नुहोस्","common.cancel":"रद्द गर्नुहोस्","common.loading":"लोड भइरहेको छ…","common.search":"खोज्नुहोस्"},N={"nav.chat":"කතාබස","nav.dashboard":"උපකරණ පුවරුව","nav.logout":"නික්මෙන්න","auth.login":"MAC වෙත ඇතුල් වන්න","auth.password":"මුරපදය","auth.signin":"ඇතුල් වන්න","chat.send":"යවන්න","chat.new":"නව කතාබස","common.loading":"පූරණය වෙමින්…","common.save":"සුරකින්න"},E={"nav.chat":"चॅट","nav.dashboard":"डॅशबोर्ड","nav.logout":"लॉग आउट","auth.login":"MAC मदीं साइन इन करात","auth.password":"पासवर्ड","auth.signin":"साइन इन","chat.send":"धाडात","chat.new":"नवें चॅट","dash.title":"डॅशबोर्ड","common.loading":"लोड जाता…","common.save":"सांबाळात"},L={"nav.chat":"चैट","nav.dashboard":"डैशबोर्ड","nav.logout":"लॉग आउट","auth.login":"MAC मे साइन इन करू","auth.password":"पासवर्ड","auth.signin":"साइन इन","chat.send":"पठाउ","chat.new":"नव चैट","dash.title":"डैशबोर्ड","common.loading":"लोड भ रहल अछि…","common.save":"सहेजू"},R={"nav.chat":"चैट","nav.dashboard":"डैशबोर्ड","nav.logout":"लॉग आउट","auth.login":"MAC में साइन इन करीं","auth.password":"पासवर्ड","auth.signin":"साइन इन","auth.signing_in":"साइन इन हो रहल बा…","chat.placeholder":"कुछ भी पूछीं…","chat.send":"भेजीं","chat.new":"नया चैट","chat.empty":"बातचीत शुरू करीं","dash.title":"डैशबोर्ड","common.loading":"लोड हो रहल बा…","common.save":"सेव करीं","common.cancel":"रद्द करीं"},I={en:a,hi:{...a,...u},raj:{...a,...g},gu:{...a,...v},mr:{...a,...p},pa:{...a,...b},bn:{...a,...w},ta:{...a,...A},te:{...a,...y},kn:{...a,...f},ml:{...a,...k},or:{...a,...C},as:{...a,...M},ur:{...a,...S},ne:{...a,..._},si:{...a,...N},kok:{...a,...E},mai:{...a,...L},bho:{...a,...R}},s=d("en");function T(n){l.find(o=>o.code===n)&&(s.set(n),typeof localStorage<"u"&&localStorage.setItem("mac_locale",n),typeof document<"u"&&(document.documentElement.dir=r.has(n)?"rtl":"ltr",document.documentElement.lang=n))}function P(){var o;if(typeof localStorage>"u")return;const n=localStorage.getItem("mac_locale")||((o=navigator.language)==null?void 0:o.split("-")[0])||"en";T(n)}const O=s,D=h(s,n=>{const o=I[n]??a;return(t,c={})=>{let e=o[t]??a[t]??t;return Object.entries(c).forEach(([i,m])=>{e=e.replaceAll(`{${i}}`,m)}),e}});export{l as S,P as i,O as l,T as s,D as t};
diff --git a/frontend/build/_app/immutable/chunks/BT_qo9Cc.js b/frontend/build/_app/immutable/chunks/BT_qo9Cc.js
new file mode 100644
index 0000000000000000000000000000000000000000..3c814204a8e428e99c76bf85db07b456f836cead
--- /dev/null
+++ b/frontend/build/_app/immutable/chunks/BT_qo9Cc.js
@@ -0,0 +1 @@
+import{T as s,f as o,U as c,V as b,W as m,X as h,Y as v}from"./CPYeCQyA.js";function d(e,r,f=!1){if(e.multiple){if(r==null)return;if(!b(r))return m();for(var a of e.options)a.selected=r.includes(t(a));return}for(a of e.options){var i=t(a);if(h(i,r)){a.selected=!0;return}}(!f||r!==void 0)&&(e.selectedIndex=-1)}function y(e){var r=new MutationObserver(()=>{d(e,e.__value)});r.observe(e,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["value"]}),c(()=>{r.disconnect()})}function S(e,r,f=r){var a=new WeakSet,i=!0;s(e,"change",u=>{var l=u?"[selected]":":checked",n;if(e.multiple)n=[].map.call(e.querySelectorAll(l),t);else{var _=e.querySelector(l)??e.querySelector("option:not([disabled])");n=_&&t(_)}f(n),e.__value=n,v!==null&&a.add(v)}),o(()=>{var u=r();if(e===document.activeElement){var l=v;if(a.has(l))return}if(d(e,u,i),i&&u===void 0){var n=e.querySelector(":checked");n!==null&&(u=t(n),f(u))}e.__value=u,i=!1}),y(e)}function t(e){return"__value"in e?e.__value:e.value}export{S as b,y as i,d as s};
diff --git a/frontend/build/_app/immutable/chunks/BcWCHg3k.js b/frontend/build/_app/immutable/chunks/BcWCHg3k.js
new file mode 100644
index 0000000000000000000000000000000000000000..487328a90e6fc218b9f289fb74ecc93ce6d2b5ee
--- /dev/null
+++ b/frontend/build/_app/immutable/chunks/BcWCHg3k.js
@@ -0,0 +1 @@
+import{h,A as M,B as f,t as T,a as m,C as g,D as v,E as H,F as A,G as L,H as O,I as R,J as S,K as w,L as D,N as $,M as b,O as I,P as N,Q as y,R as P,S as F}from"./CPYeCQyA.js";function G(l,o,_){var c,i;if(!o||o===I(String(_??"")))return;let s;const a=(c=l.__svelte_meta)==null?void 0:c.loc;a?s=`near ${a.file}:${a.line}:${a.column}`:(i=N)!=null&&i[y]&&(s=`in ${N[y]}`),P(F(s))}function k(l,o,_=!1,s=!1,a=!1,c=!1){var i=l,n="";if(_){var d=l;h&&(i=M(f(d)))}T(()=>{var t=g;if(n===(n=o()??"")){h&&m();return}if(_&&!h){t.nodes=null,d.innerHTML=n,n!==""&&v(f(d),d.lastChild);return}if(t.nodes!==null&&(H(t.nodes.start,t.nodes.end),t.nodes=null),n!==""){if(h){for(var p=A.data,e=m(),E=e;e!==null&&(e.nodeType!==L||e.data!=="");)E=e,e=O(e);if(e===null)throw R(),S;w&&!c&&G(e.parentNode,p,n),v(A,E),i=M(e);return}var C=s?$:a?b:void 0,u=D(s?"svg":a?"math":"template",C);u.innerHTML=n;var r=s||a?u:u.content;if(v(f(r),r.lastChild),s||a)for(;f(r);)i.before(f(r));else i.before(r)}})}export{k as h};
diff --git a/frontend/build/_app/immutable/chunks/BjvCllst.js b/frontend/build/_app/immutable/chunks/BjvCllst.js
new file mode 100644
index 0000000000000000000000000000000000000000..8038fe2dfc444122636c5a723e5255f5ff6fa625
--- /dev/null
+++ b/frontend/build/_app/immutable/chunks/BjvCllst.js
@@ -0,0 +1 @@
+import{al as S,f as T,am as x,u as E,C as O,an as Y,a8 as k}from"./CPYeCQyA.js";function n(r,f){return r===f||(r==null?void 0:r[k])===f}function C(r={},f,i,A){var p=S.r,h=O;return T(()=>{var s,t;return x(()=>{s=t,t=[],E(()=>{r!==i(...t)&&(f(r,...t),s&&n(i(...s),r)&&f(null,...s))})}),()=>{let a=h;for(;a!==p&&a.parent!==null&&a.parent.f&Y;)a=a.parent;const w=()=>{t&&n(i(...t),r)&&f(null,...t)},c=a.teardown;a.teardown=()=>{w(),c==null||c()}}}),r}export{C as b};
diff --git a/frontend/build/_app/immutable/chunks/BkDXvb8s.js b/frontend/build/_app/immutable/chunks/BkDXvb8s.js
new file mode 100644
index 0000000000000000000000000000000000000000..1f3c97a5e9516ac83824337e86775e4d00a5363f
--- /dev/null
+++ b/frontend/build/_app/immutable/chunks/BkDXvb8s.js
@@ -0,0 +1 @@
+import{ag as y,ah as u,ai as _,aj as g,h as t,G as o,H as i,ak as l,A as d,F as p,B as m}from"./CPYeCQyA.js";function F(n,r){let a=null,E=t;var s;if(t){a=p;for(var e=m(document.head);e!==null&&(e.nodeType!==o||e.data!==n);)e=i(e);if(e===null)l(!1);else{var f=i(e);e.remove(),d(f)}}t||(s=document.head.appendChild(y()));try{u(()=>r(s),_|g)}finally{E&&(l(!0),d(a))}}export{F as h};
diff --git a/frontend/build/_app/immutable/chunks/BprE2qdV.js b/frontend/build/_app/immutable/chunks/BprE2qdV.js
new file mode 100644
index 0000000000000000000000000000000000000000..deaa4b58a29921fc1245c2baaca01a315d8ef7ff
--- /dev/null
+++ b/frontend/build/_app/immutable/chunks/BprE2qdV.js
@@ -0,0 +1 @@
+import{s as o}from"./CISzQ7XW.js";import{ae as n}from"./CPYeCQyA.js";function e(t){o.r.on_destroy(t)}function a(){return n}async function c(){}async function i(){}export{a as c,e as o,i as s,c as t};
diff --git a/frontend/build/_app/immutable/chunks/C9ELQ_b7.js b/frontend/build/_app/immutable/chunks/C9ELQ_b7.js
new file mode 100644
index 0000000000000000000000000000000000000000..d4b8cfe2ffc2c6d6684c8fd791e82b970206382c
--- /dev/null
+++ b/frontend/build/_app/immutable/chunks/C9ELQ_b7.js
@@ -0,0 +1,2 @@
+import{aQ as $}from"./CPYeCQyA.js";const R=/[&"<]/g,S=/[&<]/g;function d(r,n){const t=String(r??""),f=n?R:S;f.lastIndex=0;let s="",u=0;for(;f.test(t);){const i=f.lastIndex-1,o=t[i];s+=t.substring(u,i)+(o==="&"?"&":o==='"'?""":"<"),u=i+1}return s+t.substring(u)}function O(r){var n,t,f="";if(typeof r=="string"||typeof r=="number")f+=r;else if(typeof r=="object")if(Array.isArray(r)){var s=r.length;for(n=0;n=0;){var o=i+u;(i===0||j.includes(f[i-1]))&&(o===f.length||j.includes(f[o]))?f=(i===0?"":f.substring(0,i))+f.substring(o+1):i=o}}return f===""?null:f}function A(r,n=!1){var t=n?" !important;":";",f="";for(var s of Object.keys(r)){var u=r[s];u!=null&&u!==""&&(f+=" "+s+": "+u+t)}return f}function p(r){return r[0]!=="-"||r[1]!=="-"?r.toLowerCase():r}function X(r,n){if(n){var t="",f,s;if(Array.isArray(n)?(f=n[0],s=n[1]):f=n,r){r=String(r).replaceAll(/\s*\/\*.*?\*\/\s*/g,"").trim();var u=!1,i=0,o=!1,a=[];f&&a.push(...Object.keys(f).map(p)),s&&a.push(...Object.keys(s).map(p));var l=0,g=-1;const h=r.length;for(var e=0;e=1e6?(e/1e6).toFixed(1)+"M":e>=1e3?(e/1e3).toFixed(1)+"K":String(e)}function i(e){if(!e)return"—";const l=Date.now()-new Date(e).getTime(),t=Math.floor(l/1e3);if(t<60)return"just now";const r=Math.floor(t/60);if(r<60)return`${r}m ago`;const a=Math.floor(r/60);return a<24?`${a}h ago`:`${Math.floor(a/24)}d ago`}async function s(e){await navigator.clipboard.writeText(e)}function m(e){return e?`
`:""}function c(e){return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}function f(e){return e?e<3?"rgba(217,116,73,0.25)":e<8?"rgba(217,116,73,0.50)":e<20?"rgba(217,116,73,0.75)":"var(--accent)":"var(--surface3)"}export{i as a,s as c,o as f,f as h,m as r};
diff --git a/frontend/build/_app/immutable/chunks/CISzQ7XW.js b/frontend/build/_app/immutable/chunks/CISzQ7XW.js
new file mode 100644
index 0000000000000000000000000000000000000000..c25d6dbc258b15e26971c32a85c819b08b8825bb
--- /dev/null
+++ b/frontend/build/_app/immutable/chunks/CISzQ7XW.js
@@ -0,0 +1 @@
+import{K as c,af as l}from"./CPYeCQyA.js";var t=null;function p(n){t=n}function a(n){return u("getContext").get(n)}function i(n,e){return u("setContext").set(n,e),e}function u(n){return t===null&&l(n),t.c??(t.c=new Map(r(t)||void 0))}function f(n){var e;t={p:t,c:null,r:null},c&&(t.function=n,t.element=(e=t.p)==null?void 0:e.element)}function _(){t=t.p}function r(n){let e=n.p;for(;e!==null;){const o=e.c;if(o!==null)return o;e=e.p}return null}export{p as a,_ as b,i as c,a as g,f as p,t as s};
diff --git a/frontend/build/_app/immutable/chunks/CPYeCQyA.js b/frontend/build/_app/immutable/chunks/CPYeCQyA.js
new file mode 100644
index 0000000000000000000000000000000000000000..2c5a1d8a0edc4a61fbbe2222ab98a82e667f5318
--- /dev/null
+++ b/frontend/build/_app/immutable/chunks/CPYeCQyA.js
@@ -0,0 +1,49 @@
+var Qr=Object.defineProperty;var In=e=>{throw TypeError(e)};var es=(e,t,n)=>t in e?Qr(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var le=(e,t,n)=>es(e,typeof t!="symbol"?t+"":t,n),rn=(e,t,n)=>t.has(e)||In("Cannot "+n);var a=(e,t,n)=>(rn(e,t,"read from private field"),n?n.call(e):t.get(e)),T=(e,t,n)=>t.has(e)?In("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,n),b=(e,t,n,r)=>(rn(e,t,"write to private field"),r?r.call(e,n):t.set(e,n),n),S=(e,t,n)=>(rn(e,t,"access private method"),n);var Kn,Xn;const Pn=(Xn=(Kn=globalThis.process)==null?void 0:Kn.env)==null?void 0:Xn.NODE_ENV,v=Pn&&!Pn.toLowerCase().startsWith("prod");function ts(e){if(v){const t=new Error(`invariant_violation
+An invariant violation occurred, meaning Svelte's internal assumptions were flawed. This is a bug in Svelte, not your app — please open an issue at https://github.com/sveltejs/svelte, citing the following message: "${e}"
+https://svelte.dev/e/invariant_violation`);throw t.name="Svelte error",t}else throw new Error("https://svelte.dev/e/invariant_violation")}function Ri(e){if(v){const t=new Error(`lifecycle_outside_component
+\`${e}(...)\` can only be used during component initialisation
+https://svelte.dev/e/lifecycle_outside_component`);throw t.name="Svelte error",t}else throw new Error("https://svelte.dev/e/lifecycle_outside_component")}var ns=Array.isArray,rs=Array.prototype.indexOf,Ze=Array.prototype.includes,ss=Array.from,Ie=Object.defineProperty,nt=Object.getOwnPropertyDescriptor,is=Object.getOwnPropertyDescriptors,as=Object.prototype,ls=Array.prototype,er=Object.getPrototypeOf,Cn=Object.isExtensible,Ni=Object.prototype.hasOwnProperty;function Mi(e){return typeof e=="function"}const We=()=>{};function Ii(e){return e()}function tr(e){for(var t=0;t{e=r,t=s});return{promise:n,resolve:e,reject:t}}function Pi(e,t){if(Array.isArray(e))return e;if(t===void 0||!(Symbol.iterator in e))return Array.from(e);const n=[];for(const r of e)if(n.push(r),n.length===t)break;return n}const L=2,ot=4,Ot=8,rr=1<<24,de=16,he=32,Pe=64,fn=128,J=512,R=1024,D=2048,ie=4096,Q=8192,re=16384,Le=32768,Dn=1<<25,mt=65536,gt=1<<17,os=1<<18,_t=1<<19,sr=1<<20,Ci=1<<25,Ce=65536,bt=1<<21,ft=1<<22,Ne=1<<23,Me=Symbol("$state"),Di=Symbol("legacy props"),Li=Symbol(""),ir=Symbol("proxy path"),ji=Symbol("hmr anchor"),ye=new class extends Error{constructor(){super(...arguments);le(this,"name","StaleReactionError");le(this,"message","The reaction that called `getAbortSignal()` was re-run or destroyed")}};var Zn;const Hi=!!((Zn=globalThis.document)!=null&&Zn.contentType)&&globalThis.document.contentType.includes("xml"),Rt=3,Xt=8;let Zt=!1,fs=!1;function qi(){Zt=!0}function ar(e){const t=new Error,n=us();return n.length===0?null:(n.unshift(`
+`),Ie(t,"stack",{value:n.join(`
+`)}),Ie(t,"name",{value:e}),t)}function us(){const e=Error.stackTraceLimit;Error.stackTraceLimit=1/0;const t=new Error().stack;if(Error.stackTraceLimit=e,!t)return[];const n=t.split(`
+`),r=[];for(let s=0;s` `reset` function cannot be called while an error is still being handled\nhttps://svelte.dev/e/svelte_boundary_reset_onerror");throw e.name="Svelte error",e}else throw new Error("https://svelte.dev/e/svelte_boundary_reset_onerror")}var ve="font-weight: bold",pe="font-weight: normal";function xs(e){v?console.warn(`%c[svelte] await_reactivity_loss
+%cDetected reactivity loss when reading \`${e}\`. This happens when state is read in an async function after an earlier \`await\`
+https://svelte.dev/e/await_reactivity_loss`,ve,pe):console.warn("https://svelte.dev/e/await_reactivity_loss")}function $s(){v?console.warn(`%c[svelte] derived_inert
+%cReading a derived belonging to a now-destroyed effect may result in stale values
+https://svelte.dev/e/derived_inert`,ve,pe):console.warn("https://svelte.dev/e/derived_inert")}function la(e,t,n){v?console.warn(`%c[svelte] hydration_attribute_changed
+%cThe \`${e}\` attribute on \`${t}\` changed its value between server and client renders. The client value, \`${n}\`, will be ignored in favour of the server value
+https://svelte.dev/e/hydration_attribute_changed`,ve,pe):console.warn("https://svelte.dev/e/hydration_attribute_changed")}function oa(e){v?console.warn(`%c[svelte] hydration_html_changed
+%c${e?`The value of an \`{@html ...}\` block ${e} changed between server and client renders. The client value will be ignored in favour of the server value`:"The value of an `{@html ...}` block changed between server and client renders. The client value will be ignored in favour of the server value"}
+https://svelte.dev/e/hydration_html_changed`,ve,pe):console.warn("https://svelte.dev/e/hydration_html_changed")}function Jt(e){v?console.warn(`%c[svelte] hydration_mismatch
+%cHydration failed because the initial UI does not match what was rendered on the server
+https://svelte.dev/e/hydration_mismatch`,ve,pe):console.warn("https://svelte.dev/e/hydration_mismatch")}function Os(){v?console.warn(`%c[svelte] lifecycle_double_unmount
+%cTried to unmount a component that was not mounted
+https://svelte.dev/e/lifecycle_double_unmount`,ve,pe):console.warn("https://svelte.dev/e/lifecycle_double_unmount")}function fa(){v?console.warn("%c[svelte] select_multiple_invalid_value\n%cThe `value` property of a `