LehongWu commited on
Commit
df31fd7
·
verified ·
1 Parent(s): 7e26c82

Upload folder using huggingface_hub

Browse files
README.md CHANGED
@@ -9,21 +9,13 @@ pinned: false
9
 
10
  # Gemini Studio Web
11
 
12
- Gemini 图片 / 视频 Web 应用(FastAPI + React)。根目录 `README.md` Hugging Face Spaces 的 YAML 头详细说明见 `docs/`。
13
-
14
- ## Documentation
15
 
16
  | Document | Contents |
17
  |----------|----------|
18
- | [**docs/SPEC_WEB_UI.md**](docs/SPEC_WEB_UI.md) | 功能与界面规格(原 PLAN) |
19
- | [**docs/WEB_DEV_GUIDE.md**](docs/WEB_DEV_GUIDE.md) | 环境、部署、Hugging Face |
20
-
21
- 索引:[docs/README.md](docs/README.md)。
22
-
23
- ---
24
-
25
- ## Hugging Face(摘要)
26
 
27
- 上传、`README` 元数据、Secrets、本地 Docker:见 **[docs/WEB_DEV_GUIDE.md §10](docs/WEB_DEV_GUIDE.md#10-hugging-face-spaces-docker)**。
28
 
29
- Space **Settings Variables and secrets** 至少配置`GEMINI_API_KEY`、`WEB_UI_PASSWORD`、`SESSION_SECRET`(大小写一致,含义见 §10.4 与 §3)
 
9
 
10
  # Gemini Studio Web
11
 
12
+ Gemini 图片 / 视频 Web(FastAPI + React)。根目录 YAML Hugging Face Spaces;说明见 `docs/`。
 
 
13
 
14
  | Document | Contents |
15
  |----------|----------|
16
+ | [docs/SPEC_WEB_UI.md](docs/SPEC_WEB_UI.md) | 功能与界面规格 |
17
+ | [docs/WEB_DEV_GUIDE.md](docs/WEB_DEV_GUIDE.md) | 环境、部署、HF Docker |
 
 
 
 
 
 
18
 
19
+ ## Hugging Face
20
 
21
+ 上传与 Secrets:见 [docs/WEB_DEV_GUIDE.md Hugging Face](docs/WEB_DEV_GUIDE.md#hugging-face-spaces-docker)。至少配置 `GEMINI_API_KEY`、`WEB_UI_PASSWORD`、`SESSION_SECRET`。
docs/README.md CHANGED
@@ -3,6 +3,6 @@
3
  | File | Contents |
4
  |------|----------|
5
  | [SPEC_WEB_UI.md](./SPEC_WEB_UI.md) | Product spec |
6
- | [WEB_DEV_GUIDE.md](./WEB_DEV_GUIDE.md) | Runbook (local, env, Docker, HF §10) |
7
 
8
- Overview: [README.md](../README.md).
 
3
  | File | Contents |
4
  |------|----------|
5
  | [SPEC_WEB_UI.md](./SPEC_WEB_UI.md) | Product spec |
6
+ | [WEB_DEV_GUIDE.md](./WEB_DEV_GUIDE.md) | Setup, env, Docker, Hugging Face |
7
 
8
+ [README.md](../README.md)
docs/SPEC_WEB_UI.md CHANGED
@@ -1,72 +1,41 @@
1
  # Web UI — product specification
2
 
3
- **Scope:** what to build and UX expectations (implementers / AI). **How to run / deploy:** [WEB_DEV_GUIDE.md](./WEB_DEV_GUIDE.md).
4
 
5
- ---
6
 
7
- # Overview
8
 
9
- Web UI for image and video generation: prompts plus optional reference images.
10
 
11
- ## 板块:AI 创作台
12
 
13
- **A. 图片生成或编辑** — 0–3 张参考图 + 提示词 → 一张图。
14
 
15
- - 思考强度:三项并列(模型 + 强度)Flash(快速,默认 minimal)、Flash(长思考,high)、Pro(长思考,high)对应 `gemini-3.1-flash-image-preview` / `gemini-3-pro-image-preview` + `thinking_level`
16
- - 宽高比:`1:1` … `21:9`(见 `generation_options.json`)。
17
- - 分辨率:`1K` / `2K` / `4K`。
18
 
19
- **B. 视频生成** — 0–3 张参考图 + 提示词 → 短视频。
20
 
21
- - 模型(可配置):`veo-3.1-generate-preview`、`veo-3.1-lite-generate-preview`、`veo-3.1-fast-generate-preview`;界面:**(标准)/(轻量)/(快速)**。Lite **不支持参考**`supports_reference_images`)。
22
- - 宽高比16:9 9:16;分辨率:720p / 1080p / 4k
23
- - 时长纯文案 4/6/8 秒(与分辨率以 API 为准)**有参考图时固定 8 秒**
 
24
 
25
- **C. 视频生成(首尾过渡)** — 至少 1 张起始帧 + 提词;结尾帧可选或「与起始相同」。
26
 
27
- - 模型:同 B;宽高比、分辨率:同 B
28
- - 时长:**固定 8 秒**(首/尾帧条件不接受 4/6 秒)。
29
 
30
- ## 第二板块:辅助工具
31
 
32
- - **超分辨率:** 与图片编辑同一路径,1 张参考图 → 更高清;默认提示词先上传原图(必填);按原图宽高比在列表中自动择近匹配,**宽高比控件放在表单最下**、可手改。分辨率默认 4K(不随原自动推断),自选 1K/2K/4K原图比例与列表都不接近时 **警告**,仍可生成。模型默认「快速」,可改「标准」。
33
- - **提取视频帧:** 上传视频 → 可拖动时间轴 → 预览时间与画面 → 「下载」导出该帧 PNG。
34
- - **图像裁剪**(前端 Canvas,`/tools/crop`,不上传服务器):裁剪框可平移、四角缩放;「自由」比例下四边白条可拖;固定比例选项与图片宽高比列表一致(另加「自由」);裁剪区即原图、结果实时更新;下载 PNG。
35
- - **替换纯色背景**(前端 Canvas,`/tools/replace-bg`):适合纯色或大块相近背景;原色/目标色(系统调色板、手动 R/G/B 或 Hex、预览图点击取色);不透明度 0–100%;RGB 距离 ≤ 容差的像素替换为目标色;预览与 PNG 下载。
36
 
37
- ## 第三板块:示例
38
 
39
- 创作台的静态示例:预置提示词与 `assets/` 素材,展示输出;不调用模型
 
 
40
 
41
- 简介:Wake-UP 人声乐团(北大 2025–2026 十佳歌手冠军)比赛彩幕相关能力展示。
42
 
43
- 1. 手写字体:两张 output
44
- 2. 图上加字:提示词 + input + output
45
- 3. 循环视频:提示词 + input + output 视频。
46
- 4. 超分辨率:input + output。
47
- 5. 视频延伸(长视频):仅 output +「敬请期待」。
48
-
49
- ## 示例脚本(仓库内)
50
-
51
- - A:`run_gen_image_image_cond.sh`、`run_gen_image_prompt_only.sh`
52
- - B:`run_gen_video_prompt_only.sh`
53
- - C:`run_gen_video_image_start_end_diff.sh`
54
-
55
- 脚本可能只覆盖单图等简化场景。完整 API 说明:[图片](https://ai.google.dev/gemini-api/docs/image-generation?hl=zh-cn)、[视频](https://ai.google.dev/gemini-api/docs/video?hl=zh-cn)。
56
-
57
- # 界面要求
58
-
59
- - 启动页:密码登录(密码来自环境变量)。
60
- - 登录后侧栏进入各功能,每项含:简介、**必填**提示词、上传区(A/B 最多三张参考图,均可空;C 起始帧必填、结尾可选、可勾选与起始相同)、模型/宽高比/分辨率(视频含时长,受 API/参考图约束)、结果展示区。
61
- - 生成中显示耗时,避免「卡住」感。
62
-
63
- # 其他
64
-
65
- - `GEMINI_API_KEY` 仅环境变量,禁止写入代码仓库。
66
- - 代码英文;界面文案与提示词可用中文。
67
- - 视觉:简洁、有一定设计感。
68
-
69
- # 稳定 URL 与可配置选项
70
-
71
- - 稳定访问域名:用自有域名 + DNS、静态 IP、反代或隧道等(见 WEB_DEV_GUIDE §8)。
72
- - 模型名、分辨率、宽高比等:**不要写死在代码里**;集中在 **`web/config/generation_options.json`**,可用 **`GENERATION_OPTIONS_PATH`** 指向其他文件。改 JSON 通常不必重编前端;改 React/样式需 `cd web/frontend && npm run build`。
 
1
  # Web UI — product specification
2
 
3
+ **What to build:** this file. **How to run / deploy:** [WEB_DEV_GUIDE.md](./WEB_DEV_GUIDE.md).
4
 
5
+ ## Overview
6
 
7
+ Image and video generation from prompts and optional reference images.
8
 
9
+ ## AI 创作台
10
 
11
+ **A. 图片** — 0–3 张参考图 + 提示词 → 张图。思考强度三档(Flash minimal / Flash high / Pro high);模型 ID 与 `thinking_level` 见后端。宽高比、分辨率见 `generation_options.json`。
12
 
13
+ **B. 视频** — 0–3 张参考图 + 提示词 → 短视频。Veo 模型可配置;Lite **`supports_reference_images: false`**。有参考时时长 **8s**;仅文案时 4/6/8s(以 API 为准)
14
 
15
+ **C. 首尾过渡**起始帧必填 + 提示词结尾帧可选或「与起始相同」。时长 **固定 8s**
 
 
16
 
17
+ ## 辅助工具
18
 
19
+ - **超分辨率:** 单张原 → 高清图;默认提示词可改;宽高比按原图自动建议表单底部可改;比例不匹配时警告仍可生成
20
+ - **提取视频帧** 本地视频 时间轴选帧 PNG
21
+ - **图像裁剪** 前端 Canvas,不上传服务器自由或固定比例 PNG
22
+ - **替换纯色背景:** 前端 Canvas;原/目标色、容差、预览与 PNG。
23
 
24
+ ##
25
 
26
+ 静态演示(预置素材),不调用模型。
 
27
 
28
+ ## 仓库脚本
29
 
30
+ `run_gen_image_*.sh`、`run_gen_video_*.sh` 能只覆盖简化场景API 文档:[片](https://ai.google.dev/gemini-api/docs/image-generation?hl=zh-cn)、[视频](https://ai.google.dev/gemini-api/docs/video?hl=zh-cn)
 
 
 
31
 
32
+ ## 界面
33
 
34
+ - 密码登录(`WEB_UI_PASSWORD`
35
+ - 侧栏进入各功能;提示词必填;参考图规则按 A/B/C;生成中显示耗时。
36
+ - 模型名、分辨率、宽高比等来自 **`web/config/generation_options.json`**(可用 **`GENERATION_OPTIONS_PATH`**),避免写死在代码里;改 JSON 通常不必重编前端。
37
 
38
+ ## 其他
39
 
40
+ - `GEMINI_API_KEY` 仅环境变量,勿入库
41
+ - 代码英文为主;界面与提示词可用中文
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/WEB_DEV_GUIDE.md CHANGED
@@ -1,18 +1,14 @@
1
  # Web UI — developer guide
2
 
3
- Set up, run, and deploy the Gemini Studio Web stack (FastAPI + React). Product scope: **[SPEC_WEB_UI.md](./SPEC_WEB_UI.md)**.
4
 
5
- **Imports:** Code lives under `VideoGeneration-release/web/`. Use **`PYTHONPATH=.`** with cwd = **`VideoGeneration-release`** (parent of `web/`). Do not set `PYTHONPATH` to `web/` alone.
6
 
7
- ## 1. Prerequisites
8
 
9
- - Python 3.10+
10
- - Node.js 18+ and npm (frontend build)
11
- - **ffmpeg** on `PATH` (strip audio from MP4s)
12
 
13
- ## 2. Python venv
14
-
15
- From **`VideoGeneration-release`**:
16
 
17
  ```bash
18
  cd /path/to/VideoGeneration-release
@@ -21,17 +17,17 @@ source .venv/bin/activate # Windows: .venv\Scripts\activate
21
  pip install -r web/requirements.txt
22
  ```
23
 
24
- ## 3. Environment variables
25
 
26
- Do not commit secrets; use a `.env` with your process manager if you like.
27
 
28
  | Variable | Purpose |
29
  |----------|---------|
30
- | `GEMINI_API_KEY` | Server-side only. |
31
  | `WEB_UI_PASSWORD` | Login password. |
32
  | `SESSION_SECRET` | Session signing, e.g. `openssl rand -hex 32`. |
33
- | `GENERATION_OPTIONS_PATH` | Optional. Overrides default `web/config/generation_options.json`. |
34
- | `SESSION_COOKIE_SECURE` | Optional. Set to `1` / `true` / `yes` to send session cookies with the **Secure** flag (use on real HTTPS, e.g. Hugging Face). Leave unset for `http://127.0.0.1` local dev. Requires proxy to set `X-Forwarded-Proto` (see §10). |
35
 
36
  ```bash
37
  export GEMINI_API_KEY="your_key"
@@ -39,14 +35,14 @@ export WEB_UI_PASSWORD="password"
39
  export SESSION_SECRET="$(openssl rand -hex 32)"
40
  ```
41
 
42
- ## 4. `generation_options.json`
43
 
44
- Edit **`web/config/generation_options.json`** (or the file from `GENERATION_OPTIONS_PATH`). The UI loads **`GET /api/config/generation-options`** after login. JSON-only changes need no frontend rebuild; React/TS/CSS changes do.
45
 
46
- - **`image`**: `models` (`value` + `label`), `aspect_ratios`, `resolutions`, `thinking_levels`.
47
- - **`video`** / **`video_frames`**: `models`, `aspect_ratios`, `resolutions`, `durations_seconds`. On **`video`**, **`supports_reference_images`** (e.g. Veo Lite = `false`). With reference images, duration is forced to **8s**. **首尾帧** route always **8s** (no 4/6s like prompt-only 720p).
48
 
49
- ## 5. Build frontend
50
 
51
  ```bash
52
  cd web/frontend
@@ -54,59 +50,44 @@ npm install
54
  npm run build
55
  ```
56
 
57
- Output: **`web/backend/static/`**. Missing build root URL returns 503.
58
 
59
- ## 6. Run server
60
 
61
  ```bash
62
  cd /path/to/VideoGeneration-release
63
  PYTHONPATH=. uvicorn web.backend.main:app --host 127.0.0.1 --port 8000
64
  ```
65
 
66
- LAN: `--host 0.0.0.0`. Dev: add `--reload`.
67
-
68
- ## 7. Dev mode (hot reload)
69
 
70
- **A** API, cwd **`VideoGeneration-release`**:
71
 
72
- ```bash
73
- PYTHONPATH=. uvicorn web.backend.main:app --reload --host 127.0.0.1 --port 8000
74
- ```
75
 
76
- **B** — Vite (`web/frontend`): `npm run dev` (proxies `/api` to 8000). Open the printed URL (e.g. `http://127.0.0.1:5173`).
77
 
78
- ## 8. Stable URL
79
 
80
- The app does not provide a hostname. Use DNS to a domain you control, static/elastic IP, reverse proxy (nginx/Caddy), or a tunnel (Cloudflare Tunnel, Tailscale Funnel, etc.). **One DNS name → current server** when you move machines.
81
 
82
- ## 9. Checklist
83
 
84
  - [ ] `ffmpeg -version`
85
  - [ ] `pip install -r web/requirements.txt`
86
  - [ ] `npm run build` in `web/frontend` at least once
87
  - [ ] `GEMINI_API_KEY`, `WEB_UI_PASSWORD`, `SESSION_SECRET` set
88
- - [ ] Uvicorn from **`VideoGeneration-release`** with `PYTHONPATH=.`
89
-
90
- ## 10. Hugging Face Spaces (Docker)
91
-
92
- Use **Docker** (`sdk: docker`), not Streamlit/Gradio. **`Dockerfile`** at repo root next to `web/` and `assets/`.
93
 
94
- ### 10.1 Create Space
95
 
96
- [Hugging Face](https://huggingface.co/) → New Space → **SDK: Docker**. Space id: **`YOUR_USERNAME/YOUR_SPACE_NAME`**.
97
 
98
- ### 10.2 Root `README.md`
99
 
100
- YAML frontmatter must validate. This repo’s README is preconfigured.
101
 
102
- - **`sdk: docker`**
103
- - **`colorFrom`** / **`colorTo`**: each must be one of **`red`**, **`yellow`**, **`green`**, **`blue`**, **`indigo`**, **`purple`**, **`pink`**, **`gray`** (other names → upload error).
104
-
105
- ### 10.3 Upload
106
-
107
- **Cwd:** **`VideoGeneration-release`** (contains `Dockerfile`, `web/`, `assets/`).
108
-
109
- **CLI** ([write token](https://huggingface.co/settings/tokens)):
110
 
111
  ```bash
112
  pip install -U huggingface_hub
@@ -115,45 +96,17 @@ cd /path/to/VideoGeneration-release
115
  hf upload YOUR_USERNAME/YOUR_SPACE_NAME . . --repo-type space
116
  ```
117
 
118
- Example:
119
-
120
- ```bash
121
- hf upload LehongWu/VideoGeneration-release . . --repo-type space
122
- ```
123
-
124
- Then check **Space → Logs** for the Docker build.
125
-
126
- **Git:**
127
-
128
- ```bash
129
- git remote add hf https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME
130
- git push -u hf main
131
- ```
132
-
133
- Or connect GitHub in Space settings and push there.
134
-
135
- ### 10.4 Secrets
136
-
137
- **Settings → Variables and secrets** — names must match exactly (case-sensitive). Same as [§3](#3-environment-variables).
138
-
139
- | Name | Purpose |
140
- |------|---------|
141
- | `GEMINI_API_KEY` | Gemini key; never commit. |
142
- | `WEB_UI_PASSWORD` | Login password. |
143
- | `SESSION_SECRET` | e.g. `openssl rand -hex 32`. |
144
 
145
- Optional: `GENERATION_OPTIONS_PATH` (custom JSON path inside the container).
146
 
147
- Saving restarts the Space. Never put secrets in Git or `README.md`.
148
 
149
- ### 10.5 Runtime
150
 
151
- - Listens on **`PORT`** or **`7860`** (`Dockerfile`: `uvicorn ... --port ${PORT:-7860}`).
152
- - **`--forwarded-allow-ips='*'`** — required behind HF’s reverse proxy so `X-Forwarded-Proto: https` is trusted. Without it, Uvicorn often sees `http`, session cookies misbehave, and the UI **keeps asking you to log in**.
153
- - **ffmpeg** in image.
154
- - First build / cold start can take minutes.
155
 
156
- ### 10.6 Local Docker
157
 
158
  ```bash
159
  cd /path/to/VideoGeneration-release
 
1
  # Web UI — developer guide
2
 
3
+ Setup, run, deploy (FastAPI + React). Product scope: **[SPEC_WEB_UI.md](./SPEC_WEB_UI.md)**.
4
 
5
+ **Imports:** Code under `VideoGeneration-release/web/`. Run Python with cwd **`VideoGeneration-release`** and **`PYTHONPATH=.`** (not `web/` alone).
6
 
7
+ ## Prerequisites
8
 
9
+ Python 3.10+, Node 18+, npm, **ffmpeg** on `PATH`.
 
 
10
 
11
+ ## Python venv
 
 
12
 
13
  ```bash
14
  cd /path/to/VideoGeneration-release
 
17
  pip install -r web/requirements.txt
18
  ```
19
 
20
+ ## Environment variables
21
 
22
+ Do not commit secrets.
23
 
24
  | Variable | Purpose |
25
  |----------|---------|
26
+ | `GEMINI_API_KEY` | Server-side API key. |
27
  | `WEB_UI_PASSWORD` | Login password. |
28
  | `SESSION_SECRET` | Session signing, e.g. `openssl rand -hex 32`. |
29
+ | `GENERATION_OPTIONS_PATH` | Optional; overrides default `web/config/generation_options.json`. |
30
+ | `SESSION_COOKIE_SECURE` | Optional. `1`/`true`/`yes` for HTTPS-only cookies (e.g. Hugging Face). Unset for local `http://127.0.0.1`. Proxy must send `X-Forwarded-Proto` (see Hugging Face / Docker below). |
31
 
32
  ```bash
33
  export GEMINI_API_KEY="your_key"
 
35
  export SESSION_SECRET="$(openssl rand -hex 32)"
36
  ```
37
 
38
+ ## `generation_options.json`
39
 
40
+ Edit **`web/config/generation_options.json`** (or path from `GENERATION_OPTIONS_PATH`). UI loads it via **`GET /api/config/generation-options`** after login. JSON-only changes need no frontend rebuild.
41
 
42
+ - **`image`**: `models`, `aspect_ratios`, `resolutions`.
43
+ - **`video`** / **`video_frames`**: `models`, `aspect_ratios`, `resolutions`, `durations_seconds`. On **`video`**, **`supports_reference_images: false`** hides reference images (e.g. Veo Lite). With reference images, duration is **8s**. **首尾帧** route is always **8s**.
44
 
45
+ ## Build frontend
46
 
47
  ```bash
48
  cd web/frontend
 
50
  npm run build
51
  ```
52
 
53
+ Output: **`web/backend/static/`**. Without it, root URL returns 503.
54
 
55
+ ## Run server
56
 
57
  ```bash
58
  cd /path/to/VideoGeneration-release
59
  PYTHONPATH=. uvicorn web.backend.main:app --host 127.0.0.1 --port 8000
60
  ```
61
 
62
+ LAN: `--host 0.0.0.0`. Dev: `--reload`.
 
 
63
 
64
+ ## Dev (hot reload)
65
 
66
+ **API:** `PYTHONPATH=. uvicorn web.backend.main:app --reload --host 127.0.0.1 --port 8000` from repo root.
 
 
67
 
68
+ **Vite:** `cd web/frontend && npm run dev` proxies `/api` to 8000; open the printed URL (e.g. `http://127.0.0.1:5173`).
69
 
70
+ ## Stable URL
71
 
72
+ Use your own DNS/domain, static IP, reverse proxy, or tunnel no hostname is baked into the app.
73
 
74
+ ## Checklist
75
 
76
  - [ ] `ffmpeg -version`
77
  - [ ] `pip install -r web/requirements.txt`
78
  - [ ] `npm run build` in `web/frontend` at least once
79
  - [ ] `GEMINI_API_KEY`, `WEB_UI_PASSWORD`, `SESSION_SECRET` set
80
+ - [ ] Uvicorn from repo root with `PYTHONPATH=.`
 
 
 
 
81
 
82
+ ## Hugging Face Spaces (Docker)
83
 
84
+ Use **Docker** Space (`sdk: docker`). **`Dockerfile`** at repo root beside `web/` and `assets/`.
85
 
86
+ ### Create & upload
87
 
88
+ [Hugging Face](https://huggingface.co/) → New Space → **SDK: Docker**. Root **`README.md`** YAML must validate (`sdk: docker`; **`colorFrom`** / **`colorTo`** each one of: `red`, `yellow`, `green`, `blue`, `indigo`, `purple`, `pink`, `gray`).
89
 
90
+ **Upload** from **`VideoGeneration-release`** (contains `Dockerfile`, `web/`, `assets/`):
 
 
 
 
 
 
 
91
 
92
  ```bash
93
  pip install -U huggingface_hub
 
96
  hf upload YOUR_USERNAME/YOUR_SPACE_NAME . . --repo-type space
97
  ```
98
 
99
+ Or add git remote `https://huggingface.co/spaces/USER/SPACE` and push.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
+ ### Secrets
102
 
103
+ **Settings Variables and secrets** same names as the env table above (case-sensitive). Optional: `GENERATION_OPTIONS_PATH`. Saving restarts the Space.
104
 
105
+ ### Runtime (Dockerfile)
106
 
107
+ Listens on **`PORT`** or **7860**. **`--forwarded-allow-ips='*'`** is required behind HF’s proxy so HTTPS and session cookies work. **ffmpeg** in image. Cold start can take minutes.
 
 
 
108
 
109
+ ### Local Docker
110
 
111
  ```bash
112
  cd /path/to/VideoGeneration-release
web/backend/static/assets/index-DK0fQxAx.js ADDED
The diff for this file is too large to render. See raw diff
 
web/backend/static/assets/index-DKu6Axh1.css ADDED
@@ -0,0 +1 @@
 
 
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:DM Sans,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:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,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}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.order-1{order:1}.order-2{order:2}.m-0{margin:0}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-auto{margin-top:auto}.line-clamp-3{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3}.block{display:block}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-2{height:.5rem}.h-20{height:5rem}.max-h-64{max-height:16rem}.max-h-\[360px\]{max-height:360px}.max-h-\[min\(70vh\,52rem\)\]{max-height:min(70vh,52rem)}.min-h-\[100px\]{min-height:100px}.min-h-\[120px\]{min-height:120px}.min-h-screen{min-height:100vh}.w-16{width:4rem}.w-20{width:5rem}.w-\[17\.5rem\]{width:17.5rem}.w-full{width:100%}.min-w-0{min-width:0px}.min-w-\[140px\]{min-width:140px}.min-w-\[15rem\]{min-width:15rem}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-5xl{max-width:64rem}.max-w-7xl{max-width:80rem}.max-w-full{max-width:100%}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.cursor-crosshair{cursor:crosshair}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize{resize:both}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.gap-7{gap:1.75rem}.space-y-14>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(3.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(3.5rem * 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))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(2rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem * var(--tw-space-y-reverse))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.whitespace-pre-wrap{white-space:pre-wrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-none{border-radius:0}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-0{border-width:0px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-amber-200\/80{border-color:#fde68acc}.border-ink{--tw-border-opacity: 1;border-color:rgb(var(--color-ink) / var(--tw-border-opacity, 1))}.border-line\/80{border-color:rgb(var(--color-line) / .8)}.border-line\/90{border-color:rgb(var(--color-line) / .9)}.border-red-200{--tw-border-opacity: 1;border-color:rgb(254 202 202 / var(--tw-border-opacity, 1))}.border-slate-200{--tw-border-opacity: 1;border-color:rgb(226 232 240 / var(--tw-border-opacity, 1))}.border-slate-200\/80{border-color:#e2e8f0cc}.border-slate-200\/90{border-color:#e2e8f0e6}.border-slate-300{--tw-border-opacity: 1;border-color:rgb(203 213 225 / var(--tw-border-opacity, 1))}.border-slate-300\/80{border-color:#cbd5e1cc}.border-slate-400\/90{border-color:#94a3b8e6}.bg-amber-50{--tw-bg-opacity: 1;background-color:rgb(255 251 235 / var(--tw-bg-opacity, 1))}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.bg-black\/5{background-color:#0000000d}.bg-ink{--tw-bg-opacity: 1;background-color:rgb(var(--color-ink) / var(--tw-bg-opacity, 1))}.bg-neutral-200{--tw-bg-opacity: 1;background-color:rgb(229 229 229 / var(--tw-bg-opacity, 1))}.bg-panel\/95{background-color:rgb(var(--color-panel) / .95)}.bg-red-50{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.bg-slate-100{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity, 1))}.bg-slate-400\/90{background-color:#94a3b8e6}.bg-slate-50\/90{background-color:#f8fafce6}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-white\/60{background-color:#fff9}.bg-white\/80{background-color:#fffc}.bg-white\/85{background-color:#ffffffd9}.bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.from-paper{--tw-gradient-from: rgb(var(--color-paper) / 1) var(--tw-gradient-from-position);--tw-gradient-to: rgb(var(--color-paper) / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.via-panel\/30{--tw-gradient-to: rgb(var(--color-panel) / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), rgb(var(--color-panel) / .3) var(--tw-gradient-via-position), var(--tw-gradient-to)}.to-paper{--tw-gradient-to: rgb(var(--color-paper) / 1) var(--tw-gradient-to-position)}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-10{padding:2.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.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}.pl-0\.5{padding-left:.125rem}.pt-6{padding-top:1.5rem}.pt-8{padding-top:2rem}.text-left{text-align:left}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.625rem;line-height:2rem}.text-3xl{font-size:2rem;line-height:2.25rem}.text-base{font-size:1.0625rem;line-height:1.5rem}.text-sm{font-size:.9375rem;line-height:1.375rem}.text-xl{font-size:1.3125rem;line-height:1.75rem}.text-xs{font-size:.8125rem;line-height:1.125rem}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.tabular-nums{--tw-numeric-spacing: tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-relaxed{line-height:1.625}.tracking-wide{letter-spacing:.025em}.text-amber-800{--tw-text-opacity: 1;color:rgb(146 64 14 / var(--tw-text-opacity, 1))}.text-amber-900{--tw-text-opacity: 1;color:rgb(120 53 15 / var(--tw-text-opacity, 1))}.text-ink{--tw-text-opacity: 1;color:rgb(var(--color-ink) / var(--tw-text-opacity, 1))}.text-ink\/70{color:rgb(var(--color-ink) / .7)}.text-ink\/80{color:rgb(var(--color-ink) / .8)}.text-ink\/85{color:rgb(var(--color-ink) / .85)}.text-ink\/90{color:rgb(var(--color-ink) / .9)}.text-ink\/95{color:rgb(var(--color-ink) / .95)}.text-mist{--tw-text-opacity: 1;color:rgb(var(--color-mist) / var(--tw-text-opacity, 1))}.text-mist\/90{color:rgb(var(--color-mist) / .9)}.text-paper{--tw-text-opacity: 1;color:rgb(var(--color-paper) / var(--tw-text-opacity, 1))}.text-red-700{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity, 1))}.text-red-800{--tw-text-opacity: 1;color:rgb(153 27 27 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.underline{text-decoration-line:underline}.underline-offset-2{text-underline-offset:2px}.accent-ink{accent-color:rgb(var(--color-ink) / 1)}.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-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px 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-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 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\/10{--tw-shadow-color: rgb(0 0 0 / .1);--tw-shadow: var(--tw-shadow-colored)}.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-sm{--tw-backdrop-blur: blur(4px);-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-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}:root,html[data-theme=blue]{--color-paper: 238 241 248;--color-panel: 216 228 242;--color-line: 168 184 208;--color-mist: 71 85 105;--color-ink: 15 23 42;--color-clay: 99 102 241}html[data-theme=cream]{--color-paper: 252 250 245;--color-panel: 241 236 226;--color-line: 214 204 188;--color-mist: 87 83 78;--color-ink: 15 23 42;--color-clay: 120 113 108}html[data-theme=gray]{--color-paper: 244 244 246;--color-panel: 232 232 235;--color-line: 186 190 198;--color-mist: 82 82 91;--color-ink: 15 23 42;--color-clay: 82 82 91}html[data-theme=lavender]{--color-paper: 247 246 252;--color-panel: 234 231 245;--color-line: 196 191 220;--color-mist: 78 74 98;--color-ink: 15 23 42;--color-clay: 109 40 217}html{font-size:100%}html[data-font-size=compact]{font-size:75%}html[data-font-size=small]{font-size:90%}html[data-font-size=standard]{font-size:105%}html[data-font-size=large]{font-size:120%}html[data-font-size=xlarge]{font-size:135%}body{min-height:100vh;--tw-bg-opacity: 1;background-color:rgb(var(--color-paper) / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(var(--color-ink) / var(--tw-text-opacity, 1));-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:DM Sans,system-ui,sans-serif;line-height:1.55}::-moz-selection{background-color:rgb(var(--color-clay) / .22)}::selection{background-color:rgb(var(--color-clay) / .22)}h1,h2,h3,.font-display{font-family:"Source Serif 4",Georgia,serif}.file\:mr-3::file-selector-button{margin-right:.75rem}.file\:rounded-lg::file-selector-button{border-radius:.5rem}.file\:border-0::file-selector-button{border-width:0px}.file\:bg-slate-200::file-selector-button{--tw-bg-opacity: 1;background-color:rgb(226 232 240 / var(--tw-bg-opacity, 1))}.file\:px-3::file-selector-button{padding-left:.75rem;padding-right:.75rem}.file\:py-1\.5::file-selector-button{padding-top:.375rem;padding-bottom:.375rem}.file\:text-sm::file-selector-button{font-size:.9375rem;line-height:1.375rem}.file\:text-ink::file-selector-button{--tw-text-opacity: 1;color:rgb(var(--color-ink) / var(--tw-text-opacity, 1))}.placeholder\:text-slate-400::-moz-placeholder{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity, 1))}.placeholder\:text-slate-400::placeholder{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity, 1))}.hover\:border-clay\/80:hover{border-color:rgb(var(--color-clay) / .8)}.hover\:bg-slate-50:hover{--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity, 1))}.hover\:bg-slate-800:hover{--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity, 1))}.hover\:bg-white\/60:hover{background-color:#fff9}.hover\:text-ink:hover{--tw-text-opacity: 1;color:rgb(var(--color-ink) / var(--tw-text-opacity, 1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--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(2px + 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)}.focus\:ring-clay\/40:focus{--tw-ring-color: rgb(var(--color-clay) / .4)}.focus\:ring-clay\/50:focus{--tw-ring-color: rgb(var(--color-clay) / .5)}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.disabled\:opacity-60:disabled{opacity:.6}.disabled\:opacity-\[0\.92\]:disabled{opacity:.92}@media (min-width: 640px){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:p-10{padding:2.5rem}.sm\:p-12{padding:3rem}.sm\:p-7{padding:1.75rem}}@media (min-width: 768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width: 1024px){.lg\:sticky{position:sticky}.lg\:top-4{top:1rem}.lg\:order-1{order:1}.lg\:order-2{order:2}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-\[1fr_minmax\(200px\,260px\)\]{grid-template-columns:1fr minmax(200px,260px)}.lg\:p-12{padding:3rem}}@media (min-width: 1280px){.xl\:max-w-\[96rem\]{max-width:96rem}}@media (min-width: 1536px){.\32xl\:max-w-\[min\(104rem\,calc\(100vw-3rem\)\)\]{max-width:min(104rem,calc(100vw - 3rem))}}
web/backend/static/index.html CHANGED
@@ -13,7 +13,8 @@
13
  <script>
14
  (function () {
15
  var V2 = 2;
16
- var valid = function (fs) {
 
17
  return (
18
  fs === "compact" ||
19
  fs === "small" ||
@@ -21,30 +22,39 @@
21
  fs === "large" ||
22
  fs === "xlarge"
23
  );
24
- };
 
 
 
25
  try {
26
  var raw = localStorage.getItem("videogen-ui-settings");
27
  var fs = "standard";
 
28
  if (raw) {
29
  var o = JSON.parse(raw);
30
  if (o && o.fontSize) {
31
- if (o.version === V2 && valid(o.fontSize)) {
32
- fs = o.fontSize;
33
  } else {
34
  if (o.fontSize === "comfortable") fs = "standard";
35
  else if (o.fontSize === "standard") fs = "small";
36
- else if (valid(o.fontSize)) fs = o.fontSize;
37
  }
38
  }
 
 
 
39
  }
40
  document.documentElement.dataset.fontSize = fs;
 
41
  } catch (e) {
42
  document.documentElement.dataset.fontSize = "standard";
 
43
  }
44
  })();
45
  </script>
46
- <script type="module" crossorigin src="/assets/index-DKLBzrVa.js"></script>
47
- <link rel="stylesheet" crossorigin href="/assets/index-CVYDo8sh.css">
48
  </head>
49
  <body>
50
  <div id="root"></div>
 
13
  <script>
14
  (function () {
15
  var V2 = 2;
16
+ var V3 = 3;
17
+ function validFs(fs) {
18
  return (
19
  fs === "compact" ||
20
  fs === "small" ||
 
22
  fs === "large" ||
23
  fs === "xlarge"
24
  );
25
+ }
26
+ function validTheme(t) {
27
+ return t === "blue" || t === "cream" || t === "gray" || t === "lavender";
28
+ }
29
  try {
30
  var raw = localStorage.getItem("videogen-ui-settings");
31
  var fs = "standard";
32
+ var theme = "blue";
33
  if (raw) {
34
  var o = JSON.parse(raw);
35
  if (o && o.fontSize) {
36
+ if (o.version === V2 || o.version === V3) {
37
+ if (validFs(o.fontSize)) fs = o.fontSize;
38
  } else {
39
  if (o.fontSize === "comfortable") fs = "standard";
40
  else if (o.fontSize === "standard") fs = "small";
41
+ else if (validFs(o.fontSize)) fs = o.fontSize;
42
  }
43
  }
44
+ if (o && o.colorTheme && validTheme(o.colorTheme)) {
45
+ theme = o.colorTheme;
46
+ }
47
  }
48
  document.documentElement.dataset.fontSize = fs;
49
+ document.documentElement.dataset.theme = theme;
50
  } catch (e) {
51
  document.documentElement.dataset.fontSize = "standard";
52
+ document.documentElement.dataset.theme = "blue";
53
  }
54
  })();
55
  </script>
56
+ <script type="module" crossorigin src="/assets/index-DK0fQxAx.js"></script>
57
+ <link rel="stylesheet" crossorigin href="/assets/index-DKu6Axh1.css">
58
  </head>
59
  <body>
60
  <div id="root"></div>
web/frontend/index.html CHANGED
@@ -13,7 +13,8 @@
13
  <script>
14
  (function () {
15
  var V2 = 2;
16
- var valid = function (fs) {
 
17
  return (
18
  fs === "compact" ||
19
  fs === "small" ||
@@ -21,25 +22,34 @@
21
  fs === "large" ||
22
  fs === "xlarge"
23
  );
24
- };
 
 
 
25
  try {
26
  var raw = localStorage.getItem("videogen-ui-settings");
27
  var fs = "standard";
 
28
  if (raw) {
29
  var o = JSON.parse(raw);
30
  if (o && o.fontSize) {
31
- if (o.version === V2 && valid(o.fontSize)) {
32
- fs = o.fontSize;
33
  } else {
34
  if (o.fontSize === "comfortable") fs = "standard";
35
  else if (o.fontSize === "standard") fs = "small";
36
- else if (valid(o.fontSize)) fs = o.fontSize;
37
  }
38
  }
 
 
 
39
  }
40
  document.documentElement.dataset.fontSize = fs;
 
41
  } catch (e) {
42
  document.documentElement.dataset.fontSize = "standard";
 
43
  }
44
  })();
45
  </script>
 
13
  <script>
14
  (function () {
15
  var V2 = 2;
16
+ var V3 = 3;
17
+ function validFs(fs) {
18
  return (
19
  fs === "compact" ||
20
  fs === "small" ||
 
22
  fs === "large" ||
23
  fs === "xlarge"
24
  );
25
+ }
26
+ function validTheme(t) {
27
+ return t === "blue" || t === "cream" || t === "gray" || t === "lavender";
28
+ }
29
  try {
30
  var raw = localStorage.getItem("videogen-ui-settings");
31
  var fs = "standard";
32
+ var theme = "blue";
33
  if (raw) {
34
  var o = JSON.parse(raw);
35
  if (o && o.fontSize) {
36
+ if (o.version === V2 || o.version === V3) {
37
+ if (validFs(o.fontSize)) fs = o.fontSize;
38
  } else {
39
  if (o.fontSize === "comfortable") fs = "standard";
40
  else if (o.fontSize === "standard") fs = "small";
41
+ else if (validFs(o.fontSize)) fs = o.fontSize;
42
  }
43
  }
44
+ if (o && o.colorTheme && validTheme(o.colorTheme)) {
45
+ theme = o.colorTheme;
46
+ }
47
  }
48
  document.documentElement.dataset.fontSize = fs;
49
+ document.documentElement.dataset.theme = theme;
50
  } catch (e) {
51
  document.documentElement.dataset.fontSize = "standard";
52
+ document.documentElement.dataset.theme = "blue";
53
  }
54
  })();
55
  </script>
web/frontend/src/context/GenerationOptionsContext.tsx CHANGED
@@ -104,16 +104,12 @@ export function GenerationOptionsProvider({ children }: { children: ReactNode })
104
  );
105
  }
106
 
107
- /** Image Flash + video Fast — default selection prefers these IDs. */
108
  const FAST_MODEL_IDS = new Set([
109
  "gemini-3.1-flash-image-preview",
110
  "veo-3.1-fast-generate-preview",
111
  ]);
112
 
113
- /**
114
- * Default model: always the "快速" variant when present in `models`
115
- * (by id or label), else first entry, else `fallback`.
116
- */
117
  export function defaultFastModelValue(
118
  models: { value: string; label?: string }[],
119
  fallback: string,
 
104
  );
105
  }
106
 
 
107
  const FAST_MODEL_IDS = new Set([
108
  "gemini-3.1-flash-image-preview",
109
  "veo-3.1-fast-generate-preview",
110
  ]);
111
 
112
+ /** Prefer Flash/Fast id, else label 快速, else first or `fallback`. */
 
 
 
113
  export function defaultFastModelValue(
114
  models: { value: string; label?: string }[],
115
  fallback: string,
web/frontend/src/context/UiSettingsContext.tsx CHANGED
@@ -10,31 +10,38 @@ import {
10
  } from "react";
11
 
12
  const STORAGE_KEY = "videogen-ui-settings";
13
- const SETTINGS_VERSION = 2;
14
 
15
- /**
16
- * Keys match English: `standard` = default "中" (Standard). `small` = 较小 (Smaller).
17
- * Legacy v1 stored `comfortable` for 中 and `standard` for 较小 — migrated on read.
18
- */
19
  export type FontSizePreset = "compact" | "small" | "standard" | "large" | "xlarge";
20
 
 
 
 
21
  type UiSettings = {
22
  version: number;
23
  fontSize: FontSizePreset;
 
24
  };
25
 
26
  const defaultSettings: UiSettings = {
27
  version: SETTINGS_VERSION,
28
  fontSize: "standard",
 
29
  };
30
 
31
  const PRESETS: FontSizePreset[] = ["compact", "small", "standard", "large", "xlarge"];
32
 
 
 
33
  function isPreset(v: string): v is FontSizePreset {
34
  return PRESETS.includes(v as FontSizePreset);
35
  }
36
 
37
- /** Migrate v1 storage where `standard` meant 较小 and `comfortable` meant 中. */
 
 
 
38
  function migrateV1FontSize(raw: string | undefined): FontSizePreset | null {
39
  if (!raw) return null;
40
  if (raw === "comfortable") return "standard";
@@ -50,16 +57,28 @@ function readStored(): UiSettings {
50
  const parsed = JSON.parse(raw) as {
51
  version?: number;
52
  fontSize?: string;
 
53
  };
54
- let fs: FontSizePreset | null = null;
55
- if (parsed.version === SETTINGS_VERSION && parsed.fontSize && isPreset(parsed.fontSize)) {
56
- fs = parsed.fontSize;
57
- } else {
58
- fs = migrateV1FontSize(parsed.fontSize);
 
 
 
 
 
 
 
59
  }
60
- if (fs) {
61
- return { version: SETTINGS_VERSION, fontSize: fs };
 
 
62
  }
 
 
63
  } catch {
64
  /* ignore */
65
  }
@@ -70,9 +89,15 @@ function applyFontSizeToDocument(fontSize: FontSizePreset) {
70
  document.documentElement.dataset.fontSize = fontSize;
71
  }
72
 
 
 
 
 
73
  type UiSettingsContextValue = {
74
  fontSize: FontSizePreset;
75
  setFontSize: (v: FontSizePreset) => void;
 
 
76
  };
77
 
78
  const UiSettingsContext = createContext<UiSettingsContextValue | null>(null);
@@ -82,6 +107,7 @@ export function UiSettingsProvider({ children }: { children: ReactNode }) {
82
  const s = readStored();
83
  if (typeof document !== "undefined") {
84
  applyFontSizeToDocument(s.fontSize);
 
85
  }
86
  return s;
87
  });
@@ -90,10 +116,18 @@ export function UiSettingsProvider({ children }: { children: ReactNode }) {
90
  applyFontSizeToDocument(settings.fontSize);
91
  }, [settings.fontSize]);
92
 
 
 
 
 
93
  useEffect(() => {
94
  localStorage.setItem(
95
  STORAGE_KEY,
96
- JSON.stringify({ version: SETTINGS_VERSION, fontSize: settings.fontSize }),
 
 
 
 
97
  );
98
  }, [settings]);
99
 
@@ -101,9 +135,18 @@ export function UiSettingsProvider({ children }: { children: ReactNode }) {
101
  setSettings((s) => ({ ...s, fontSize }));
102
  }, []);
103
 
 
 
 
 
104
  const value = useMemo(
105
- () => ({ fontSize: settings.fontSize, setFontSize }),
106
- [settings.fontSize, setFontSize],
 
 
 
 
 
107
  );
108
 
109
  return <UiSettingsContext.Provider value={value}>{children}</UiSettingsContext.Provider>;
 
10
  } from "react";
11
 
12
  const STORAGE_KEY = "videogen-ui-settings";
13
+ const SETTINGS_VERSION = 3;
14
 
15
+ /** Font scale presets; v1 `comfortable`/`standard` names migrated on read. */
 
 
 
16
  export type FontSizePreset = "compact" | "small" | "standard" | "large" | "xlarge";
17
 
18
+ /** Sidebar/page palette via `html[data-theme]`. */
19
+ export type ColorTheme = "blue" | "cream" | "gray" | "lavender";
20
+
21
  type UiSettings = {
22
  version: number;
23
  fontSize: FontSizePreset;
24
+ colorTheme: ColorTheme;
25
  };
26
 
27
  const defaultSettings: UiSettings = {
28
  version: SETTINGS_VERSION,
29
  fontSize: "standard",
30
+ colorTheme: "blue",
31
  };
32
 
33
  const PRESETS: FontSizePreset[] = ["compact", "small", "standard", "large", "xlarge"];
34
 
35
+ const THEMES: ColorTheme[] = ["blue", "cream", "gray", "lavender"];
36
+
37
  function isPreset(v: string): v is FontSizePreset {
38
  return PRESETS.includes(v as FontSizePreset);
39
  }
40
 
41
+ function isTheme(v: string): v is ColorTheme {
42
+ return THEMES.includes(v as ColorTheme);
43
+ }
44
+
45
  function migrateV1FontSize(raw: string | undefined): FontSizePreset | null {
46
  if (!raw) return null;
47
  if (raw === "comfortable") return "standard";
 
57
  const parsed = JSON.parse(raw) as {
58
  version?: number;
59
  fontSize?: string;
60
+ colorTheme?: string;
61
  };
62
+
63
+ let fs: FontSizePreset = "standard";
64
+ if (parsed.fontSize) {
65
+ if (
66
+ (parsed.version === 2 || parsed.version === SETTINGS_VERSION) &&
67
+ isPreset(parsed.fontSize)
68
+ ) {
69
+ fs = parsed.fontSize;
70
+ } else {
71
+ const m = migrateV1FontSize(parsed.fontSize);
72
+ if (m) fs = m;
73
+ }
74
  }
75
+
76
+ let colorTheme: ColorTheme = "blue";
77
+ if (parsed.colorTheme && isTheme(parsed.colorTheme)) {
78
+ colorTheme = parsed.colorTheme;
79
  }
80
+
81
+ return { version: SETTINGS_VERSION, fontSize: fs, colorTheme };
82
  } catch {
83
  /* ignore */
84
  }
 
89
  document.documentElement.dataset.fontSize = fontSize;
90
  }
91
 
92
+ function applyColorThemeToDocument(theme: ColorTheme) {
93
+ document.documentElement.dataset.theme = theme;
94
+ }
95
+
96
  type UiSettingsContextValue = {
97
  fontSize: FontSizePreset;
98
  setFontSize: (v: FontSizePreset) => void;
99
+ colorTheme: ColorTheme;
100
+ setColorTheme: (v: ColorTheme) => void;
101
  };
102
 
103
  const UiSettingsContext = createContext<UiSettingsContextValue | null>(null);
 
107
  const s = readStored();
108
  if (typeof document !== "undefined") {
109
  applyFontSizeToDocument(s.fontSize);
110
+ applyColorThemeToDocument(s.colorTheme);
111
  }
112
  return s;
113
  });
 
116
  applyFontSizeToDocument(settings.fontSize);
117
  }, [settings.fontSize]);
118
 
119
+ useLayoutEffect(() => {
120
+ applyColorThemeToDocument(settings.colorTheme);
121
+ }, [settings.colorTheme]);
122
+
123
  useEffect(() => {
124
  localStorage.setItem(
125
  STORAGE_KEY,
126
+ JSON.stringify({
127
+ version: SETTINGS_VERSION,
128
+ fontSize: settings.fontSize,
129
+ colorTheme: settings.colorTheme,
130
+ }),
131
  );
132
  }, [settings]);
133
 
 
135
  setSettings((s) => ({ ...s, fontSize }));
136
  }, []);
137
 
138
+ const setColorTheme = useCallback((colorTheme: ColorTheme) => {
139
+ setSettings((s) => ({ ...s, colorTheme }));
140
+ }, []);
141
+
142
  const value = useMemo(
143
+ () => ({
144
+ fontSize: settings.fontSize,
145
+ setFontSize,
146
+ colorTheme: settings.colorTheme,
147
+ setColorTheme,
148
+ }),
149
+ [settings.fontSize, settings.colorTheme, setFontSize, setColorTheme],
150
  );
151
 
152
  return <UiSettingsContext.Provider value={value}>{children}</UiSettingsContext.Provider>;
web/frontend/src/index.css CHANGED
@@ -2,12 +2,45 @@
2
  @tailwind components;
3
  @tailwind utilities;
4
 
5
- /*
6
- * Root rem scale via <html data-font-size="..."> (UiSettingsContext, index.html).
7
- * Keys: compact · small · standard · large · xlarge
8
- * CN: · 较小 · 中 · 较大 · 大 — EN: Extra small · Small · Standard · Large · Extra large
9
- * `standard` = 中 (default). `small` = 较小 (not the old ambiguous "standard").
10
- */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  html {
12
  font-size: 100%;
13
  }
@@ -39,7 +72,7 @@ body {
39
  }
40
 
41
  ::selection {
42
- background-color: rgb(99 102 241 / 0.22);
43
  }
44
 
45
  h1,
 
2
  @tailwind components;
3
  @tailwind utilities;
4
 
5
+ /* Themes: html[data-theme]; RGB triplets for Tailwind alpha (e.g. bg-panel/95). */
6
+ :root,
7
+ html[data-theme="blue"] {
8
+ --color-paper: 238 241 248;
9
+ --color-panel: 216 228 242;
10
+ --color-line: 168 184 208;
11
+ --color-mist: 71 85 105;
12
+ --color-ink: 15 23 42;
13
+ --color-clay: 99 102 241;
14
+ }
15
+
16
+ html[data-theme="cream"] {
17
+ --color-paper: 252 250 245;
18
+ --color-panel: 241 236 226;
19
+ --color-line: 214 204 188;
20
+ --color-mist: 87 83 78;
21
+ --color-ink: 15 23 42;
22
+ --color-clay: 120 113 108;
23
+ }
24
+
25
+ html[data-theme="gray"] {
26
+ --color-paper: 244 244 246;
27
+ --color-panel: 232 232 235;
28
+ --color-line: 186 190 198;
29
+ --color-mist: 82 82 91;
30
+ --color-ink: 15 23 42;
31
+ --color-clay: 82 82 91;
32
+ }
33
+
34
+ html[data-theme="lavender"] {
35
+ --color-paper: 247 246 252;
36
+ --color-panel: 234 231 245;
37
+ --color-line: 196 191 220;
38
+ --color-mist: 78 74 98;
39
+ --color-ink: 15 23 42;
40
+ --color-clay: 109 40 217;
41
+ }
42
+
43
+ /* Rem scale: html[data-font-size] from UiSettingsContext / index.html */
44
  html {
45
  font-size: 100%;
46
  }
 
72
  }
73
 
74
  ::selection {
75
+ background-color: rgb(var(--color-clay) / 0.22);
76
  }
77
 
78
  h1,
web/frontend/src/pages/Layout.tsx CHANGED
@@ -2,7 +2,11 @@ import { useEffect, useState } from "react";
2
  import { Link, Navigate, Outlet, useLocation, useNavigate } from "react-router-dom";
3
  import { fetchMe, logout } from "../api";
4
  import { GenerationOptionsProvider } from "../context/GenerationOptionsContext";
5
- import { type FontSizePreset, useUiSettings } from "../context/UiSettingsContext";
 
 
 
 
6
 
7
  type NavItem = { to: string; title: string; sub: string };
8
 
@@ -39,6 +43,13 @@ function NavLink({ item, active }: { item: NavItem; active: boolean }) {
39
  );
40
  }
41
 
 
 
 
 
 
 
 
42
  const FONT_SIZE_OPTIONS: { value: FontSizePreset; label: string; labelEn: string }[] = [
43
  { value: "compact", label: "小", labelEn: "Extra small" },
44
  { value: "small", label: "较小", labelEn: "Small" },
@@ -48,7 +59,7 @@ const FONT_SIZE_OPTIONS: { value: FontSizePreset; label: string; labelEn: string
48
  ];
49
 
50
  export function Layout() {
51
- const { fontSize, setFontSize } = useUiSettings();
52
  const [ok, setOk] = useState<boolean | null>(null);
53
  const loc = useLocation();
54
  const navigate = useNavigate();
@@ -114,6 +125,24 @@ export function Layout() {
114
  </div>
115
 
116
  <div className="mt-auto space-y-4">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  <div>
118
  <p className="text-sm font-semibold text-ink tracking-wide mb-2">字体大小</p>
119
  <label htmlFor="ui-font-size" className="sr-only">
 
2
  import { Link, Navigate, Outlet, useLocation, useNavigate } from "react-router-dom";
3
  import { fetchMe, logout } from "../api";
4
  import { GenerationOptionsProvider } from "../context/GenerationOptionsContext";
5
+ import {
6
+ type ColorTheme,
7
+ type FontSizePreset,
8
+ useUiSettings,
9
+ } from "../context/UiSettingsContext";
10
 
11
  type NavItem = { to: string; title: string; sub: string };
12
 
 
43
  );
44
  }
45
 
46
+ const COLOR_THEME_OPTIONS: { value: ColorTheme; label: string }[] = [
47
+ { value: "blue", label: "淡蓝" },
48
+ { value: "cream", label: "淡米黄" },
49
+ { value: "gray", label: "浅灰" },
50
+ { value: "lavender", label: "淡紫灰" },
51
+ ];
52
+
53
  const FONT_SIZE_OPTIONS: { value: FontSizePreset; label: string; labelEn: string }[] = [
54
  { value: "compact", label: "小", labelEn: "Extra small" },
55
  { value: "small", label: "较小", labelEn: "Small" },
 
59
  ];
60
 
61
  export function Layout() {
62
+ const { fontSize, setFontSize, colorTheme, setColorTheme } = useUiSettings();
63
  const [ok, setOk] = useState<boolean | null>(null);
64
  const loc = useLocation();
65
  const navigate = useNavigate();
 
125
  </div>
126
 
127
  <div className="mt-auto space-y-4">
128
+ <div>
129
+ <p className="text-sm font-semibold text-ink tracking-wide mb-2">界面配色</p>
130
+ <label htmlFor="ui-color-theme" className="sr-only">
131
+ 界面配色
132
+ </label>
133
+ <select
134
+ id="ui-color-theme"
135
+ value={colorTheme}
136
+ onChange={(e) => setColorTheme(e.target.value as ColorTheme)}
137
+ className="w-full rounded-lg border border-slate-400/90 bg-white px-3 py-2.5 text-base text-ink shadow-sm focus:outline-none focus:ring-2 focus:ring-clay/40"
138
+ >
139
+ {COLOR_THEME_OPTIONS.map((o) => (
140
+ <option key={o.value} value={o.value}>
141
+ {o.label}
142
+ </option>
143
+ ))}
144
+ </select>
145
+ </div>
146
  <div>
147
  <p className="text-sm font-semibold text-ink tracking-wide mb-2">字体大小</p>
148
  <label htmlFor="ui-font-size" className="sr-only">
web/frontend/src/pages/Login.tsx CHANGED
@@ -42,8 +42,8 @@ export function Login() {
42
  }
43
 
44
  return (
45
- <div className="min-h-screen flex items-center justify-center p-6 sm:p-10 bg-gradient-to-br from-paper via-indigo-50/35 to-violet-100/45">
46
- <div className="w-full max-w-md rounded-2xl border border-line/80 bg-white/85 backdrop-blur-sm shadow-xl shadow-indigo-950/10 p-10 sm:p-12">
47
  <h1 className="font-display text-3xl font-semibold text-ink mb-2">AI创作台</h1>
48
  <p className="text-base text-mist mb-8">输入密码后即可使用</p>
49
  <form onSubmit={onSubmit} className="space-y-5">
 
42
  }
43
 
44
  return (
45
+ <div className="min-h-screen flex items-center justify-center p-6 sm:p-10 bg-gradient-to-br from-paper via-panel/30 to-paper">
46
+ <div className="w-full max-w-md rounded-2xl border border-line/80 bg-white/85 backdrop-blur-sm shadow-xl shadow-black/10 p-10 sm:p-12">
47
  <h1 className="font-display text-3xl font-semibold text-ink mb-2">AI创作台</h1>
48
  <p className="text-base text-mist mb-8">输入密码后即可使用</p>
49
  <form onSubmit={onSubmit} className="space-y-5">
web/frontend/tailwind.config.js CHANGED
@@ -18,18 +18,13 @@ export default {
18
  serif: ['"Source Serif 4"', "Georgia", "serif"],
19
  },
20
  colors: {
21
- /** Page backgroundcool misty blue-gray (replaces warm cream) */
22
- paper: "#eef1f8",
23
- /** Sidebar / secondary surfaces — slightly deeper vs paper for contrast */
24
- panel: "#d8e4f2",
25
- /** Borders — a step darker than before */
26
- line: "#a8b8d0",
27
- /** Primary text */
28
- ink: "#0f172a",
29
- /** Accents: focus rings, links, subtle highlights */
30
- clay: "#6366f1",
31
- /** Secondary / muted text — readable but clearly below ink */
32
- mist: "#475569",
33
  },
34
  },
35
  },
 
18
  serif: ['"Source Serif 4"', "Georgia", "serif"],
19
  },
20
  colors: {
21
+ /** Theme tokensRGB channels in index.css per html[data-theme] */
22
+ paper: "rgb(var(--color-paper) / <alpha-value>)",
23
+ panel: "rgb(var(--color-panel) / <alpha-value>)",
24
+ line: "rgb(var(--color-line) / <alpha-value>)",
25
+ ink: "rgb(var(--color-ink) / <alpha-value>)",
26
+ clay: "rgb(var(--color-clay) / <alpha-value>)",
27
+ mist: "rgb(var(--color-mist) / <alpha-value>)",
 
 
 
 
 
28
  },
29
  },
30
  },