@saluzi/saluzi-edu 0.2.64 → 0.2.66

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.
@@ -13,8 +13,13 @@ import { createWorkItem } from '../../services/work-dispatch'
13
13
  import { publishSessionEvent } from '../../services/transport'
14
14
  import { loadPersistedEvents } from '../../services/event-persistence'
15
15
  import { getEventBus } from '../../transport/event-bus'
16
+ import { touchEnvironmentActivity } from '../../services/environment'
16
17
  import { acceptCliHeaders, apiKeyAuth } from '../../auth/middleware'
17
- import { bridgeAuth, resolveUserIdUuid } from '../../auth/bridge-auth'
18
+ import {
19
+ bridgeAuth,
20
+ resolveAttachCaller,
21
+ type AttachCaller,
22
+ } from '../../auth/bridge-auth'
18
23
  import { getDb } from '../../db/sqlite'
19
24
  import {
20
25
  storeBindSession,
@@ -25,6 +30,7 @@ import {
25
30
  storeListSessions,
26
31
  storeListSessionsByEnvironment,
27
32
  storeRebindSessionEnvironment,
33
+ type SessionRecord,
28
34
  } from '../../store'
29
35
 
30
36
  const app = new Hono()
@@ -33,36 +39,17 @@ const app = new Hono()
33
39
  // Session Attach — bind an existing session to a CLI-registered environment
34
40
  // ---------------------------------------------------------------------------
35
41
 
36
- /** Caller identity resolved for attach endpoints. */
37
- type AttachCaller = {
38
- /** Resolved users.id UUID — null for a bare admin API key. */
39
- uuid: string | null
40
- isApiAdmin: boolean
41
- /** True when no identity at all was presented (bare admin API key). */
42
- hasIdentity: boolean
43
- }
44
-
45
- /**
46
- * Resolve the bridge caller identity for attach endpoints.
47
- *
48
- * bridgeAuth's userId may be a UUID (rct_* token), a username (in-memory
49
- * token) or an arbitrary X-Username string (shared admin API key) — only
50
- * users-table UUIDs can hold session management rights, so everything goes
51
- * through resolveUserIdUuid (G9: the API layer must self-enforce
52
- * canManageSession; the ingress WS accepting a global API key is not a
53
- * boundary we may lean on).
54
- */
55
- function resolveAttachCaller(c: import('hono').Context): AttachCaller {
56
- const raw = c.get('userId') as string | undefined
57
- const isApiAdmin = (c.get('isApiAdmin') as boolean | undefined) ?? false
58
- if (!raw) {
59
- return { uuid: null, isApiAdmin, hasIdentity: false }
60
- }
61
- return {
62
- uuid: resolveUserIdUuid(getDb(), raw),
63
- isApiAdmin,
64
- hasIdentity: true,
65
- }
42
+ /** 503 body shared by every attach-family route on a DB outage. */
43
+ function dbUnavailable(c: import('hono').Context): Response {
44
+ return c.json(
45
+ {
46
+ error: {
47
+ type: 'database_unavailable',
48
+ message: 'Database unavailable',
49
+ },
50
+ },
51
+ 503,
52
+ )
66
53
  }
67
54
 
68
55
  /**
@@ -150,15 +137,15 @@ function isSessionOccupied(
150
137
  }
151
138
 
152
139
  /** Shape returned by GET /attachable and consumed by the CLI picker. */
153
- function toAttachableEntry(session: ReturnType<typeof getSession>) {
140
+ function toAttachableEntry(session: SessionRecord) {
154
141
  return {
155
- id: session!.id,
156
- title: session!.title,
157
- status: session!.status,
158
- source: session!.source,
159
- environment_id: session!.environment_id ?? null,
160
- updated_at: session!.updated_at,
161
- busy: isSessionOccupied(session!.id, null),
142
+ id: session.id,
143
+ title: session.title,
144
+ status: session.status,
145
+ source: session.source,
146
+ environment_id: session.environmentId,
147
+ updated_at: session.updatedAt.getTime() / 1000,
148
+ busy: isSessionOccupied(session.id, null),
162
149
  }
163
150
  }
164
151
 
@@ -171,6 +158,19 @@ app.post('/', acceptCliHeaders, apiKeyAuth, async c => {
171
158
  const body = await c.req.json()
172
159
  const username = c.get('userId')
173
160
 
161
+ // Worker liveness: this route is the claim-wait loop's only traffic (an
162
+ // unowned env retries POST /v1/sessions every 5s until the claim URL is
163
+ // opened, up to 24h). Without touching the env here the disconnect monitor
164
+ // flips it to 'disconnected' after RCS_DISCONNECT_TIMEOUT, and when the
165
+ // claim finally lands the session is created but createWorkItem rejects
166
+ // the non-active env — a session that exists yet never receives work, so
167
+ // the CLI polls 204s forever and never connects. Touch BEFORE the unowned
168
+ // 409 so the retry loop itself keeps the env alive; touchEnvironmentActivity
169
+ // also reactivates an env the monitor already flipped.
170
+ if (typeof body.environment_id === 'string' && body.environment_id) {
171
+ touchEnvironmentActivity(body.environment_id)
172
+ }
173
+
174
174
  // Resolve env ownership to bind session_owners.
175
175
  let envOwnerUserId: string | null = null
176
176
  let envTeamId: string | null = null
@@ -240,7 +240,14 @@ app.post('/', acceptCliHeaders, apiKeyAuth, async c => {
240
240
  * worker. */
241
241
  app.get('/attachable', bridgeAuth, async c => {
242
242
  const caller = resolveAttachCaller(c)
243
- if (caller.uuid === null && caller.hasIdentity && !caller.isApiAdmin) {
243
+ if (caller.dbUnavailable) {
244
+ return dbUnavailable(c)
245
+ }
246
+ // An explicit-but-unresolvable identity (e.g. admin API key +
247
+ // X-Username matching no user) is a misconfiguration, not "see
248
+ // everything": 403, mirroring POST /:id/attach's ensureAttachAccess —
249
+ // the listing previously was the laxer of the two.
250
+ if (caller.uuid === null && caller.hasIdentity) {
244
251
  return c.json(
245
252
  {
246
253
  error: {
@@ -258,16 +265,22 @@ app.get('/attachable', bridgeAuth, async c => {
258
265
  )
259
266
  }
260
267
 
261
- const db = getDb()
262
- const attachable = storeListSessions()
268
+ const uuid = caller.uuid
269
+ const open = storeListSessions()
263
270
  .filter(s => !isSessionClosedStatus(s.status))
264
271
  .filter(s => s.source !== 'acp')
265
- .filter(
266
- s => caller.uuid === null || canManageSession(s.id, caller.uuid, db),
267
- )
268
- .map(s => toAttachableEntry(getSession(s.id)))
272
+ // Bare admin API key (uuid null) sees all; an identity sees only what it
273
+ // can manage. getDb() is touched only when the filter actually runs.
274
+ let visible = open
275
+ if (uuid !== null) {
276
+ const db = getDb()
277
+ visible = open.filter(s => canManageSession(s.id, uuid, db))
278
+ }
269
279
 
270
- return c.json(attachable, 200)
280
+ return c.json(
281
+ visible.map(s => toAttachableEntry(s)),
282
+ 200,
283
+ )
271
284
  })
272
285
 
273
286
  /** POST /v1/sessions/:id/attach — Bind an existing session to a CLI
@@ -317,6 +330,9 @@ async function performAttach(
317
330
  },
318
331
  ): Promise<Response> {
319
332
  const { sessionId, envId, caller, skipAccessCheck } = opts
333
+ if (caller.dbUnavailable) {
334
+ return dbUnavailable(c)
335
+ }
320
336
  const session = getSession(sessionId)
321
337
  if (!session) {
322
338
  return c.json(
@@ -325,28 +341,49 @@ async function performAttach(
325
341
  )
326
342
  }
327
343
 
328
- // Idempotent re-entry BEFORE authorization: a CLI retrying after a network
329
- // blip re-presents the same session+env pair — returning the existing
330
- // binding is not a new grant, so it must not depend on the caller still
331
- // holding manage rights (e.g. a doReconnect re-bind where the bound
332
- // session was assigned by a team admin, or an expired share). The check
333
- // only matches an existing work item on the SAME environment — no state
334
- // is created.
344
+ // Idempotent re-entry BEFORE session-manage authorization: a CLI retrying
345
+ // after a network blip re-presents the same session+env pair — returning
346
+ // the existing binding is not a new grant, so it must not depend on the
347
+ // caller still holding manage rights (e.g. a doReconnect re-bind where the
348
+ // bound session was assigned by a team admin, or an expired share). The
349
+ // check only matches an existing work item on the SAME environment — no
350
+ // state is created.
351
+ //
352
+ // Still gated on environment ownership (or admin): the CLI re-entering is
353
+ // the env's own worker, so ownership always holds for legitimate retries,
354
+ // while a random authenticated caller who merely knows session+env IDs
355
+ // must not be able to probe the binding (the response echoes the work
356
+ // item id). Non-owners fall through to the full checks below, which
357
+ // reject them.
335
358
  const activeItem = storeGetActiveWorkItemBySession(sessionId)
336
359
  if (activeItem && activeItem.environmentId === envId) {
337
- log(
338
- `[RCS] Attach idempotent re-entry: session=${sessionId} env=${envId} work=${activeItem.id}`,
339
- )
340
- return c.json(
341
- {
342
- status: 'attached',
343
- session_id: sessionId,
344
- work_item_id: activeItem.id,
345
- rebind: false,
346
- idempotent: true,
347
- },
348
- 200,
349
- )
360
+ let idempotentOwner = false
361
+ try {
362
+ const ownership = storeGetEnvironmentOwnership(envId)
363
+ idempotentOwner =
364
+ caller.isApiAdmin ||
365
+ (caller.uuid !== null && ownership?.ownerUserId === caller.uuid)
366
+ } catch (err) {
367
+ logError(
368
+ `[RCS] Ownership lookup failed during attach re-entry (session=${sessionId} env=${envId}): ${err}`,
369
+ )
370
+ return dbUnavailable(c)
371
+ }
372
+ if (idempotentOwner) {
373
+ log(
374
+ `[RCS] Attach idempotent re-entry: session=${sessionId} env=${envId} work=${activeItem.id}`,
375
+ )
376
+ return c.json(
377
+ {
378
+ status: 'attached',
379
+ session_id: sessionId,
380
+ work_item_id: activeItem.id,
381
+ rebind: false,
382
+ idempotent: true,
383
+ },
384
+ 200,
385
+ )
386
+ }
350
387
  }
351
388
 
352
389
  // Authorization: identity-based attach requires manage rights (G9). The
@@ -416,18 +453,34 @@ async function performAttach(
416
453
  // owns (the env it just registered). Admin may attach on behalf.
417
454
  const ownership = storeGetEnvironmentOwnership(envId)
418
455
  if (!ownership?.ownerUserId) {
419
- return c.json(
420
- {
421
- error: {
422
- type: 'environment_unowned',
423
- message:
424
- 'Environment has no owner yet; claim it via the printed URL before attaching sessions',
456
+ // The documented self-hosted worker setup authenticates with a bare
457
+ // RCS_API_KEYS admin key (SALUZI_BRIDGE_OAUTH_TOKEN, no
458
+ // SALUZI_BRIDGE_USERNAME) — its env is unowned until claimed, so a hard
459
+ // 409 here made `/rc --attach <id>` fail out of the box with a claim URL
460
+ // the direct-attach path never prints. Bare admin keys are trusted
461
+ // credentials (ensureAttachAccess already audited their session-manage
462
+ // bypass) — allow the bind. Identity callers still get the claim-flow
463
+ // guidance (the wait-mode CLI prints the URL).
464
+ if (!caller.isApiAdmin || caller.hasIdentity) {
465
+ return c.json(
466
+ {
467
+ error: {
468
+ type: 'environment_unowned',
469
+ message:
470
+ 'Environment has no owner yet; claim it via the printed URL before attaching sessions',
471
+ },
425
472
  },
426
- },
427
- 409,
473
+ 409,
474
+ )
475
+ }
476
+ log(
477
+ `[audit] Bare admin API key attached session ${sessionId} to unowned env ${envId}`,
428
478
  )
429
479
  }
430
- if (!caller.isApiAdmin && ownership.ownerUserId !== caller.uuid) {
480
+ // Optional chain: the bare-admin fallthrough above leaves ownership
481
+ // possibly-null/unowned here — but isApiAdmin is true on that path, so the
482
+ // short-circuit keeps the semantics identical.
483
+ if (!caller.isApiAdmin && ownership?.ownerUserId !== caller.uuid) {
431
484
  return c.json(
432
485
  {
433
486
  error: {
@@ -482,9 +535,17 @@ async function performAttach(
482
535
  workItemId = await createWorkItem(envId, sessionId)
483
536
  } catch (err) {
484
537
  // Roll the rebind back so a failed dispatch doesn't leave the session
485
- // pointing at an environment that never picked it up.
486
- if (rebind && session.environment_id) {
487
- storeRebindSessionEnvironment(sessionId, session.environment_id)
538
+ // pointing at an environment that never picked it up — including back to
539
+ // "no environment" when the session had none. A rollback failure must
540
+ // not mask the original dispatch error.
541
+ if (rebind) {
542
+ try {
543
+ storeRebindSessionEnvironment(sessionId, session.environment_id)
544
+ } catch (rollbackErr) {
545
+ logError(
546
+ `[RCS] Attach rollback failed for session ${sessionId}: ${rollbackErr}`,
547
+ )
548
+ }
488
549
  }
489
550
  return c.json(
490
551
  {
@@ -570,6 +631,9 @@ app.post('/attach-code/exchange', bridgeAuth, async c => {
570
631
  * wrong layer (P0-1). */
571
632
  app.get('/:id/events', bridgeAuth, async c => {
572
633
  const caller = resolveAttachCaller(c)
634
+ if (caller.dbUnavailable) {
635
+ return dbUnavailable(c)
636
+ }
573
637
  const sessionId =
574
638
  resolveExistingSessionId(c.req.param('id')!) ?? c.req.param('id')!
575
639
  const session = getSession(sessionId)
@@ -96,6 +96,31 @@ app.post('/sessions', sessionAuth, async c => {
96
96
  const session = getSession(sessionId)
97
97
  return c.json(toWebSessionResponse(session!), 200)
98
98
  }
99
+
100
+ // In-process (replBridge) workers register with workerType='repl' and
101
+ // maxSessions=1 — they can only serve one session at a time. Reject
102
+ // multi-session creation with a helpful error guiding the user to
103
+ // `slz rc` (spawn mode). MUST run before createSession: the new session
104
+ // is created WITH body.environment_id, so checking after would always
105
+ // see the just-created session and 409 every request (leaving a stray
106
+ // bound-but-never-dispatched session behind on each retry).
107
+ if (env?.workerType === 'repl') {
108
+ const existing = storeListSessionsByEnvironment(env.id).filter(
109
+ s => !isSessionClosedStatus(s.status),
110
+ )
111
+ if (existing.length > 0) {
112
+ return c.json(
113
+ {
114
+ error: {
115
+ type: 'worker_at_capacity',
116
+ message:
117
+ '该 worker 为 in-process 模式(slz 默认),只支持单会话。请用 `slz rc` 启动 spawn 模式 worker 支持多会话。',
118
+ },
119
+ },
120
+ 409,
121
+ )
122
+ }
123
+ }
99
124
  }
100
125
 
101
126
  const teamId = typeof body.team_id === 'string' ? body.team_id : null
@@ -120,28 +145,6 @@ app.post('/sessions', sessionAuth, async c => {
120
145
 
121
146
  // Dispatch work to environment if specified
122
147
  if (body.environment_id) {
123
- // In-process (replBridge) workers register with workerType='repl' and
124
- // maxSessions=1 — they can only serve one session at a time. Reject
125
- // multi-session creation with a helpful error guiding the user to
126
- // `slz rc` (spawn mode).
127
- const env = storeGetEnvironment(body.environment_id)
128
- if (env?.workerType === 'repl') {
129
- const existing = storeListSessionsByEnvironment(env.id).filter(
130
- s => !isSessionClosedStatus(s.status),
131
- )
132
- if (existing.length > 0) {
133
- return c.json(
134
- {
135
- error: {
136
- type: 'worker_at_capacity',
137
- message:
138
- '该 worker 为 in-process 模式(slz 默认),只支持单会话。请用 `slz rc` 启动 spawn 模式 worker 支持多会话。',
139
- },
140
- },
141
- 409,
142
- )
143
- }
144
- }
145
148
  try {
146
149
  const spawnMode =
147
150
  body.spawn_mode === 'worktree' || body.spawn_mode === 'same-dir'
@@ -277,6 +277,31 @@ export function updatePollTime(envId: string) {
277
277
  storeUpdateEnvironment(envId, { lastPollAt: new Date() })
278
278
  }
279
279
 
280
+ /**
281
+ * Worker-traffic liveness signal: refresh lastPollAt and, when the disconnect
282
+ * monitor had flipped the env to 'disconnected', reactivate it.
283
+ *
284
+ * The flip is based on a stale heartbeat — a worker actively hitting the env
285
+ * (work poll, assignment poll, session-creation retry during claim wait)
286
+ * proves it is alive. Without reactivation the CLI polls a zombie env
287
+ * forever: the work-poll route never 404s, so the client's env-lost recovery
288
+ * never fires, and any session created on the flipped env gets no work item
289
+ * (createWorkItem rejects non-active envs) — a session that exists but can
290
+ * never connect. 'deregistered' is terminal and stays dead.
291
+ */
292
+ export function touchEnvironmentActivity(envId: string) {
293
+ const env = storeGetEnvironment(envId)
294
+ if (!env) return
295
+ if (env.status === 'disconnected') {
296
+ storeUpdateEnvironment(envId, { status: 'active', lastPollAt: new Date() })
297
+ log(
298
+ `[RCS] Environment ${envId} reactivated by worker traffic (was disconnected)`,
299
+ )
300
+ } else if (env.status === 'active') {
301
+ storeUpdateEnvironment(envId, { lastPollAt: new Date() })
302
+ }
303
+ }
304
+
280
305
  export function listActiveEnvironments() {
281
306
  return storeListActiveEnvironments()
282
307
  }
@@ -586,10 +586,13 @@ export function storeListSessionsByEnvironment(envId: string): SessionRecord[] {
586
586
  * storeUpdateSession's patch type intentionally excludes environment_id
587
587
  * because ownership-binding flows must never re-point a session as a side
588
588
  * effect. Attach is the one flow with an explicit re-bind contract.
589
+ *
590
+ * environmentId may be null — the rollback path (dispatch failure in
591
+ * performAttach) restores "no environment" when the session had none.
589
592
  */
590
593
  export function storeRebindSessionEnvironment(
591
594
  sessionId: string,
592
- environmentId: string,
595
+ environmentId: string | null,
593
596
  ): boolean {
594
597
  const rec = sessions.get(sessionId)
595
598
  if (!rec) return false
@@ -1018,6 +1021,13 @@ export function storeIssueAttachCode(
1018
1021
  sessionId: string,
1019
1022
  ttlMs = 15 * 60 * 1000,
1020
1023
  ): AttachCodeRecord {
1024
+ // Sweep expired codes: consume() only removes a code when it is actually
1025
+ // presented, so unconsumed expired codes would otherwise accumulate for
1026
+ // the process lifetime. Map deletion during iteration is safe.
1027
+ const now = Date.now()
1028
+ for (const [code, record] of attachCodes) {
1029
+ if (record.expiresAt.getTime() <= now) attachCodes.delete(code)
1030
+ }
1021
1031
  const code = `atc_${randomUUID().replace(/-/g, '')}`
1022
1032
  const record: AttachCodeRecord = {
1023
1033
  code,
@@ -1045,6 +1055,12 @@ export function storeResetAttachCodes() {
1045
1055
  attachCodes.clear()
1046
1056
  }
1047
1057
 
1058
+ /** Test helper — number of attach codes currently held (expired codes that
1059
+ * have not been swept yet still count). */
1060
+ export function storeAttachCodesSize(): number {
1061
+ return attachCodes.size
1062
+ }
1063
+
1048
1064
  // ---------- Reset (for tests) ----------
1049
1065
 
1050
1066
  export function storeReset() {
@@ -1 +0,0 @@
1
- @import "https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap";@layer components;@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-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-content:""}}}@layer theme{:root,:host{--font-sans:"Poppins", system-ui, -apple-system, sans-serif;--font-serif:Georgia, "Times New Roman", serif;--font-mono:"JetBrains Mono", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-200:oklch(88.5% .062 18.334);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-800:oklch(44.4% .177 26.899);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-md:28rem;--container-2xl:42rem;--container-3xl:48rem;--container-4xl:56rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wider:.05em;--leading-tight:1.25;--leading-relaxed:1.625;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--ease-out:cubic-bezier(0, 0, .2, 1);--animate-spin:spin 1s linear infinite;--blur-sm:8px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--radius:.625rem;--font-display:"Poppins", sans-serif;--color-surface-3:#d4bfae;--color-text-primary:#372e37;--color-text-secondary:#737373;--color-background:#f0e0d3;--color-foreground:#372e37;--color-card:#e6d2c2;--color-primary:#7d5a8a;--color-primary-foreground:#f0e0d3;--color-secondary:#e6d2c2;--color-secondary-foreground:#372e37;--color-muted:#e6d2c2;--color-muted-foreground:#737373;--color-accent:#7d5a8a;--color-accent-foreground:#f0e0d3;--color-destructive:#4b1535;--color-border:#7d5a8a;--color-border-light:#e6d2c2;--color-ring:#7d5a8a}}@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%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}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;-webkit-text-decoration:inherit;-webkit-text-decoration: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]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}html,body{background:var(--color-background);color:var(--color-text-primary);font-family:var(--font-sans);-webkit-font-smoothing:antialiased}*,:before,:after{border-color:var(--color-border)}}@layer utilities{.pointer-events-none{pointer-events:none}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing) * 0)}.top-4{top:calc(var(--spacing) * 4)}.top-full{top:100%}.right-0{right:calc(var(--spacing) * 0)}.right-2{right:calc(var(--spacing) * 2)}.right-4{right:calc(var(--spacing) * 4)}.bottom-2{bottom:calc(var(--spacing) * 2)}.bottom-4{bottom:calc(var(--spacing) * 4)}.left-2{left:calc(var(--spacing) * 2)}.left-4{left:calc(var(--spacing) * 4)}.z-10{z-index:10}.z-50{z-index:50}.container{width:100%}@media (width>=40rem){.container{max-width:40rem}}@media (width>=48rem){.container{max-width:48rem}}@media (width>=64rem){.container{max-width:64rem}}@media (width>=80rem){.container{max-width:80rem}}@media (width>=96rem){.container{max-width:96rem}}.mx-1{margin-inline:calc(var(--spacing) * 1)}.mx-auto{margin-inline:auto}.my-1{margin-block:calc(var(--spacing) * 1)}.my-2{margin-block:calc(var(--spacing) * 2)}.my-4{margin-block:calc(var(--spacing) * 4)}.my-6{margin-block:calc(var(--spacing) * 6)}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-5{margin-top:calc(var(--spacing) * 5)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.mb-10{margin-bottom:calc(var(--spacing) * 10)}.mb-12{margin-bottom:calc(var(--spacing) * 12)}.mb-14{margin-bottom:calc(var(--spacing) * 14)}.mb-16{margin-bottom:calc(var(--spacing) * 16)}.mb-20{margin-bottom:calc(var(--spacing) * 20)}.ml-1{margin-left:calc(var(--spacing) * 1)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-3{margin-left:calc(var(--spacing) * 3)}.ml-auto{margin-left:auto}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.list-item{display:list-item}.table{display:table}.table-cell{display:table-cell}.table-row{display:table-row}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.size-full{width:100%;height:100%}.h-2\.5{height:calc(var(--spacing) * 2.5)}.h-3{height:calc(var(--spacing) * 3)}.h-4{height:calc(var(--spacing) * 4)}.h-7{height:calc(var(--spacing) * 7)}.h-8{height:calc(var(--spacing) * 8)}.h-16{height:calc(var(--spacing) * 16)}.h-\[46px\]{height:46px}.h-\[calc\(100vh-2rem\)\]{height:calc(100vh - 2rem)}.h-auto{height:auto}.h-full{height:100%}.h-screen{height:100vh}.min-h-28{min-height:calc(var(--spacing) * 28)}.min-h-\[200px\]{min-height:200px}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-2\.5{width:calc(var(--spacing) * 2.5)}.w-3{width:calc(var(--spacing) * 3)}.w-4{width:calc(var(--spacing) * 4)}.w-7{width:calc(var(--spacing) * 7)}.w-8{width:calc(var(--spacing) * 8)}.w-56{width:calc(var(--spacing) * 56)}.w-64{width:calc(var(--spacing) * 64)}.w-full{width:100%}.w-screen{width:100vw}.max-w-2xl{max-width:var(--container-2xl)}.max-w-3xl{max-width:var(--container-3xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-5xl{max-width:var(--container-5xl)}.max-w-full{max-width:100%}.max-w-md{max-width:var(--container-md)}.max-w-none{max-width:none}.min-w-0{min-width:calc(var(--spacing) * 0)}.min-w-\[120px\]{min-width:120px}.flex-1{flex:1}.shrink-0{flex-shrink:0}.border-collapse{border-collapse:collapse}.origin-center{transform-origin:50%}.rotate-\[0\.3deg\]{rotate:.3deg}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-spin{animation:var(--animate-spin)}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.list-inside{list-style-position:inside}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.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-5{gap:calc(var(--spacing) * 5)}.gap-10{gap:calc(var(--spacing) * 10)}: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-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 2) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-x-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-border>:not(:last-child)){border-color:var(--color-border)}.self-start{align-self:flex-start}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:var(--radius)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xl{border-radius:var(--radius-xl)}.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-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-l-4{border-left-style:var(--tw-border-style);border-left-width:4px}.border-border{border-color:var(--color-border)}.border-border-light{border-color:var(--color-border-light)}.border-current{border-color:currentColor}.border-destructive{border-color:var(--color-destructive)}.border-muted-foreground\/30{border-color:#7373734d}@supports (color:color-mix(in lab, red, red)){.border-muted-foreground\/30{border-color:color-mix(in oklab, var(--color-muted-foreground) 30%, transparent)}}.border-primary{border-color:var(--color-primary)}.border-red-200{border-color:var(--color-red-200)}.border-transparent{border-color:#0000}.border-white\/10{border-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.border-white\/10{border-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}.bg-\[\#0b0e14\]{background-color:#0b0e14}.bg-\[\#27c93f\]{background-color:#27c93f}.bg-\[\#ff5f56\]{background-color:#ff5f56}.bg-\[\#ffbd2e\]{background-color:#ffbd2e}.bg-accent{background-color:var(--color-accent)}.bg-background{background-color:var(--color-background)}.bg-background\/80{background-color:#f0e0d3cc}@supports (color:color-mix(in lab, red, red)){.bg-background\/80{background-color:color-mix(in oklab, var(--color-background) 80%, transparent)}}.bg-background\/90{background-color:#f0e0d3e6}@supports (color:color-mix(in lab, red, red)){.bg-background\/90{background-color:color-mix(in oklab, var(--color-background) 90%, transparent)}}.bg-background\/95{background-color:#f0e0d3f2}@supports (color:color-mix(in lab, red, red)){.bg-background\/95{background-color:color-mix(in oklab, var(--color-background) 95%, transparent)}}.bg-black\/10{background-color:#0000001a}@supports (color:color-mix(in lab, red, red)){.bg-black\/10{background-color:color-mix(in oklab, var(--color-black) 10%, transparent)}}.bg-card{background-color:var(--color-card)}.bg-destructive{background-color:var(--color-destructive)}.bg-muted{background-color:var(--color-muted)}.bg-muted\/40{background-color:#e6d2c266}@supports (color:color-mix(in lab, red, red)){.bg-muted\/40{background-color:color-mix(in oklab, var(--color-muted) 40%, transparent)}}.bg-muted\/80{background-color:#e6d2c2cc}@supports (color:color-mix(in lab, red, red)){.bg-muted\/80{background-color:color-mix(in oklab, var(--color-muted) 80%, transparent)}}.bg-primary{background-color:var(--color-primary)}.bg-primary\/10{background-color:#7d5a8a1a}@supports (color:color-mix(in lab, red, red)){.bg-primary\/10{background-color:color-mix(in oklab, var(--color-primary) 10%, transparent)}}.bg-red-50{background-color:var(--color-red-50)}.bg-red-100{background-color:var(--color-red-100)}.bg-secondary{background-color:var(--color-secondary)}.bg-surface-3{background-color:var(--color-surface-3)}.bg-white\/5{background-color:#ffffff0d}@supports (color:color-mix(in lab, red, red)){.bg-white\/5{background-color:color-mix(in oklab, var(--color-white) 5%, transparent)}}.p-1{padding:calc(var(--spacing) * 1)}.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-5{padding:calc(var(--spacing) * 5)}.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-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-8{padding-inline:calc(var(--spacing) * 8)}.px-10{padding-inline:calc(var(--spacing) * 10)}.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-3{padding-block:calc(var(--spacing) * 3)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-12{padding-block:calc(var(--spacing) * 12)}.py-16{padding-block:calc(var(--spacing) * 16)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-8{padding-bottom:calc(var(--spacing) * 8)}.pl-2{padding-left:calc(var(--spacing) * 2)}.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-display{font-family:var(--font-display)}.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-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--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-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[13px\]{font-size:13px}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.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)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[0\.2em\]{--tw-tracking:.2em;letter-spacing:.2em}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.wrap-anywhere{overflow-wrap:anywhere}.whitespace-normal{white-space:normal}.whitespace-nowrap{white-space:nowrap}.text-\[\#7dd3fc\]{color:#7dd3fc}.text-\[\#27c93f\]{color:#27c93f}.text-\[\#ffbd2e\]{color:#ffbd2e}.text-accent-foreground{color:var(--color-accent-foreground)}.text-destructive{color:var(--color-destructive)}.text-foreground{color:var(--color-foreground)}.text-muted-foreground{color:var(--color-muted-foreground)}.text-primary{color:var(--color-primary)}.text-primary-foreground{color:var(--color-primary-foreground)}.text-primary\/80{color:#7d5a8acc}@supports (color:color-mix(in lab, red, red)){.text-primary\/80{color:color-mix(in oklab, var(--color-primary) 80%, transparent)}}.text-red-600{color:var(--color-red-600)}.text-red-700{color:var(--color-red-700)}.text-red-800{color:var(--color-red-800)}.text-secondary-foreground{color:var(--color-secondary-foreground)}.lowercase{text-transform:lowercase}.uppercase{text-transform:uppercase}.italic{font-style:italic}.underline{text-decoration-line:underline}.opacity-0{opacity:0}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-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-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.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))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-shadow{transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.\[counter-increment\:line_0\]{counter-increment:line 0}.\[counter-reset\:line\]{counter-reset:line}@media (hover:hover){.group-hover\:block:is(:where(.group):hover *){display:block}.group-hover\:translate-x-0\.5:is(:where(.group):hover *){--tw-translate-x:calc(var(--spacing) * .5);translate:var(--tw-translate-x) var(--tw-translate-y)}.group-hover\:translate-x-1:is(:where(.group):hover *){--tw-translate-x:calc(var(--spacing) * 1);translate:var(--tw-translate-x) var(--tw-translate-y)}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}}.before\:mr-4:before{content:var(--tw-content);margin-right:calc(var(--spacing) * 4)}.before\:inline-block:before{content:var(--tw-content);display:inline-block}.before\:w-4:before{content:var(--tw-content);width:calc(var(--spacing) * 4)}.before\:w-6:before{content:var(--tw-content);width:calc(var(--spacing) * 6)}.before\:text-right:before{content:var(--tw-content);text-align:right}.before\:font-mono:before{content:var(--tw-content);font-family:var(--font-mono)}.before\:text-\[13px\]:before{content:var(--tw-content);font-size:13px}.before\:text-muted-foreground\/50:before{content:var(--tw-content);color:#73737380}@supports (color:color-mix(in lab, red, red)){.before\:text-muted-foreground\/50:before{color:color-mix(in oklab, var(--color-muted-foreground) 50%, transparent)}}.before\:content-\[counter\(line\)\]:before{--tw-content:counter(line);content:var(--tw-content)}.before\:select-none:before{content:var(--tw-content);-webkit-user-select:none;user-select:none}.before\:\[counter-increment\:line\]:before{content:var(--tw-content);counter-increment:line}:is(.\*\:first\:mt-0>*):first-child{margin-top:calc(var(--spacing) * 0)}:is(.\*\:last\:mb-0>*):last-child{margin-bottom:calc(var(--spacing) * 0)}:is(.\*\:last\:after\:inline>*):last-child:after{content:var(--tw-content);display:inline}:is(.\*\:last\:after\:align-baseline>*):last-child:after{content:var(--tw-content);vertical-align:baseline}:is(.\*\:last\:after\:content-\(--streamdown-caret\)>*):last-child:after{--tw-content:var(--streamdown-caret);content:var(--tw-content)}@media (hover:hover){.hover\:bg-background:hover{background-color:var(--color-background)}.hover\:bg-muted:hover{background-color:var(--color-muted)}.hover\:bg-muted\/40:hover{background-color:#e6d2c266}@supports (color:color-mix(in lab, red, red)){.hover\:bg-muted\/40:hover{background-color:color-mix(in oklab, var(--color-muted) 40%, transparent)}}.hover\:bg-white\/5:hover{background-color:#ffffff0d}@supports (color:color-mix(in lab, red, red)){.hover\:bg-white\/5:hover{background-color:color-mix(in oklab, var(--color-white) 5%, transparent)}}.hover\:text-foreground:hover{color:var(--color-foreground)}.hover\:opacity-80:hover{opacity:.8}.hover\:shadow-md:hover{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,var(--color-ring));box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus-visible\:ring-primary:focus-visible{--tw-ring-color:var(--color-primary)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media (width>=40rem){.sm\:px-4{padding-inline:calc(var(--spacing) * 4)}.sm\:px-10{padding-inline:calc(var(--spacing) * 10)}.sm\:px-14{padding-inline:calc(var(--spacing) * 14)}.sm\:px-16{padding-inline:calc(var(--spacing) * 16)}.sm\:py-16{padding-block:calc(var(--spacing) * 16)}.sm\:py-20{padding-block:calc(var(--spacing) * 20)}}@media (width>=48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (width>=64rem){.lg\:block{display:block}}@media (prefers-color-scheme:dark){.dark\:bg-\(--shiki-dark-bg\)\!{background-color:var(--shiki-dark-bg)!important}.dark\:text-\(--shiki-dark\)\!{color:var(--shiki-dark)!important}}.\[\&_svg\]\:h-auto svg{height:auto}.\[\&_svg\]\:w-auto svg{width:auto}.\[\&\>div\]\:h-full>div{height:100%}.\[\&\>div\]\:overflow-hidden>div{overflow:hidden}.\[\&\>p\]\:inline>p{display:inline}li .\[li_\&\]\:pl-6{padding-left:calc(var(--spacing) * 6)}}.dark{--color-surface-0:#1a1917;--color-surface-1:#222120;--color-surface-2:#2c2a28;--color-surface-3:#3a3835;--color-text-primary:#efeee9;--color-text-secondary:#969088;--color-text-muted:#5e5a54;--color-bg-inverted:#f6f5f2;--color-text-inverted:#2c2a28;--color-border:#3a3835;--color-border-light:#2c2a28;--color-background:#1a1917;--color-foreground:#efeee9;--color-card:#222120;--color-card-foreground:#efeee9;--color-popover:#1a1917;--color-popover-foreground:#efeee9;--color-primary:#7d5a8a;--color-primary-foreground:#efeee9;--color-secondary:#2c2a28;--color-secondary-foreground:#efeee9;--color-muted:#2c2a28;--color-muted-foreground:#969088;--color-accent:#7d5a8a;--color-accent-foreground:#efeee9;--color-destructive:#4b1535;--color-input:#3a3835;--color-ring:#7d5a8a}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:0 0}::-webkit-scrollbar-thumb{background:var(--color-surface-3);border-radius:3px}.chapter-content{font-family:var(--font-sans);color:var(--color-text-primary);font-size:1.0625rem;line-height:1.75}.chapter-content p,.chapter-content ul,.chapter-content ol,.chapter-content blockquote,.chapter-content pre,.chapter-content table,.chapter-content hr,.chapter-content img{margin-top:1.25rem;margin-bottom:1.25rem}.chapter-content>:first-child,.chapter-content>div>:first-child{margin-top:0}.chapter-content>:last-child,.chapter-content>div>:last-child{margin-bottom:0}.chapter-content h1,.chapter-content h2,.chapter-content h3,.chapter-content h4,.chapter-content h5,.chapter-content h6{font-family:var(--font-display);letter-spacing:-.015em;color:var(--color-text-primary);margin-top:2.5rem;margin-bottom:1rem;font-weight:600;line-height:1.3}.chapter-content h1{margin-top:0;font-size:1.875rem}.chapter-content h2{border-bottom:1px solid var(--color-border-light);padding-bottom:.5rem;font-size:1.5rem}.chapter-content h3{font-size:1.25rem}.chapter-content h4{font-size:1.125rem}.chapter-content p{line-height:1.75}.chapter-content a{color:var(--color-primary);border-bottom:1px solid #0000;text-decoration:none;transition:border-color .2s}.chapter-content a:hover{border-bottom-color:var(--color-primary)}.chapter-content strong{color:var(--color-text-primary);font-weight:600}.chapter-content em{font-style:italic}.chapter-content ul,.chapter-content ol{padding-left:1.5rem}.chapter-content ul{list-style:outside}.chapter-content ol{list-style:decimal}.chapter-content li{margin-top:.5rem;margin-bottom:.5rem;line-height:1.7}.chapter-content li::marker{color:var(--color-primary)}.chapter-content blockquote{border-left:3px solid var(--color-primary);color:var(--color-text-secondary);padding:.5rem 0 .5rem 1.25rem;font-style:italic}.chapter-content code{font-family:var(--font-mono);background:var(--color-muted);color:var(--color-primary);border-radius:.375rem;padding:.125rem .375rem;font-size:.875em;font-weight:500}.chapter-content pre{background:var(--color-card);border:1px solid var(--color-border-light);border-radius:.75rem;padding:1.25rem 1.5rem;font-size:.875rem;line-height:1.6;overflow-x:auto}.chapter-content pre code{color:inherit;font-weight:inherit;background:0 0;border-radius:0;padding:0}.chapter-content pre code>span:last-child:empty{display:none}.chapter-content table{border-collapse:collapse;width:100%;font-size:.9375rem}.chapter-content th{text-align:left;font-family:var(--font-display);color:var(--color-text-primary);border-bottom:2px solid var(--color-primary);background:var(--color-muted);padding:.75rem 1rem;font-weight:600}.chapter-content td{border-bottom:1px solid var(--color-border-light);color:var(--color-text-primary);vertical-align:top;padding:.75rem 1rem}.chapter-content tr:hover td{background:var(--color-muted)}.chapter-content hr{border:none;border-top:1px solid var(--color-border-light)}.chapter-content img{border-radius:.625rem;max-width:100%;height:auto}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-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-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@keyframes spin{to{transform:rotate(360deg)}}