plotlink-ows 0.1.18 → 1.0.4

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.
@@ -1,122 +1,428 @@
1
- import { useRef, useEffect, useCallback } from "react";
1
+ import { useRef, useEffect, useCallback, useState } from "react";
2
2
  import { Terminal } from "@xterm/xterm";
3
3
  import { FitAddon } from "@xterm/addon-fit";
4
+ import { SerializeAddon } from "@xterm/addon-serialize";
4
5
  import "@xterm/xterm/css/xterm.css";
5
6
 
6
7
  interface TerminalPanelProps {
7
8
  token: string;
9
+ storyName: string | null;
10
+ authFetch: (url: string, opts?: RequestInit) => Promise<Response>;
11
+ onSelectStory?: (storyName: string) => void;
8
12
  }
9
13
 
10
- export function TerminalPanel({ token }: TerminalPanelProps) {
11
- const containerRef = useRef<HTMLDivElement>(null);
12
- const termRef = useRef<Terminal | null>(null);
13
- const wsRef = useRef<WebSocket | null>(null);
14
- const fitRef = useRef<FitAddon | null>(null);
14
+ interface TerminalSession {
15
+ term: Terminal;
16
+ fit: FitAddon;
17
+ serialize: SerializeAddon;
18
+ ws: WebSocket | null;
19
+ container: HTMLDivElement;
20
+ observer: ResizeObserver;
21
+ connected: boolean;
22
+ _retried?: boolean;
23
+ }
15
24
 
16
- const connect = useCallback(() => {
17
- if (!containerRef.current) return;
25
+ const THEME = {
26
+ background: "#F0EBE1",
27
+ foreground: "#2C1810",
28
+ cursor: "#8B4513",
29
+ cursorAccent: "#F0EBE1",
30
+ selectionBackground: "#D4C5B0",
31
+ selectionForeground: "#2C1810",
32
+ black: "#2C1810",
33
+ red: "#A63D40",
34
+ green: "#4A7A4A",
35
+ yellow: "#8B6914",
36
+ blue: "#4A6FA5",
37
+ magenta: "#7B4B8A",
38
+ cyan: "#3D7A7A",
39
+ white: "#3A2A1E",
40
+ brightBlack: "#8B7355",
41
+ brightRed: "#B85C5C", // muted red — readable as text, soft as diff bg
42
+ brightGreen: "#5A8A5A", // muted green — readable as text, soft as diff bg
43
+ brightYellow: "#A07D1C",
44
+ brightBlue: "#5A82BA",
45
+ brightMagenta: "#8E5D9F",
46
+ brightCyan: "#5A8F8F",
47
+ brightWhite: "#4A3728",
48
+ };
18
49
 
19
- // Initialize terminal
20
- const term = new Terminal({
21
- scrollback: 5000,
22
- fontSize: 13,
23
- fontFamily: '"Geist Mono", ui-monospace, monospace',
24
- lineHeight: 1.4,
25
- letterSpacing: 0.5,
26
- cursorBlink: true,
27
- cursorStyle: "block",
28
- theme: {
29
- background: "#F0EBE1",
30
- foreground: "#2C1810",
31
- cursor: "#8B4513",
32
- cursorAccent: "#F0EBE1",
33
- selectionBackground: "#D4C5B0",
34
- selectionForeground: "#2C1810",
35
- black: "#2C1810",
36
- red: "#CC3333",
37
- green: "#5B7A2E",
38
- yellow: "#8B6914",
39
- blue: "#4A6FA5",
40
- magenta: "#7B4B8A",
41
- cyan: "#4A8B8B",
42
- white: "#2C1810",
43
- brightBlack: "#8B7355",
44
- brightRed: "#E04040",
45
- brightGreen: "#6B8F38",
46
- brightYellow: "#A07D1C",
47
- brightBlue: "#5A82BA",
48
- brightMagenta: "#8E5D9F",
49
- brightCyan: "#5A9F9F",
50
- brightWhite: "#4A3728",
51
- },
52
- allowTransparency: false,
53
- drawBoldTextInBrightColors: true,
54
- });
50
+ const DB_NAME = "plotlink-terminal";
51
+ const DB_VERSION = 1;
52
+ const STORE_NAME = "scrollback";
53
+ const MAX_SCROLLBACK_BYTES = 10 * 1024 * 1024; // 10MB per story
55
54
 
56
- const fitAddon = new FitAddon();
57
- term.loadAddon(fitAddon);
58
- term.open(containerRef.current);
59
- fitRef.current = fitAddon;
60
- termRef.current = term;
55
+ // ---- IndexedDB helpers ----
56
+ function openDb(): Promise<IDBDatabase> {
57
+ return new Promise((resolve, reject) => {
58
+ const req = indexedDB.open(DB_NAME, DB_VERSION);
59
+ req.onupgradeneeded = () => {
60
+ const db = req.result;
61
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
62
+ db.createObjectStore(STORE_NAME);
63
+ }
64
+ };
65
+ req.onsuccess = () => resolve(req.result);
66
+ req.onerror = () => reject(req.error);
67
+ });
68
+ }
61
69
 
62
- // Fit after a frame
63
- requestAnimationFrame(() => {
64
- try { fitAddon.fit(); } catch { /* ignore */ }
65
- });
70
+ async function saveScrollback(storyName: string, data: string): Promise<void> {
71
+ // Enforce size limit
72
+ const trimmed = data.length > MAX_SCROLLBACK_BYTES ? data.slice(-MAX_SCROLLBACK_BYTES) : data;
73
+ const db = await openDb();
74
+ return new Promise((resolve, reject) => {
75
+ const tx = db.transaction(STORE_NAME, "readwrite");
76
+ tx.objectStore(STORE_NAME).put(trimmed, storyName);
77
+ tx.oncomplete = () => { db.close(); resolve(); };
78
+ tx.onerror = () => { db.close(); reject(tx.error); };
79
+ });
80
+ }
81
+
82
+ async function loadScrollback(storyName: string): Promise<string | null> {
83
+ const db = await openDb();
84
+ return new Promise((resolve, reject) => {
85
+ const tx = db.transaction(STORE_NAME, "readonly");
86
+ const req = tx.objectStore(STORE_NAME).get(storyName);
87
+ req.onsuccess = () => { db.close(); resolve(req.result ?? null); };
88
+ req.onerror = () => { db.close(); reject(req.error); };
89
+ });
90
+ }
91
+
92
+ // Sessions live outside React state to avoid ref-in-effect lint issues
93
+ const sessions = new Map<string, TerminalSession>();
94
+
95
+ export function TerminalPanel({ token, storyName, authFetch, onSelectStory }: TerminalPanelProps) {
96
+ const wrapperRef = useRef<HTMLDivElement>(null);
97
+ const authFetchRef = useRef(authFetch);
98
+ const [sessionList, setSessionList] = useState<string[]>([]);
99
+ const [disconnected, setDisconnected] = useState<Set<string>>(new Set());
66
100
 
67
- // WebSocket connection
101
+ const connectWsRef = useRef<(name: string, session: TerminalSession, resume: boolean) => void>(() => {});
102
+
103
+ useEffect(() => { authFetchRef.current = authFetch; }, [authFetch]);
104
+
105
+ const safeFit = useCallback((session: TerminalSession) => {
106
+ const { width } = session.container.getBoundingClientRect();
107
+ if (width < 50) return; // Skip fit if container has no real dimensions
108
+ try {
109
+ session.fit.fit();
110
+ if (session.ws?.readyState === WebSocket.OPEN) {
111
+ session.ws.send(JSON.stringify({ type: "resize", cols: session.term.cols, rows: session.term.rows }));
112
+ }
113
+ } catch { /* ignore */ }
114
+ }, []);
115
+
116
+ const showSession = useCallback((name: string | null) => {
117
+ for (const [key, session] of sessions) {
118
+ session.container.style.display = key === name ? "block" : "none";
119
+ }
120
+ if (name) {
121
+ const active = sessions.get(name);
122
+ if (active) {
123
+ // setTimeout gives browser time to compute layout after display change
124
+ setTimeout(() => safeFit(active), 50);
125
+ }
126
+ }
127
+ }, [safeFit]);
128
+
129
+ const connectWs = useCallback((name: string, session: TerminalSession, resume: boolean) => {
68
130
  const wsProto = window.location.protocol === "https:" ? "wss:" : "ws:";
69
- const ws = new WebSocket(`${wsProto}//${window.location.host}/ws/terminal?token=${token}`);
70
- wsRef.current = ws;
131
+ const ws = new WebSocket(
132
+ `${wsProto}//${window.location.host}/ws/terminal?story=${encodeURIComponent(name)}&token=${token}&resume=${resume}`
133
+ );
71
134
 
72
135
  ws.onopen = () => {
73
- ws.send(JSON.stringify({ type: "resize", cols: term.cols, rows: term.rows }));
136
+ session.connected = true;
137
+ session._retried = false;
138
+ setDisconnected((prev) => { const next = new Set(prev); next.delete(name); return next; });
139
+ ws.send(JSON.stringify({ type: "resize", cols: session.term.cols, rows: session.term.rows }));
74
140
  };
75
141
 
76
142
  ws.onmessage = (e) => {
77
- term.write(e.data);
143
+ session.term.write(e.data);
78
144
  };
79
145
 
80
- ws.onclose = () => {
81
- term.write("\r\n\x1b[33m[Terminal disconnected]\x1b[0m\r\n");
146
+ ws.onclose = (event) => {
147
+ session.connected = false;
148
+ if (session.ws === ws) {
149
+ session.ws = null;
150
+ // Save scrollback before marking disconnected
151
+ try {
152
+ const data = session.serialize.serialize();
153
+ saveScrollback(name, data).catch(() => {});
154
+ } catch { /* ignore */ }
155
+
156
+ // Code 4000 = resume failed, auto-reconnect fresh (once only)
157
+ if (event.code === 4000 && !session._retried) {
158
+ session._retried = true;
159
+ session.term.write("\r\n\x1b[33m[Resume failed — starting fresh session...]\x1b[0m\r\n");
160
+ connectWsRef.current(name, session, false);
161
+ return;
162
+ }
163
+
164
+ setDisconnected((prev) => new Set(prev).add(name));
165
+ }
82
166
  };
83
167
 
84
- term.onData((data) => {
168
+ session.term.onData((data) => {
85
169
  if (ws.readyState === WebSocket.OPEN) {
86
170
  ws.send(data);
87
171
  }
88
172
  });
89
173
 
90
- // Resize observer
174
+ session.ws = ws;
175
+ }, [token]);
176
+
177
+ useEffect(() => { connectWsRef.current = connectWs; }, [connectWs]);
178
+
179
+ const createSession = useCallback(async (name: string, opts?: { resume?: boolean; autoConnect?: boolean }) => {
180
+ if (!wrapperRef.current || sessions.has(name)) return;
181
+ const { resume = false, autoConnect = true } = opts ?? {};
182
+
183
+ const container = document.createElement("div");
184
+ container.style.width = "100%";
185
+ container.style.height = "100%";
186
+ container.style.display = "none";
187
+ wrapperRef.current.appendChild(container);
188
+
189
+ const term = new Terminal({
190
+ cols: 80, // Fallback minimum until FitAddon computes actual size
191
+ scrollback: 5000,
192
+ fontSize: 13,
193
+ fontFamily: '"Geist Mono", ui-monospace, monospace',
194
+ lineHeight: 1.05,
195
+ letterSpacing: 0,
196
+ cursorBlink: true,
197
+ cursorStyle: "block",
198
+ theme: THEME,
199
+ allowTransparency: false,
200
+ drawBoldTextInBrightColors: false,
201
+ minimumContrastRatio: 7, // High contrast — compensates for dim text halving
202
+ });
203
+
204
+ const fit = new FitAddon();
205
+ const serialize = new SerializeAddon();
206
+ term.loadAddon(fit);
207
+ term.loadAddon(serialize);
208
+ term.open(container);
209
+
210
+ // Apply padding to term.element so FitAddon measures correctly
211
+ if (term.element) {
212
+ term.element.style.paddingLeft = "10px";
213
+ }
214
+
215
+ const session: TerminalSession = { term, fit, serialize, ws: null, container, observer: null as unknown as ResizeObserver, connected: false };
216
+
91
217
  const observer = new ResizeObserver(() => {
218
+ const { width } = container.getBoundingClientRect();
219
+ if (width < 50) return; // Skip if container not yet laid out
92
220
  try {
93
- fitAddon.fit();
94
- if (ws.readyState === WebSocket.OPEN) {
95
- ws.send(JSON.stringify({ type: "resize", cols: term.cols, rows: term.rows }));
221
+ fit.fit();
222
+ if (session.ws?.readyState === WebSocket.OPEN) {
223
+ session.ws.send(JSON.stringify({ type: "resize", cols: term.cols, rows: term.rows }));
96
224
  }
97
225
  } catch { /* ignore */ }
98
226
  });
99
- observer.observe(containerRef.current);
227
+ observer.observe(container);
228
+ session.observer = observer;
229
+ sessions.set(name, session);
230
+ setSessionList((prev) => [...prev, name]);
231
+
232
+ // Restore scrollback from IndexedDB
233
+ try {
234
+ const saved = await loadScrollback(name);
235
+ if (saved) {
236
+ term.write(saved);
237
+ }
238
+ } catch { /* ignore */ }
239
+
240
+ if (autoConnect) {
241
+ connectWs(name, session, resume);
242
+ } else {
243
+ // Show as disconnected so overlay appears
244
+ setDisconnected((prev) => new Set(prev).add(name));
245
+ }
246
+
247
+ // Defer initial fit — container may still be display:none
248
+ setTimeout(() => safeFit(session), 50);
249
+ }, [connectWs, safeFit]);
250
+
251
+ const reconnectSession = useCallback(async (name: string, resume: boolean) => {
252
+ const session = sessions.get(name);
253
+ if (!session) return;
254
+
255
+ // Close existing WS if any
256
+ if (session.ws) {
257
+ session.ws.close();
258
+ session.ws = null;
259
+ }
260
+
261
+ if (!resume) {
262
+ // Kill old server PTY so a fresh one spawns on reconnect
263
+ await authFetchRef.current(`/api/terminal/${encodeURIComponent(name)}`, { method: "DELETE" }).catch(() => {});
264
+ session.term.clear();
265
+ }
266
+
267
+ connectWs(name, session, resume);
268
+ }, [connectWs]);
269
+
270
+ const destroySession = useCallback((name: string) => {
271
+ const session = sessions.get(name);
272
+ if (!session) return;
100
273
 
274
+ // Save scrollback before destroying
275
+ try {
276
+ const data = session.serialize.serialize();
277
+ saveScrollback(name, data).catch(() => {});
278
+ } catch { /* ignore */ }
279
+
280
+ session.observer.disconnect();
281
+ if (session.ws) session.ws.close();
282
+ session.term.dispose();
283
+ session.container.remove();
284
+ sessions.delete(name);
285
+ setSessionList((prev) => prev.filter((s) => s !== name));
286
+ setDisconnected((prev) => { const next = new Set(prev); next.delete(name); return next; });
287
+
288
+ authFetch(`/api/terminal/${encodeURIComponent(name)}`, { method: "DELETE" }).catch(() => {});
289
+ }, [authFetch]);
290
+
291
+ // Auto-spawn + show/hide when story changes
292
+ useEffect(() => {
293
+ if (!storyName) return;
294
+ if (!sessions.has(storyName)) {
295
+ // Check if a previous session exists — if so, show overlay instead of auto-connecting
296
+ authFetchRef.current(`/api/terminal/session/${encodeURIComponent(storyName)}`)
297
+ .then((res) => res.ok ? res.json() : null)
298
+ .then((data) => {
299
+ if (!sessions.has(storyName)) { // guard against race
300
+ const hasStoredSession = data?.sessionId && !data?.running;
301
+ createSession(storyName, { autoConnect: !hasStoredSession });
302
+ showSession(storyName);
303
+ }
304
+ })
305
+ .catch(() => {
306
+ if (!sessions.has(storyName)) {
307
+ createSession(storyName);
308
+ showSession(storyName);
309
+ }
310
+ });
311
+ } else {
312
+ showSession(storyName);
313
+ }
314
+ }, [storyName, createSession, showSession]);
315
+
316
+ // Periodic scrollback save (every 30s for active session)
317
+ useEffect(() => {
318
+ const interval = setInterval(() => {
319
+ for (const [name, session] of sessions) {
320
+ if (session.connected) {
321
+ try {
322
+ const data = session.serialize.serialize();
323
+ saveScrollback(name, data).catch(() => {});
324
+ } catch { /* ignore */ }
325
+ }
326
+ }
327
+ }, 30000);
328
+ return () => clearInterval(interval);
329
+ }, []);
330
+
331
+ // Cleanup all sessions on unmount
332
+ useEffect(() => {
101
333
  return () => {
102
- observer.disconnect();
103
- ws.close();
104
- term.dispose();
334
+ for (const [name, session] of sessions) {
335
+ // Save scrollback before cleanup
336
+ try {
337
+ const data = session.serialize.serialize();
338
+ saveScrollback(name, data).catch(() => {});
339
+ } catch { /* ignore */ }
340
+ session.observer.disconnect();
341
+ if (session.ws) session.ws.close();
342
+ session.term.dispose();
343
+ session.container.remove();
344
+ authFetchRef.current(`/api/terminal/${encodeURIComponent(name)}`, { method: "DELETE" }).catch(() => {});
345
+ }
346
+ sessions.clear();
105
347
  };
106
- }, [token]);
348
+ }, []);
107
349
 
108
- useEffect(() => {
109
- const cleanup = connect();
110
- return cleanup;
111
- }, [connect]);
350
+ const isDisconnected = storyName ? disconnected.has(storyName) : false;
351
+ const isEmpty = sessionList.length === 0;
112
352
 
113
353
  return (
114
354
  <div className="h-full flex flex-col">
115
- <div className="px-3 py-1.5 border-b border-border text-xs text-muted font-mono flex items-center gap-2">
116
- <span className="w-2 h-2 rounded-full bg-green-600" />
117
- Claude CLI
355
+ {/* Session tabs hidden when no sessions */}
356
+ {!isEmpty && (
357
+ <div className="px-2 py-1 border-b border-border flex items-center gap-1 overflow-x-auto">
358
+ {sessionList.map((name) => (
359
+ <div
360
+ key={name}
361
+ onClick={() => onSelectStory?.(name)}
362
+ className={`flex items-center gap-1 px-2 py-0.5 rounded text-xs font-mono cursor-pointer ${
363
+ name === storyName
364
+ ? "bg-accent/10 text-accent"
365
+ : "text-muted hover:text-foreground"
366
+ }`}
367
+ >
368
+ <span className={`w-1.5 h-1.5 rounded-full ${
369
+ disconnected.has(name) ? "bg-amber-500" : name === storyName ? "bg-green-600" : "bg-muted/50"
370
+ }`} />
371
+ <span className="truncate max-w-[120px]">{name}</span>
372
+ <button
373
+ onClick={(e) => {
374
+ e.stopPropagation();
375
+ destroySession(name);
376
+ }}
377
+ className="ml-0.5 text-muted hover:text-error text-[10px] leading-none"
378
+ title="Close terminal"
379
+ >
380
+ ×
381
+ </button>
382
+ </div>
383
+ ))
384
+ }
385
+ </div>
386
+ )}
387
+
388
+ {/* Terminal containers — always rendered so wrapperRef is available */}
389
+ <div className="relative flex-1 min-h-0">
390
+ <div ref={wrapperRef} className="h-full" />
391
+
392
+ {/* Empty state overlay */}
393
+ {isEmpty && (
394
+ <div className="absolute inset-0 flex items-center justify-center text-muted">
395
+ <div className="text-center">
396
+ <p className="text-lg font-serif">Select a story on the left menu</p>
397
+ <p className="text-sm mt-1">to start an AI Writer session</p>
398
+ </div>
399
+ </div>
400
+ )}
401
+
402
+ {/* Reconnect overlay */}
403
+ {isDisconnected && storyName && (
404
+ <div className="absolute inset-0 flex items-center justify-center" style={{ background: "rgba(240, 235, 225, 0.9)" }}>
405
+ <div className="text-center space-y-3">
406
+ <p className="text-sm font-serif text-foreground">Terminal disconnected</p>
407
+ <div className="flex items-center gap-2">
408
+ <button
409
+ onClick={() => reconnectSession(storyName, true)}
410
+ className="px-4 py-1.5 bg-accent text-white text-sm rounded hover:bg-accent-dim"
411
+ >
412
+ Resume Session
413
+ </button>
414
+ <button
415
+ onClick={() => reconnectSession(storyName, false)}
416
+ className="px-4 py-1.5 border border-border text-sm rounded hover:bg-surface"
417
+ >
418
+ Start Fresh
419
+ </button>
420
+ </div>
421
+ <p className="text-xs text-muted">Resume continues your previous Claude conversation</p>
422
+ </div>
423
+ </div>
424
+ )}
118
425
  </div>
119
- <div ref={containerRef} className="flex-1 min-h-0" />
120
426
  </div>
121
427
  );
122
428
  }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Copyright (c) 2014 The xterm.js authors. All rights reserved.
3
+ * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
4
+ * https://github.com/chjj/term.js
5
+ * @license MIT
6
+ *
7
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ * of this software and associated documentation files (the "Software"), to deal
9
+ * in the Software without restriction, including without limitation the rights
10
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ * copies of the Software, and to permit persons to whom the Software is
12
+ * furnished to do so, subject to the following conditions:
13
+ *
14
+ * The above copyright notice and this permission notice shall be included in
15
+ * all copies or substantial portions of the Software.
16
+ *
17
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
+ * THE SOFTWARE.
24
+ *
25
+ * Originally forked from (with the author's permission):
26
+ * Fabrice Bellard's javascript vt100 for jslinux:
27
+ * http://bellard.org/jslinux/
28
+ * Copyright (c) 2011 Fabrice Bellard
29
+ * The original design remains. The terminal itself
30
+ * has been extended to include xterm CSI codes, among
31
+ * other features.
32
+ */.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .xterm-accessibility-tree:not(.debug) *::selection{color:transparent}.xterm .xterm-accessibility-tree{font-family:monospace;-webkit-user-select:text;user-select:text;white-space:pre}.xterm .xterm-accessibility-tree>div{transform-origin:left;width:fit-content}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}.xterm .xterm-scrollable-element>.scrollbar{cursor:default}.xterm .xterm-scrollable-element>.scrollbar>.scra{cursor:pointer;font-size:11px!important}.xterm .xterm-scrollable-element>.visible{opacity:1;background:#0000;transition:opacity .1s linear;z-index:11}.xterm .xterm-scrollable-element>.invisible{opacity:0;pointer-events:none}.xterm .xterm-scrollable-element>.invisible.fade{transition:opacity .8s linear}.xterm .xterm-scrollable-element>.shadow{position:absolute;display:none}.xterm .xterm-scrollable-element>.shadow.top{display:block;top:0;left:3px;height:3px;width:100%;box-shadow:var(--vscode-scrollbar-shadow, #000) 0 6px 6px -6px inset}.xterm .xterm-scrollable-element>.shadow.left{display:block;top:3px;left:0;height:100%;width:3px;box-shadow:var(--vscode-scrollbar-shadow, #000) 6px 0 6px -6px inset}.xterm .xterm-scrollable-element>.shadow.top-left-corner{display:block;top:0;left:0;height:3px;width:3px}.xterm .xterm-scrollable-element>.shadow.top.left{box-shadow:var(--vscode-scrollbar-shadow, #000) 6px 0 6px -6px inset}/*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-outline-style:solid}}}@layer theme{:root,:host{--font-serif:ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-700:oklch(50.5% .213 27.518);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-lg:32rem;--container-2xl:42rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-medium:500;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wider:.05em;--leading-relaxed:1.625;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent;font-family:Inter,system-ui,-apple-system,sans-serif;line-height:1.5}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;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.absolute{position:absolute}.relative{position:relative}.inset-0{inset:calc(var(--spacing) * 0)}.start{inset-inline-start:var(--spacing)}.top-1{top:calc(var(--spacing) * 1)}.right-1{right:calc(var(--spacing) * 1)}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.prose{color:var(--tw-prose-body);max-width:65ch}.prose :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where([class~=lead]):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-lead);margin-top:1.2em;margin-bottom:1.2em;font-size:1.25em;line-height:1.6}.prose :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-links);font-weight:500;text-decoration:underline}.prose :where(strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-bold);font-weight:600}.prose :where(a strong):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(blockquote strong):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(thead th strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em;list-style-type:decimal}.prose :where(ol[type=A]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=A s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=I]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type=I s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type="1"]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal}.prose :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em;list-style-type:disc}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-counters);font-weight:400}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-bullets)}.prose :where(dt):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.25em;font-weight:600}.prose :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){border-color:var(--tw-prose-hr);border-top-width:1px;margin-top:3em;margin-bottom:3em}.prose :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-quotes);border-inline-start-width:.25rem;border-inline-start-color:var(--tw-prose-quote-borders);quotes:"“""”""‘""’";margin-top:1.6em;margin-bottom:1.6em;padding-inline-start:1em;font-style:italic;font-weight:500}.prose :where(blockquote p:first-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:open-quote}.prose :where(blockquote p:last-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:close-quote}.prose :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:0;margin-bottom:.888889em;font-size:2.25em;font-weight:800;line-height:1.11111}.prose :where(h1 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:900}.prose :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:2em;margin-bottom:1em;font-size:1.5em;font-weight:700;line-height:1.33333}.prose :where(h2 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:800}.prose :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.6em;margin-bottom:.6em;font-size:1.25em;font-weight:600;line-height:1.6}.prose :where(h3 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.5em;margin-bottom:.5em;font-weight:600;line-height:1.5}.prose :where(h4 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(picture):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em;display:block}.prose :where(video):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-kbd);box-shadow:0 0 0 1px var(--tw-prose-kbd-shadows),0 3px 0 var(--tw-prose-kbd-shadows);padding-top:.1875em;padding-inline-end:.375em;padding-bottom:.1875em;border-radius:.3125rem;padding-inline-start:.375em;font-family:inherit;font-size:.875em;font-weight:500}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-code);font-size:.875em;font-weight:600}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):before,.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:"`"}.prose :where(a code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h1 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h2 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.875em}.prose :where(h3 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.9em}.prose :where(h4 code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(blockquote code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(thead th code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-pre-code);background-color:var(--tw-prose-pre-bg);padding-top:.857143em;padding-inline-end:1.14286em;padding-bottom:.857143em;border-radius:.375rem;margin-top:1.71429em;margin-bottom:1.71429em;padding-inline-start:1.14286em;font-size:.875em;font-weight:400;line-height:1.71429;overflow-x:auto}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:inherit;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit;background-color:#0000;border-width:0;border-radius:0;padding:0}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):before,.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:none}.prose :where(table):not(:where([class~=not-prose],[class~=not-prose] *)){table-layout:auto;width:100%;margin-top:2em;margin-bottom:2em;font-size:.875em;line-height:1.71429}.prose :where(thead):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-th-borders)}.prose :where(thead th):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);vertical-align:bottom;padding-inline-end:.571429em;padding-bottom:.571429em;padding-inline-start:.571429em;font-weight:600}.prose :where(tbody tr):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-td-borders)}.prose :where(tbody tr:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:0}.prose :where(tbody td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:baseline}.prose :where(tfoot):not(:where([class~=not-prose],[class~=not-prose] *)){border-top-width:1px;border-top-color:var(--tw-prose-th-borders)}.prose :where(tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:top}.prose :where(th,td):not(:where([class~=not-prose],[class~=not-prose] *)){text-align:start}.prose :where(figure>*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(figcaption):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-captions);margin-top:.857143em;font-size:.875em;line-height:1.42857}.prose{--tw-prose-body:oklch(37.3% .034 259.733);--tw-prose-headings:oklch(21% .034 264.665);--tw-prose-lead:oklch(44.6% .03 256.802);--tw-prose-links:oklch(21% .034 264.665);--tw-prose-bold:oklch(21% .034 264.665);--tw-prose-counters:oklch(55.1% .027 264.364);--tw-prose-bullets:oklch(87.2% .01 258.338);--tw-prose-hr:oklch(92.8% .006 264.531);--tw-prose-quotes:oklch(21% .034 264.665);--tw-prose-quote-borders:oklch(92.8% .006 264.531);--tw-prose-captions:oklch(55.1% .027 264.364);--tw-prose-kbd:oklch(21% .034 264.665);--tw-prose-kbd-shadows:oklab(21% -.00316127 -.0338527/.1);--tw-prose-code:oklch(21% .034 264.665);--tw-prose-pre-code:oklch(92.8% .006 264.531);--tw-prose-pre-bg:oklch(27.8% .033 256.848);--tw-prose-th-borders:oklch(87.2% .01 258.338);--tw-prose-td-borders:oklch(92.8% .006 264.531);--tw-prose-invert-body:oklch(87.2% .01 258.338);--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:oklch(70.7% .022 261.325);--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:oklch(70.7% .022 261.325);--tw-prose-invert-bullets:oklch(44.6% .03 256.802);--tw-prose-invert-hr:oklch(37.3% .034 259.733);--tw-prose-invert-quotes:oklch(96.7% .003 264.542);--tw-prose-invert-quote-borders:oklch(37.3% .034 259.733);--tw-prose-invert-captions:oklch(70.7% .022 261.325);--tw-prose-invert-kbd:#fff;--tw-prose-invert-kbd-shadows:#ffffff1a;--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:oklch(87.2% .01 258.338);--tw-prose-invert-pre-bg:#00000080;--tw-prose-invert-th-borders:oklch(44.6% .03 256.802);--tw-prose-invert-td-borders:oklch(37.3% .034 259.733);font-size:1rem;line-height:1.75}.prose :where(picture>img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(li):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;margin-bottom:.5em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.375em}.prose :where(.prose>ul>li p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(.prose>ul>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ul>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(.prose>ol>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ol>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where(dd):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;padding-inline-start:1.625em}.prose :where(hr+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h2+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h3+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h4+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(thead th:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(thead th:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(tbody td,tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){padding-top:.571429em;padding-inline-end:.571429em;padding-bottom:.571429em;padding-inline-start:.571429em}.prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(.prose>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(.prose>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.ml-0\.5{margin-left:calc(var(--spacing) * .5)}.ml-1{margin-left:calc(var(--spacing) * 1)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-0\.5{height:calc(var(--spacing) * .5)}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-14{height:calc(var(--spacing) * 14)}.h-\[calc\(100vh-3\.5rem\)\]{height:calc(100vh - 3.5rem)}.h-full{height:100%}.h-screen{height:100vh}.min-h-0{min-height:calc(var(--spacing) * 0)}.w-0\.5{width:calc(var(--spacing) * .5)}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-56{width:calc(var(--spacing) * 56)}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-\[120px\]{max-width:120px}.max-w-lg{max-width:var(--container-lg)}.max-w-none{max-width:none}.max-w-sm{max-width:var(--container-sm)}.min-w-0{min-width:calc(var(--spacing) * 0)}.flex-1{flex:1}.flex-shrink-0{flex-shrink:0}.cursor-pointer{cursor:pointer}.resize{resize:both}.resize-none{resize:none}.list-inside{list-style-position:inside}.list-decimal{list-style-type:decimal}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.flex-col{flex-direction:column}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1.5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-accent{border-color:var(--accent)}.border-accent-dim\/30{border-color:var(--accent-dim)}@supports (color:color-mix(in lab,red,red)){.border-accent-dim\/30{border-color:color-mix(in oklab,var(--accent-dim) 30%,transparent)}}.border-accent\/30{border-color:var(--accent)}@supports (color:color-mix(in lab,red,red)){.border-accent\/30{border-color:color-mix(in oklab,var(--accent) 30%,transparent)}}.border-amber-600\/30{border-color:#dd74004d}@supports (color:color-mix(in lab,red,red)){.border-amber-600\/30{border-color:color-mix(in oklab,var(--color-amber-600) 30%,transparent)}}.border-border{border-color:var(--border)}.border-green-700\/30{border-color:#0081384d}@supports (color:color-mix(in lab,red,red)){.border-green-700\/30{border-color:color-mix(in oklab,var(--color-green-700) 30%,transparent)}}.border-red-700\/30{border-color:#bf000f4d}@supports (color:color-mix(in lab,red,red)){.border-red-700\/30{border-color:color-mix(in oklab,var(--color-red-700) 30%,transparent)}}.border-transparent{border-color:#0000}.bg-accent,.bg-accent\/10{background-color:var(--accent)}@supports (color:color-mix(in lab,red,red)){.bg-accent\/10{background-color:color-mix(in oklab,var(--accent) 10%,transparent)}}.bg-amber-500{background-color:var(--color-amber-500)}.bg-background{background-color:var(--bg)}.bg-green-600{background-color:var(--color-green-600)}.bg-muted\/50{background-color:var(--text-muted)}@supports (color:color-mix(in lab,red,red)){.bg-muted\/50{background-color:color-mix(in oklab,var(--text-muted) 50%,transparent)}}.bg-surface{background-color:var(--bg-surface)}.p-1\.5{padding:calc(var(--spacing) * 1.5)}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.pt-1\.5{padding-top:calc(var(--spacing) * 1.5)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pr-16{padding-right:calc(var(--spacing) * 16)}.pl-3{padding-left:calc(var(--spacing) * 3)}.pl-4{padding-left:calc(var(--spacing) * 4)}.text-center{text-align:center}.text-left{text-align:left}.font-mono{font-family:var(--font-mono)}.font-serif{font-family:var(--font-serif)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.break-all{word-break:break-all}.text-accent{color:var(--accent)}.text-accent-dim{color:var(--accent-dim)}.text-amber-600{color:var(--color-amber-600)}.text-amber-700{color:var(--color-amber-700)}.text-error{color:var(--error)}.text-foreground{color:var(--text)}.text-green-700{color:var(--color-green-700)}.text-muted{color:var(--text-muted)}.text-red-700{color:var(--color-red-700)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.italic{font-style:italic}.underline{text-decoration-line:underline}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.outline-none{--tw-outline-style:none;outline-style:none}.placeholder\:text-muted\/50::placeholder{color:var(--text-muted)}@supports (color:color-mix(in lab,red,red)){.placeholder\:text-muted\/50::placeholder{color:color-mix(in oklab,var(--text-muted) 50%,transparent)}}@media(hover:hover){.hover\:border-accent:hover{border-color:var(--accent)}.hover\:border-error:hover{border-color:var(--error)}.hover\:bg-accent-dim:hover{background-color:var(--accent-dim)}.hover\:bg-accent\/10:hover{background-color:var(--accent)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-accent\/10:hover{background-color:color-mix(in oklab,var(--accent) 10%,transparent)}}.hover\:bg-border\/50:hover{background-color:var(--border)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-border\/50:hover{background-color:color-mix(in oklab,var(--border) 50%,transparent)}}.hover\:bg-surface:hover{background-color:var(--bg-surface)}.hover\:text-accent:hover{color:var(--accent)}.hover\:text-error:hover{color:var(--error)}.hover\:text-foreground:hover{color:var(--text)}.hover\:opacity-80:hover{opacity:.8}}.focus\:border-accent:focus{border-color:var(--accent)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}}:root{--bg:#e8dfd0;--bg-surface:#f0ebe1;--bg-shelf:#ddd3c2;--text:#2c1810;--text-muted:#8b7355;--accent:#8b4513;--accent-dim:#6b3410;--border:#d4c5b0;--error:#c33;--paper-bg:#f5f0e8}body{background:var(--bg);color:var(--text);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:Inter,system-ui,-apple-system,sans-serif}::selection{background:var(--accent);color:#fff}h1,h2,h3,h4{font-family:Lora,Georgia,Times New Roman,serif}.prose{--tw-prose-body:var(--text);--tw-prose-headings:var(--text);--tw-prose-links:var(--accent);--tw-prose-bold:var(--text);--tw-prose-quotes:var(--text-muted);--tw-prose-quote-borders:var(--border);--tw-prose-code:var(--text);--tw-prose-hr:var(--border)}.prose,.prose p,.prose li,.prose blockquote{font-family:Lora,Georgia,Times New Roman,serif}code,pre{font-family:Geist Mono,ui-monospace,monospace}.xterm .xterm-dim{opacity:1!important;color:#8b7355!important}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}