@konstantdotcloud/boombox 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -0
- package/dist/boombox.js +49665 -0
- package/dist/index.d.ts +423 -0
- package/dist/index.js +45031 -0
- package/package.json +53 -0
- package/ui/README.md +142 -0
- package/ui/dist/assets/index-BhlAEUz4.css +1 -0
- package/ui/dist/assets/index-Ci77tcND.js +68 -0
- package/ui/dist/assets/index-Ci77tcND.js.map +1 -0
- package/ui/dist/index.html +14 -0
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@konstantdotcloud/boombox",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local Boombox runtime for Konstant cassettes — CLI, stdio MCP server, and local Hono proxy.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"boombox": "dist/boombox.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"ui/dist",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup && npm run build:ui",
|
|
19
|
+
"build:ui": "cd ui && npm install --silent && npm run build",
|
|
20
|
+
"dev": "tsx bin/boombox.ts",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:watch": "vitest",
|
|
23
|
+
"prepublishOnly": "npm run build",
|
|
24
|
+
"release:patch": "npm version patch --no-git-tag-version && npm publish --access public",
|
|
25
|
+
"release:minor": "npm version minor --no-git-tag-version && npm publish --access public",
|
|
26
|
+
"release:major": "npm version major --no-git-tag-version && npm publish --access public"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=20"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@hono/node-server": "^1.19.9",
|
|
36
|
+
"@modelcontextprotocol/sdk": "^1.25.2",
|
|
37
|
+
"chalk": "^5.6.2",
|
|
38
|
+
"commander": "^12.1.0",
|
|
39
|
+
"hono": "^4.11.9",
|
|
40
|
+
"open": "^10.1.0",
|
|
41
|
+
"prompts": "^2.4.2",
|
|
42
|
+
"smol-toml": "^1.3.1",
|
|
43
|
+
"zod": "^3.25.76"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^20.0.0",
|
|
47
|
+
"@types/prompts": "^2.4.9",
|
|
48
|
+
"tsup": "^8.3.5",
|
|
49
|
+
"tsx": "^4.7.0",
|
|
50
|
+
"typescript": "^5.0.0",
|
|
51
|
+
"vitest": "^3.2.4"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/ui/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# @konstant/boombox-ui
|
|
2
|
+
|
|
3
|
+
Boombox local UI — the cassette-deck console a tenant operator runs on their laptop.
|
|
4
|
+
|
|
5
|
+
This package is the **foundation scaffold** delivered in Wave 5g. It boots the
|
|
6
|
+
shell, theme system, mode system, primitives, and a route table populated with
|
|
7
|
+
placeholder pages. Per-screen translations land in Wave 5h+.
|
|
8
|
+
|
|
9
|
+
## Framework decision
|
|
10
|
+
|
|
11
|
+
**React + Vite.** Three reasons:
|
|
12
|
+
|
|
13
|
+
1. The Boombox design package ships as React JSX. Staying React makes the port
|
|
14
|
+
1:1 — no JSX→Svelte translation drift on tokens, transport, or shell.
|
|
15
|
+
2. `@konstantdotcloud/boombox` is a separate package. The Konstant tenant admin moving
|
|
16
|
+
to Svelte does not bind this surface.
|
|
17
|
+
3. The team can ship Boombox UI without learning a new framework. Cassette-deck
|
|
18
|
+
primitives are visually heavy; React keeps the design contract in one shape.
|
|
19
|
+
|
|
20
|
+
## Layout
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
ui/
|
|
24
|
+
├── index.html Vite entry, top-level <html data-theme="amber" ...>
|
|
25
|
+
├── src/
|
|
26
|
+
│ ├── main.tsx React root
|
|
27
|
+
│ ├── BoomboxApp.tsx theme + mode providers + router
|
|
28
|
+
│ ├── styles/
|
|
29
|
+
│ │ └── design-system.css verbatim copy of design package styles.css
|
|
30
|
+
│ ├── primitives/ Icon, Reels, ConfBar, LED, Tag, Panel, Btn,
|
|
31
|
+
│ │ VisibilityBadge, TrustBadge, GrantBadge
|
|
32
|
+
│ ├── shell/
|
|
33
|
+
│ │ ├── BoomboxShell.tsx top nav, theme switcher, mode select, footer
|
|
34
|
+
│ │ ├── ThemeProvider.tsx amber / paper / phosphor (localStorage)
|
|
35
|
+
│ │ ├── ModeProvider.tsx driver / reviewer / attendee / calm / overlay
|
|
36
|
+
│ │ │ (URL ?mode=)
|
|
37
|
+
│ │ └── routes.ts route table — spec for follow-up waves
|
|
38
|
+
│ ├── api/
|
|
39
|
+
│ │ ├── client.ts typed fetch wrapper
|
|
40
|
+
│ │ └── types.ts cassette / room / run / approval-gate types
|
|
41
|
+
│ └── pages/
|
|
42
|
+
│ └── PlaceholderPage.tsx single placeholder for unimplemented screens
|
|
43
|
+
└── tests/
|
|
44
|
+
├── BoomboxApp.test.tsx
|
|
45
|
+
├── ThemeProvider.test.tsx
|
|
46
|
+
├── primitives.test.tsx
|
|
47
|
+
└── routes.test.tsx
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Scripts
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pnpm install
|
|
54
|
+
pnpm dev # vite dev server, http://localhost:5173
|
|
55
|
+
pnpm build # tsc --noEmit && vite build → dist/
|
|
56
|
+
pnpm test # vitest run
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Theme system
|
|
60
|
+
|
|
61
|
+
`<ThemeProvider>` writes `data-theme` to `<html>` and persists to
|
|
62
|
+
`localStorage["boombox.theme"]`. Three themes: `amber` (default), `paper`,
|
|
63
|
+
`phosphor`. The theme switcher in the top bar (A/P/G) flips it.
|
|
64
|
+
|
|
65
|
+
## Mode system
|
|
66
|
+
|
|
67
|
+
`<ModeProvider>` reads `?mode=` from the URL and writes `data-stance` to
|
|
68
|
+
`<html>`. Five modes: `driver`, `reviewer`, `attendee`, `calm`, `overlay`.
|
|
69
|
+
Switching the dropdown updates the URL.
|
|
70
|
+
|
|
71
|
+
## Routes
|
|
72
|
+
|
|
73
|
+
| Path | Screen | Wave |
|
|
74
|
+
|------|--------|------|
|
|
75
|
+
| `/` | `CassetteRack` | 5h |
|
|
76
|
+
| `/queue` | `MeetingRoomsQueue` | 5h |
|
|
77
|
+
| `/cassette/:id` | `CassetteRoomDetail` | 5h |
|
|
78
|
+
| `/run/:id` | `PacketApprovalScreen` | 5h |
|
|
79
|
+
| `/run/:id/attendee` | `AttendeeRoomScreen` | 5i |
|
|
80
|
+
| `/series/:id` | `SeriesScreen` | 5i |
|
|
81
|
+
| `/workfield/:id` | `WorkfieldScreen` | 5i |
|
|
82
|
+
| `/admin` | `AdminScreen` | 5j |
|
|
83
|
+
| `/calm` | `CalmModeScreen` | 5j |
|
|
84
|
+
|
|
85
|
+
Every route currently renders `<PlaceholderPage>` showing the screen name,
|
|
86
|
+
the wave where it lands, and any URL params.
|
|
87
|
+
|
|
88
|
+
## API client
|
|
89
|
+
|
|
90
|
+
`src/api/client.ts` wraps `fetch` with auth + base URL. Configure via:
|
|
91
|
+
|
|
92
|
+
- `VITE_BOOMBOX_API_URL` env var at build time, or
|
|
93
|
+
- `localStorage["boombox.api.token"]` for the bearer token, or
|
|
94
|
+
- programmatic `configureApi({ baseUrl, token })`.
|
|
95
|
+
|
|
96
|
+
The default base URL is `http://localhost:8787` (the local Hono proxy).
|
|
97
|
+
|
|
98
|
+
## Integration points (open work)
|
|
99
|
+
|
|
100
|
+
These are not blockers for the foundation, but they are the integration seams
|
|
101
|
+
that follow-up waves need to wire:
|
|
102
|
+
|
|
103
|
+
1. **API base URL bootstrap.** Today the client reads `VITE_BOOMBOX_API_URL`
|
|
104
|
+
or falls back to `http://localhost:8787`. Production behavior (when the UI
|
|
105
|
+
is served by `boombox serve`) should pull `gateway_url` from
|
|
106
|
+
`~/.boombox/config.toml` and inject it into the page (e.g. via a
|
|
107
|
+
`<script>window.__BOOMBOX__ = {...}</script>` shim emitted by the local
|
|
108
|
+
server).
|
|
109
|
+
2. **Auth token bootstrap.** Today the token is read from
|
|
110
|
+
`localStorage["boombox.api.token"]`. Real flow: the local Hono proxy
|
|
111
|
+
already injects `Authorization: Bearer <api_key>` for `/api/*`. The UI
|
|
112
|
+
should hit the proxy directly with no token, since the proxy adds it.
|
|
113
|
+
`client.ts` accepts a null token for that path.
|
|
114
|
+
3. **`boombox serve` static-serve integration.** Currently `pnpm build`
|
|
115
|
+
produces `ui/dist/`. The CLI's `boombox serve --http` should mount that
|
|
116
|
+
directory (or fetch the latest UI bundle from the gateway). Tracked as a
|
|
117
|
+
TODO in `src/cli/serve.ts`.
|
|
118
|
+
4. **Cloud API endpoint shapes.** `api/types.ts` mirrors the cassette
|
|
119
|
+
substrate. Once Wave 5b-ApprovalGate endpoints stabilize, regenerate the
|
|
120
|
+
types so this package stays a faithful client (instead of duplicating).
|
|
121
|
+
5. **Tweaks panel / pressure / era presets.** The design package's Era
|
|
122
|
+
(Field '79 / Studio '89 / Issue 04) and Pressure controls are not yet
|
|
123
|
+
wired here. Theme + Stance are. Era + Pressure land alongside the first
|
|
124
|
+
real screens in Wave 5h, since they reshape the chrome the screens render
|
|
125
|
+
into.
|
|
126
|
+
|
|
127
|
+
## Critical TODOs for Wave 5h+
|
|
128
|
+
|
|
129
|
+
- `CassetteRack` (`/`) — port `RackScreen` from `app.jsx` + `RackView` from
|
|
130
|
+
`cards.jsx`. Wire to `listCassettes()`.
|
|
131
|
+
- `MeetingRoomsQueue` (`/queue`) — port from `screens.jsx`. Wire to
|
|
132
|
+
`listRooms()`.
|
|
133
|
+
- `CassetteRoomDetail` (`/cassette/:id`) — port `RoomDetailScreen`. Wire to
|
|
134
|
+
`getRoom()` and `listApprovalGates()`.
|
|
135
|
+
- `PacketApprovalScreen` (`/run/:id`) — port `PacketApprovalScreen`. Wire to
|
|
136
|
+
`getRun()` + `approveGate()`.
|
|
137
|
+
- Port `RunStateTransport`, `ProofCard`, `VuMeter`, `KV`, `ReplayLink` from
|
|
138
|
+
`primitives.jsx` — these are screen-level primitives, not foundation
|
|
139
|
+
primitives, so they live with the screens that use them.
|
|
140
|
+
- `AdminScreen` (`/admin`) — port `admin.jsx`.
|
|
141
|
+
- `CalmModeScreen` (`/calm`) — port `calm.jsx`. This bypasses the shell.
|
|
142
|
+
- Add Era + Pressure controls + presets (`app.jsx` ERA_PRESETS / STANCE_PRESETS).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import"https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Geist:wght@300;400;500;600;700;800&display=swap";:root,[data-theme=amber]{--bg: #0a0a0a;--bg-1: #111110;--bg-2: #16161410;--panel: #131311;--panel-2: #1a1a17;--panel-3: #201f1b;--rule: #2a2823;--rule-strong: #3a372f;--rule-soft: #1f1d19;--ink: #f3ead4;--ink-2: #d8cca8;--ink-3: #a89b78;--ink-4: #6e6650;--ink-5: #46402f;--ink-mute: #2e2a1f;--amber: #d97706;--amber-hi: #f59e0b;--amber-glow: #fbbf2433;--amber-soft: #d9770622;--signal: var(--amber);--signal-hi: var(--amber-hi);--signal-glow: var(--amber-glow);--signal-soft: var(--amber-soft);--ok: #7fc97a;--ok-soft: #7fc97a18;--warn: #f5a524;--warn-soft: #f5a52420;--hold: #c9613a;--hold-soft: #c9613a22;--fail: #ef4444;--fail-soft: #ef444420;--info: #6aa6c9;--info-soft: #6aa6c920;--vis-private: #c9613a;--vis-org: #d97706;--vis-room: #6aa6c9;--vis-public: #7fc97a;--tape-1: #d97706;--tape-2: #c9613a;--tape-3: #6aa6c9;--tape-4: #7fc97a;--tape-5: #b48ead;--tape-6: #e8d4a8;--shadow: 0 1px 0 #00000080, 0 8px 24px #00000040;--inset: inset 0 1px 0 #ffffff08, inset 0 -1px 0 #00000060;--glow: 0 0 0 1px var(--signal), 0 0 24px var(--signal-glow)}[data-theme=paper]{--bg: #ece7dc;--bg-1: #e3ddcf;--bg-2: #d8d1c0;--panel: #f6f1e6;--panel-2: #ede7d8;--panel-3: #e1d9c5;--rule: #c8bfa6;--rule-strong: #a89e83;--rule-soft: #d9d1bb;--ink: #1a1814;--ink-2: #2e2a22;--ink-3: #5a513e;--ink-4: #847a62;--ink-5: #a89e83;--ink-mute: #c8bfa6;--signal: #ff5500;--signal-hi: #ff7a33;--signal-glow: #ff550020;--signal-soft: #ff550018;--ok: #2f7a3a;--ok-soft: #2f7a3a14;--warn: #b97300;--warn-soft: #b9730018;--hold: #a44318;--hold-soft: #a4431814;--fail: #b91c1c;--fail-soft: #b91c1c14;--info: #2c6e8c;--info-soft: #2c6e8c14;--vis-private: #a44318;--vis-org: #ff5500;--vis-room: #2c6e8c;--vis-public: #2f7a3a;--tape-1: #ff5500;--tape-2: #a44318;--tape-3: #2c6e8c;--tape-4: #2f7a3a;--tape-5: #6b3f7e;--tape-6: #d4a84a;--shadow: 0 1px 0 #ffffff80, 0 6px 16px #00000018;--inset: inset 0 1px 0 #ffffff80, inset 0 -1px 0 #00000010;--glow: 0 0 0 1px var(--signal), 0 0 0 4px var(--signal-soft)}[data-theme=phosphor]{--bg: #060a09;--bg-1: #09110e;--bg-2: #0c1714;--panel: #0d1614;--panel-2: #11201c;--panel-3: #142823;--rule: #1f3a32;--rule-strong: #2a5347;--rule-soft: #142822;--ink: #c0f5d8;--ink-2: #8de0b3;--ink-3: #5cb78a;--ink-4: #3a8862;--ink-5: #265a42;--ink-mute: #163524;--signal: #5cf2a8;--signal-hi: #8aff c2;--signal-hi: #88ffc2;--signal-glow: #5cf2a833;--signal-soft: #5cf2a818;--ok: #5cf2a8;--ok-soft: #5cf2a818;--warn: #f5d524;--warn-soft: #f5d52418;--hold: #ff8c5c;--hold-soft: #ff8c5c18;--fail: #ff5c7a;--fail-soft: #ff5c7a18;--info: #5cd5f2;--info-soft: #5cd5f218;--vis-private: #ff8c5c;--vis-org: #f5d524;--vis-room: #5cd5f2;--vis-public: #5cf2a8;--tape-1: #5cf2a8;--tape-2: #5cd5f2;--tape-3: #f5d524;--tape-4: #ff8c5c;--tape-5: #c08af2;--tape-6: #88ffc2;--shadow: 0 1px 0 #00000080, 0 8px 24px #00000060;--inset: inset 0 1px 0 #ffffff08, inset 0 -1px 0 #00000080;--glow: 0 0 0 1px var(--signal), 0 0 24px var(--signal-glow)}:root{--font-mono: "JetBrains Mono", ui-monospace, "Berkeley Mono", monospace;--font-sans: "Geist", system-ui, sans-serif;--t-xs: 10px;--t-sm: 11px;--t-md: 12px;--t-lg: 13px;--t-xl: 15px;--t-2xl: 18px;--t-3xl: 22px;--t-4xl: 32px;--t-5xl: 48px;--s-1: 4px;--s-2: 8px;--s-3: 12px;--s-4: 16px;--s-5: 20px;--s-6: 24px;--s-7: 32px;--s-8: 48px;--r-1: 2px;--r-2: 4px;--r-3: 6px;--r-4: 10px}*{box-sizing:border-box}html,body,#root{height:100%;margin:0;padding:0}body{background:var(--bg);color:var(--ink);font-family:var(--font-sans);font-size:var(--t-md);font-feature-settings:"ss01","cv11","tnum";-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}::-moz-selection{background:var(--signal);color:var(--bg)}::selection{background:var(--signal);color:var(--bg)}[data-theme=phosphor] body:after,[data-theme=amber][data-tactility=high] body:after{content:"";position:fixed;top:0;right:0;bottom:0;left:0;pointer-events:none;background:repeating-linear-gradient(0deg,transparent 0 2px,#00000018 2px 3px);z-index:1000;mix-blend-mode:multiply;opacity:.4}.mono{font-family:var(--font-mono);font-feature-settings:"tnum","zero","ss01"}.up{text-transform:uppercase;letter-spacing:.08em}.dim{color:var(--ink-3)}.dim2{color:var(--ink-4)}.hi{color:var(--ink)}.sig{color:var(--signal)}.kbd{font-family:var(--font-mono);font-size:10px;padding:1px 5px;border:1px solid var(--rule);border-radius:3px;color:var(--ink-3);background:var(--panel)}.rule{height:1px;background:var(--rule)}.tag{display:inline-flex;align-items:center;gap:6px;font-family:var(--font-mono);font-size:var(--t-xs);text-transform:uppercase;letter-spacing:.08em;padding:2px 6px;border:1px solid var(--rule);border-radius:var(--r-1);color:var(--ink-2);background:transparent;white-space:nowrap;height:18px}.tag.solid{background:var(--panel-2)}.tag.signal{color:var(--signal);border-color:var(--signal);background:var(--signal-soft)}.tag.ok{color:var(--ok);border-color:color-mix(in oklab,var(--ok) 50%,var(--rule));background:var(--ok-soft)}.tag.warn{color:var(--warn);border-color:color-mix(in oklab,var(--warn) 50%,var(--rule));background:var(--warn-soft)}.tag.hold{color:var(--hold);border-color:color-mix(in oklab,var(--hold) 50%,var(--rule));background:var(--hold-soft)}.tag.fail{color:var(--fail);border-color:color-mix(in oklab,var(--fail) 50%,var(--rule));background:var(--fail-soft)}.tag.info{color:var(--info);border-color:color-mix(in oklab,var(--info) 50%,var(--rule));background:var(--info-soft)}.tag.dot:before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor;box-shadow:0 0 6px currentColor}.tag.lg{height:22px;font-size:var(--t-sm);padding:3px 8px}.led{width:6px;height:6px;border-radius:50%;background:var(--ink-mute);display:inline-block}.led.on{background:var(--ok);box-shadow:0 0 6px var(--ok)}.led.warn{background:var(--warn);box-shadow:0 0 6px var(--warn)}.led.hold{background:var(--hold);box-shadow:0 0 6px var(--hold)}.led.fail{background:var(--fail);box-shadow:0 0 6px var(--fail)}.led.signal{background:var(--signal);box-shadow:0 0 8px var(--signal)}.led.pulse{animation:pulse var(--pulse-dur, 1.6s) infinite}@keyframes pulse{0%,to{opacity:1}50%{opacity:.35}}@keyframes spin{to{transform:rotate(360deg)}}@keyframes blink{50%{opacity:.25}}[data-pressure-band=low] .reel-spin{animation-duration:9s;opacity:.75}[data-pressure-band=low] .led.pulse{animation:none;opacity:.85}[data-pressure-band=high] .led.signal{box-shadow:0 0 calc(8px + 8px * var(--pressure, .5)) var(--signal)}[data-stance=attendee] .led.pulse,[data-stance=attendee] .reel-spin{animation:none}[data-stance=reviewer] .device{box-shadow:none}.btn{font-family:var(--font-mono);font-size:var(--t-sm);text-transform:uppercase;letter-spacing:.1em;padding:7px 12px;border:1px solid var(--rule-strong);background:var(--panel-2);color:var(--ink);border-radius:var(--r-2);cursor:pointer;display:inline-flex;align-items:center;gap:8px;height:30px;white-space:nowrap;transition:background 80ms,border-color 80ms,color 80ms}.btn:hover{border-color:var(--ink-3);background:var(--panel-3)}.btn:active{transform:translateY(1px)}.btn.ghost{background:transparent;border-color:var(--rule);color:var(--ink-2)}.btn.ghost:hover{background:var(--panel-2);color:var(--ink)}.btn.signal{background:var(--signal);color:var(--bg);border-color:var(--signal);font-weight:600}.btn.signal:hover{background:var(--signal-hi);border-color:var(--signal-hi)}.btn.danger{color:var(--fail);border-color:color-mix(in oklab,var(--fail) 40%,var(--rule))}.btn.danger:hover{background:var(--fail-soft)}.btn.sm{height:24px;padding:4px 8px;font-size:var(--t-xs)}.btn.icon{padding:0;width:30px;justify-content:center}.btn.icon.sm{width:24px}.btn[disabled]{opacity:.35;pointer-events:none}.panel{background:var(--panel);border:1px solid var(--rule);border-radius:var(--r-2)}.panel.flat{background:transparent}.panel.deep{background:var(--bg-1)}.panel-h{display:flex;align-items:center;gap:10px;padding:10px 12px;border-bottom:1px solid var(--rule);font-family:var(--font-mono);font-size:var(--t-xs);text-transform:uppercase;letter-spacing:.12em;color:var(--ink-3)}.panel-h .title{color:var(--ink);letter-spacing:.12em}.panel-h .spacer{flex:1}.panel-b{padding:12px}*::-webkit-scrollbar{width:8px;height:8px}*::-webkit-scrollbar-track{background:transparent}*::-webkit-scrollbar-thumb{background:var(--rule);border-radius:4px}*::-webkit-scrollbar-thumb:hover{background:var(--rule-strong)}.row{display:flex;gap:8px;align-items:center}.col{display:flex;flex-direction:column;gap:8px}.between{justify-content:space-between}.flex1{flex:1}.gap2{gap:8px}.gap3{gap:12px}.gap4{gap:16px}@keyframes reel-spin{to{transform:rotate(-360deg)}}.reel-spin{animation:reel-spin var(--reel-dur, 4s) linear infinite;transform-origin:center}.tape-track{height:4px;background:var(--ink-mute);border-radius:2px;position:relative;overflow:hidden}.tape-fill{position:absolute;top:0;right:0;bottom:0;left:0;background:linear-gradient(90deg,var(--signal),var(--signal-hi));width:var(--p, 0%);box-shadow:0 0 8px var(--signal-glow)}.device{background:radial-gradient(60% 100% at 50% 0%,color-mix(in oklab,var(--panel-2) 70%,transparent),transparent 60%),repeating-linear-gradient(0deg,transparent 0 3px,color-mix(in oklab,#ffffff 2%,transparent) 3px 4px),var(--panel);border:1px solid var(--rule);box-shadow:var(--shadow),var(--inset)}[data-theme=paper] .device{background:radial-gradient(60% 100% at 50% 0%,color-mix(in oklab,var(--panel-2) 70%,transparent),transparent 60%),repeating-linear-gradient(0deg,transparent 0 3px,color-mix(in oklab,#000000 2%,transparent) 3px 4px),var(--panel)}.frame{border:1px solid var(--rule);background:var(--panel)}.tnum{font-variant-numeric:tabular-nums}.trunc{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.sec-h{display:flex;align-items:baseline;gap:12px;padding:12px 0 8px;font-family:var(--font-mono);font-size:var(--t-xs);text-transform:uppercase;letter-spacing:.14em;color:var(--ink-3)}.sec-h .num{color:var(--signal)}.sec-h .line{flex:1;height:1px;background:var(--rule)}.calm-root{min-height:100vh;background:var(--bg);color:var(--ink);font-family:var(--font-sans, "Geist", system-ui, sans-serif);display:flex;flex-direction:column}.calm-topstrip{display:flex;align-items:center;justify-content:space-between;padding:14px 28px;border-bottom:1px solid var(--rule-soft);background:linear-gradient(180deg,var(--panel),transparent)}.calm-brand{display:flex;align-items:center;gap:10px}.calm-mark{display:inline-grid;place-items:center;width:22px;height:22px;border:1px solid var(--signal);color:var(--signal);background:var(--panel-3);border-radius:2px;font-size:11px;box-shadow:0 0 6px var(--signal-glow)}.calm-wordmark{font-family:var(--font-mono);font-size:11px;letter-spacing:.22em;color:var(--ink-2);font-weight:600}.calm-mode-tag{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.2em;padding:2px 7px;border:1px solid var(--rule-strong);border-radius:2px;color:var(--ink-3);background:var(--panel-2)}.calm-page{max-width:720px;margin:56px auto 80px;padding:0 32px;width:100%;box-sizing:border-box}.calm-eyebrow{font-family:var(--font-mono);font-size:10.5px;letter-spacing:.18em;color:var(--ink-3);text-transform:uppercase;margin-bottom:18px}.calm-h1{font-size:44px;line-height:1.06;letter-spacing:-.02em;font-weight:600;margin:0 0 12px;color:var(--ink);text-wrap:pretty}.calm-sub{font-family:var(--font-mono);font-size:13px;color:var(--ink-3);margin-bottom:26px;letter-spacing:.02em}.calm-lede{font-size:18px;line-height:1.55;color:var(--ink-2);margin:0 0 28px;font-weight:400;text-wrap:pretty}.calm-lede strong{color:var(--ink);font-weight:600}.calm-rule{border:0;border-top:1px solid var(--rule-soft);margin:8px 0 28px}.calm-section{margin:0 0 32px}.calm-h2{font-size:14px;font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.14em;color:var(--signal);font-weight:600;margin:0 0 12px}.calm-h3{font-size:13px;font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.12em;color:var(--ink-2);font-weight:600;margin:0 0 10px}.calm-p{font-size:16.5px;line-height:1.6;color:var(--ink-2);margin:0 0 12px;text-wrap:pretty}.calm-p strong{color:var(--ink);font-weight:600}.calm-p em{color:var(--ink-2);font-style:italic}.calm-meta{font-family:var(--font-mono);font-size:11px;color:var(--ink-4);letter-spacing:.04em;margin:4px 0 0}.calm-actions{display:flex;gap:12px;margin:36px 0 28px;align-items:center;flex-wrap:wrap}.calm-btn-primary{background:var(--signal);color:var(--bg);border:1px solid var(--signal);padding:14px 22px;font-family:var(--font-sans, "Geist", system-ui);font-size:15px;font-weight:600;letter-spacing:.01em;border-radius:2px;cursor:pointer;box-shadow:0 0 12px var(--signal-glow);transition:filter .15s}.calm-btn-primary:hover{filter:brightness(1.1)}.calm-btn-primary:disabled{opacity:.4;cursor:not-allowed;box-shadow:none}.calm-btn-secondary{background:transparent;color:var(--ink-2);border:1px solid var(--rule-strong);padding:14px 22px;font-family:var(--font-sans, "Geist", system-ui);font-size:15px;font-weight:500;letter-spacing:.01em;border-radius:2px;cursor:pointer;transition:border-color .15s,color .15s}.calm-btn-secondary:hover{border-color:var(--ink-3);color:var(--ink)}.calm-btn-ghost{background:transparent;color:var(--ink-3);border:0;padding:8px 0;font-family:var(--font-mono);font-size:11px;letter-spacing:.12em;cursor:pointer;text-transform:uppercase;transition:color .15s}.calm-btn-ghost:hover{color:var(--ink)}.calm-btn-ghost--top{letter-spacing:.18em}.calm-sendback{margin:28px 0;padding:20px;border:1px solid var(--rule);border-radius:3px;background:var(--panel)}.calm-textarea{width:100%;background:var(--bg-1);border:1px solid var(--rule-strong);color:var(--ink);padding:12px 14px;font-family:var(--font-sans, "Geist", system-ui);font-size:14.5px;line-height:1.5;border-radius:2px;margin-bottom:14px;box-sizing:border-box;resize:vertical;outline:none}.calm-textarea:focus{border-color:var(--signal);box-shadow:0 0 0 1px var(--signal-soft)}.calm-footer{margin-top:56px;padding-top:22px;border-top:1px solid var(--rule-soft);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px}.calm-foot-meta{font-family:var(--font-mono);font-size:10.5px;color:var(--ink-4);letter-spacing:.06em}.calm-done{text-align:left;padding:40px 0}[data-theme=paper] .calm-root{background:var(--bg)}[data-theme=paper] .calm-btn-primary{box-shadow:none}@media (max-width: 640px){.calm-page{margin:32px auto 60px;padding:0 20px}.calm-h1{font-size:32px}.calm-lede{font-size:16px}.calm-p{font-size:15.5px}.calm-actions{flex-direction:column;align-items:stretch}.calm-btn-primary,.calm-btn-secondary{width:100%;text-align:center}}
|