From 90930f8bd74f953a04589ed2891d18372cb494d5 Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Thu, 6 Aug 2026 04:57:16 +0000 Subject: [PATCH] feat(examples,docs): migrate the reference hubs to initHub; add Nitro & Hono examples, Bun smoke, framework guides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both reference hosts now assemble through one initHub() call while keeping their hand-built viewer UIs as protocol demos: the Vite example shares Vite's own http server for the WS upgrade at /__devframes/__ws (zero extra ports) and the Next example collapses its encoded catch-all routes into a single app/%5F_devframes/[[...path]]/route.ts delegating to hub.handler. New minimal examples prove the middleware story end to end: - examples/nitro-devframe-hub — Nitro v3, one catch-all route (+ an index route for the namespace root), devframe packages kept external so import.meta.url asset resolution survives bundling - examples/hono-devframe-hub — one runtime-agnostic app file served by @hono/node-server on Node and Bun.serve on Bun (fetch-upgrade tier); scripts/smoke-bun.ts exercises fetch + WS RPC + embedded.js on Bun initHub grows what the migrations needed: devframes entries with dock overrides, rpcDeclarations passthrough, a route-safe id guard (DF8004), a bind-retry for the auto side-car, and a buffered embedded.js body that survives dev-worker proxies. Docs: adapters/initiate (mount snippets for Vite/Nitro/Hono/Next/Nuxt/ SvelteKit, WS binding precedence, auth posture) and guide/hub-initiate (the namespace, the ui slot, single hub Auth, singular-vs-hub table). --- docs/.vitepress/config.ts | 2 + docs/adapters/initiate.md | 112 ++++++ docs/errors/DF8004.md | 34 ++ docs/guide/hub-initiate.md | 78 +++++ eslint.config.js | 2 + examples/hono-devframe-hub/README.md | 22 ++ examples/hono-devframe-hub/package.json | 25 ++ examples/hono-devframe-hub/src/app.ts | 70 ++++ examples/hono-devframe-hub/src/bun.ts | 18 + examples/hono-devframe-hub/src/node.ts | 10 + examples/hono-devframe-hub/tsconfig.json | 19 + examples/next-devframe-hub/README.md | 33 +- .../client/app/%5F_[id]/[[...path]]/route.ts | 22 -- .../app/%5F_devframes/[[...path]]/route.ts | 25 ++ .../app/%5F_hub/%5F_connection.json/route.ts | 15 - .../next-devframe-hub/src/client/app/page.tsx | 2 +- .../src/client/devframe/demo-devframe-b.ts | 2 +- .../src/client/devframe/demo-devframe.ts | 4 +- .../src/client/devframe/next-devframe-hub.ts | 327 ++++++++---------- .../src/client/devframe/tabbed-devframe.ts | 2 +- .../src/client/next.config.mjs | 3 +- .../tests/next-devframe-hub.test.ts | 70 +++- examples/nitro-devframe-hub/README.md | 18 + examples/nitro-devframe-hub/hub.ts | 34 ++ examples/nitro-devframe-hub/nitro.config.ts | 22 ++ examples/nitro-devframe-hub/package.json | 22 ++ .../routes/__devframes/[...path].ts | 8 + .../routes/__devframes/index.ts | 8 + examples/nitro-devframe-hub/routes/index.ts | 26 ++ examples/nitro-devframe-hub/tsconfig.json | 23 ++ examples/vite-devframe-hub/README.md | 12 +- examples/vite-devframe-hub/package.json | 3 +- examples/vite-devframe-hub/src/client/main.ts | 2 +- examples/vite-devframe-hub/src/devframe-b.ts | 4 +- examples/vite-devframe-hub/src/devframe.ts | 4 +- examples/vite-devframe-hub/src/tabbed-tool.ts | 4 +- .../src/vite-devframe-hub.ts | 209 +++++------ examples/vite-devframe-hub/vite.config.ts | 38 +- knip.jsonc | 4 +- packages/hub-ui/src/client/.generated/css.ts | 2 +- packages/hub/src/node/diagnostics.ts | 4 + packages/hub/src/node/initiate.ts | 89 ++++- pnpm-lock.yaml | 314 ++++++++++++++++- pnpm-workspace.yaml | 3 + scripts/smoke-bun.ts | 75 ++++ .../@devframes/hub/initiate.snapshot.d.ts | 7 +- .../tsnapi/@devframes/hub/node.snapshot.d.ts | 10 +- 47 files changed, 1430 insertions(+), 412 deletions(-) create mode 100644 docs/adapters/initiate.md create mode 100644 docs/errors/DF8004.md create mode 100644 docs/guide/hub-initiate.md create mode 100644 examples/hono-devframe-hub/README.md create mode 100644 examples/hono-devframe-hub/package.json create mode 100644 examples/hono-devframe-hub/src/app.ts create mode 100644 examples/hono-devframe-hub/src/bun.ts create mode 100644 examples/hono-devframe-hub/src/node.ts create mode 100644 examples/hono-devframe-hub/tsconfig.json delete mode 100644 examples/next-devframe-hub/src/client/app/%5F_[id]/[[...path]]/route.ts create mode 100644 examples/next-devframe-hub/src/client/app/%5F_devframes/[[...path]]/route.ts delete mode 100644 examples/next-devframe-hub/src/client/app/%5F_hub/%5F_connection.json/route.ts create mode 100644 examples/nitro-devframe-hub/README.md create mode 100644 examples/nitro-devframe-hub/hub.ts create mode 100644 examples/nitro-devframe-hub/nitro.config.ts create mode 100644 examples/nitro-devframe-hub/package.json create mode 100644 examples/nitro-devframe-hub/routes/__devframes/[...path].ts create mode 100644 examples/nitro-devframe-hub/routes/__devframes/index.ts create mode 100644 examples/nitro-devframe-hub/routes/index.ts create mode 100644 examples/nitro-devframe-hub/tsconfig.json create mode 100644 scripts/smoke-bun.ts diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index fb28314f..1f201826 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -32,6 +32,7 @@ function guideItems(prefix: string) { { text: 'Security', link: `${prefix}/guide/security` }, { text: 'Standalone CLI', link: `${prefix}/guide/standalone-cli` }, { text: 'Hub', link: `${prefix}/guide/hub` }, + { text: 'Serve a Hub Anywhere', link: `${prefix}/guide/hub-initiate` }, { text: 'Deep Linking', link: `${prefix}/guide/deep-linking` }, { text: 'Client Scripts & Context', link: `${prefix}/guide/client-context` }, { text: 'Agent-Native (experimental)', link: `${prefix}/guide/agent-native` }, @@ -43,6 +44,7 @@ function adaptersItems(prefix: string) { { text: 'Overview', link: `${prefix}/adapters/` }, { text: 'CLI (cac)', link: `${prefix}/adapters/cac` }, { text: 'Dev', link: `${prefix}/adapters/dev` }, + { text: 'Initiate (middleware)', link: `${prefix}/adapters/initiate` }, { text: 'Build', link: `${prefix}/adapters/build` }, { text: 'Vite', link: `${prefix}/adapters/vite` }, { text: 'Embedded', link: `${prefix}/adapters/embedded` }, diff --git a/docs/adapters/initiate.md b/docs/adapters/initiate.md new file mode 100644 index 00000000..3a3c4cc0 --- /dev/null +++ b/docs/adapters/initiate.md @@ -0,0 +1,112 @@ +# Initiate (standard middleware) + +Serve a devframe from inside any app that can mount a catch-all route: `initDevframe(def)` returns a live instance whose `.handler` — a web-standard `(request: Request) => Promise` — carries the whole surface (the SPA, `__connection.json` discovery, the WebSocket RPC endpoint, the auth gate, and the optional MCP route) under one mount base. + +```ts +import { initDevframe } from 'devframe/initiate' +import myDevframe from './devframe' + +const devtools = initDevframe(myDevframe, { key: 'my-tool' }) +// devtools.handler, devtools.nodeMiddleware, devtools.websocket, +// devtools.ready, devtools.context, devtools.connectionMeta(), devtools.close() +``` + +The factory is synchronous and initializes eagerly; `handler`/`nodeMiddleware` await readiness internally, so hosts never race the boot. The default base is the hosted rule — `def.basePath` or `/__/`. + +## Mount the handler + +::: code-group + +```ts [Vite] +import { initDevframe } from 'devframe/initiate' +// vite.config.ts — connect-style middleware + Vite's own server for the socket +import { defineConfig } from 'vite' +import myDevframe from './devframe' + +export default defineConfig({ + plugins: [{ + name: 'my-tool', + apply: 'serve', + configureServer(server) { + const devtools = initDevframe(myDevframe, { + key: 'my-tool', + server: server.httpServer ?? undefined, + }) + server.middlewares.use(devtools.nodeMiddleware) + }, + }], +}) +``` + +```ts [Nitro] +// routes/__my-tool/[...path].ts — plus routes/__my-tool/index.ts (same body) +// for the namespace root, since a catch-all doesn't match its own empty path. +import { defineHandler } from 'nitro' +import { devtools } from '../../devtools' + +export default defineHandler(event => devtools.handler(event.req)) +``` + +```ts [Hono] +// server.ts — the same file runs on Node and Bun +import { Hono } from 'hono' +import { devtools } from './devtools' + +const app = new Hono() +app.all('/__my-tool/*', c => devtools.handler(c.req.raw, c.env)) +``` + +```ts [Next.js] +import { initDevframe } from 'devframe/initiate' +// app/%5F_my-tool/[[...path]]/route.ts — Next reserves `_`-prefixed +// folders, so the segment is URL-encoded (`%5F_` decodes to `__`). +import myDevframe from '@/devframe' + +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +const devtools = initDevframe(myDevframe, { key: 'my-tool' }) +export const GET = devtools.handler +``` + +```ts [Nuxt] +// server/middleware/devtools.ts +import { devtools } from '../devtools' + +export default defineEventHandler((event) => { + const { pathname } = new URL(toWebRequest(event).url) + if (pathname === '/__my-tool' || pathname.startsWith('/__my-tool/')) + return devtools.handler(toWebRequest(event)) +}) +``` + +```ts [SvelteKit] +// src/routes/%5F_my-tool/[...path]/+server.ts +import myDevframe from '$lib/devframe' +import { initDevframe } from 'devframe/initiate' + +const devtools = initDevframe(myDevframe, { key: 'my-tool' }) +export const GET = ({ request }) => devtools.handler(request) +``` + +::: + +For frameworks with dev-time module reloading (Next, Nitro, SvelteKit), always set `key` — a re-evaluation returns the live instance instead of leaking WebSocket servers (`DF0053` reports an intentional replacement when the options changed). + +## The WebSocket binding + +Fetch handlers hand over `Request`s, so the RPC socket needs its own binding. The instance resolves it in precedence order and advertises the result in `__connection.json` — the browser client follows whatever is advertised: + +1. **`ws.port`** — an explicit side-car port. +2. **`server`** — share the host's `node:http` server; the upgrade binds at `__ws`. Zero extra ports, and the socket follows the app through proxies and HTTPS. +3. **`ws.url` alone** — advertise an external endpoint verbatim; the server behind that URL owns the transport (wire the instance's `context` into your own server with `startHttpAndWs`). Combined with `server`/`ws.port`, `ws.url` overrides only the advertisement — the tunnel pattern. +4. **Bun** — same-origin fetch upgrades: pass the `Bun.serve` server as `handler`'s second argument and wire `Bun.serve({ websocket: devtools.websocket })`. +5. **Default** — an eager side-car on a free port, started at init so the meta is stable from the first request. + +## Auth + +The instance **gates by default** — a handler mounted inside an app server is reachable by anything that can open its socket. Devframe's interactive OTP handler is wired automatically and prints its code/magic-link banner once the public origin is known (derived from the first request, or the `origin` option). Pass `auth: false` for a single-user localhost setup, or a `DevframeAuthHandler` for a custom scheme. + +## Relation to the other adapters + +`createDevServer`, `viteDevBridge`, and `@devframes/next` are assembled from this instance internally — the handler is the one wiring underneath every serving path. To host **many** devframes behind one namespace with shared transport and docks, use the hub's counterpart: [`initHub`](../guide/hub-initiate). diff --git a/docs/errors/DF8004.md b/docs/errors/DF8004.md new file mode 100644 index 00000000..5f493b46 --- /dev/null +++ b/docs/errors/DF8004.md @@ -0,0 +1,34 @@ +--- +outline: deep +--- + +# DF8004: Devframe Id Is Not a Mountable URL Segment + +## Message + +> Devframe id "`{id}`" is not a mountable URL segment — the hub mounts each frame at `/`. + +## Cause + +`initHub` derives each frame's mount base from its id (`/__devframes//`), and that segment is routed by h3 — where `:` and `*` are route-pattern markers and `/` ends the segment. An id carrying those characters either crashes route registration or matches the wrong paths. + +## Example + +```ts +import { initHub } from '@devframes/hub/initiate' + +initHub({ + devframes: [defineDevframe({ id: 'devframes:plugin:my-tool', /* … */ })], // ✗ throws DF8004 +}) + +// ✓ Good — route-safe id (letters, digits, `_`, `-`, `.`): +defineDevframe({ id: 'devframes_plugin_my-tool', /* … */ }) +``` + +## Fix + +Set a route-safe `id` on the definition — letters, digits, `_`, `-`, and `.` only. Plugins that accept an `id` option can be re-instantiated with a safe one; RPC function ids (the colon-namespaced `devframes:plugin::` convention) are unaffected — this constraint applies to the devframe id alone. + +## Source + +- [`packages/hub/src/node/initiate.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/initiate.ts) — `initHub` throws this while mounting the `devframes` list. diff --git a/docs/guide/hub-initiate.md b/docs/guide/hub-initiate.md new file mode 100644 index 00000000..c968de48 --- /dev/null +++ b/docs/guide/hub-initiate.md @@ -0,0 +1,78 @@ +# Serve a Hub Anywhere + +`initHub()` from `@devframes/hub/initiate` puts a whole multi-devframe devtools installation behind one web-standard handler: mount it on a single catch-all route and every frame, the shared RPC socket, the single auth gate, discovery, and the optional UI are live under one namespace (default `/__devframes/`). + +```ts +import { createUi } from '@devframes/hub-ui' +import { initHub } from '@devframes/hub/initiate' +import { createInspectDevframe } from '@devframes/plugin-inspect' +import { createTerminalsDevframe } from '@devframes/plugin-terminals' + +export const hub = initHub({ + key: 'devtools', + devframes: [createInspectDevframe(), createTerminalsDevframe()], + ui: createUi(), + configure(ctx) { + ctx.commands.register({ id: 'app:hello', title: 'Hello', handler: () => 'hi' }) + }, +}) +``` + +Every mounted devframe runs its `setup()` against the **shared hub context**: one merged RPC registry (frames can call each other's functions), one shared-state store, one WebSocket transport, one Auth. The instance mirrors `initDevframe`'s surface — `handler`, `nodeMiddleware`, `websocket` (Bun), `ready`, `context`, `connectionMeta()`, `close()` — and the same mount snippets apply with the base swapped to `/__devframes/`; see [the initiate adapter](../adapters/initiate#mount-the-handler). + +## The namespace + +| Path | Serves | +| --- | --- | +| `/` | the `ui.viewer` SPA — or the index document when the hub runs headless | +| `/` | each mounted devframe's SPA, with its own `__connection.json` pointing at the shared socket | +| `embedded.js` | the `ui.embedded` bootstrap (`404` without one) | +| `__connection.json` | connection meta for the shared RPC socket | +| `__ws` | the WebSocket upgrade route (shared-`server` and Bun tiers) | +| `__index.json` | the machine-readable index: frames, endpoints | +| `__client-imports.js` | the dock client-script import map for external viewers | +| `__mcp` | the aggregate MCP endpoint over the whole tool registry (opt-in via `mcp`) | + +Frame ids become URL segments, so they are validated: reserved names throw `DF8000`, and ids must be route-safe (`DF8004`). + +## The `ui` slot + +The hub is headless — `DevframeHubUi` is pure data, and whoever fills it decides what a viewer looks like: + +```ts +interface DevframeHubUi { + viewer?: { distDir: string } // a standalone SPA served at the namespace root + embedded?: { entry: string } // a prebuilt bootstrap served at embedded.js +} +``` + +`@devframes/hub-ui`'s `createUi()` is the reference implementation: a standalone viewer plus the floating dock — one ` + +`, +)) diff --git a/examples/hono-devframe-hub/src/bun.ts b/examples/hono-devframe-hub/src/bun.ts new file mode 100644 index 00000000..53d2a5d4 --- /dev/null +++ b/examples/hono-devframe-hub/src/bun.ts @@ -0,0 +1,18 @@ +import process from 'node:process' +import { app, hub } from './app' + +// Bun tier: WebSocket upgrades complete through `hub.handler(request, +// server)` on the app's own origin — no side-car port. `Bun.serve` needs +// the instance's `websocket` handlers wired alongside the fetch handler. +const port = Number(process.env.PORT ?? 5179) + +export default { + port, + fetch: app.fetch, + websocket: hub.websocket, +} + +void hub.ready.then(() => { + // eslint-disable-next-line no-console + console.log(`hono-devframe-hub (bun) on http://localhost:${port} — devtools at /__devframes/`) +}) diff --git a/examples/hono-devframe-hub/src/node.ts b/examples/hono-devframe-hub/src/node.ts new file mode 100644 index 00000000..d3a1cb61 --- /dev/null +++ b/examples/hono-devframe-hub/src/node.ts @@ -0,0 +1,10 @@ +import process from 'node:process' +import { serve } from '@hono/node-server' +import { app, hub } from './app' + +const port = Number(process.env.PORT ?? 5179) +serve({ fetch: app.fetch, port, hostname: '0.0.0.0' }) +void hub.ready.then(() => { + // eslint-disable-next-line no-console + console.log(`hono-devframe-hub on http://localhost:${port} — devtools at /__devframes/`) +}) diff --git a/examples/hono-devframe-hub/tsconfig.json b/examples/hono-devframe-hub/tsconfig.json new file mode 100644 index 00000000..11916e28 --- /dev/null +++ b/examples/hono-devframe-hub/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": [ + "esnext", + "dom" + ], + "types": [ + "node" + ] + }, + "include": [ + "src" + ], + "exclude": [ + "dist", + "node_modules" + ] +} diff --git a/examples/next-devframe-hub/README.md b/examples/next-devframe-hub/README.md index bd69987c..7ea29373 100644 --- a/examples/next-devframe-hub/README.md +++ b/examples/next-devframe-hub/README.md @@ -1,8 +1,8 @@ # Next Devframe Hub -A tiny, copyable **vite-devtools-style hub on Next.js**. [vite-devtools](https://github.com/vitejs/devtools) is the full Vite viewer built on `@devframes/hub`; this example wears the same shape — an icon dock, an iframe stage, a subsystem drawer — but hosts it from a Next.js App Router app, lazily starting a side-car RPC/WS server from a Node route handler. It's the reference for bringing the same integrations to any non-Vite host. +A tiny, copyable **vite-devtools-style hub on Next.js**. [vite-devtools](https://github.com/vitejs/devtools) is the full Vite viewer built on `@devframes/hub`; this example wears the same shape — an icon dock, an iframe stage, a subsystem drawer — but hosts it from a Next.js App Router app with one `initHub()` instance behind one catch-all route. It's the reference for bringing the same integrations to any non-Vite host. -`src/client/devframe/next-devframe-hub.ts` is the entire host. +`src/client/devframe/next-devframe-hub.ts` is the entire host: a single `initHub()` call from `@devframes/hub/initiate`. ## Run it @@ -13,32 +13,45 @@ pnpm --filter next-devframe-hub dev Open the printed URL. The dock on the left lists every mounted tool with its icon: -- **Git**, **Terminals**, **Code Server**, **RPC & State Inspector**, **A11y Inspector** — the built-in plugins, each mounted with `mountDevframe` +- **Git**, **Terminals**, **Code Server**, **RPC & State Inspector**, **A11y Inspector** — the built-in plugins, each an entry in `initHub`'s `devframes` list - **Next Demo Tool** / **Next Demo Tool B** — two trivial static SPAs that show the bare mount path Selecting a tool loads its SPA in the stage. The bottom drawer mirrors the hub's **Commands**, **Messages**, and **Terminals** subsystems, plus a button that dispatches a command through `hub:commands:execute`. -The A11y Inspector shows a live axe-core report of this hub's own page: the host serves the plugin's in-page agent module (`a11yAgentBundlePath`) same-origin through the catch-all route and attaches it as the a11y dock's `clientScript`; the hub client runtime — `createDevframeClientHost()` booted in `app/page.tsx` — imports it into the page, so the docked panel and the agent share the origin their BroadcastChannel rides. +The A11y Inspector shows a live axe-core report of this hub's own page: the host serves the plugin's in-page agent module (`a11yAgentBundlePath`) same-origin inside the hub namespace and attaches it as the a11y dock's `clientScript` (the `{ devframe, dock }` entry form); the hub client runtime — `createDevframeClientHost()` booted in `app/page.tsx` — imports it into the page, so the docked panel and the agent share the origin their BroadcastChannel rides. The **RPC & State Inspector** carries an **Instances** tab that lists every devframe dev server running on your machine. The host registers itself in the shared registry (`~/.devframe/instances/`) on startup via `registerDevframeInstance()`, so it shows up as "this instance"; start another example (e.g. `pnpm --filter vite-devframe-hub dev`, or any `node bin.mjs` CLI example) in a second terminal and it appears there too, each linking to its own SPA. +## One namespace, one route + +`initHub()` answers everything under **`/__devframes/`** through a single web-standard `(request: Request) => Promise` handler — exactly the shape a Next App Router route handler returns. One optional catch-all route delegates to it: + +- `/__devframes//` — each mounted devframe's SPA and its `__connection.json` +- `/__devframes/__connection.json` — hub discovery; advertises the side-car WebSocket (Next route handlers can't accept upgrades, so the instance starts its own eager WS side-car and the meta carries its port) +- `/__devframes/__index.json` — the frame index and endpoint map +- `/__devframes/__client-imports.js` — the dock client-script import map +- `/__devframes/__mcp` — the aggregate MCP endpoint (Streamable-HTTP) over the whole hub tool registry + +Next.js reserves `_`-prefixed segment folders, so the route directory URL-encodes the leading underscore: `app/%5F_devframes/[[...path]]/route.ts`. + +`initHub`'s `key` memoizes the instance on `globalThis`, so Next's dev-time module re-evaluation returns the live instance instead of leaking side-car servers. + ## What the example proves -- `createHubContext()` boots a hub with no Vite-specific code path; a `DevframeHost` impl plugs Next specifics (static mounts, connection meta, storage, origin) in uniformly -- `mountDevframe(ctx, def)` registers any `DevframeDefinition` as a dock and serves both its SPA and its `__connection.json`, so the embedded SPA connects straight back to the hub +- `initHub()` boots a whole hub with no Vite-specific code path — devframes, shared RPC registry, WS transport, MCP, and discovery behind one framework-agnostic handler +- Every `devframes` entry is mounted as a dock and served at `/__devframes//` with its own `__connection.json`, so the embedded SPA connects straight back to the hub - The browser reads `devframe:docks` / `devframe:commands` shared state and dispatches commands over RPC — byte-for-byte the same protocol the Vite host speaks - `createDevframeClientHost()` boots the hub's framework-level client runtime in the host page: it publishes the shared client context and imports each dock's `clientScript` (here, the a11y agent) so plugins run code in the page being inspected ## Hosting built-in plugins in a bundler -The plugins run node-side (child processes, the native `zigpty` PTY backend) and resolve their SPA dist via `new URL(..., import.meta.url)`. Next's bundler would try to inline that, so the host loads them through a bundler-ignored dynamic `import()` and sets `skipTrailingSlashRedirect` (see `next.config.mjs`) so each SPA's relative assets resolve under `/__/`. This is the recipe for any bundled (webpack/Turbopack) host. +The plugins run node-side (child processes, the native `zigpty` PTY backend) and resolve their SPA dist via `new URL(..., import.meta.url)`. Next's bundler would try to inline that, so the host loads them through a bundler-ignored dynamic `import()` and sets `skipTrailingSlashRedirect` (see `next.config.mjs`) so each SPA's relative assets resolve under `/__devframes//`. This is the recipe for any bundled (webpack/Turbopack) host. ## Files | File | Role | |---|---| -| `src/client/devframe/next-devframe-hub.ts` | The Next host — hub context, static-mount registry (incl. the a11y agent), side-car WS, instance-registry registration | -| `src/client/app/%5F_hub/%5F_connection.json/route.ts` | Boots the singleton host and serves `/__hub/__connection.json` | -| `src/client/app/%5F_[id]/[[...path]]/route.ts` | Serves each mounted SPA and its connection meta under `/__/` | +| `src/client/devframe/next-devframe-hub.ts` | The Next host — one `initHub()` call: devframes (incl. the a11y agent's dock `clientScript`), hub RPCs, commands, the json-render dock, instance-registry registration | +| `src/client/app/%5F_devframes/[[...path]]/route.ts` | The one catch-all — delegates every `/__devframes/*` request to the instance's `handler` | | `src/client/app/page.tsx` | The browser UI that consumes the hub protocol | | `src/client/app/icons.ts` | Offline Phosphor icons for the dock | diff --git a/examples/next-devframe-hub/src/client/app/%5F_[id]/[[...path]]/route.ts b/examples/next-devframe-hub/src/client/app/%5F_[id]/[[...path]]/route.ts deleted file mode 100644 index c99bdcc7..00000000 --- a/examples/next-devframe-hub/src/client/app/%5F_[id]/[[...path]]/route.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ensureNextDevframeHub } from '../../../devframe/next-devframe-hub' - -export const runtime = 'nodejs' -export const dynamic = 'force-dynamic' - -/** - * Catch-all for every mounted devframe SPA (`/__git/…`, `/__terminals/…`, the - * a11y agent module, …), their `/__connection.json` discovery fetches, - * and the in-process MCP endpoint (`/__hub/__mcp`). The `@devframes/next` - * bridge owns all of it — static serving (with SPA fallback, content types, - * and traversal guarding via devframe's shared `serveStaticHandler`), the - * connection-meta responses, and the MCP mount. - * - * MCP speaks Streamable-HTTP: `POST` (requests), `GET` (the SSE stream), and - * `DELETE` (session teardown) all route to the same bridge `fetch`. - */ -async function handler(request: Request): Promise { - const hub = await ensureNextDevframeHub() - return hub.fetch(request) -} - -export { handler as DELETE, handler as GET, handler as POST } diff --git a/examples/next-devframe-hub/src/client/app/%5F_devframes/[[...path]]/route.ts b/examples/next-devframe-hub/src/client/app/%5F_devframes/[[...path]]/route.ts new file mode 100644 index 00000000..a881c8d8 --- /dev/null +++ b/examples/next-devframe-hub/src/client/app/%5F_devframes/[[...path]]/route.ts @@ -0,0 +1,25 @@ +import { ensureNextDevframeHub } from '../../../devframe/next-devframe-hub' + +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +/** + * The whole hub behind one route: `initHub`'s web-standard `handler` serves + * every mounted devframe SPA (`/__devframes/git/…`, `/__devframes/terminals/…`, + * the a11y agent module, …), each frame's `__connection.json`, the hub-level + * discovery endpoints (`/__devframes/__connection.json`, `__index.json`, + * `__client-imports.js`), and the aggregate MCP endpoint + * (`/__devframes/__mcp`). + * + * Next.js reserves `_`-prefixed segment folders, so `__devframes` is + * URL-encoded as `%5F_devframes` in the app directory name. + * + * MCP speaks Streamable-HTTP: `POST` (requests), `GET` (the SSE stream), and + * `DELETE` (session teardown) all route to the same handler. + */ +async function handler(request: Request): Promise { + const hub = await ensureNextDevframeHub() + return hub.handler(request) +} + +export { handler as DELETE, handler as GET, handler as POST } diff --git a/examples/next-devframe-hub/src/client/app/%5F_hub/%5F_connection.json/route.ts b/examples/next-devframe-hub/src/client/app/%5F_hub/%5F_connection.json/route.ts deleted file mode 100644 index f1ee760d..00000000 --- a/examples/next-devframe-hub/src/client/app/%5F_hub/%5F_connection.json/route.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ensureNextDevframeHub } from '../../../devframe/next-devframe-hub' - -export const runtime = 'nodejs' -export const dynamic = 'force-dynamic' - -/** - * The hub client (`app/page.tsx`) discovers the side-car WS endpoint here. The - * `@devframes/next` bridge answers it — `/__hub` is registered as a connection - * base in the host setup — so this delegates to the same `fetch` as the - * catch-all SPA route. - */ -export async function GET(request: Request): Promise { - const hub = await ensureNextDevframeHub() - return hub.fetch(request) -} diff --git a/examples/next-devframe-hub/src/client/app/page.tsx b/examples/next-devframe-hub/src/client/app/page.tsx index 8ffd8806..cdfbc06d 100644 --- a/examples/next-devframe-hub/src/client/app/page.tsx +++ b/examples/next-devframe-hub/src/client/app/page.tsx @@ -15,7 +15,7 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { createReactJsonRenderDockRenderer } from '../json-render/dock-renderer' import { dockIconSvg } from './icons' -const HUB_BASE = '/__hub/' +const HUB_BASE = '/__devframes/' interface Status { text: string diff --git a/examples/next-devframe-hub/src/client/devframe/demo-devframe-b.ts b/examples/next-devframe-hub/src/client/devframe/demo-devframe-b.ts index 38da33ad..b4653439 100644 --- a/examples/next-devframe-hub/src/client/devframe/demo-devframe-b.ts +++ b/examples/next-devframe-hub/src/client/devframe/demo-devframe-b.ts @@ -7,7 +7,7 @@ import pkg from '../../../package.json' with { type: 'json' } const HERE = dirname(fileURLToPath(import.meta.url)) export default defineDevframe({ - id: 'example:next-demo-tool-b', + id: 'next-demo-tool-b', name: 'Next Demo Tool B', version: pkg.version, packageName: pkg.name, diff --git a/examples/next-devframe-hub/src/client/devframe/demo-devframe.ts b/examples/next-devframe-hub/src/client/devframe/demo-devframe.ts index 6a620091..021dbefe 100644 --- a/examples/next-devframe-hub/src/client/devframe/demo-devframe.ts +++ b/examples/next-devframe-hub/src/client/devframe/demo-devframe.ts @@ -7,7 +7,9 @@ import pkg from '../../../package.json' with { type: 'json' } const HERE = dirname(fileURLToPath(import.meta.url)) export default defineDevframe({ - id: 'example:next-demo-tool', + // Colon-free: the hub instance derives the mount path (`/__devframes//`) + // from the id, and `:` is a route-param marker to the router underneath. + id: 'next-demo-tool', name: 'Next Demo Tool', version: pkg.version, packageName: pkg.name, diff --git a/examples/next-devframe-hub/src/client/devframe/next-devframe-hub.ts b/examples/next-devframe-hub/src/client/devframe/next-devframe-hub.ts index 563a8778..225f6f2c 100644 --- a/examples/next-devframe-hub/src/client/devframe/next-devframe-hub.ts +++ b/examples/next-devframe-hub/src/client/devframe/next-devframe-hub.ts @@ -1,16 +1,14 @@ +import type { HubDevframeEntry, HubInstance } from '@devframes/hub/initiate' import type { DevframeHubContext } from '@devframes/hub/node' -import type { DevframeNextHost } from '@devframes/next' -import type { StartedServer } from 'devframe/node' -import type { ConnectionMeta, DevframeDefinition } from 'devframe/types' +import type { DevframeInstanceRegistration } from 'devframe/node' +import type { DevframeDefinition } from 'devframe/types' import { homedir } from 'node:os' import process from 'node:process' import { fileURLToPath } from 'node:url' import { defineHubRpcFunction } from '@devframes/hub' -import { createHubContext, mountDevframe } from '@devframes/hub/node' +import { DEVFRAMES_HUB_BASE, initHub } from '@devframes/hub/initiate' import { toJsonRenderDockEntry } from '@devframes/json-render/hub' -import { createDevframeNextHost } from '@devframes/next' -import { registerDevframeInstance, startHttpAndWs } from 'devframe/node' -import { getPort } from 'get-port-please' +import { registerDevframeInstance } from 'devframe/node' import { createDashboardView } from 'json-render/dashboard' import { dirname, join } from 'pathe' import demoDevframe from './demo-devframe' @@ -64,8 +62,11 @@ async function loadAssetsDevframe(): Promise { return (mod.createAssetsDevframe as (options: { dir: string, watch: boolean }) => DevframeDefinition)({ dir, watch: false }) } -/** URL base the a11y agent module is served under (same-origin, catch-all route). */ -const A11Y_AGENT_MOUNT_BASE = '/__df-a11y-agent/' +/** + * URL base the a11y agent module is served under — inside the hub namespace, + * so the one catch-all route reaches it. + */ +const A11Y_AGENT_MOUNT_BASE = `${DEVFRAMES_HUB_BASE}df-a11y-agent/` interface A11yAgentMount { /** The a11y devframe's dock id — the dock the client script attaches to. */ @@ -100,25 +101,18 @@ async function loadA11yAgentMount(): Promise { } export interface NextDevframeHubOptions { - /** Preferred port for the side-car RPC/WS server. Default: a free port near 9877. */ + /** Pin the side-car RPC/WS port. Default: `initHub` walks a free port near 9777. */ port?: number /** Hostname for the side-car server. Default: `localhost`. */ host?: string /** Workspace root used by hub host capabilities. Default: `process.cwd()`. */ cwd?: string - /** Devframes to mount as docks. */ - devframes?: DevframeDefinition[] -} - -export interface StartedNextDevframeHub extends StartedServer { - context: DevframeHubContext - connectionMeta: ConnectionMeta & { backend: 'websocket', websocket: number } /** - * The bridge's WHATWG-`fetch` handler. Both the catch-all SPA route and the - * `__hub/__connection.json` route delegate to it — it serves every mounted - * SPA and answers `/__connection.json` for the hub and each devframe. + * Memoize the instance on `globalThis` under this key (`initHub`'s `key`), + * so dev-time module re-evaluation returns the live instance instead of + * leaking side-car servers. */ - fetch: DevframeNextHost['fetch'] + key?: string } const nextHubMessagesList = defineHubRpcFunction({ @@ -148,19 +142,72 @@ const nextHubTerminalsList = defineHubRpcFunction({ }), }) +/** + * The entire Next host: one `initHub()` call. The instance mounts every + * devframe under `/__devframes//`, merges their RPC registries onto one + * WebSocket side-car, serves the discovery endpoints (`__connection.json`, + * `__index.json`, `__client-imports.js`) and the aggregate MCP route — all + * behind the one web-standard `handler` the App Router catch-all delegates to. + */ export async function nextDevframeHub( options: NextDevframeHubOptions = {}, -): Promise { +): Promise { const cwd = options.cwd ?? process.cwd() const hostName = options.host ?? 'localhost' const nextPort = Number(process.env.PORT ?? 3000) + const origin = `http://${hostName}:${nextPort}` + + // Serve the a11y inspector's in-page agent same-origin (inside the hub + // namespace, via the catch-all route) and attach it to the a11y dock as its + // client script. The hub client runtime booted in `app/page.tsx` imports it + // into the host page, where it scans this hub live; the panel iframe shares + // the origin, so their BroadcastChannel connects. + const a11yAgent = await loadA11yAgentMount() + + // Demo devframes alongside the dogfooded built-in plugin packages. The + // shared-iframe soft-navigation demo mounts as a `subTabs` anchor (a shared + // `frameId` + the postmessage protocol) so the client host attaches the + // frame-nav adapter, materializing one client-only dock per tab the SPA's + // shim reports — all sharing one iframe. + const devframes: (DevframeDefinition | HubDevframeEntry)[] = [ + demoDevframe, + demoDevframeB, + ...(await loadBuiltinPlugins()).map(def => + a11yAgent && def.id === a11yAgent.dockId + ? { devframe: def, dock: { clientScript: { importFrom: a11yAgent.importFrom } } } + : def, + ), + await loadAssetsDevframe(), + { + devframe: tabbedDevframe, + dock: { + category: 'app', + frameId: 'next-tabbed-tool', + subTabs: { protocol: 'postmessage' }, + }, + }, + ] + + let registration: DevframeInstanceRegistration | undefined - // The Next host bridge: its `host` accumulates every `mountStatic` / - // `mountConnectionMeta` call into a single `fetch` handler (backed by - // devframe's shared `serveStaticHandler`), which the App Router routes - // delegate to — no hand-rolled static serving or path matching here. - const nextHost = createDevframeNextHost({ - resolveOrigin: () => `http://${hostName}:${nextPort}`, + const hub = initHub({ + ...(options.key ? { key: options.key } : {}), + base: DEVFRAMES_HUB_BASE, + cwd, + origin, + host: hostName, + // Single-user localhost demo: the side-car is reachable only on loopback, + // so it opts out of the gate for a no-friction dev experience. A hub + // reachable beyond localhost should gate (see `docs/guide/security.md`). + auth: false, + // The aggregate MCP endpoint at `/__devframes/__mcp` — the hub's agent + // surface (agent-flagged commands, plugin tools, `devframe:state:read`) + // over the same catch-all route as the SPAs. + mcp: true, + // Omitted `server` (Next route handlers can't accept WS upgrades), so the + // instance starts its default eager side-car WebSocket server; a `port` + // option pins it. + ...(options.port != null ? { ws: { port: options.port } } : {}), getStorageDir(scope) { if (scope === 'workspace') return join(cwd, '.devframe') @@ -168,168 +215,94 @@ export async function nextDevframeHub( return join(cwd, 'node_modules/.next-devframe-hub') return join(homedir(), '.next-devframe-hub') }, - }) - const host = nextHost.host - - // Register the hub's own connection base so the hub client (app/page.tsx) - // can discover the side-car WS via `/__connection.json`; the mounted - // devframes each register their own base through `mountDevframe`. - host.mountConnectionMeta?.('/__hub') - - const port = options.port ?? await getPort({ host: hostName, port: 9877, random: false }) - - const context = await createHubContext({ - cwd, - workspaceRoot: cwd, - mode: 'dev', - host, - builtinRpcDeclarations: [ + rpcDeclarations: [ nextHubMessagesList, nextHubTerminalsList, ], - }) - - context.commands.register({ - id: 'example:next-devframe-hub:ping', - title: 'Next Hub: Ping', - icon: 'ph:bell-duotone', - category: 'hub', - // Opt this command into the agent surface: it shows up as an MCP tool - // on the in-process endpoint mounted below. - agent: { - description: 'Ping the hub to confirm it is alive. Returns "pong". Safe to call freely.', - safety: 'read', - }, - handler: () => 'pong', - }) + devframes, + async configure(ctx) { + ctx.commands.register({ + id: 'example:next-devframe-hub:ping', + title: 'Next Hub: Ping', + icon: 'ph:bell-duotone', + category: 'hub', + // Opt this command into the agent surface: it shows up as an MCP tool + // on the aggregate endpoint at `__mcp`. + agent: { + description: 'Ping the hub to confirm it is alive. Returns "pong". Safe to call freely.', + safety: 'read', + }, + handler: () => 'pong', + }) - // The hub no longer synthesizes built-in docks — a high-level integration - // registers the viewer's native views it wants, declaring the `~builtin` - // category itself so this Settings tab groups and sorts last. - context.docks.register({ - type: '~builtin', - id: '~settings', - title: 'Settings', - icon: 'ph:gear-duotone', - category: '~builtin', - }) - - // Demo devframes alongside the dogfooded built-in plugin packages. - const devframes = options.devframes - ?? [demoDevframe, demoDevframeB, ...await loadBuiltinPlugins(), await loadAssetsDevframe()] - - await context.messages.add({ - level: 'success', - message: 'Next Devframe Hub started', - description: `Side-car WS on port ${port}. ${devframes.length} devframe(s) registered.`, - }) + // The hub synthesizes no built-in docks — a high-level integration + // registers the viewer's native views it wants, declaring the `~builtin` + // category itself so this Settings tab groups and sorts last. + ctx.docks.register({ + type: '~builtin', + id: '~settings', + title: 'Settings', + icon: 'ph:gear-duotone', + category: '~builtin', + }) - // Serve the a11y inspector's in-page agent same-origin (via the catch-all - // route) and attach it to the a11y dock as its client script. The hub client - // runtime booted in `app/page.tsx` imports it into the host page, where it - // scans this hub live; the panel iframe shares the origin, so their - // BroadcastChannel connects. - const a11yAgent = await loadA11yAgentMount() - if (a11yAgent) - host.mountStatic(A11Y_AGENT_MOUNT_BASE, a11yAgent.dir) + if (a11yAgent) + await ctx.host.mountStatic(A11Y_AGENT_MOUNT_BASE, a11yAgent.dir) - for (const def of devframes) { - const clientScript = a11yAgent && def.id === a11yAgent.dockId - ? { importFrom: a11yAgent.importFrom } - : undefined - await mountDevframe(context, def, clientScript ? { dock: { clientScript } } : undefined) - } + // Dogfood the opt-in JSON-render hub integration: author a view on the + // hub context and project it onto a `json-render` dock. The client + // (app/page.tsx) renders it with a mini React registry. + const jsonRenderView = createDashboardView(ctx) + ctx.docks.register(toJsonRenderDockEntry(jsonRenderView, { + id: 'example:json-render', + title: 'JSON Render', + icon: 'ph:layout-duotone', + category: 'app', + })) - // Dogfood the opt-in JSON-render hub integration: author a view on the hub - // context and project it onto a `json-render` dock. The client (app/page.tsx) - // renders it with a mini React registry (registry replacement). - const jsonRenderView = createDashboardView(context) - context.docks.register(toJsonRenderDockEntry(jsonRenderView, { - id: 'example:json-render', - title: 'JSON Render', - icon: 'ph:layout-duotone', - category: 'app', - })) + await ctx.messages.add({ + level: 'success', + message: 'Next Devframe Hub started', + description: `${devframes.length} devframe(s) mounted under ${DEVFRAMES_HUB_BASE}.`, + }) - // Shared-iframe soft-navigation demo. mountDevframe serves the SPA and - // registers its iframe dock; the `dock` override marks it a `subTabs` anchor - // (a shared `frameId` + the postmessage protocol) so the client host attaches - // the frame-nav adapter, materializing one client-only dock per tab the SPA's - // shim reports — all sharing this one iframe. - await mountDevframe(context, tabbedDevframe, { - dock: { - category: 'app', - frameId: 'next-tabbed-tool', - subTabs: { protocol: 'postmessage' }, + // Record the instance in the global registry so `devframe connect` + // discovers this hub — running inside the Next dev server — like any + // standalone devframe. In-process hosts register explicitly; the origin + // is the Next app's own. + registration = registerDevframeInstance({ + pid: process.pid, + port: nextPort, + origin, + basePath: DEVFRAMES_HUB_BASE, + id: 'example:next-devframe-hub', + name: 'Next Devframe Hub', + rootDir: cwd, + mcp: { path: `${DEVFRAMES_HUB_BASE}__mcp` }, + startedAt: Date.now(), + }) }, }) - // Single-user localhost demo: the side-car is reachable only on loopback, so - // it opts out of the gate for a no-friction dev experience. A hub reachable - // beyond localhost should gate (see `docs/guide/security.md`). - const started = await startHttpAndWs({ - context, - host: hostName, - port, - auth: false, - }) - - // Serve MCP in-process on the Next app's own origin (the `/_next/mcp` - // shape): the hub's agent surface — agent-flagged commands, plugin tools - // (git status/log/diff, terminals), `devframe:state:read` — over the same catch-all - // route as the SPAs, no side-car port involved. - const mcpPath = '/__hub/__mcp' - await nextHost.mountMcp(context, mcpPath, { - serverName: 'example:next-devframe-hub', - }) - - const connectionMeta = { - backend: 'websocket' as const, - websocket: started.port, - mcp: { path: mcpPath }, - } - // Publish the live meta to the bridge now the WS port is known, so every - // registered `/__connection.json` (hub + mounted devframes) resolves. - nextHost.setConnectionMeta(connectionMeta) - - // Record the instance in the global registry so `devframe connect` - // discovers this hub — running inside the Next dev server — like any - // standalone devframe. In-process hosts register explicitly; the origin is - // the Next app's own. - const registration = registerDevframeInstance({ - pid: process.pid, - port: nextPort, - origin: `http://${hostName}:${nextPort}`, - basePath: '/__hub/', - id: 'example:next-devframe-hub', - name: 'Next Devframe Hub', - rootDir: cwd, - mcp: { path: mcpPath }, - startedAt: Date.now(), - }) - const closeStarted = started.close - started.close = async () => { - registration.unregister() - await closeStarted() + const closeHub = hub.close + return { + ...hub, + close: async () => { + registration?.unregister() + registration = undefined + await closeHub() + }, } - - return Object.assign(started, { - context, - connectionMeta, - fetch: nextHost.fetch, - }) } -const GLOBAL_KEY = '__nextDevframeHub' - -type GlobalWithHub = typeof globalThis & { - [GLOBAL_KEY]?: Promise -} +let hubPromise: Promise | undefined -export function ensureNextDevframeHub( - options: NextDevframeHubOptions = {}, -): Promise { - const globalHub = globalThis as GlobalWithHub - globalHub[GLOBAL_KEY] ??= nextDevframeHub(options) - return globalHub[GLOBAL_KEY] +/** + * The route-facing singleton. `initHub`'s `key` memoizes the live instance on + * `globalThis` across dev-time module re-evaluations; the module-level promise + * just avoids re-running the plugin loading per request. + */ +export function ensureNextDevframeHub(): Promise { + hubPromise ??= nextDevframeHub({ key: 'next-devframe-hub' }) + return hubPromise } diff --git a/examples/next-devframe-hub/src/client/devframe/tabbed-devframe.ts b/examples/next-devframe-hub/src/client/devframe/tabbed-devframe.ts index 59e68e5a..49a4c730 100644 --- a/examples/next-devframe-hub/src/client/devframe/tabbed-devframe.ts +++ b/examples/next-devframe-hub/src/client/devframe/tabbed-devframe.ts @@ -15,7 +15,7 @@ const HERE = dirname(fileURLToPath(import.meta.url)) * and switching docks soft-navigates inside it instead of reloading. */ export default defineDevframe({ - id: 'example:next-tabbed-tool', + id: 'next-tabbed-tool', name: 'Next Tabbed Tool', version: pkg.version, packageName: pkg.name, diff --git a/examples/next-devframe-hub/src/client/next.config.mjs b/examples/next-devframe-hub/src/client/next.config.mjs index 242dbabb..94b41942 100644 --- a/examples/next-devframe-hub/src/client/next.config.mjs +++ b/examples/next-devframe-hub/src/client/next.config.mjs @@ -2,7 +2,8 @@ import { withDevframe } from '@devframes/next' // `withDevframe` applies the settings a devframe host requires (currently // `skipTrailingSlashRedirect: true`, so mounted SPAs' relative assets under -// `/__/` resolve instead of 404-ing on Next's trailing-slash redirect). +// `/__devframes//` resolve instead of 404-ing on Next's trailing-slash +// redirect). /** @type {import('next').NextConfig} */ const nextConfig = withDevframe({ images: { unoptimized: true }, diff --git a/examples/next-devframe-hub/tests/next-devframe-hub.test.ts b/examples/next-devframe-hub/tests/next-devframe-hub.test.ts index 3975ccfe..65ce53da 100644 --- a/examples/next-devframe-hub/tests/next-devframe-hub.test.ts +++ b/examples/next-devframe-hub/tests/next-devframe-hub.test.ts @@ -1,40 +1,74 @@ +import type { HubInstance } from '@devframes/hub/initiate' import { createRpcClient } from 'devframe/rpc/client' import { createWsRpcChannel } from 'devframe/rpc/transports/ws-client' +import { getPort } from 'get-port-please' import { afterEach, describe, expect, it, vi } from 'vitest' import { WebSocket } from 'ws' import { nextDevframeHub } from '../src/client/devframe/next-devframe-hub' vi.stubGlobal('WebSocket', WebSocket) -function bootRpc(port: number) { - const channel = createWsRpcChannel({ url: `ws://127.0.0.1:${port}` }) +/** The side-car WS port advertised by the hub's connection meta. */ +function wsPortOf(hub: HubInstance): number { + const ws = hub.connectionMeta().websocket + if (typeof ws === 'object' && typeof ws.port === 'number') + return ws.port + throw new Error('expected a side-car websocket meta with a port') +} + +function bootRpc(hub: HubInstance) { + const channel = createWsRpcChannel({ url: `ws://127.0.0.1:${wsPortOf(hub)}/__ws` }) return createRpcClient({}, { channel }) } describe('next-devframe-hub (example)', () => { - let server: Awaited> | undefined + let hub: HubInstance | undefined afterEach(async () => { - await server?.close() - server = undefined + await hub?.close() + hub = undefined }) - it('returns connection meta pointing at the WS backend and in-process MCP', async () => { - server = await nextDevframeHub({ host: '127.0.0.1' }) + it('returns connection meta pointing at the side-car WS and the aggregate MCP', async () => { + const port = await getPort({ host: '127.0.0.1', port: 19310 }) + hub = await nextDevframeHub({ host: '127.0.0.1', port }) + await hub.ready - expect(server.connectionMeta).toEqual({ + expect(hub.connectionMeta()).toEqual({ backend: 'websocket', - websocket: server.port, - mcp: { path: '/__hub/__mcp' }, + websocket: { port, path: '__ws' }, + mcp: { path: '__mcp' }, }) }) + it('serves hub discovery and every frame under the one /__devframes/ namespace', async () => { + hub = await nextDevframeHub({ host: '127.0.0.1' }) + await hub.ready + + const origin = 'http://127.0.0.1:3000' + const meta = await (await hub.handler(new Request(`${origin}/__devframes/__connection.json`))).json() + expect(meta.websocket).toEqual({ port: wsPortOf(hub), path: '__ws' }) + + const index = await (await hub.handler(new Request(`${origin}/__devframes/__index.json`))).json() + expect(index.base).toBe('/__devframes/') + const frameIds = index.frames.map((f: { id: string }) => f.id) + expect(frameIds).toContain('next-demo-tool') + expect(frameIds).toContain('next-tabbed-tool') + + // Each frame's SPA and its discovery meta live at `/`. + const spa = await hub.handler(new Request(`${origin}/__devframes/next-demo-tool/`)) + expect(await spa.text()).toContain('Next Demo Tool') + const frameMeta = await (await hub.handler(new Request(`${origin}/__devframes/next-demo-tool/__connection.json`))).json() + expect(frameMeta.websocket).toEqual({ port: wsPortOf(hub), path: '__ws' }) + }) + it('registers a hub-owned settings dock and the mounted plugin docks', async () => { - server = await nextDevframeHub({ host: '127.0.0.1' }) + hub = await nextDevframeHub({ host: '127.0.0.1' }) + const ctx = await hub.context - const docks = server.context.docks.values() + const docks = ctx.docks.values() const dockIds = docks.map(d => d.id) - expect(dockIds).toContain('example:next-demo-tool') + expect(dockIds).toContain('next-demo-tool') // The hub synthesizes no built-in docks; the integration registers the // settings tab itself, declaring the `~builtin` category explicitly. expect(dockIds).toContain('~settings') @@ -47,18 +81,20 @@ describe('next-devframe-hub (example)', () => { }) it('lists startup and demo messages through the kit-local RPC', async () => { - server = await nextDevframeHub({ host: '127.0.0.1' }) + hub = await nextDevframeHub({ host: '127.0.0.1' }) + await hub.ready - const rpc = bootRpc(server.port) + const rpc = bootRpc(hub) const messages = await rpc.$call('example:next-devframe-hub:messages:list') as { message: string }[] expect(messages.map(m => m.message)).toContain('Next Devframe Hub started') expect(messages.map(m => m.message)).toContain('Next demo devframe loaded') }) it('executes the ping command through the hub command RPC', async () => { - server = await nextDevframeHub({ host: '127.0.0.1' }) + hub = await nextDevframeHub({ host: '127.0.0.1' }) + await hub.ready - const rpc = bootRpc(server.port) + const rpc = bootRpc(hub) await expect( rpc.$call('hub:commands:execute', 'example:next-devframe-hub:ping'), ).resolves.toBe('pong') diff --git a/examples/nitro-devframe-hub/README.md b/examples/nitro-devframe-hub/README.md new file mode 100644 index 00000000..a3967ef8 --- /dev/null +++ b/examples/nitro-devframe-hub/README.md @@ -0,0 +1,18 @@ +# nitro-devframe-hub + +The minimal [Nitro](https://nitro.build) host for `@devframes/hub`: one `initHub()` call, one catch-all route, and the whole devtools installation is live under `/__devframes/`. + +```sh +pnpm --filter nitro-devframe-hub dev +``` + +Open — the host page carries the floating dock via one script tag — or for the standalone viewer. + +## How it works + +- [`hub.ts`](./hub.ts) — `initHub({ devframes, ui: createUi(), key })`: mounts the Inspect and Messages plugins against one shared hub context, fills the hub's `ui` slot with `@devframes/hub-ui`'s prebuilt viewer + floating-dock bootstrap, and memoizes the instance across Nitro's dev-time module reloads. +- [`routes/__devframes/[...path].ts`](./routes/__devframes/%5B...path%5D.ts) (and its `index.ts` sibling for the namespace root) — the delegation: every request under `/__devframes/` becomes `hub.handler(event.req)`, web-standard Request in, Response out. Everything — frame SPAs, `__connection.json`, `__index.json`, `embedded.js`, `__client-imports.js` — flows through it. +- [`nitro.config.ts`](./nitro.config.ts) — keeps the devframe packages external so their prebuilt client assets resolve from the packages themselves rather than Nitro's build output. +- The RPC WebSocket runs on an eager side-car port (Nitro handlers hand over `Request`s, so the socket lives on its own port), advertised through `__connection.json`; the browser client discovers it automatically. + +The same `initHub` instance mounts identically on Vite, Hono, and Next.js — see the sibling examples. diff --git a/examples/nitro-devframe-hub/hub.ts b/examples/nitro-devframe-hub/hub.ts new file mode 100644 index 00000000..94783547 --- /dev/null +++ b/examples/nitro-devframe-hub/hub.ts @@ -0,0 +1,34 @@ +import { createUi } from '@devframes/hub-ui' +import { initHub } from '@devframes/hub/initiate' +import { createInspectDevframe } from '@devframes/plugin-inspect' +import { createMessagesDevframe } from '@devframes/plugin-messages' + +// The whole devtools installation in one call: two plugins mounted under +// /__devframes/, the reference UI filling the hub's ui slot (the standalone +// viewer at the namespace root + the floating dock at embedded.js), and the +// RPC socket on an eager side-car port advertised via __connection.json — +// Nitro's route handlers never deal with WebSocket upgrades. +// +// `key` memoizes the instance on globalThis, so Nitro's dev-time module +// reloads return the live hub instead of leaking side-car servers. +export const hub = initHub({ + key: 'nitro-devframe-hub', + devframes: [ + createInspectDevframe(), + createMessagesDevframe(), + ], + ui: createUi(), + // Single-user localhost demo: reachable only on loopback, so it opts out + // of the gate for a no-friction dev experience. A hub reachable beyond + // localhost should gate (see docs/guide/security.md). + auth: false, + configure(ctx) { + ctx.commands.register({ + id: 'example:nitro-devframe-hub:ping', + title: 'Nitro Hub · Ping', + icon: 'ph:bell-duotone', + category: 'kit', + handler: () => 'pong', + }) + }, +}) diff --git a/examples/nitro-devframe-hub/nitro.config.ts b/examples/nitro-devframe-hub/nitro.config.ts new file mode 100644 index 00000000..91d84853 --- /dev/null +++ b/examples/nitro-devframe-hub/nitro.config.ts @@ -0,0 +1,22 @@ +import { defineNitroConfig } from 'nitro/config' + +// The devframe packages resolve their prebuilt client assets relative to +// import.meta.url — keep them external so those paths stay inside the +// packages instead of pointing into Nitro's build output (workspace-linked +// packages are otherwise inlined by the dev bundler). +const devframePackages = [ + 'devframe', + '@devframes/hub', + '@devframes/hub/initiate', + '@devframes/hub-ui', + '@devframes/plugin-inspect', + '@devframes/plugin-messages', +] + +export default defineNitroConfig({ + compatibilityDate: '2026-08-01', + serverDir: '.', + rolldownConfig: { + external: id => devframePackages.some(name => id === name || id.startsWith(`${name}/`)), + }, +}) diff --git a/examples/nitro-devframe-hub/package.json b/examples/nitro-devframe-hub/package.json new file mode 100644 index 00000000..b3f849f3 --- /dev/null +++ b/examples/nitro-devframe-hub/package.json @@ -0,0 +1,22 @@ +{ + "name": "nitro-devframe-hub", + "type": "module", + "version": "0.8.1", + "private": true, + "description": "Minimal Nitro host for @devframes/hub \u2014 one catch-all route mounts the whole devtools namespace.", + "scripts": { + "dev": "nitro dev", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@devframes/hub": "workspace:*", + "@devframes/hub-ui": "workspace:*", + "@devframes/plugin-inspect": "workspace:*", + "@devframes/plugin-messages": "workspace:*", + "devframe": "workspace:*", + "nitro": "catalog:deps" + }, + "devDependencies": { + "@types/node": "catalog:types" + } +} diff --git a/examples/nitro-devframe-hub/routes/__devframes/[...path].ts b/examples/nitro-devframe-hub/routes/__devframes/[...path].ts new file mode 100644 index 00000000..b70d0950 --- /dev/null +++ b/examples/nitro-devframe-hub/routes/__devframes/[...path].ts @@ -0,0 +1,8 @@ +import { defineHandler } from 'nitro' +import { hub } from '../../hub' + +// The whole hub namespace behind one catch-all route: web-standard Request +// in, Response out (h3 v2 events carry the Request on `event.req`). +// Everything — frame SPAs, __connection.json, __index.json, embedded.js, +// __client-imports.js — flows through here. +export default defineHandler(event => hub.handler(event.req)) diff --git a/examples/nitro-devframe-hub/routes/__devframes/index.ts b/examples/nitro-devframe-hub/routes/__devframes/index.ts new file mode 100644 index 00000000..ee71aa53 --- /dev/null +++ b/examples/nitro-devframe-hub/routes/__devframes/index.ts @@ -0,0 +1,8 @@ +import { defineHandler } from 'nitro' +import { hub } from '../../hub' + +// The namespace root itself (`/__devframes/`) — the standalone viewer from +// `@devframes/hub-ui`. The `[...path]` sibling covers every path beneath it, +// but a catch-all doesn't match its own empty subpath, so the root needs its +// own route. +export default defineHandler(event => hub.handler(event.req)) diff --git a/examples/nitro-devframe-hub/routes/index.ts b/examples/nitro-devframe-hub/routes/index.ts new file mode 100644 index 00000000..5ab9d68b --- /dev/null +++ b/examples/nitro-devframe-hub/routes/index.ts @@ -0,0 +1,26 @@ +import { defineHandler } from 'nitro' + +// The host app: any page becomes devtools-equipped with one dev-only script +// tag — the floating dock mounts itself and connects through the namespace +// the __devframes routes serve. +export default defineHandler(() => new Response( + ` + + + + + Nitro Devframe Hub + + +

Nitro Devframe Hub

+

This page is the host app. The devtools ride along:

+ + + +`, + { headers: { 'content-type': 'text/html; charset=utf-8' } }, +)) diff --git a/examples/nitro-devframe-hub/tsconfig.json b/examples/nitro-devframe-hub/tsconfig.json new file mode 100644 index 00000000..cf1d7ad8 --- /dev/null +++ b/examples/nitro-devframe-hub/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": [ + "esnext", + "dom" + ], + "types": [ + "node" + ] + }, + "include": [ + "hub.ts", + "routes", + "nitro.config.ts" + ], + "exclude": [ + "dist", + "node_modules", + ".nitro", + ".output" + ] +} diff --git a/examples/vite-devframe-hub/README.md b/examples/vite-devframe-hub/README.md index 6b8c1fc1..b5550363 100644 --- a/examples/vite-devframe-hub/README.md +++ b/examples/vite-devframe-hub/README.md @@ -2,7 +2,7 @@ A tiny, copyable **vite-devtools-style hub**. [vite-devtools](https://github.com/vitejs/devtools) is the full viewer that docks many Vite integrations behind one icon rail on top of `@devframes/hub`; this example is the smallest thing shaped like it — an icon dock, an iframe stage, and a drawer of hub subsystems — so you can see the whole protocol and build your own viewer from it. -`src/vite-devframe-hub.ts` is the entire host: ~120 lines of Vite plugin that wires `@devframes/hub` into a Vite dev server. Every framework's hub host follows the same shape. +`src/vite-devframe-hub.ts` is the entire host: a small Vite plugin around one `initHub()` call (from `@devframes/hub/initiate`). The instance mounts every devframe under one namespace — `/__devframes//` — merges their RPC registries onto one WebSocket that upgrades on Vite's own dev server at `/__devframes/__ws`, and serves the discovery endpoints (`/__devframes/__connection.json`, `__index.json`, `__client-imports.js`) — all behind one connect-style middleware that self-filters by the base and hands everything else back to Vite. Every framework's hub host follows the same shape. ## Run it @@ -13,7 +13,7 @@ pnpm --filter vite-devframe-hub dev Open the printed URL. The dock on the left lists every mounted tool with its icon: -- **Git**, **Terminals**, **Code Server**, **RPC & State Inspector**, **A11y Inspector** — the built-in plugins, each a published `DevframeDefinition` mounted with `mountDevframe` +- **Git**, **Terminals**, **Code Server**, **RPC & State Inspector**, **A11y Inspector** — the built-in plugins, each a published `DevframeDefinition` passed to the host's `devframes` option - **Demo Tool** / **Demo Tool B** — two trivial static SPAs that show the bare mount path Selecting a tool loads its SPA in the stage. The bottom drawer mirrors the hub's **Commands**, **Messages**, and **Terminals** subsystems, plus a button that dispatches a command through `hub:commands:execute`. @@ -24,8 +24,8 @@ The **RPC & State Inspector** carries an **Instances** tab that lists every devf ## What the example proves -- `createHubContext()` boots a hub with no Vite-specific code path; a `DevframeHost` impl plugs framework specifics (static mounts, connection meta, storage, origin) in uniformly -- `mountDevframe(ctx, def)` registers any `DevframeDefinition` as a dock and serves both its SPA and its `__connection.json`, so the embedded SPA connects straight back to the hub +- `initHub()` boots a hub with no Vite-specific code path: `server.middlewares.use(instance.nodeMiddleware)` plus Vite's `httpServer` for the shared WebSocket upgrade is the entire framework adapter +- Every `devframes` entry is served at `/__devframes//` with its own `__connection.json`, so each embedded SPA connects straight back to the hub; `/__devframes/__index.json` lists the mounted frames and endpoints for any external viewer - Real integrations work end to end through the mount path — the inspector lists every plugin's RPC functions live, terminals stream over the hub, and code-server launches an authenticated editor - The browser reads `devframe:docks` / `devframe:commands` shared state and dispatches commands over RPC — no hub classes imported on the client - `createDevframeClientHost()` boots the hub's framework-level client runtime in the host page: it publishes the shared client context and imports each dock's `clientScript` (here, the a11y agent) so plugins run code in the page being inspected @@ -38,8 +38,8 @@ The dock UI is plain DOM in `src/client/`. To skin your own viewer, read the sam | File | Role | |---|---| -| `src/vite-devframe-hub.ts` | The Vite host — hub context, static + connection-meta mounts, side-car WS, instance-registry registration | -| `vite.config.ts` | Mounts the built-in plugins via the host's `devframes` option; attaches the a11y agent as its dock's `clientScript` | +| `src/vite-devframe-hub.ts` | The Vite host — one `initHub()` call mounted as connect middleware, plus instance-registry registration | +| `vite.config.ts` | Passes the built-in plugins and demo devframes to the host's `devframes` option; attaches the a11y agent as its dock's `clientScript` | | `src/client/main.ts` | The browser UI that consumes the hub protocol | | `src/client/icons.ts` | Offline Phosphor icons for the dock | | `index.html` | The UI shell | diff --git a/examples/vite-devframe-hub/package.json b/examples/vite-devframe-hub/package.json index d4f84edd..9e03894f 100644 --- a/examples/vite-devframe-hub/package.json +++ b/examples/vite-devframe-hub/package.json @@ -3,7 +3,7 @@ "type": "module", "version": "0.8.2", "private": true, - "description": "Protocol-witness example — a tiny Vite Devframe Hub built on @devframes/hub that exercises every hub subsystem end-to-end.", + "description": "Protocol-witness example \u2014 a tiny Vite Devframe Hub built on @devframes/hub that exercises every hub subsystem end-to-end.", "homepage": "https://github.com/devframes/devframe/tree/main/examples/vite-devframe-hub", "scripts": { "dev": "pnpm -C ../.. run build && vite", @@ -33,7 +33,6 @@ "devDependencies": { "@iconify-json/ph": "catalog:frontend", "@vitejs/plugin-vue": "catalog:build", - "get-port-please": "catalog:deps", "pathe": "catalog:deps", "unocss": "catalog:frontend", "vite": "catalog:build" diff --git a/examples/vite-devframe-hub/src/client/main.ts b/examples/vite-devframe-hub/src/client/main.ts index 676fb202..83d33dfe 100644 --- a/examples/vite-devframe-hub/src/client/main.ts +++ b/examples/vite-devframe-hub/src/client/main.ts @@ -13,7 +13,7 @@ import { dockIconSvg } from './icons' import 'virtual:uno.css' import '@antfu/design/styles.css' -const HUB_BASE = '/__hub/' +const HUB_BASE = '/__devframes/' const connEl = document.querySelector('#conn')! const docksEl = document.querySelector('#docks')! diff --git a/examples/vite-devframe-hub/src/devframe-b.ts b/examples/vite-devframe-hub/src/devframe-b.ts index 342829ae..cc7baef5 100644 --- a/examples/vite-devframe-hub/src/devframe-b.ts +++ b/examples/vite-devframe-hub/src/devframe-b.ts @@ -4,7 +4,9 @@ import { defineDevframe } from 'devframe/types' import pkg from '../package.json' with { type: 'json' } export default defineDevframe({ - id: 'example:demo-tool-b', + // Colon-free: the hub instance derives the mount path (`/__devframes//`) + // from the id, and `:` is a route-param marker to the router underneath. + id: 'demo-tool-b', name: 'Demo Tool B', version: pkg.version, packageName: pkg.name, diff --git a/examples/vite-devframe-hub/src/devframe.ts b/examples/vite-devframe-hub/src/devframe.ts index 9b5a483e..b24afadf 100644 --- a/examples/vite-devframe-hub/src/devframe.ts +++ b/examples/vite-devframe-hub/src/devframe.ts @@ -13,7 +13,9 @@ import pkg from '../package.json' with { type: 'json' } * threads a hub-augmented context through `d.setup`. */ export default defineDevframe({ - id: 'example:demo-tool', + // Colon-free: the hub instance derives the mount path (`/__devframes//`) + // from the id, and `:` is a route-param marker to the router underneath. + id: 'demo-tool', name: 'Demo Tool', version: pkg.version, packageName: pkg.name, diff --git a/examples/vite-devframe-hub/src/tabbed-tool.ts b/examples/vite-devframe-hub/src/tabbed-tool.ts index cc72940f..d0c00edd 100644 --- a/examples/vite-devframe-hub/src/tabbed-tool.ts +++ b/examples/vite-devframe-hub/src/tabbed-tool.ts @@ -13,7 +13,9 @@ import pkg from '../package.json' with { type: 'json' } * multi-tab tool like Nuxt DevTools. */ export default defineDevframe({ - id: 'example:tabbed-tool', + // Colon-free: the hub instance derives the mount path (`/__devframes//`) + // from the id, and `:` is a route-param marker to the router underneath. + id: 'tabbed-tool', name: 'Tabbed Tool', version: pkg.version, packageName: pkg.name, diff --git a/examples/vite-devframe-hub/src/vite-devframe-hub.ts b/examples/vite-devframe-hub/src/vite-devframe-hub.ts index bfc68590..555dbcaa 100644 --- a/examples/vite-devframe-hub/src/vite-devframe-hub.ts +++ b/examples/vite-devframe-hub/src/vite-devframe-hub.ts @@ -1,25 +1,33 @@ +import type { HubDevframeEntry, HubInstance } from '@devframes/hub/initiate' import type { DevframeHubContext } from '@devframes/hub/node' import type { ClientScriptEntry } from '@devframes/hub/types' import type { DevframeInstanceRegistration } from 'devframe/node' -import type { DevframeDefinition, DevframeHost } from 'devframe/types' +import type { DevframeDefinition } from 'devframe/types' import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite' +import { Server as NodeHttpServer } from 'node:http' import { homedir } from 'node:os' import process from 'node:process' import { defineHubRpcFunction } from '@devframes/hub' -import { createHubContext, mountDevframe } from '@devframes/hub/node' -import { DEVFRAME_CONNECTION_META_FILENAME } from 'devframe/constants' -import { registerDevframeInstance, startHttpAndWs } from 'devframe/node' -import { serveStaticNodeMiddleware } from 'devframe/utils/serve-static' -import { getPort } from 'get-port-please' +import { DEVFRAMES_HUB_BASE, initHub } from '@devframes/hub/initiate' +import { registerDevframeInstance } from 'devframe/node' import { join } from 'pathe' export interface ViteDevframeHubOptions { - /** Mount path for the hub's connection-meta endpoint. Default: `/__hub/`. */ + /** + * Mount base the hub answers under — every frame lives at `/`. + * Default: `/__devframes/`. + */ base?: string - /** Preferred port for the side-car RPC/WS server. Default: a free port near 9777. */ + /** + * Pin a side-car port for the RPC/WS server. By default the WebSocket + * shares Vite's own http server, upgrading at `__ws`. + */ port?: number - /** Devframes to mount as docks. */ - devframes?: DevframeDefinition[] + /** + * Devframes to mount as docks. Wrap an entry in `{ devframe, dock }` to + * customize its synthesized iframe dock (category, `frameId`, `subTabs`, …). + */ + devframes?: (DevframeDefinition | HubDevframeEntry)[] /** * Per-dock client scripts, keyed by devframe id. Attached to the mounted * iframe dock so the hub client runtime imports them into the host page @@ -28,8 +36,9 @@ export interface ViteDevframeHubOptions { clientScripts?: Record /** * Called once the hub context is created (after devframes are mounted), - * before the server starts. Lets the composition register extra surfaces on - * the context — e.g. a `json-render` dock via `@devframes/json-render`. + * inside `initHub`'s `configure` step. Lets the composition register extra + * surfaces on the context — e.g. a `json-render` dock via + * `@devframes/json-render`. */ onContextReady?: (context: DevframeHubContext) => void | Promise } @@ -65,19 +74,31 @@ const viteHubTerminalsList = defineHubRpcFunction({ /** * A deliberately tiny Vite plugin that wires `@devframes/hub` into a Vite - * dev server: creates a hub context, implements the framework-neutral - * `DevframeHost` surface, and exposes the side-car WS endpoint to the - * browser via Vite middleware at `__connection.json`. + * dev server: one `initHub()` call assembles the whole hub — every devframe + * mounted under `/`, one merged RPC registry on one WebSocket + * (upgrading on Vite's own server at `__ws`), and the discovery + * endpoints (`__connection.json`, `__index.json`, `__client-imports.js`) — + * behind one connect-style middleware that self-filters by the base. * * This file is the entire Vite host — every other framework's hub host is * the same shape: a thin layer that adapts a framework's dev server to the hub. */ export function viteDevframeHub(options: ViteDevframeHubOptions = {}): Plugin { - const base = normalizeBase(options.base ?? '/__hub/') + const base = normalizeBase(options.base ?? DEVFRAMES_HUB_BASE) let viteConfig: ResolvedConfig | undefined - let started: { close: () => Promise } | undefined + let instance: HubInstance | undefined let registration: DevframeInstanceRegistration | undefined + // Every teardown path funnels here: drop the instance-registry record, + // then close the hub (WS binding / side-car, mounted frames' resources). + const teardown = async (): Promise => { + registration?.unregister() + registration = undefined + const previous = instance + instance = undefined + await previous?.close().catch(() => {}) + } + return { name: 'vite-devframe-hub', apply: 'serve', @@ -88,44 +109,43 @@ export function viteDevframeHub(options: ViteDevframeHubOptions = {}): Plugin { async configureServer(server: ViteDevServer) { // Vite re-invokes `configureServer` on each restart. Tear down the - // previous server so we don't leak the WS port, and drop the previous - // registry record so a restart doesn't leave a ghost instance behind. - await started?.close().catch(() => {}) - started = undefined - registration?.unregister() - registration = undefined + // previous instance so we don't leak the WS binding, and drop the + // previous registry record so a restart doesn't leave a ghost instance. + await teardown() const cwd = viteConfig!.root - // Prefer 9777 but keep booting when it's taken (e.g. a lingering - // previous instance) — walk the range, then fall back to a random free - // port. Clients discover whatever was chosen via `__connection.json`. - const port = options.port ?? await getPort({ port: 9777, portRange: [9777, 9877] }) - - // Serve the side-car's connection meta (`__connection.json`) at a URL - // base so a browser loaded there can discover the WS endpoint via - // `connectDevframe()`'s relative `./__connection.json` fetch. - const serveConnectionMeta = (metaBase: string): void => { - const metaPath = `${metaBase}${DEVFRAME_CONNECTION_META_FILENAME}` - server.middlewares.use(metaPath, (_req, res) => { - res.setHeader('Content-Type', 'application/json') - res.end(JSON.stringify({ backend: 'websocket', websocket: port })) - }) - } - const host: DevframeHost = { - mountStatic(base, distDir) { - server.middlewares.use(base, serveStaticNodeMiddleware(distDir)) - }, - // Serve `__connection.json` for each mounted devframe so its - // SPA connects to the hub without relying on same-origin parent-window - // inheritance — which breaks for cross-origin / sandboxed iframes. - mountConnectionMeta(base) { - serveConnectionMeta(base) - }, - resolveOrigin() { + // Attach each configured client script to its devframe's mount entry, + // so the hub client runtime imports it into the host page. + const devframes = (options.devframes ?? []).map((entry) => { + const def = 'devframe' in entry ? entry.devframe : entry + const clientScript = options.clientScripts?.[def.id] + if (!clientScript) + return entry + return 'devframe' in entry + ? { ...entry, dock: { clientScript, ...entry.dock } } + : { devframe: def, dock: { clientScript } } + }) + + const hub = initHub({ + base, + cwd, + // Resolved lazily — Vite knows its local URL only once listening. + origin: () => { const resolved = server.resolvedUrls?.local?.[0] return resolved ? new URL(resolved).origin : 'http://localhost:5173' }, + // Single-user localhost demo: the hub is reachable only on loopback, + // so it opts out of the gate for a no-friction dev experience. A hub + // reachable beyond localhost should gate (see `docs/guide/security.md`). + auth: false, + // Share Vite's own http server for the WebSocket upgrade at + // `__ws` — no side-car port to discover. A `port` option pins + // a side-car server instead, and an https/http2 dev server (where + // Vite hands us a non-`node:http` server) falls back to `initHub`'s + // eager side-car — clients discover either via `__connection.json`. + server: server.httpServer instanceof NodeHttpServer ? server.httpServer : undefined, + ...(options.port != null ? { ws: { port: options.port } } : {}), getStorageDir(scope) { if (scope === 'workspace') return join(cwd, '.devframe') @@ -133,14 +153,7 @@ export function viteDevframeHub(options: ViteDevframeHubOptions = {}): Plugin { return join(cwd, 'node_modules/.vite-devframe-hub') return join(homedir(), '.vite-devframe-hub') }, - } - - const context = await createHubContext({ - cwd, - workspaceRoot: cwd, - mode: 'dev', - host, - builtinRpcDeclarations: [ + rpcDeclarations: [ // The minimal hub ships its own `messages:list` and `terminals:list` // RPCs so the UI has something to read. A full hub kit would // likely standardise these (alongside the built-in @@ -148,42 +161,33 @@ export function viteDevframeHub(options: ViteDevframeHubOptions = {}): Plugin { viteHubMessagesList, viteHubTerminalsList, ], + devframes, + async configure(ctx) { + // Seed a sample command directly on the hub so the UI + // shows something even without any plugged-in devframes. + ctx.commands.register({ + id: 'example:vite-devframe-hub:ping', + title: 'Vite Hub · Ping', + icon: 'ph:bell-duotone', + category: 'kit', + handler: () => 'pong', + }) + await ctx.messages.add({ + level: 'success', + message: 'Vite Devframe Hub started', + description: options.port != null + ? `Side-car WS on port ${options.port}. ${devframes.length} devframe(s) mounted under ${base}.` + : `WS shared on the Vite server at ${base}__ws. ${devframes.length} devframe(s) mounted under ${base}.`, + }) + + await options.onContextReady?.(ctx) + }, }) + instance = hub - // Seed a sample command directly on the hub so the UI - // shows something even without any plugged-in devframes. - context.commands.register({ - id: 'example:vite-devframe-hub:ping', - title: 'Vite Hub · Ping', - icon: 'ph:bell-duotone', - category: 'kit', - handler: () => 'pong', - }) - await context.messages.add({ - level: 'success', - message: 'Vite Devframe Hub started', - description: `Side-car WS on port ${port}. ${options.devframes?.length ?? 0} devframe(s) registered.`, - }) - - for (const def of options.devframes ?? []) { - const clientScript = options.clientScripts?.[def.id] - await mountDevframe(context, def, clientScript ? { dock: { clientScript } } : undefined) - } - - await options.onContextReady?.(context) - - started = await startHttpAndWs({ - context, - port, - // Single-user localhost demo: the side-car is reachable only on - // loopback, so it opts out of the gate for a no-friction dev - // experience. A hub reachable beyond localhost should gate (see - // `docs/guide/security.md`). - auth: false, - }) - - // Tell the hub UI (served at `base`) where to find the WS endpoint. - serveConnectionMeta(base) + // One namespace, one catch-all: the middleware serves everything under + // `base` and `next()`s the rest back to Vite. + server.middlewares.use(hub.nodeMiddleware) // Record this hub in the global instance registry (`~/.devframe/instances/`) // so discovery tooling — `devframe connect` and the inspector's Instances @@ -191,10 +195,13 @@ export function viteDevframeHub(options: ViteDevframeHubOptions = {}): Plugin { // automatically; an in-process host like this one registers explicitly, // reusing the Vite dev server's own origin (where `__connection.json` // is served). Registration waits for the server to be listening so the - // origin/port are known. Folded into `started.close` so every teardown - // path (restart, httpServer close, `closeBundle`) also unregisters. + // origin/port are known; `teardown` unregisters on every close path + // (restart, httpServer close, `closeBundle`). const register = (): void => { - const origin = host.resolveOrigin() + if (instance !== hub) + return + const resolved = server.resolvedUrls?.local?.[0] + const origin = resolved ? new URL(resolved).origin : 'http://localhost:5173' const url = new URL(origin) registration = registerDevframeInstance({ pid: process.pid, @@ -213,21 +220,15 @@ export function viteDevframeHub(options: ViteDevframeHubOptions = {}): Plugin { else server.httpServer?.once('listening', register) - const closeStarted = started.close - started.close = async () => { - registration?.unregister() - registration = undefined - await closeStarted() - } - server.httpServer?.once('close', () => { - void started?.close().catch(() => {}) + if (instance !== hub) + return + void teardown() }) }, async closeBundle() { - await started?.close().catch(() => {}) - started = undefined + await teardown() }, } } diff --git a/examples/vite-devframe-hub/vite.config.ts b/examples/vite-devframe-hub/vite.config.ts index 08506acb..307c0c08 100644 --- a/examples/vite-devframe-hub/vite.config.ts +++ b/examples/vite-devframe-hub/vite.config.ts @@ -1,9 +1,8 @@ -import { mountDevframe } from '@devframes/hub/node' import { toJsonRenderDockEntry } from '@devframes/json-render/hub' import a11yDevframe, { a11yAgentBundlePath } from '@devframes/plugin-a11y' import assetsDevframe from '@devframes/plugin-assets' import codeServerDevframe from '@devframes/plugin-code-server' -import dataInspectorDevframe from '@devframes/plugin-data-inspector' +import { createDataInspectorDevframe } from '@devframes/plugin-data-inspector' import { registerDataSource } from '@devframes/plugin-data-inspector/registry' import gitDevframe from '@devframes/plugin-git' import inspectDevframe from '@devframes/plugin-inspect' @@ -20,6 +19,12 @@ import demoDevframeB from './src/devframe-b' import tabbedToolDevframe from './src/tabbed-tool' import { viteDevframeHub } from './src/vite-devframe-hub' +// Colon-free id override: the hub instance derives each frame's mount path +// (`/__devframes//`) from its id, and `:` — which the plugin's default id +// (`devframes:plugin:data-inspector`) carries — is a route-param marker to +// the router underneath. +const dataInspectorDevframe = createDataInspectorDevframe({ id: 'devframes_plugin_data-inspector' }) + export default defineConfig({ resolve: { alias }, // Dev tooling reached from arbitrary hostnames (LAN IPs, tunnels, tailnets): @@ -74,6 +79,20 @@ export default defineConfig({ messagesDevframe, ogDevframe, assetsDevframe, + // Shared-iframe soft-navigation demo. The hub instance serves the SPA + // and registers its iframe dock; the `dock` override marks it a + // `subTabs` anchor (a shared `frameId` + the postmessage protocol) so + // the client host attaches the frame-nav adapter, materializing one + // client-only dock per tab the SPA's shim reports — all sharing this + // one iframe. + { + devframe: tabbedToolDevframe, + dock: { + category: 'app', + frameId: 'tabbed-tool', + subTabs: { protocol: 'postmessage' }, + }, + }, ], // Attach the a11y inspector's in-page agent as its dock's client script. // The hub client runtime (booted in src/client/main.ts) imports it into @@ -85,7 +104,7 @@ export default defineConfig({ // Dogfood the opt-in JSON-render hub integration: author a view on the // hub context and project it onto a `json-render` dock. The client host // (src/client/main.ts) renders it via @devframes/json-render-ui. - onContextReady: async (context) => { + onContextReady: (context) => { const view = createDashboardView(context) context.docks.register(toJsonRenderDockEntry(view, { id: 'example:json-render', @@ -93,19 +112,6 @@ export default defineConfig({ icon: 'ph:layout-duotone', category: 'app', })) - - // Shared-iframe soft-navigation demo. mountDevframe serves the SPA and - // registers its iframe dock; the `dock` override marks it a `subTabs` - // anchor (a shared `frameId` + the postmessage protocol) so the client - // host attaches the frame-nav adapter, materializing one client-only - // dock per tab the SPA's shim reports — all sharing this one iframe. - await mountDevframe(context, tabbedToolDevframe, { - dock: { - category: 'app', - frameId: 'tabbed-tool', - subTabs: { protocol: 'postmessage' }, - }, - }) }, }), ], diff --git a/knip.jsonc b/knip.jsonc index 4ab1553f..2f3f2195 100644 --- a/knip.jsonc +++ b/knip.jsonc @@ -23,7 +23,7 @@ // `skills/devframe/templates/*.ts` are the devframe-authoring skill's // copy-and-run reference snippets (`skills/devframe/SKILL.md`) — each // one a standalone, runnable example, not imported by anything else. - "entry": ["design/*.ts", "skills/devframe/templates/*.ts"], + "entry": ["design/*.ts", "skills/devframe/templates/*.ts", "scripts/smoke-bun.ts"], // `spa-devframe.ts` imports `valibot` to illustrate schema validation // for a consumer's own devframe — it's a dependency of the example a // reader copies out, not of this repo's root tooling. @@ -87,7 +87,7 @@ ] }, "packages/hub": { - "entry": ["src/{index,constants}.ts", "src/{client,node,types}/index.ts", "src/node/initiate.ts"] + "entry": ["src/{index,constants}.ts", "src/{client,node,types}/index.ts", "src/node/{index,initiate}.ts"] }, "packages/json-render": { // `src/node/index.ts` is already picked up via `tsdown.config.ts` diff --git a/packages/hub-ui/src/client/.generated/css.ts b/packages/hub-ui/src/client/.generated/css.ts index 8101d9b8..5f58f4dc 100644 --- a/packages/hub-ui/src/client/.generated/css.ts +++ b/packages/hub-ui/src/client/.generated/css.ts @@ -1,3 +1,3 @@ /* eslint-disable eslint-comments/no-unlimited-disable */ /* eslint-disable */ -export default "/*\n1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)\n2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)\n2. [UnoCSS]: allow to override the default border color with css var `--un-default-border-color`\n*/\n\n*,\n::before,\n::after {\n box-sizing: border-box; /* 1 */\n border-width: 0; /* 2 */\n border-style: solid; /* 2 */\n border-color: var(--un-default-border-color, #e5e7eb); /* 2 */\n}\n\n::before,\n::after {\n --un-content: '';\n}\n\n/*\n1. Use a consistent sensible line-height in all browsers.\n2. Prevent adjustments of font size after orientation changes in iOS.\n3. Use a more readable tab size.\n4. Use the user's configured `sans` font-family by default.\n5. Use the user's configured `sans` font-feature-settings by default.\n6. Use the user's configured `sans` font-variation-settings by default.\n7. Disable tap highlights on iOS.\n*/\n\nhtml,\n:host {\n line-height: 1.5; /* 1 */\n -webkit-text-size-adjust: 100%; /* 2 */\n -moz-tab-size: 4; /* 3 */\n tab-size: 4; /* 3 */\n font-family: ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\"; /* 4 */\n font-feature-settings: normal; /* 5 */\n font-variation-settings: normal; /* 6 */\n -webkit-tap-highlight-color: transparent; /* 7 */\n}\n\n/*\n1. Remove the margin in all browsers.\n2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.\n*/\n\nbody {\n margin: 0; /* 1 */\n line-height: inherit; /* 2 */\n}\n\n/*\n1. Add the correct height in Firefox.\n2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)\n3. Ensure horizontal rules are visible by default.\n*/\n\nhr {\n height: 0; /* 1 */\n color: inherit; /* 2 */\n border-top-width: 1px; /* 3 */\n}\n\n/*\nAdd the correct text decoration in Chrome, Edge, and Safari.\n*/\n\nabbr:where([title]) {\n text-decoration: underline dotted;\n}\n\n/*\nRemove the default font size and weight for headings.\n*/\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n font-size: inherit;\n font-weight: inherit;\n}\n\n/*\nReset links to optimize for opt-in styling instead of opt-out.\n*/\n\na {\n color: inherit;\n text-decoration: inherit;\n}\n\n/*\nAdd the correct font weight in Edge and Safari.\n*/\n\nb,\nstrong {\n font-weight: bolder;\n}\n\n/*\n1. Use the user's configured `mono` font-family by default.\n2. Use the user's configured `mono` font-feature-settings by default.\n3. Use the user's configured `mono` font-variation-settings by default.\n4. Correct the odd `em` font sizing in all browsers.\n*/\n\ncode,\nkbd,\nsamp,\npre {\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace; /* 1 */\n font-feature-settings: normal; /* 2 */\n font-variation-settings: normal; /* 3 */\n font-size: 1em; /* 4 */\n}\n\n/*\nAdd the correct font size in all browsers.\n*/\n\nsmall {\n font-size: 80%;\n}\n\n/*\nPrevent `sub` and `sup` elements from affecting the line height in all browsers.\n*/\n\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -0.25em;\n}\n\nsup {\n top: -0.5em;\n}\n\n/*\n1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)\n2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)\n3. Remove gaps between table borders by default.\n*/\n\ntable {\n text-indent: 0; /* 1 */\n border-color: inherit; /* 2 */\n border-collapse: collapse; /* 3 */\n}\n\n/*\n1. Change the font styles in all browsers.\n2. Remove the margin in Firefox and Safari.\n3. Remove default padding in all browsers.\n*/\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n font-family: inherit; /* 1 */\n font-feature-settings: inherit; /* 1 */\n font-variation-settings: inherit; /* 1 */\n font-size: 100%; /* 1 */\n font-weight: inherit; /* 1 */\n line-height: inherit; /* 1 */\n color: inherit; /* 1 */\n margin: 0; /* 2 */\n padding: 0; /* 3 */\n}\n\n/*\nRemove the inheritance of text transform in Edge and Firefox.\n*/\n\nbutton,\nselect {\n text-transform: none;\n}\n\n/*\n1. Correct the inability to style clickable types in iOS and Safari.\n2. Remove default button styles.\n*/\n\nbutton,\n[type='button'],\n[type='reset'],\n[type='submit'] {\n -webkit-appearance: button; /* 1 */\n background-color: transparent; /* 2 */\n background-image: none; /* 2 */\n}\n\n/*\nUse the modern Firefox focus style for all focusable elements.\n*/\n\n:-moz-focusring {\n outline: auto;\n}\n\n/*\nRemove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)\n*/\n\n:-moz-ui-invalid {\n box-shadow: none;\n}\n\n/*\nAdd the correct vertical alignment in Chrome and Firefox.\n*/\n\nprogress {\n vertical-align: baseline;\n}\n\n/*\nCorrect the cursor style of increment and decrement buttons in Safari.\n*/\n\n::-webkit-inner-spin-button,\n::-webkit-outer-spin-button {\n height: auto;\n}\n\n/*\n1. Correct the odd appearance in Chrome and Safari.\n2. Correct the outline style in Safari.\n*/\n\n[type='search'] {\n -webkit-appearance: textfield; /* 1 */\n outline-offset: -2px; /* 2 */\n}\n\n/*\nRemove the inner padding in Chrome and Safari on macOS.\n*/\n\n::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n/*\n1. Correct the inability to style clickable types in iOS and Safari.\n2. Change font properties to `inherit` in Safari.\n*/\n\n::-webkit-file-upload-button {\n -webkit-appearance: button; /* 1 */\n font: inherit; /* 2 */\n}\n\n/*\nAdd the correct display in Chrome and Safari.\n*/\n\nsummary {\n display: list-item;\n}\n\n/*\nRemoves the default spacing for appropriate elements.\n*/\n\nblockquote,\ndl,\ndd,\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\nhr,\nfigure,\np,\npre {\n margin: 0;\n}\n\nfieldset {\n margin: 0;\n padding: 0;\n}\n\nlegend {\n padding: 0;\n}\n\nol,\nul,\nmenu {\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\ndialog {\n padding: 0;\n}\n\n/*\nPrevent resizing textareas horizontally by default.\n*/\n\ntextarea {\n resize: vertical;\n}\n\n/*\n1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)\n2. Set the default placeholder color to the user's configured gray 400 color.\n*/\n\ninput::placeholder,\ntextarea::placeholder {\n opacity: 1; /* 1 */\n color: #9ca3af; /* 2 */\n}\n\n/*\nSet the default cursor for buttons.\n*/\n\nbutton,\n[role=\"button\"] {\n cursor: pointer;\n}\n\n/*\nMake sure disabled buttons don't get the pointer cursor.\n*/\n\n:disabled {\n cursor: default;\n}\n\n/*\n1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)\n2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)\n This can trigger a poorly considered lint error in some tools but is included by design.\n*/\n\nimg,\nsvg,\nvideo,\ncanvas,\naudio,\niframe,\nembed,\nobject {\n display: block; /* 1 */\n vertical-align: middle; /* 2 */\n}\n\n/*\nConstrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)\n*/\n\nimg,\nvideo {\n max-width: 100%;\n height: auto;\n}\n\n/*\nMake elements with the HTML hidden attribute stay hidden by default.\n*/\n\n[hidden]:where(:not([hidden=\"until-found\"])) {\n display: none;\n}\n\n@property --un-ease{syntax:\"*\";inherits:false;}@property --un-bg-opacity{syntax:\"\";inherits:false;initial-value:100%;}@property --un-backdrop-blur{syntax:\"*\";inherits:false;}@property --un-backdrop-brightness{syntax:\"*\";inherits:false;}@property --un-backdrop-contrast{syntax:\"*\";inherits:false;}@property --un-backdrop-grayscale{syntax:\"*\";inherits:false;}@property --un-backdrop-hue-rotate{syntax:\"*\";inherits:false;}@property --un-backdrop-invert{syntax:\"*\";inherits:false;}@property --un-backdrop-opacity{syntax:\"*\";inherits:false;}@property --un-backdrop-saturate{syntax:\"*\";inherits:false;}@property --un-backdrop-sepia{syntax:\"*\";inherits:false;}@property --un-bg-opacity{syntax:\"\";inherits:false;initial-value:100%;}@property --un-inset-ring-color{syntax:\"*\";inherits:false;}@property --un-inset-ring-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000;}@property --un-inset-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000;}@property --un-inset-shadow-color{syntax:\"*\";inherits:false;}@property --un-ring-color{syntax:\"*\";inherits:false;}@property --un-ring-inset{syntax:\"*\";inherits:false;}@property --un-ring-offset-color{syntax:\"*\";inherits:false;}@property --un-ring-offset-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000;}@property --un-ring-offset-width{syntax:\"\";inherits:false;initial-value:0px;}@property --un-ring-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000;}@property --un-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000;}@property --un-shadow-color{syntax:\"*\";inherits:false;}@property --un-text-opacity{syntax:\"\";inherits:false;initial-value:100%;}:root {\n --un-text-opacity: 100%;\n}\n\n/*\n * Thin/transparent scrollbar, matching packages/ui/src/styles/global.css —\n * that stylesheet only reaches the Nuxt plugin apps, not this injected\n * webcomponents shell, which otherwise falls back to the browser default\n * (thick, opaque-gutter) scrollbar clashing with the translucent dock.\n * `:host`, not `:root` — this stylesheet is adopted into the dock's shadow\n * root (`defineCustomElement(..., { shadowRoot: true, styles: [css] })`),\n * where `:root` only ever matches the top-level document's .\n * Rules below are unscoped (not `#devframes-anchor ...`) since the\n * shadow boundary already keeps them from leaking onto the host page, and\n * scoping under the anchor's id would miss edge-docked mode entirely\n * (`DockEmbedded.vue` renders `DockEdge`/`#devframes-edge-panel` and\n * `Dock`/`#devframes-anchor` as mutually exclusive siblings).\n */\n:host {\n --app-scrollbar-size: 6px;\n --app-scrollbar-radius: 1px;\n --app-scrollbar-thumb: #8884;\n --app-scrollbar-thumb-hover: #8885;\n}\n\n::-webkit-scrollbar {\n width: var(--app-scrollbar-size);\n}\n\n::-webkit-scrollbar:horizontal {\n height: var(--app-scrollbar-size);\n}\n\n::-webkit-scrollbar-corner {\n background: transparent;\n}\n\n::-webkit-scrollbar-thumb {\n background-color: var(--app-scrollbar-thumb);\n transition: background 0.2s ease;\n border-radius: var(--app-scrollbar-radius);\n}\n\n::-webkit-scrollbar-thumb:hover {\n background-color: var(--app-scrollbar-thumb-hover);\n}\n\n::-webkit-scrollbar-track {\n border-radius: var(--app-scrollbar-radius);\n background: transparent;\n}\n\n#devframes-anchor {\n font-size:15px;font-family:var(--font-sans);width:calc(var(--spacing) * 0);transform-origin:center;box-sizing:border-box;position:fixed;z-index:2147483644;\n transform: translate(-50%, -50%) rotate(0);\n}\n\n#devframes-anchor #devframes-dock-container {\n display:flex;width:max-content;left:calc(var(--spacing) * 0);top:calc(var(--spacing) * 0);position:absolute;\n height: var(--devframes-dock-height, 40px);\n min-width: var(--devframes-dock-min-width, 100px);\n transform: translate(-50%, -50%);\n}\n\n#devframes-anchor.devframes-vertical #devframes-dock-container {\n transform: translate(-50%, -50%) rotate(90deg);\n transition-property:all;transition-timing-function:var(--un-ease, var(--default-transition-timingFunction));transition-duration:var(--un-duration, var(--default-transition-duration));--un-duration:500ms;transition-duration:500ms;\n}\n\n#devframes-anchor #devframes-dock {\n margin:auto;border-radius:calc(infinity * 1px);height:100%;-webkit-user-select:none;user-select:none;touch-action:none;\n ;color:color-mix(in oklab, #333 var(--un-text-opacity), transparent);background-color:color-mix(in srgb, var(--colors-white) 80%, transparent);--un-backdrop-blur:blur(7px);-webkit-backdrop-filter:var(--un-backdrop-blur,) var(--un-backdrop-brightness,) var(--un-backdrop-contrast,) var(--un-backdrop-grayscale,) var(--un-backdrop-hue-rotate,) var(--un-backdrop-invert,) var(--un-backdrop-opacity,) var(--un-backdrop-saturate,) var(--un-backdrop-sepia,);backdrop-filter:var(--un-backdrop-blur,) var(--un-backdrop-brightness,) var(--un-backdrop-contrast,) var(--un-backdrop-grayscale,) var(--un-backdrop-hue-rotate,) var(--un-backdrop-invert,) var(--un-backdrop-opacity,) var(--un-backdrop-saturate,) var(--un-backdrop-sepia,);--un-shadow:0 1px 3px 0 var(--un-shadow-color, rgb(0 0 0 / 0.1)),0 1px 2px -1px var(--un-shadow-color, rgb(0 0 0 / 0.1));box-shadow:var(--un-inset-shadow), var(--un-inset-ring-shadow), var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);\n transition-property:all;transition-timing-function:var(--un-ease, var(--default-transition-timingFunction));transition-duration:var(--un-duration, var(--default-transition-duration));--un-duration:500ms;transition-duration:500ms;\n height: var(--devframes-dock-height, 40px);\n width: calc-size(max-content, size);\n}.dark #devframes-anchor #devframes-dock{color:color-mix(in srgb, var(--colors-white) var(--un-text-opacity), transparent);background-color:color-mix(in oklab, #111 80%, transparent);}@supports (color: color-mix(in lab, red, red)){.dark #devframes-anchor #devframes-dock{color:color-mix(in oklab, var(--colors-white) var(--un-text-opacity), transparent);}}@supports (color: color-mix(in lab, red, red)){#devframes-anchor #devframes-dock{background-color:color-mix(in oklab, var(--colors-white) 80%, transparent);}}\n\n#devframes-anchor.devframes-minimized #devframes-dock {\n padding-inline:calc(var(--spacing) * 0);padding-block:2px;\n width: var(--devframes-dock-minimized-size, 22px);\n height: var(--devframes-dock-minimized-size, 22px);\n}\n\n#devframes-anchor.devframes-minimized .devframes-dock-bracket {\n opacity:50%;width:calc(var(--spacing) * 1.5);\n}\n\n#devframes-anchor:hover #devframes-glowing {\n opacity:60%;\n}\n\n#devframes-anchor #devframes-glowing {\n border-radius:calc(infinity * 1px);opacity:0%;pointer-events:none;transition-property:all;transition-timing-function:var(--un-ease, var(--default-transition-timingFunction));transition-duration:var(--un-duration, var(--default-transition-duration));--un-duration:1000ms;transition-duration:1000ms;--un-ease:var(--ease-out);transition-timing-function:var(--ease-out);left:calc(var(--spacing) * 0);top:calc(var(--spacing) * 0);position:absolute;\n width: var(--devframes-dock-glow-size, 160px);\n height: var(--devframes-dock-glow-size, 160px);\n filter: blur(var(--devframes-dock-glow-blur, 60px));\n transform: translate(-50%, -50%);\n background-image: linear-gradient(45deg, #61d9ff, #6b84fd, #715ebd);\n}\n\n@media print {\n #devframes-anchor {\n display:none;\n }\n}\n\n.devframes-resize-handle-horizontal {\n margin-block:-5px;border-radius:var(--radius-md);height:10px;cursor:ns-resize;left:6px;right:6px;position:absolute;\n}\n.devframes-resize-handle-vertical {\n margin-inline:-5px;border-radius:var(--radius-md);width:10px;cursor:ew-resize;top:6px;bottom:calc(var(--spacing) * 0);position:absolute;\n}\n.devframes-resize-handle-corner {\n margin:-6px;border-radius:var(--radius-md);width:14px;height:14px;position:absolute;\n}\n.devframes-resize-handle {\n z-index:30;\n}\n.devframes-resize-handle:hover {\n ;background-color:color-mix(in srgb, var(--colors-gray-400) 10%, transparent);\n}@supports (color: color-mix(in lab, red, red)){.devframes-resize-handle:hover{background-color:color-mix(in oklab, var(--colors-gray-400) 10%, transparent);}}\n\n@keyframes devframes-shake {\n 0%, 100% { transform: translateX(0); }\n 20%, 60% { transform: translateX(-5px); }\n 40%, 80% { transform: translateX(5px); }\n}\n.devframes-shake {\n animation: devframes-shake 0.4s cubic-bezier(0.36, 0.07, 0.19, 0.97);\n}\n@media (prefers-reduced-motion: reduce) {\n .devframes-shake {\n animation: none;\n }\n}\n\n/* layer: properties */\n@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{--un-bg-opacity:100%;--un-border-opacity:100%;--un-caret-opacity:100%;--un-ease:initial;--un-fill-opacity:100%;--un-leading:initial;--un-ring-opacity:100%;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-space-y-reverse:initial;--un-text-opacity:100%;--un-translate-x:initial;--un-translate-y:initial;--un-translate-z:initial;}}\n@property --un-text-opacity{syntax:\"\";inherits:false;initial-value:100%;}\n@property --un-leading{syntax:\"*\";inherits:false;}\n@property --un-outline-style{syntax:\"*\";inherits:false;initial-value:solid;}\n@property --un-caret-opacity{syntax:\"\";inherits:false;initial-value:100%;}\n@property --un-border-opacity{syntax:\"\";inherits:false;initial-value:100%;}\n@property --un-bg-opacity{syntax:\"\";inherits:false;initial-value:100%;}\n@property --un-ring-opacity{syntax:\"\";inherits:false;initial-value:100%;}\n@property --un-inset-ring-color{syntax:\"*\";inherits:false;}\n@property --un-inset-ring-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000;}\n@property --un-inset-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000;}\n@property --un-inset-shadow-color{syntax:\"*\";inherits:false;}\n@property --un-ring-color{syntax:\"*\";inherits:false;}\n@property --un-ring-inset{syntax:\"*\";inherits:false;}\n@property --un-ring-offset-color{syntax:\"*\";inherits:false;}\n@property --un-ring-offset-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000;}\n@property --un-ring-offset-width{syntax:\"\";inherits:false;initial-value:0px;}\n@property --un-ring-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000;}\n@property --un-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000;}\n@property --un-shadow-color{syntax:\"*\";inherits:false;}\n@property --un-translate-x{syntax:\"*\";inherits:false;initial-value:0;}\n@property --un-translate-y{syntax:\"*\";inherits:false;initial-value:0;}\n@property --un-translate-z{syntax:\"*\";inherits:false;initial-value:0;}\n@property --un-scale-x{syntax:\"*\";inherits:false;initial-value:1;}\n@property --un-scale-y{syntax:\"*\";inherits:false;initial-value:1;}\n@property --un-scale-z{syntax:\"*\";inherits:false;initial-value:1;}\n@property --un-ease{syntax:\"*\";inherits:false;}\n@property --un-fill-opacity{syntax:\"\";inherits:false;initial-value:100%;}\n@property --un-backdrop-blur{syntax:\"*\";inherits:false;}\n@property --un-backdrop-brightness{syntax:\"*\";inherits:false;}\n@property --un-backdrop-contrast{syntax:\"*\";inherits:false;}\n@property --un-backdrop-grayscale{syntax:\"*\";inherits:false;}\n@property --un-backdrop-hue-rotate{syntax:\"*\";inherits:false;}\n@property --un-backdrop-invert{syntax:\"*\";inherits:false;}\n@property --un-backdrop-opacity{syntax:\"*\";inherits:false;}\n@property --un-backdrop-saturate{syntax:\"*\";inherits:false;}\n@property --un-backdrop-sepia{syntax:\"*\";inherits:false;}\n@property --un-blur{syntax:\"*\";inherits:false;}\n@property --un-brightness{syntax:\"*\";inherits:false;}\n@property --un-contrast{syntax:\"*\";inherits:false;}\n@property --un-drop-shadow{syntax:\"*\";inherits:false;}\n@property --un-grayscale{syntax:\"*\";inherits:false;}\n@property --un-hue-rotate{syntax:\"*\";inherits:false;}\n@property --un-invert{syntax:\"*\";inherits:false;}\n@property --un-saturate{syntax:\"*\";inherits:false;}\n@property --un-sepia{syntax:\"*\";inherits:false;}\n@property --un-numeric-figure{syntax:\"*\";inherits:false;}\n@property --un-numeric-fraction{syntax:\"*\";inherits:false;}\n@property --un-numeric-spacing{syntax:\"*\";inherits:false;}\n@property --un-ordinal{syntax:\"*\";inherits:false;}\n@property --un-slashed-zero{syntax:\"*\";inherits:false;}\n@property --un-space-y-reverse{syntax:\"*\";inherits:false;initial-value:0;}\n/* layer: theme */\n:root, :host { --spacing: 0.25rem; --font-sans: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"Helvetica Neue\",Arial,\"Noto Sans\",sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\",\"Noto Color Emoji\"; --default-transition-timingFunction: cubic-bezier(0.4, 0, 0.2, 1); --default-transition-duration: 150ms; --ease-out: cubic-bezier(0, 0, 0.2, 1); --radius-md: 0.375rem; --colors-gray-400: oklch(70.7% 0.022 261.325); --colors-white: #fff; --container-lg: 32rem; --radius-lg: 0.5rem; --radius-DEFAULT: 0.25rem; --font-mono: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,\"Liberation Mono\",\"Courier New\",monospace; --colors-primary-DEFAULT: #3a6a45; --colors-blue-DEFAULT: oklch(70.7% 0.165 254.624); --colors-red-500: oklch(63.7% 0.237 25.331); --radius-xl: 0.75rem; --fontWeight-medium: 500; --colors-black: #000; --colors-primary-500: #578861; --ease-in: cubic-bezier(0.4, 0, 1, 1); --colors-primary-600: #40704b; --colors-primary-700: #295935; --colors-red-600: oklch(57.7% 0.245 27.325); --colors-primary-400: #75a67e; --colors-amber-DEFAULT: oklch(82.8% 0.189 84.429); --colors-orange-DEFAULT: oklch(75% 0.183 55.934); --text-xs-fontSize: 0.75rem; --text-xs-lineHeight: 1rem; --text-sm-fontSize: 0.875rem; --text-sm-lineHeight: 1.25rem; --text-2xl-fontSize: 1.5rem; --text-2xl-lineHeight: 2rem; --text-base-fontSize: 1rem; --text-base-lineHeight: 1.5rem; --colors-primary-300: #93c69d; --colors-red-400: oklch(70.4% 0.191 22.216); --leading-none: 1; --colors-neutral-800: oklch(26.9% 0 0); --colors-neutral-200: oklch(92.2% 0 0); --fontWeight-bold: 700; --tracking-wide: 0.025em; --colors-gray-DEFAULT: oklch(70.7% 0.022 261.325); --tracking-tight: -0.025em; --colors-red-DEFAULT: oklch(70.4% 0.191 22.216); --colors-purple-DEFAULT: oklch(71.4% 0.203 305.504); --leading-relaxed: 1.625; --colors-green-DEFAULT: oklch(79.2% 0.209 151.711); --text-xl-fontSize: 1.25rem; --text-xl-lineHeight: 1.75rem; --default-font-family: var(--font-sans); --default-monoFont-family: var(--font-mono); }\n/* layer: base */\n *, ::after, ::before, ::backdrop, ::file-selector-button { box-sizing: border-box; margin: 0; padding: 0; border: 0 solid; } html, :host { line-height: 1.5; -webkit-text-size-adjust: 100%; tab-size: 4; 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-featureSettings, normal); font-variation-settings: var(--default-font-variationSettings, 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; text-decoration: inherit; } b, strong { font-weight: bolder; } code, kbd, samp, pre { font-family: var( --default-monoFont-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace ); font-feature-settings: var(--default-monoFont-featureSettings, normal); font-variation-settings: var(--default-monoFont-variationSettings, normal); font-size: 1em; } small { font-size: 80%; } sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } sub { bottom: -0.25em; } sup { top: -0.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 { display: block; vertical-align: middle; } img, video { max-width: 100%; height: auto; } button, input, select, optgroup, textarea, ::file-selector-button { font: inherit; font-feature-settings: inherit; font-variation-settings: inherit; letter-spacing: inherit; color: inherit; border-radius: 0; background-color: transparent; opacity: 1; } :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: 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, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-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']), ::file-selector-button { appearance: button; } ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { height: auto; } [hidden]:where(:not([hidden~='until-found'])) { display: none !important; }\n/* layer: preflights */\n*,::before,::after{border-color:#8882}\n/* layer: icons */\n.i-ph-arrow-clockwise{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M240 56v48a8 8 0 0 1-8 8h-48a8 8 0 0 1 0-16h27.4l-26.59-24.36l-.25-.24a80 80 0 1 0-1.67 114.78a8 8 0 0 1 11 11.63A95.44 95.44 0 0 1 128 224h-1.32a96 96 0 1 1 69.07-164L224 85.8V56a8 8 0 1 1 16 0'/%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-arrow-clockwise-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M216 128a88 88 0 1 1-88-88a88 88 0 0 1 88 88' opacity='.2'/%3E%3Cpath d='M240 56v48a8 8 0 0 1-8 8h-48a8 8 0 0 1 0-16h27.4l-26.59-24.36l-.25-.24a80 80 0 1 0-1.67 114.78a8 8 0 0 1 11 11.63A95.44 95.44 0 0 1 128 224h-1.32a96 96 0 1 1 69.07-164L224 85.8V56a8 8 0 1 1 16 0'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-arrow-counter-clockwise{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M224 128a96 96 0 0 1-94.71 96H128a95.38 95.38 0 0 1-65.9-26.2a8 8 0 0 1 11-11.63a80 80 0 1 0-1.67-114.78a3 3 0 0 1-.26.25L44.59 96H72a8 8 0 0 1 0 16H24a8 8 0 0 1-8-8V56a8 8 0 0 1 16 0v29.8L60.25 60A96 96 0 0 1 224 128'/%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-arrow-square-out-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M184 80v128a8 8 0 0 1-8 8H48a8 8 0 0 1-8-8V80a8 8 0 0 1 8-8h128a8 8 0 0 1 8 8' opacity='.2'/%3E%3Cpath d='M224 104a8 8 0 0 1-16 0V59.32l-66.33 66.34a8 8 0 0 1-11.32-11.32L196.68 48H152a8 8 0 0 1 0-16h64a8 8 0 0 1 8 8Zm-40 24a8 8 0 0 0-8 8v72H48V80h72a8 8 0 0 0 0-16H48a16 16 0 0 0-16 16v128a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-72a8 8 0 0 0-8-8'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-arrows-counter-clockwise-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M216 128a88 88 0 1 1-88-88a88 88 0 0 1 88 88' opacity='.2'/%3E%3Cpath d='M88 104H40a8 8 0 0 1-8-8V48a8 8 0 0 1 16 0v28.69l14.63-14.63A95.43 95.43 0 0 1 130 33.94h.53a95.36 95.36 0 0 1 67.07 27.33a8 8 0 0 1-11.18 11.44a79.52 79.52 0 0 0-55.89-22.77h-.45a79.56 79.56 0 0 0-56.14 23.43L59.31 88H88a8 8 0 0 1 0 16m128 48h-48a8 8 0 0 0 0 16h28.69l-14.63 14.63a79.56 79.56 0 0 1-56.13 23.43h-.45a79.52 79.52 0 0 1-55.89-22.77a8 8 0 1 0-11.18 11.44a95.36 95.36 0 0 0 67.07 27.33h.52a95.43 95.43 0 0 0 67.36-28.12L208 179.31V208a8 8 0 0 0 16 0v-48a8 8 0 0 0-8-8'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-arrows-out-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M208 48v160H48V48Z' opacity='.2'/%3E%3Cpath d='M216 48v48a8 8 0 0 1-16 0V67.31l-42.34 42.35a8 8 0 0 1-11.32-11.32L188.69 56H160a8 8 0 0 1 0-16h48a8 8 0 0 1 8 8M98.34 146.34L56 188.69V160a8 8 0 0 0-16 0v48a8 8 0 0 0 8 8h48a8 8 0 0 0 0-16H67.31l42.35-42.34a8 8 0 0 0-11.32-11.32M208 152a8 8 0 0 0-8 8v28.69l-42.34-42.35a8 8 0 0 0-11.32 11.32L188.69 200H160a8 8 0 0 0 0 16h48a8 8 0 0 0 8-8v-48a8 8 0 0 0-8-8M67.31 56H96a8 8 0 0 0 0-16H48a8 8 0 0 0-8 8v48a8 8 0 0 0 16 0V67.31l42.34 42.35a8 8 0 0 0 11.32-11.32Z'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-cards-three-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M216 104v96a8 8 0 0 1-8 8H48a8 8 0 0 1-8-8v-96a8 8 0 0 1 8-8h160a8 8 0 0 1 8 8' opacity='.2'/%3E%3Cpath d='M208 88H48a16 16 0 0 0-16 16v96a16 16 0 0 0 16 16h160a16 16 0 0 0 16-16v-96a16 16 0 0 0-16-16m0 112H48v-96h160zM48 64a8 8 0 0 1 8-8h144a8 8 0 0 1 0 16H56a8 8 0 0 1-8-8m16-32a8 8 0 0 1 8-8h112a8 8 0 0 1 0 16H72a8 8 0 0 1-8-8'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-caret-down{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='m213.66 101.66l-80 80a8 8 0 0 1-11.32 0l-80-80a8 8 0 0 1 11.32-11.32L128 164.69l74.34-74.35a8 8 0 0 1 11.32 11.32'/%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-caret-left{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M165.66 202.34a8 8 0 0 1-11.32 11.32l-80-80a8 8 0 0 1 0-11.32l80-80a8 8 0 0 1 11.32 11.32L91.31 128Z'/%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-caret-right{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='m181.66 133.66l-80 80a8 8 0 0 1-11.32-11.32L164.69 128L90.34 53.66a8 8 0 0 1 11.32-11.32l80 80a8 8 0 0 1 0 11.32'/%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-caret-up{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M213.66 165.66a8 8 0 0 1-11.32 0L128 91.31l-74.34 74.35a8 8 0 0 1-11.32-11.32l80-80a8 8 0 0 1 11.32 0l80 80a8 8 0 0 1 0 11.32'/%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-check-bold{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='m232.49 80.49l-128 128a12 12 0 0 1-17 0l-56-56a12 12 0 1 1 17-17L96 183L215.51 63.51a12 12 0 0 1 17 17Z'/%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-circle-notch{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M232 128a104 104 0 0 1-208 0c0-41 23.81-78.36 60.66-95.27a8 8 0 0 1 6.68 14.54C60.15 61.59 40 93.27 40 128a88 88 0 0 0 176 0c0-34.73-20.15-66.41-51.34-80.73a8 8 0 0 1 6.68-14.54C208.19 49.64 232 87 232 128'/%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-dots-six-vertical{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M104 60a12 12 0 1 1-12-12a12 12 0 0 1 12 12m60 12a12 12 0 1 0-12-12a12 12 0 0 0 12 12m-72 44a12 12 0 1 0 12 12a12 12 0 0 0-12-12m72 0a12 12 0 1 0 12 12a12 12 0 0 0-12-12m-72 68a12 12 0 1 0 12 12a12 12 0 0 0-12-12m72 0a12 12 0 1 0 12 12a12 12 0 0 0-12-12'/%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-dots-three-outline-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M152 128a24 24 0 1 1-24-24a24 24 0 0 1 24 24M48 104a24 24 0 1 0 24 24a24 24 0 0 0-24-24m160 0a24 24 0 1 0 24 24a24 24 0 0 0-24-24' opacity='.2'/%3E%3Cpath d='M128 96a32 32 0 1 0 32 32a32 32 0 0 0-32-32m0 48a16 16 0 1 1 16-16a16 16 0 0 1-16 16M48 96a32 32 0 1 0 32 32a32 32 0 0 0-32-32m0 48a16 16 0 1 1 16-16a16 16 0 0 1-16 16m160-48a32 32 0 1 0 32 32a32 32 0 0 0-32-32m0 48a16 16 0 1 1 16-16a16 16 0 0 1-16 16'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-eye-slash{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M53.92 34.62a8 8 0 1 0-11.84 10.76l19.24 21.17C25 88.84 9.38 123.2 8.69 124.76a8 8 0 0 0 0 6.5c.35.79 8.82 19.57 27.65 38.4C61.43 194.74 93.12 208 128 208a127.1 127.1 0 0 0 52.07-10.83l22 24.21a8 8 0 1 0 11.84-10.76Zm47.33 75.84l41.67 45.85a32 32 0 0 1-41.67-45.85M128 192c-30.78 0-57.67-11.19-79.93-33.25A133.2 133.2 0 0 1 25 128c4.69-8.79 19.66-33.39 47.35-49.38l18 19.75a48 48 0 0 0 63.66 70l14.73 16.2A112 112 0 0 1 128 192m6-95.43a8 8 0 0 1 3-15.72a48.16 48.16 0 0 1 38.77 42.64a8 8 0 0 1-7.22 8.71a6 6 0 0 1-.75 0a8 8 0 0 1-8-7.26A32.09 32.09 0 0 0 134 96.57m113.28 34.69c-.42.94-10.55 23.37-33.36 43.8a8 8 0 1 1-10.67-11.92a132.8 132.8 0 0 0 27.8-35.14a133.2 133.2 0 0 0-23.12-30.77C185.67 75.19 158.78 64 128 64a118.4 118.4 0 0 0-19.36 1.57A8 8 0 1 1 106 49.79A134 134 0 0 1 128 48c34.88 0 66.57 13.26 91.66 38.35c18.83 18.83 27.3 37.62 27.65 38.41a8 8 0 0 1 0 6.5Z'/%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-eye-slash-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M128 56c-80 0-112 72-112 72s32 72 112 72s112-72 112-72s-32-72-112-72m0 112a40 40 0 1 1 40-40a40 40 0 0 1-40 40' opacity='.2'/%3E%3Cpath d='M53.92 34.62a8 8 0 1 0-11.84 10.76l19.24 21.17C25 88.84 9.38 123.2 8.69 124.76a8 8 0 0 0 0 6.5c.35.79 8.82 19.57 27.65 38.4C61.43 194.74 93.12 208 128 208a127.1 127.1 0 0 0 52.07-10.83l22 24.21a8 8 0 1 0 11.84-10.76Zm47.33 75.84l41.67 45.85a32 32 0 0 1-41.67-45.85M128 192c-30.78 0-57.67-11.19-79.93-33.25A133.2 133.2 0 0 1 25 128c4.69-8.79 19.66-33.39 47.35-49.38l18 19.75a48 48 0 0 0 63.66 70l14.73 16.2A112 112 0 0 1 128 192m6-95.43a8 8 0 0 1 3-15.72a48.16 48.16 0 0 1 38.77 42.64a8 8 0 0 1-7.22 8.71a6 6 0 0 1-.75 0a8 8 0 0 1-8-7.26A32.09 32.09 0 0 0 134 96.57m113.28 34.69c-.42.94-10.55 23.37-33.36 43.8a8 8 0 1 1-10.67-11.92a132.8 132.8 0 0 0 27.8-35.14a133.2 133.2 0 0 0-23.12-30.77C185.67 75.19 158.78 64 128 64a118.4 118.4 0 0 0-19.36 1.57A8 8 0 1 1 106 49.79A134 134 0 0 1 128 48c34.88 0 66.57 13.26 91.66 38.35c18.83 18.83 27.3 37.62 27.65 38.41a8 8 0 0 1 0 6.5Z'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-globe{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M128 24a104 104 0 1 0 104 104A104.12 104.12 0 0 0 128 24m88 104a87.6 87.6 0 0 1-3.33 24h-38.51a157.4 157.4 0 0 0 0-48h38.51a87.6 87.6 0 0 1 3.33 24m-114 40h52a115.1 115.1 0 0 1-26 45a115.3 115.3 0 0 1-26-45m-3.9-16a140.8 140.8 0 0 1 0-48h59.88a140.8 140.8 0 0 1 0 48ZM40 128a87.6 87.6 0 0 1 3.33-24h38.51a157.4 157.4 0 0 0 0 48H43.33A87.6 87.6 0 0 1 40 128m114-40h-52a115.1 115.1 0 0 1 26-45a115.3 115.3 0 0 1 26 45m52.33 0h-35.62a135.3 135.3 0 0 0-22.3-45.6A88.29 88.29 0 0 1 206.37 88Zm-98.74-45.6A135.3 135.3 0 0 0 85.29 88H49.63a88.29 88.29 0 0 1 57.96-45.6M49.63 168h35.66a135.3 135.3 0 0 0 22.3 45.6A88.29 88.29 0 0 1 49.63 168m98.78 45.6a135.3 135.3 0 0 0 22.3-45.6h35.66a88.29 88.29 0 0 1-57.96 45.6'/%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-keyboard-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M232 64v128a8 8 0 0 1-8 8H32a8 8 0 0 1-8-8V64a8 8 0 0 1 8-8h192a8 8 0 0 1 8 8' opacity='.2'/%3E%3Cpath d='M224 48H32a16 16 0 0 0-16 16v128a16 16 0 0 0 16 16h192a16 16 0 0 0 16-16V64a16 16 0 0 0-16-16m0 144H32V64h192zm-16-64a8 8 0 0 1-8 8H56a8 8 0 0 1 0-16h144a8 8 0 0 1 8 8m0-32a8 8 0 0 1-8 8H56a8 8 0 0 1 0-16h144a8 8 0 0 1 8 8M72 160a8 8 0 0 1-8 8h-8a8 8 0 0 1 0-16h8a8 8 0 0 1 8 8m96 0a8 8 0 0 1-8 8H96a8 8 0 0 1 0-16h64a8 8 0 0 1 8 8m40 0a8 8 0 0 1-8 8h-8a8 8 0 0 1 0-16h8a8 8 0 0 1 8 8'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-laptop-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M216 72v104H40V72a16 16 0 0 1 16-16h144a16 16 0 0 1 16 16' opacity='.2'/%3E%3Cpath d='M232 168h-8V72a24 24 0 0 0-24-24H56a24 24 0 0 0-24 24v96h-8a8 8 0 0 0-8 8v16a24 24 0 0 0 24 24h176a24 24 0 0 0 24-24v-16a8 8 0 0 0-8-8M48 72a8 8 0 0 1 8-8h144a8 8 0 0 1 8 8v96H48Zm176 120a8 8 0 0 1-8 8H40a8 8 0 0 1-8-8v-8h192ZM152 88a8 8 0 0 1-8 8h-32a8 8 0 0 1 0-16h32a8 8 0 0 1 8 8'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-layout-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M104 104v104H40a8 8 0 0 1-8-8v-96Z' opacity='.2'/%3E%3Cpath d='M216 40H40a16 16 0 0 0-16 16v144a16 16 0 0 0 16 16h176a16 16 0 0 0 16-16V56a16 16 0 0 0-16-16m0 16v40H40V56ZM40 112h56v88H40Zm176 88H112v-88h104z'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-magnifying-glass-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M192 112a80 80 0 1 1-80-80a80 80 0 0 1 80 80' opacity='.2'/%3E%3Cpath d='m229.66 218.34l-50.06-50.06a88.21 88.21 0 1 0-11.32 11.31l50.06 50.07a8 8 0 0 0 11.32-11.32M40 112a72 72 0 1 1 72 72a72.08 72.08 0 0 1-72-72'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-moon-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M227.89 147.89A96 96 0 1 1 108.11 28.11a96.09 96.09 0 0 0 119.78 119.78' opacity='.2'/%3E%3Cpath d='M233.54 142.23a8 8 0 0 0-8-2a88.08 88.08 0 0 1-109.8-109.8a8 8 0 0 0-10-10a104.84 104.84 0 0 0-52.91 37A104 104 0 0 0 136 224a103.1 103.1 0 0 0 62.52-20.88a104.84 104.84 0 0 0 37-52.91a8 8 0 0 0-1.98-7.98m-44.64 48.11A88 88 0 0 1 65.66 67.11a89 89 0 0 1 31.4-26A106 106 0 0 0 96 56a104.11 104.11 0 0 0 104 104a106 106 0 0 0 14.92-1.06a89 89 0 0 1-26.02 31.4'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-paint-brush-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M224 32c0 32.81-31.64 67.43-58.64 91.05A84.4 84.4 0 0 0 133 90.64c23.57-27 58.19-58.64 91-58.64' opacity='.2'/%3E%3Cpath d='M232 32a8 8 0 0 0-8-8c-44.08 0-89.31 49.71-114.43 82.63A60 60 0 0 0 32 164c0 30.88-19.54 44.73-20.47 45.37A8 8 0 0 0 16 224h76a60 60 0 0 0 57.37-77.57C182.3 121.31 232 76.08 232 32M92 208H34.63C41.38 198.41 48 183.92 48 164a44 44 0 1 1 44 44m32.42-94.45q5.14-6.66 10.09-12.55A76.2 76.2 0 0 1 155 121.49q-5.9 4.94-12.55 10.09a60.5 60.5 0 0 0-18.03-18.03m42.7-2.68a92.6 92.6 0 0 0-22-22c31.78-34.53 55.75-45 69.9-47.91c-2.85 14.16-13.37 38.13-47.9 69.91'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-push-pin{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='m235.32 81.37l-60.69-60.68a16 16 0 0 0-22.63 0l-53.63 53.8c-10.66-3.34-35-7.37-60.4 13.14a16 16 0 0 0-1.29 23.78L85 159.71l-42.66 42.63a8 8 0 0 0 11.32 11.32L96.29 171l48.29 48.29A16 16 0 0 0 155.9 224h1.13a15.93 15.93 0 0 0 11.64-6.33c19.64-26.1 17.75-47.32 13.19-60L235.33 104a16 16 0 0 0-.01-22.63M224 92.69l-57.27 57.46a8 8 0 0 0-1.49 9.22c9.46 18.93-1.8 38.59-9.34 48.62L48 100.08c12.08-9.74 23.64-12.31 32.48-12.31A40.1 40.1 0 0 1 96.81 91a8 8 0 0 0 9.25-1.51L163.32 32L224 92.68Z'/%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-push-pin-fill{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='m235.33 104l-53.47 53.65c4.56 12.67 6.45 33.89-13.19 60A15.93 15.93 0 0 1 157 224h-1.13a16 16 0 0 1-11.32-4.69L96.29 171l-42.63 42.66a8 8 0 0 1-11.32-11.32L85 159.71l-48.3-48.3A16 16 0 0 1 38 87.63c25.42-20.51 49.75-16.48 60.4-13.14L152 20.7a16 16 0 0 1 22.63 0l60.69 60.68a16 16 0 0 1 .01 22.62'/%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-rocket-launch-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M184 120v61.65a8 8 0 0 1-2.34 5.65l-34.35 34.35a8 8 0 0 1-13.57-4.53L128 176Zm-48-48H74.35a8 8 0 0 0-5.65 2.34l-34.35 34.35a8 8 0 0 0 4.53 13.57L80 128ZM40 216c37.65 0 50.69-19.69 54.56-28.18l-26.38-26.38C59.69 165.31 40 178.35 40 216' opacity='.2'/%3E%3Cpath d='M223.85 47.12a16 16 0 0 0-15-15c-12.58-.75-44.73.4-71.41 27.07L132.69 64H74.36A15.9 15.9 0 0 0 63 68.68L28.7 103a16 16 0 0 0 9.07 27.16l38.47 5.37l44.21 44.21l5.37 38.49a15.94 15.94 0 0 0 10.78 12.92a16.1 16.1 0 0 0 5.1.83a15.9 15.9 0 0 0 11.3-4.68l34.32-34.3a15.9 15.9 0 0 0 4.68-11.36v-58.33l4.77-4.77c26.68-26.68 27.83-58.83 27.08-71.42M74.36 80h42.33l-39.53 39.52L40 114.34Zm74.41-9.45a76.65 76.65 0 0 1 59.11-22.47a76.46 76.46 0 0 1-22.42 59.16L128 164.68L91.32 128ZM176 181.64L141.67 216l-5.19-37.17L176 139.31Zm-74.16 9.5C97.34 201 82.29 224 40 224a8 8 0 0 1-8-8c0-42.29 23-57.34 32.86-61.85a8 8 0 0 1 6.64 14.56c-6.43 2.93-20.62 12.36-23.12 38.91c26.55-2.5 36-16.69 38.91-23.12a8 8 0 1 1 14.56 6.64Z'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-shield-check-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M216 56v56c0 96-88 120-88 120s-88-24-88-120V56a8 8 0 0 1 8-8h160a8 8 0 0 1 8 8' opacity='.2'/%3E%3Cpath d='M208 40H48a16 16 0 0 0-16 16v56c0 52.72 25.52 84.67 46.93 102.19c23.06 18.86 46 25.26 47 25.53a8 8 0 0 0 4.2 0c1-.27 23.91-6.67 47-25.53C198.48 196.67 224 164.72 224 112V56a16 16 0 0 0-16-16m0 72c0 37.07-13.66 67.16-40.6 89.42a129.3 129.3 0 0 1-39.4 22.2a128.3 128.3 0 0 1-38.92-21.81C61.82 179.51 48 149.3 48 112V56h160ZM82.34 141.66a8 8 0 0 1 11.32-11.32L112 148.69l50.34-50.35a8 8 0 0 1 11.32 11.32l-56 56a8 8 0 0 1-11.32 0Z'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-sign-out-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M224 56v144a16 16 0 0 1-16 16H48V40h160a16 16 0 0 1 16 16' opacity='.2'/%3E%3Cpath d='M120 216a8 8 0 0 1-8 8H48a8 8 0 0 1-8-8V40a8 8 0 0 1 8-8h64a8 8 0 0 1 0 16H56v160h56a8 8 0 0 1 8 8m109.66-93.66l-40-40a8 8 0 0 0-11.32 11.32L204.69 120H112a8 8 0 0 0 0 16h92.69l-26.35 26.34a8 8 0 0 0 11.32 11.32l40-40a8 8 0 0 0 0-11.32'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-spinner-gap-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96' opacity='.2'/%3E%3Cpath d='M136 32v32a8 8 0 0 1-16 0V32a8 8 0 0 1 16 0m88 88h-32a8 8 0 0 0 0 16h32a8 8 0 0 0 0-16m-45.09 47.6a8 8 0 0 0-11.31 11.31l22.62 22.63a8 8 0 0 0 11.32-11.32ZM128 184a8 8 0 0 0-8 8v32a8 8 0 0 0 16 0v-32a8 8 0 0 0-8-8m-50.91-16.4l-22.63 22.62a8 8 0 0 0 11.32 11.32l22.62-22.63a8 8 0 0 0-11.31-11.31M72 128a8 8 0 0 0-8-8H32a8 8 0 0 0 0 16h32a8 8 0 0 0 8-8m-6.22-73.54a8 8 0 0 0-11.32 11.32L77.09 88.4A8 8 0 0 0 88.4 77.09Z'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-square-half-bottom-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M208 128v72a8 8 0 0 1-8 8H56a8 8 0 0 1-8-8v-72Z' opacity='.2'/%3E%3Cpath d='M200 40H56a16 16 0 0 0-16 16v144a16 16 0 0 0 16 16h144a16 16 0 0 0 16-16V56a16 16 0 0 0-16-16m0 16v64H56V56Zm0 144H56v-64h144z'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-sun-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M184 128a56 56 0 1 1-56-56a56 56 0 0 1 56 56' opacity='.2'/%3E%3Cpath d='M120 40V16a8 8 0 0 1 16 0v24a8 8 0 0 1-16 0m72 88a64 64 0 1 1-64-64a64.07 64.07 0 0 1 64 64m-16 0a48 48 0 1 0-48 48a48.05 48.05 0 0 0 48-48M58.34 69.66a8 8 0 0 0 11.32-11.32l-16-16a8 8 0 0 0-11.32 11.32Zm0 116.68l-16 16a8 8 0 0 0 11.32 11.32l16-16a8 8 0 0 0-11.32-11.32M192 72a8 8 0 0 0 5.66-2.34l16-16a8 8 0 0 0-11.32-11.32l-16 16A8 8 0 0 0 192 72m5.66 114.34a8 8 0 0 0-11.32 11.32l16 16a8 8 0 0 0 11.32-11.32ZM48 128a8 8 0 0 0-8-8H16a8 8 0 0 0 0 16h24a8 8 0 0 0 8-8m80 80a8 8 0 0 0-8 8v24a8 8 0 0 0 16 0v-24a8 8 0 0 0-8-8m112-88h-24a8 8 0 0 0 0 16h24a8 8 0 0 0 0-16'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-warning-duotone,\n.i-ph\\:warning-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M215.46 216H40.54c-12.62 0-20.54-13.21-14.41-23.91l87.46-151.87c6.3-11 22.52-11 28.82 0l87.46 151.87c6.13 10.7-1.79 23.91-14.41 23.91' opacity='.2'/%3E%3Cpath d='M236.8 188.09L149.35 36.22a24.76 24.76 0 0 0-42.7 0L19.2 188.09a23.51 23.51 0 0 0 0 23.72A24.35 24.35 0 0 0 40.55 224h174.9a24.35 24.35 0 0 0 21.33-12.19a23.51 23.51 0 0 0 .02-23.72m-13.87 15.71a8.5 8.5 0 0 1-7.48 4.2H40.55a8.5 8.5 0 0 1-7.48-4.2a7.59 7.59 0 0 1 0-7.72l87.45-151.87a8.75 8.75 0 0 1 15 0l87.45 151.87a7.59 7.59 0 0 1-.04 7.72M120 144v-40a8 8 0 0 1 16 0v40a8 8 0 0 1-16 0m20 36a12 12 0 1 1-12-12a12 12 0 0 1 12 12'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-warning-fill{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M236.8 188.09L149.35 36.22a24.76 24.76 0 0 0-42.7 0L19.2 188.09a23.51 23.51 0 0 0 0 23.72A24.35 24.35 0 0 0 40.55 224h174.9a24.35 24.35 0 0 0 21.33-12.19a23.51 23.51 0 0 0 .02-23.72M120 104a8 8 0 0 1 16 0v40a8 8 0 0 1-16 0Zm8 88a12 12 0 1 1 12-12a12 12 0 0 1-12 12'/%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-wrench-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M224 96a64 64 0 0 1-94.94 56L73 217a24 24 0 0 1-34-34l65-56.06a64 64 0 0 1 80-90.29L144 80l5.66 26.34L176 112l43.35-40A63.8 63.8 0 0 1 224 96' opacity='.2'/%3E%3Cpath d='M226.76 69a8 8 0 0 0-12.84-2.88l-40.3 37.19l-17.23-3.7l-3.7-17.23l37.19-40.3A8 8 0 0 0 187 29.24A72 72 0 0 0 88 96a72.3 72.3 0 0 0 6 28.94L33.79 177c-.15.12-.29.26-.43.39a32 32 0 0 0 45.26 45.26c.13-.13.27-.28.39-.42L131.06 162A72 72 0 0 0 232 96a71.6 71.6 0 0 0-5.24-27M160 152a56.14 56.14 0 0 1-27.07-7a8 8 0 0 0-9.92 1.77l-55.9 64.74a16 16 0 0 1-22.62-22.62L109.18 133a8 8 0 0 0 1.77-9.93a56 56 0 0 1 58.36-82.31l-31.2 33.81a8 8 0 0 0-1.94 7.1l5.66 26.33a8 8 0 0 0 6.14 6.14l26.35 5.66a8 8 0 0 0 7.1-1.94l33.81-31.2A56.06 56.06 0 0 1 160 152'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-x{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M205.66 194.34a8 8 0 0 1-11.32 11.32L128 139.31l-66.34 66.35a8 8 0 0 1-11.32-11.32L116.69 128L50.34 61.66a8 8 0 0 1 11.32-11.32L128 116.69l66.34-66.35a8 8 0 0 1 11.32 11.32L139.31 128Z'/%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph\\:bug-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M208 128v16a80 80 0 0 1-160 0v-16Z' opacity='.2'/%3E%3Cpath d='M144 92a12 12 0 1 1 12 12a12 12 0 0 1-12-12m-44-12a12 12 0 1 0 12 12a12 12 0 0 0-12-12m116 64a87.8 87.8 0 0 1-3 23l22.24 9.72A8 8 0 0 1 232 192a7.9 7.9 0 0 1-3.2-.67L207.38 182a88 88 0 0 1-158.76 0l-21.42 9.33a7.9 7.9 0 0 1-3.2.67a8 8 0 0 1-3.2-15.33L43 167a87.8 87.8 0 0 1-3-23v-8H16a8 8 0 0 1 0-16h24v-8a87.8 87.8 0 0 1 3-23l-22.2-9.67a8 8 0 1 1 6.4-14.66L48.62 74a88 88 0 0 1 158.76 0l21.42-9.36a8 8 0 0 1 6.4 14.66L213 89.05a87.8 87.8 0 0 1 3 23v8h24a8 8 0 0 1 0 16h-24ZM56 120h144v-8a72 72 0 0 0-144 0Zm64 95.54V136H56v8a72.08 72.08 0 0 0 64 71.54M200 144v-8h-64v79.54A72.08 72.08 0 0 0 200 144'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph\\:check{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='m229.66 77.66l-128 128a8 8 0 0 1-11.32 0l-56-56a8 8 0 0 1 11.32-11.32L96 188.69L218.34 66.34a8 8 0 0 1 11.32 11.32'/%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph\\:check-circle-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96' opacity='.2'/%3E%3Cpath d='M173.66 98.34a8 8 0 0 1 0 11.32l-56 56a8 8 0 0 1-11.32 0l-24-24a8 8 0 0 1 11.32-11.32L112 148.69l50.34-50.35a8 8 0 0 1 11.32 0M232 128A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph\\:info-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96' opacity='.2'/%3E%3Cpath d='M144 176a8 8 0 0 1-8 8a16 16 0 0 1-16-16v-40a8 8 0 0 1 0-16a16 16 0 0 1 16 16v40a8 8 0 0 1 8 8m88-48A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88m-92-32a12 12 0 1 0-12-12a12 12 0 0 0 12 12'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph\\:plug-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='m212 132l-58.63 58.63a32 32 0 0 1-45.25 0l-42.75-42.75a32 32 0 0 1 0-45.25L124 44Z' opacity='.2'/%3E%3Cpath d='M237.66 66.34a8 8 0 0 0-11.32 0L192 100.69L155.31 64l34.35-34.34a8 8 0 1 0-11.32-11.32L144 52.69l-26.34-26.35a8 8 0 0 0-11.32 11.32l6.35 6.34l-53 53a40 40 0 0 0 0 56.57l15.71 15.71l-49.06 49.06a8 8 0 0 0 11.32 11.32l49.09-49.09l15.71 15.71a40 40 0 0 0 56.57 0l53-53l6.34 6.35a8 8 0 0 0 11.32-11.32L203.31 112l34.35-34.34a8 8 0 0 0 0-11.32M147.72 185a24 24 0 0 1-33.95 0L71 142.23a24 24 0 0 1 0-33.95l53-53L200.69 132Z'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph\\:x-circle-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96' opacity='.2'/%3E%3Cpath d='M165.66 101.66L139.31 128l26.35 26.34a8 8 0 0 1-11.32 11.32L128 139.31l-26.34 26.35a8 8 0 0 1-11.32-11.32L116.69 128l-26.35-26.34a8 8 0 0 1 11.32-11.32L128 116.69l26.34-26.35a8 8 0 0 1 11.32 11.32M232 128A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n/* layer: shortcuts */\n.badge{font-size:var(--text-xs-fontSize);line-height:var(--un-leading, var(--text-xs-lineHeight));--un-leading:var(--leading-none);line-height:var(--leading-none);--un-font-weight:var(--fontWeight-medium);font-weight:var(--fontWeight-medium);padding-inline:calc(var(--spacing) * 1.5);padding-block:calc(var(--spacing) * 0.5);border-radius:var(--radius-md);display:inline-flex;gap:calc(var(--spacing) * 1);align-items:center;}\n.color-base{color:color-mix(in srgb, var(--colors-neutral-800) var(--un-text-opacity), transparent);}\n.dark .color-base{color:color-mix(in srgb, var(--colors-neutral-200) var(--un-text-opacity), transparent);}\n.border-base{border-color:color-mix(in oklab, #8882 var(--un-border-opacity), transparent);}\n.hover\\:border-base:hover{border-color:color-mix(in oklab, #8882 var(--un-border-opacity), transparent);}\n.bg-active{background-color:color-mix(in srgb, var(--colors-primary-DEFAULT) 10%, transparent);}\n.dark .bg-active{background-color:color-mix(in srgb, var(--colors-primary-DEFAULT) 15%, transparent);}\n.bg-base{background-color:color-mix(in srgb, var(--colors-white) var(--un-bg-opacity), transparent);}\n.dark .bg-base{background-color:color-mix(in oklab, #111 var(--un-bg-opacity), transparent);}\n.bg-glass\\:80{background-color:color-mix(in srgb, var(--colors-white) 80%, transparent);--un-backdrop-blur:blur(7px);-webkit-backdrop-filter:var(--un-backdrop-blur,) var(--un-backdrop-brightness,) var(--un-backdrop-contrast,) var(--un-backdrop-grayscale,) var(--un-backdrop-hue-rotate,) var(--un-backdrop-invert,) var(--un-backdrop-opacity,) var(--un-backdrop-saturate,) var(--un-backdrop-sepia,);backdrop-filter:var(--un-backdrop-blur,) var(--un-backdrop-brightness,) var(--un-backdrop-contrast,) var(--un-backdrop-grayscale,) var(--un-backdrop-hue-rotate,) var(--un-backdrop-invert,) var(--un-backdrop-opacity,) var(--un-backdrop-saturate,) var(--un-backdrop-sepia,);}\n.dark .bg-glass\\:80{background-color:color-mix(in oklab, #111 80%, transparent);}\n.bg-secondary{background-color:color-mix(in oklab, #f6f6f6 var(--un-bg-opacity), transparent);}\n.dark .bg-secondary{background-color:color-mix(in oklab, #101010 var(--un-bg-opacity), transparent);}\n.dark .hover\\:bg-active:hover{background-color:color-mix(in srgb, var(--colors-primary-DEFAULT) 15%, transparent);}\n.hover\\:bg-active:hover{background-color:color-mix(in srgb, var(--colors-primary-DEFAULT) 10%, transparent);}\n.dark .op-fade{opacity:55%;}\n.op-fade{opacity:65%;}\n.dark .op-mute{opacity:25%;}\n.op-mute{opacity:30%;}\n.container{width:100%;}\n.z-command-palette{z-index:2147483646;}\n.z-dock-confirm,\n.z-dock-toast{z-index:2147483647;}\n.z-floating-anchor{z-index:2147483644;}\n.z-floating-tooltip{z-index:2147483645;}\n@supports (color: color-mix(in lab, red, red)){\n.color-base{color:color-mix(in oklab, var(--colors-neutral-800) var(--un-text-opacity), transparent);}\n.dark .color-base{color:color-mix(in oklab, var(--colors-neutral-200) var(--un-text-opacity), transparent);}\n.bg-active{background-color:color-mix(in oklab, var(--colors-primary-DEFAULT) 10%, transparent);}\n.dark .bg-active{background-color:color-mix(in oklab, var(--colors-primary-DEFAULT) 15%, transparent);}\n.bg-base{background-color:color-mix(in oklab, var(--colors-white) var(--un-bg-opacity), transparent);}\n.bg-glass\\:80{background-color:color-mix(in oklab, var(--colors-white) 80%, transparent);}\n.dark .hover\\:bg-active:hover{background-color:color-mix(in oklab, var(--colors-primary-DEFAULT) 15%, transparent);}\n.hover\\:bg-active:hover{background-color:color-mix(in oklab, var(--colors-primary-DEFAULT) 10%, transparent);}\n}\n@media (min-width: 40rem){\n.container{max-width:40rem;}\n}\n@media (min-width: 48rem){\n.container{max-width:48rem;}\n}\n@media (min-width: 64rem){\n.container{max-width:64rem;}\n}\n@media (min-width: 80rem){\n.container{max-width:80rem;}\n}\n@media (min-width: 96rem){\n.container{max-width:96rem;}\n}\n/* layer: default */\n@property --scroll-fade-t{syntax:\"\";inherits:false;initial-value:0px}@property --scroll-fade-b{syntax:\"\";inherits:false;initial-value:0px}@property --scroll-fade-s{syntax:\"\";inherits:false;initial-value:0px}@property --scroll-fade-e{syntax:\"\";inherits:false;initial-value:0px}@property --scroll-fade-mask{syntax:\"*\";inherits:false}\n@property --shimmer-angle{syntax:\"\";inherits:true;initial-value:20deg}@property --shimmer-image{syntax:\"*\";inherits:false}@property --shimmer-text-fill{syntax:\"*\";inherits:false}\n.text-\\[10px\\]{font-size:10px;}\n.text-\\[15px\\]{font-size:15px;}\n.text-0\\.6em{font-size:0.6em;}\n.text-2\\.75{font-size:0.6875rem;}\n.text-2xl{font-size:var(--text-2xl-fontSize);line-height:var(--un-leading, var(--text-2xl-lineHeight));}\n.text-base{font-size:var(--text-base-fontSize);line-height:var(--un-leading, var(--text-base-lineHeight));}\n.text-sm{font-size:var(--text-sm-fontSize);line-height:var(--un-leading, var(--text-sm-lineHeight));}\n.text-xl{font-size:var(--text-xl-fontSize);line-height:var(--un-leading, var(--text-xl-lineHeight));}\n.text-xs{font-size:var(--text-xs-fontSize);line-height:var(--un-leading, var(--text-xs-lineHeight));}\n.dark .dark\\:text-primary-300{color:color-mix(in srgb, var(--colors-primary-300) var(--un-text-opacity), transparent);}\n.dark .dark\\:text-red-400{color:color-mix(in srgb, var(--colors-red-400) var(--un-text-opacity), transparent);}\n.text-amber{color:color-mix(in srgb, var(--colors-amber-DEFAULT) var(--un-text-opacity), transparent);}\n.text-blue{color:color-mix(in srgb, var(--colors-blue-DEFAULT) var(--un-text-opacity), transparent);}\n.text-gray{color:color-mix(in srgb, var(--colors-gray-DEFAULT) var(--un-text-opacity), transparent);}\n.text-green{color:color-mix(in srgb, var(--colors-green-DEFAULT) var(--un-text-opacity), transparent);}\n.text-orange{color:color-mix(in srgb, var(--colors-orange-DEFAULT) var(--un-text-opacity), transparent);}\n.text-primary{color:color-mix(in srgb, var(--colors-primary-DEFAULT) var(--un-text-opacity), transparent);}\n.text-primary-600{color:color-mix(in srgb, var(--colors-primary-600) var(--un-text-opacity), transparent);}\n.text-primary-700{color:color-mix(in srgb, var(--colors-primary-700) var(--un-text-opacity), transparent);}\n.text-red{color:color-mix(in srgb, var(--colors-red-DEFAULT) var(--un-text-opacity), transparent);}\n.text-red-500{color:color-mix(in srgb, var(--colors-red-500) var(--un-text-opacity), transparent);}\n.text-red-600{color:color-mix(in srgb, var(--colors-red-600) var(--un-text-opacity), transparent);}\n.text-white{color:color-mix(in srgb, var(--colors-white) var(--un-text-opacity), transparent);}\n.leading-4{--un-leading:calc(var(--spacing) * 4);line-height:calc(var(--spacing) * 4);}\n.leading-5{--un-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5);}\n.leading-none{--un-leading:var(--leading-none);line-height:var(--leading-none);}\n.leading-relaxed{--un-leading:var(--leading-relaxed);line-height:var(--leading-relaxed);}\n.tracking-tight{--un-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight);}\n.tracking-wide{--un-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide);}\n.font-bold{--un-font-weight:var(--fontWeight-bold);font-weight:var(--fontWeight-bold);}\n.font-medium{--un-font-weight:var(--fontWeight-medium);font-weight:var(--fontWeight-medium);}\n.font-mono{font-family:var(--font-mono);}\n.font-sans{font-family:var(--font-sans);}\n.tab{-moz-tab-size:4;-o-tab-size:4;tab-size:4;}\n.m-auto,\n.ma{margin:auto;}\n.m1{margin:calc(var(--spacing) * 1);}\n.mx--1{margin-inline:calc(var(--spacing) * -1);}\n.mx--2{margin-inline:calc(var(--spacing) * -2);}\n.mx-auto{margin-inline:auto;}\n.my-2{margin-block:calc(var(--spacing) * 2);}\n.my0\\.5{margin-block:calc(var(--spacing) * 0.5);}\n.my1{margin-block:calc(var(--spacing) * 1);}\n.mb-1\\.5{margin-bottom:calc(var(--spacing) * 1.5);}\n.mb-3{margin-bottom:calc(var(--spacing) * 3);}\n.mb-4{margin-bottom:calc(var(--spacing) * 4);}\n.ml-2{margin-left:calc(var(--spacing) * 2);}\n.ml-6{margin-left:calc(var(--spacing) * 6);}\n.mr-1{margin-right:calc(var(--spacing) * 1);}\n.mt-0\\.5{margin-top:calc(var(--spacing) * 0.5);}\n.mt-1\\.5{margin-top:calc(var(--spacing) * 1.5);}\n.mt-6,\n.mt6{margin-top:calc(var(--spacing) * 6);}\n.mt-auto{margin-top:auto;}\n.mt2{margin-top:calc(var(--spacing) * 2);}\n.mt4{margin-top:calc(var(--spacing) * 4);}\n.p-0\\.5{padding:calc(var(--spacing) * 0.5);}\n.p-1\\.5,\n.p1\\.5{padding:calc(var(--spacing) * 1.5);}\n.p-4{padding:calc(var(--spacing) * 4);}\n.p-5{padding:calc(var(--spacing) * 5);}\n.p1{padding:calc(var(--spacing) * 1);}\n.p2{padding:calc(var(--spacing) * 2);}\n.p6{padding:calc(var(--spacing) * 6);}\n.p8{padding:calc(var(--spacing) * 8);}\n.px,\n.px-4,\n.px4{padding-inline:calc(var(--spacing) * 4);}\n.px-0\\.5{padding-inline:calc(var(--spacing) * 0.5);}\n.px-1,\n.px1{padding-inline:calc(var(--spacing) * 1);}\n.px-1\\.5{padding-inline:calc(var(--spacing) * 1.5);}\n.px-2,\n.px2{padding-inline:calc(var(--spacing) * 2);}\n.px-2\\.5{padding-inline:calc(var(--spacing) * 2.5);}\n.px-3,\n.px3{padding-inline:calc(var(--spacing) * 3);}\n.px-6{padding-inline:calc(var(--spacing) * 6);}\n.py-0{padding-block:calc(var(--spacing) * 0);}\n.py-0\\.5{padding-block:calc(var(--spacing) * 0.5);}\n.py-1,\n.py1{padding-block:calc(var(--spacing) * 1);}\n.py-1\\.5,\n.py1\\.5{padding-block:calc(var(--spacing) * 1.5);}\n.py-2,\n.py2{padding-block:calc(var(--spacing) * 2);}\n.py-2\\.5,\n.py2\\.5{padding-block:calc(var(--spacing) * 2.5);}\n.py-3{padding-block:calc(var(--spacing) * 3);}\n.py-6{padding-block:calc(var(--spacing) * 6);}\n.py-8{padding-block:calc(var(--spacing) * 8);}\n.pb2\\.5{padding-bottom:calc(var(--spacing) * 2.5);}\n.pt-\\[20vh\\]{padding-top:20vh;}\n.pt-6{padding-top:calc(var(--spacing) * 6);}\n.pt2{padding-top:calc(var(--spacing) * 2);}\n.text-center{text-align:center;}\n.text-left{text-align:left;}\n.outline{outline-style:var(--un-outline-style);outline-width:1px;}\n.outline-none{--un-outline-style:none;outline-style:none;}\n.caret-primary-500{caret-color:color-mix(in srgb, var(--colors-primary-500) var(--un-caret-opacity), transparent);}\n.border{border-width:1px;}\n.border-2{border-width:2px;}\n.border-b{border-bottom-width:1px;}\n.border-b-0{border-bottom-width:0px;}\n.border-b-1\\.5{border-bottom-width:1.5px;}\n.border-l{border-left-width:1px;}\n.border-r{border-right-width:1px;}\n.border-r-1\\.5{border-right-width:1.5px;}\n.border-t{border-top-width:1px;}\n.border-t-0{border-top-width:0px;}\n.last\\:border-b-0:last-child{border-bottom-width:0px;}\n.border-amber\\/20{border-color:color-mix(in srgb, var(--colors-amber-DEFAULT) 20%, transparent);}\n.border-current{border-color:currentColor;}\n.border-primary-400\\/60{border-color:color-mix(in srgb, var(--colors-primary-400) 60%, transparent);}\n.border-primary\\/30{border-color:color-mix(in srgb, var(--colors-primary-DEFAULT) 30%, transparent);}\n.border-red-500\\/70{border-color:color-mix(in srgb, var(--colors-red-500) 70%, transparent);}\n.border-transparent{border-color:transparent;}\n.dark .dark\\:border-primary-400\\/50{border-color:color-mix(in srgb, var(--colors-primary-400) 50%, transparent);}\n.focus-within\\:border-gray\\/15:focus-within{border-color:color-mix(in srgb, var(--colors-gray-DEFAULT) 15%, transparent);}\n.hover\\:border-gray\\/10:hover{border-color:color-mix(in srgb, var(--colors-gray-DEFAULT) 10%, transparent);}\n.focus\\:border-primary-500:focus{border-color:color-mix(in srgb, var(--colors-primary-500) var(--un-border-opacity), transparent);}\n.focus\\:border-primary\\/40:focus{border-color:color-mix(in srgb, var(--colors-primary-DEFAULT) 40%, transparent);}\n.border-t-transparent{border-top-color:transparent;}\n.rounded{border-radius:var(--radius-DEFAULT);}\n.rounded-full{border-radius:calc(infinity * 1px);}\n.rounded-lg{border-radius:var(--radius-lg);}\n.rounded-md{border-radius:var(--radius-md);}\n.rounded-xl{border-radius:var(--radius-xl);}\n.rounded-r{border-top-right-radius:var(--radius-DEFAULT);border-bottom-right-radius:var(--radius-DEFAULT);}\n.rounded-t-md{border-top-left-radius:var(--radius-md);border-top-right-radius:var(--radius-md);}\n.rounded-tr-md{border-top-right-radius:var(--radius-md);}\n.bg-\\[\\#8881\\]{background-color:color-mix(in oklab, #8881 var(--un-bg-opacity), transparent);}\n.bg-amber{background-color:color-mix(in srgb, var(--colors-amber-DEFAULT) var(--un-bg-opacity), transparent);}\n.bg-amber\\/10{background-color:color-mix(in srgb, var(--colors-amber-DEFAULT) 10%, transparent);}\n.bg-black\\/30,\n.dark .dark\\:bg-black\\/30{background-color:color-mix(in srgb, var(--colors-black) 30%, transparent);}\n.bg-blue{background-color:color-mix(in srgb, var(--colors-blue-DEFAULT) var(--un-bg-opacity), transparent);}\n.bg-blue\\/10{background-color:color-mix(in srgb, var(--colors-blue-DEFAULT) 10%, transparent);}\n.bg-gray{background-color:color-mix(in srgb, var(--colors-gray-DEFAULT) var(--un-bg-opacity), transparent);}\n.bg-gray\\/10{background-color:color-mix(in srgb, var(--colors-gray-DEFAULT) 10%, transparent);}\n.bg-gray\\/20{background-color:color-mix(in srgb, var(--colors-gray-DEFAULT) 20%, transparent);}\n.bg-gray\\/3{background-color:color-mix(in srgb, var(--colors-gray-DEFAULT) 3%, transparent);}\n.bg-gray\\/30{background-color:color-mix(in srgb, var(--colors-gray-DEFAULT) 30%, transparent);}\n.bg-gray\\/5{background-color:color-mix(in srgb, var(--colors-gray-DEFAULT) 5%, transparent);}\n.bg-green{background-color:color-mix(in srgb, var(--colors-green-DEFAULT) var(--un-bg-opacity), transparent);}\n.bg-orange\\/10{background-color:color-mix(in srgb, var(--colors-orange-DEFAULT) 10%, transparent);}\n.bg-primary{background-color:color-mix(in srgb, var(--colors-primary-DEFAULT) var(--un-bg-opacity), transparent);}\n.bg-primary-500\\/20{background-color:color-mix(in srgb, var(--colors-primary-500) 20%, transparent);}\n.bg-primary-600{background-color:color-mix(in srgb, var(--colors-primary-600) var(--un-bg-opacity), transparent);}\n.bg-primary\\/10{background-color:color-mix(in srgb, var(--colors-primary-DEFAULT) 10%, transparent);}\n.bg-primary\\/15{background-color:color-mix(in srgb, var(--colors-primary-DEFAULT) 15%, transparent);}\n.bg-primary\\/20{background-color:color-mix(in srgb, var(--colors-primary-DEFAULT) 20%, transparent);}\n.bg-red{background-color:color-mix(in srgb, var(--colors-red-DEFAULT) var(--un-bg-opacity), transparent);}\n.bg-red-500\\/12{background-color:color-mix(in srgb, var(--colors-red-500) 12%, transparent);}\n.bg-red\\/10{background-color:color-mix(in srgb, var(--colors-red-DEFAULT) 10%, transparent);}\n.bg-transparent{background-color:transparent;}\n.bg-white{background-color:color-mix(in srgb, var(--colors-white) var(--un-bg-opacity), transparent);}\n.bg-white\\/50{background-color:color-mix(in srgb, var(--colors-white) 50%, transparent);}\n.dark .dark\\:bg-black\\/45{background-color:color-mix(in srgb, var(--colors-black) 45%, transparent);}\n.hover\\:bg-\\[\\#8881\\]:hover{background-color:color-mix(in oklab, #8881 var(--un-bg-opacity), transparent);}\n.hover\\:bg-\\[\\#8882\\]:hover{background-color:color-mix(in oklab, #8882 var(--un-bg-opacity), transparent);}\n.hover\\:bg-\\[\\#8883\\]:hover{background-color:color-mix(in oklab, #8883 var(--un-bg-opacity), transparent);}\n.hover\\:bg-gray\\/10:hover{background-color:color-mix(in srgb, var(--colors-gray-DEFAULT) 10%, transparent);}\n.hover\\:bg-gray\\/15:hover{background-color:color-mix(in srgb, var(--colors-gray-DEFAULT) 15%, transparent);}\n.hover\\:bg-gray\\/20:hover{background-color:color-mix(in srgb, var(--colors-gray-DEFAULT) 20%, transparent);}\n.hover\\:bg-gray\\/5:hover{background-color:color-mix(in srgb, var(--colors-gray-DEFAULT) 5%, transparent);}\n.hover\\:bg-orange\\/20:hover{background-color:color-mix(in srgb, var(--colors-orange-DEFAULT) 20%, transparent);}\n.hover\\:bg-primary-700:hover{background-color:color-mix(in srgb, var(--colors-primary-700) var(--un-bg-opacity), transparent);}\n.hover\\:bg-primary\\/20:hover{background-color:color-mix(in srgb, var(--colors-primary-DEFAULT) 20%, transparent);}\n.hover\\:bg-primary\\/25:hover{background-color:color-mix(in srgb, var(--colors-primary-DEFAULT) 25%, transparent);}\n.hover\\:bg-red-500\\/20:hover{background-color:color-mix(in srgb, var(--colors-red-500) 20%, transparent);}\n.hover\\:bg-red\\/20:hover{background-color:color-mix(in srgb, var(--colors-red-DEFAULT) 20%, transparent);}\n.op0,\n.opacity-0{opacity:0%;}\n.op100,\n.opacity-100,\n.group:hover .group-hover\\:opacity-100{opacity:100%;}\n.op25{opacity:25%;}\n.op30{opacity:30%;}\n.op40{opacity:40%;}\n.op50,\n.group:hover .group-hover\\:op50{opacity:50%;}\n.op60{opacity:60%;}\n.op60\\!{opacity:60% !important;}\n.op70{opacity:70%;}\n.op75{opacity:75%;}\n.op80{opacity:80%;}\n.hover\\:op100:hover{opacity:100%;}\n.hover\\:op60:hover{opacity:60%;}\n.hover\\:op70:hover{opacity:70%;}\n.hover\\:op80:hover{opacity:80%;}\n.disabled\\:op40:disabled{opacity:40%;}\n.disabled\\:op50:disabled{opacity:50%;}\n.line-through{text-decoration-line:line-through;}\n.flex{display:flex;}\n.inline-flex{display:inline-flex;}\n.flex-1{flex:1 1 0%;}\n.flex-auto{flex:1 1 auto;}\n.flex-none{flex:none;}\n.shrink-0{flex-shrink:0;}\n.flex-row{flex-direction:row;}\n.flex-row-reverse{flex-direction:row-reverse;}\n.flex-col{flex-direction:column;}\n.flex-col-reverse{flex-direction:column-reverse;}\n.flex-wrap{flex-wrap:wrap;}\n.gap-0{gap:calc(var(--spacing) * 0);}\n.gap-0\\.5{gap:calc(var(--spacing) * 0.5);}\n.gap-1{gap:calc(var(--spacing) * 1);}\n.gap-1\\.5{gap:calc(var(--spacing) * 1.5);}\n.gap-2{gap:calc(var(--spacing) * 2);}\n.gap-3{gap:calc(var(--spacing) * 3);}\n.gap-4{gap:calc(var(--spacing) * 4);}\n.gap-6{gap:calc(var(--spacing) * 6);}\n.gap-x-2{column-gap:calc(var(--spacing) * 2);}\n.gap-y-0\\.5{row-gap:calc(var(--spacing) * 0.5);}\n.grid{display:grid;}\n.cols-\\[max-content_1fr\\]{grid-template-columns:max-content 1fr;}\n.h-0\\.5{height:calc(var(--spacing) * 0.5);}\n.h-1\\.5{height:calc(var(--spacing) * 1.5);}\n.h-10{height:calc(var(--spacing) * 10);}\n.h-14{height:calc(var(--spacing) * 14);}\n.h-16{height:calc(var(--spacing) * 16);}\n.h-20px{height:20px;}\n.h-24{height:calc(var(--spacing) * 24);}\n.h-3{height:calc(var(--spacing) * 3);}\n.h-3\\.5{height:calc(var(--spacing) * 3.5);}\n.h-4{height:calc(var(--spacing) * 4);}\n.h-4\\.5{height:calc(var(--spacing) * 4.5);}\n.h-5{height:calc(var(--spacing) * 5);}\n.h-6{height:calc(var(--spacing) * 6);}\n.h-7{height:calc(var(--spacing) * 7);}\n.h-8{height:calc(var(--spacing) * 8);}\n.h-full{height:100%;}\n.h-px{height:1px;}\n.h-screen{height:100vh;}\n.max-h-\\[60vh\\]{max-height:60vh;}\n.max-w-108{max-width:calc(var(--spacing) * 108);}\n.max-w-200{max-width:calc(var(--spacing) * 200);}\n.max-w-220px{max-width:220px;}\n.max-w-64{max-width:calc(var(--spacing) * 64);}\n.max-w-92{max-width:calc(var(--spacing) * 92);}\n.max-w-96{max-width:calc(var(--spacing) * 96);}\n.max-w-full{max-width:100%;}\n.min-h-0{min-height:calc(var(--spacing) * 0);}\n.min-h-16{min-height:calc(var(--spacing) * 16);}\n.min-h-5{min-height:calc(var(--spacing) * 5);}\n.min-w-0{min-width:calc(var(--spacing) * 0);}\n.min-w-28{min-width:calc(var(--spacing) * 28);}\n.min-w-36{min-width:calc(var(--spacing) * 36);}\n.min-w-40{min-width:calc(var(--spacing) * 40);}\n.min-w-44{min-width:calc(var(--spacing) * 44);}\n.w-\\[40px\\]{width:40px;}\n.w-1\\.5{width:calc(var(--spacing) * 1.5);}\n.w-10{width:calc(var(--spacing) * 10);}\n.w-11{width:calc(var(--spacing) * 11);}\n.w-12{width:calc(var(--spacing) * 12);}\n.w-16{width:calc(var(--spacing) * 16);}\n.w-2\\.5{width:calc(var(--spacing) * 2.5);}\n.w-20px{width:20px;}\n.w-24{width:calc(var(--spacing) * 24);}\n.w-2px{width:2px;}\n.w-3{width:calc(var(--spacing) * 3);}\n.w-3\\.5{width:calc(var(--spacing) * 3.5);}\n.w-4{width:calc(var(--spacing) * 4);}\n.w-4\\.5{width:calc(var(--spacing) * 4.5);}\n.w-5{width:calc(var(--spacing) * 5);}\n.w-6{width:calc(var(--spacing) * 6);}\n.w-64{width:calc(var(--spacing) * 64);}\n.w-7{width:calc(var(--spacing) * 7);}\n.w-72{width:calc(var(--spacing) * 72);}\n.w-8{width:calc(var(--spacing) * 8);}\n.w-96{width:calc(var(--spacing) * 96);}\n.w-fit{width:fit-content;}\n.w-full{width:100%;}\n.w-lg{width:var(--container-lg);}\n.w-max{width:max-content;}\n.w-px{width:1px;}\n.w-screen{width:100vw;}\n.inline{display:inline;}\n.block{display:block;}\n.contents{display:contents;}\n.dark .dark-hidden,\n.hidden,\n.light .light-hidden{display:none;}\n.visible{visibility:visible;}\n.invisible{visibility:hidden;}\n.collapse{visibility:collapse;}\n.cursor-default{cursor:default;}\n.cursor-pointer{cursor:pointer;}\n.cursor-not-allowed{cursor:not-allowed;}\n.cursor-grab{cursor:grab;}\n.pointer-events-auto{pointer-events:auto;}\n.pointer-events-none{pointer-events:none;}\n.disabled\\:pointer-events-none:disabled{pointer-events:none;}\n.resize{resize:both;}\n.select-none{-webkit-user-select:none;user-select:none;}\n.whitespace-pre-wrap{white-space:pre-wrap;}\n.ws-nowrap{white-space:nowrap;}\n.text-balance{text-wrap:balance;}\n.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}\n.uppercase{text-transform:uppercase;}\n.capitalize{text-transform:capitalize;}\n.italic{font-style:italic;}\n.ring-1\\.5{--un-ring-shadow:var(--un-ring-inset,) 0 0 0 calc(1.5px + var(--un-ring-offset-width)) var(--un-ring-color, currentColor);box-shadow:var(--un-inset-shadow), var(--un-inset-ring-shadow), var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);}\n.focus\\:ring-1:focus{--un-ring-shadow:var(--un-ring-inset,) 0 0 0 calc(1px + var(--un-ring-offset-width)) var(--un-ring-color, currentColor);box-shadow:var(--un-inset-shadow), var(--un-inset-ring-shadow), var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);}\n.focus\\:ring-3:focus{--un-ring-shadow:var(--un-ring-inset,) 0 0 0 calc(3px + var(--un-ring-offset-width)) var(--un-ring-color, currentColor);box-shadow:var(--un-inset-shadow), var(--un-inset-ring-shadow), var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);}\n.focus-visible\\:ring-3:focus-visible{--un-ring-shadow:var(--un-ring-inset,) 0 0 0 calc(3px + var(--un-ring-offset-width)) var(--un-ring-color, currentColor);box-shadow:var(--un-inset-shadow), var(--un-inset-ring-shadow), var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);}\n.ring-purple\\/50{--un-ring-color:color-mix(in srgb, var(--colors-purple-DEFAULT) 50%, transparent);}\n.focus\\:ring-primary-500\\/25:focus{--un-ring-color:color-mix(in srgb, var(--colors-primary-500) 25%, transparent);}\n.focus\\:ring-primary\\/30:focus{--un-ring-color:color-mix(in srgb, var(--colors-primary-DEFAULT) 30%, transparent);}\n.focus-visible\\:ring-primary-500\\/30:focus-visible{--un-ring-color:color-mix(in srgb, var(--colors-primary-500) 30%, transparent);}\n.focus-visible\\:ring-primary-500\\/40:focus-visible{--un-ring-color:color-mix(in srgb, var(--colors-primary-500) 40%, transparent);}\n.focus-visible\\:ring-red-500\\/30:focus-visible{--un-ring-color:color-mix(in srgb, var(--colors-red-500) 30%, transparent);}\n.shadow,\n.shadow-sm{--un-shadow:0 1px 3px 0 var(--un-shadow-color, rgb(0 0 0 / 0.1)),0 1px 2px -1px var(--un-shadow-color, rgb(0 0 0 / 0.1));box-shadow:var(--un-inset-shadow), var(--un-inset-ring-shadow), var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);}\n.shadow-xl{--un-shadow:0 20px 25px -5px var(--un-shadow-color, rgb(0 0 0 / 0.1)),0 8px 10px -6px var(--un-shadow-color, rgb(0 0 0 / 0.1));box-shadow:var(--un-inset-shadow), var(--un-inset-ring-shadow), var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);}\n.-translate-y-2{--un-translate-y:calc(var(--spacing) * -2);translate:var(--un-translate-x) var(--un-translate-y);}\n.translate-x--1\\/2{--un-translate-x:-50%;translate:var(--un-translate-x) var(--un-translate-y);}\n.translate-x-1{--un-translate-x:calc(var(--spacing) * 1);translate:var(--un-translate-x) var(--un-translate-y);}\n.translate-x-4{--un-translate-x:calc(var(--spacing) * 4);translate:var(--un-translate-x) var(--un-translate-y);}\n.translate-x-5{--un-translate-x:calc(var(--spacing) * 5);translate:var(--un-translate-x) var(--un-translate-y);}\n.translate-y--1\\/2{--un-translate-y:-50%;translate:var(--un-translate-x) var(--un-translate-y);}\n.translate-y-0{--un-translate-y:calc(var(--spacing) * 0);translate:var(--un-translate-x) var(--un-translate-y);}\n.rotate--45{rotate:-45deg;}\n.rotate-0{rotate:0deg;}\n.rotate-180{rotate:180deg;}\n.rotate-270{rotate:270deg;}\n.rotate-90{rotate:90deg;}\n.scale-100{--un-scale-x:100%;--un-scale-y:100%;scale:var(--un-scale-x) var(--un-scale-y);}\n.scale-120{--un-scale-x:120%;--un-scale-y:120%;scale:var(--un-scale-x) var(--un-scale-y);}\n.scale-98{--un-scale-x:98%;--un-scale-y:98%;scale:var(--un-scale-x) var(--un-scale-y);}\n.hover\\:scale-110:hover{--un-scale-x:110%;--un-scale-y:110%;scale:var(--un-scale-x) var(--un-scale-y);}\n.scale-y--100{--un-scale-y:-100%;scale:var(--un-scale-x) var(--un-scale-y);}\n.transform{transform:var(--un-rotate-x) var(--un-rotate-y) var(--un-rotate-z) var(--un-skew-x) var(--un-skew-y);}\n.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,--un-gradient-from,--un-gradient-via,--un-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter;transition-timing-function:var(--un-ease, var(--default-transition-timingFunction));transition-duration:var(--un-duration, var(--default-transition-duration));}\n.transition-all{transition-property:all;transition-timing-function:var(--un-ease, var(--default-transition-timingFunction));transition-duration:var(--un-duration, var(--default-transition-duration));}\n.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,--un-gradient-from,--un-gradient-via,--un-gradient-to;transition-timing-function:var(--un-ease, var(--default-transition-timingFunction));transition-duration:var(--un-duration, var(--default-transition-duration));}\n.transition-opacity{transition-property:opacity;transition-timing-function:var(--un-ease, var(--default-transition-timingFunction));transition-duration:var(--un-duration, var(--default-transition-duration));}\n.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--un-ease, var(--default-transition-timingFunction));transition-duration:var(--un-duration, var(--default-transition-duration));}\n.duration-150{--un-duration:150ms;transition-duration:150ms;}\n.duration-200{--un-duration:200ms;transition-duration:200ms;}\n.duration-300{--un-duration:300ms;transition-duration:300ms;}\n.delay-200{transition-delay:200ms;}\n.ease-in{--un-ease:var(--ease-in);transition-timing-function:var(--ease-in);}\n.ease-out{--un-ease:var(--ease-out);transition-timing-function:var(--ease-out);}\n.items-start{align-items:flex-start;}\n.items-center{align-items:center;}\n.box-border{box-sizing:border-box;}\n.inset-0{inset:calc(var(--spacing) * 0);}\n.bottom-0{bottom:calc(var(--spacing) * 0);}\n.bottom-4{bottom:calc(var(--spacing) * 4);}\n.bottom-4px{bottom:4px;}\n.left--1{left:calc(var(--spacing) * -1);}\n.left-0{left:calc(var(--spacing) * 0);}\n.left-1\\/2{left:50%;}\n.left-2{left:calc(var(--spacing) * 2);}\n.left-5px{left:5px;}\n.right--1{right:calc(var(--spacing) * -1);}\n.right--1px{right:-1px;}\n.right-0{right:calc(var(--spacing) * 0);}\n.right-0\\.5{right:calc(var(--spacing) * 0.5);}\n.right-2{right:calc(var(--spacing) * 2);}\n.right-4{right:calc(var(--spacing) * 4);}\n.top--32px{top:-32px;}\n.top-0\\.5{top:calc(var(--spacing) * 0.5);}\n.top-1{top:calc(var(--spacing) * 1);}\n.top-1\\/2{top:50%;}\n.top-4px{top:4px;}\n.justify-end{justify-content:flex-end;}\n.justify-center{justify-content:center;}\n.justify-between{justify-content:space-between;}\n.absolute{position:absolute;}\n.fixed{position:fixed;}\n.relative{position:relative;}\n.static{position:static;}\n.z--1{z-index:-1;}\n.of-hidden,\n.overflow-hidden{overflow:hidden;}\n.overflow-auto{overflow:auto;}\n.of-x-hidden{overflow-x:hidden;}\n.of-y-auto{overflow-y:auto;}\n.of-y-hidden{overflow-y:hidden;}\n.dark .dark\\:fill-white{fill:color-mix(in srgb, var(--colors-white) var(--un-fill-opacity), transparent);}\n.fill-black{fill:color-mix(in srgb, var(--colors-black) var(--un-fill-opacity), transparent);}\n@keyframes pulse{0%, 100% {opacity:1} 50% {opacity:.5}}\n@keyframes spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}\n.animate-pulse{animation:pulse 2s cubic-bezier(0.4,0,.6,1) infinite;}\n.animate-spin{animation:spin 1s linear infinite;}\n.backdrop-blur-0{--un-backdrop-blur:blur(0px);-webkit-backdrop-filter:var(--un-backdrop-blur,) var(--un-backdrop-brightness,) var(--un-backdrop-contrast,) var(--un-backdrop-grayscale,) var(--un-backdrop-hue-rotate,) var(--un-backdrop-invert,) var(--un-backdrop-opacity,) var(--un-backdrop-saturate,) var(--un-backdrop-sepia,);backdrop-filter:var(--un-backdrop-blur,) var(--un-backdrop-brightness,) var(--un-backdrop-contrast,) var(--un-backdrop-grayscale,) var(--un-backdrop-hue-rotate,) var(--un-backdrop-invert,) var(--un-backdrop-opacity,) var(--un-backdrop-saturate,) var(--un-backdrop-sepia,);}\n.backdrop-blur-1{--un-backdrop-blur:blur(1px);-webkit-backdrop-filter:var(--un-backdrop-blur,) var(--un-backdrop-brightness,) var(--un-backdrop-contrast,) var(--un-backdrop-grayscale,) var(--un-backdrop-hue-rotate,) var(--un-backdrop-invert,) var(--un-backdrop-opacity,) var(--un-backdrop-saturate,) var(--un-backdrop-sepia,);backdrop-filter:var(--un-backdrop-blur,) var(--un-backdrop-brightness,) var(--un-backdrop-contrast,) var(--un-backdrop-grayscale,) var(--un-backdrop-hue-rotate,) var(--un-backdrop-invert,) var(--un-backdrop-opacity,) var(--un-backdrop-saturate,) var(--un-backdrop-sepia,);}\n.blur-2xl{--un-blur:blur(40px);filter:var(--un-blur,) var(--un-brightness,) var(--un-contrast,) var(--un-grayscale,) var(--un-hue-rotate,) var(--un-invert,) var(--un-saturate,) var(--un-sepia,) var(--un-drop-shadow,);}\n.saturate-0{--un-saturate:saturate(0%);filter:var(--un-blur,) var(--un-brightness,) var(--un-contrast,) var(--un-grayscale,) var(--un-hue-rotate,) var(--un-invert,) var(--un-saturate,) var(--un-sepia,) var(--un-drop-shadow,);}\n.table{display:table;}\n.tabular-nums{--un-numeric-spacing:tabular-nums;font-variant-numeric:var(--un-ordinal,) var(--un-slashed-zero,) var(--un-numeric-figure,) var(--un-numeric-spacing,) var(--un-numeric-fraction,);}\n.space-y-0\\.5{\n:where(&>:not(:last-child)){--un-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 0.5) * var(--un-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 0.5) * calc(1 - var(--un-space-y-reverse)));}\n}\n@supports (color: color-mix(in lab, red, red)){\n.dark .dark\\:text-primary-300{color:color-mix(in oklab, var(--colors-primary-300) var(--un-text-opacity), transparent);}\n.dark .dark\\:text-red-400{color:color-mix(in oklab, var(--colors-red-400) var(--un-text-opacity), transparent);}\n.text-amber{color:color-mix(in oklab, var(--colors-amber-DEFAULT) var(--un-text-opacity), transparent);}\n.text-blue{color:color-mix(in oklab, var(--colors-blue-DEFAULT) var(--un-text-opacity), transparent);}\n.text-gray{color:color-mix(in oklab, var(--colors-gray-DEFAULT) var(--un-text-opacity), transparent);}\n.text-green{color:color-mix(in oklab, var(--colors-green-DEFAULT) var(--un-text-opacity), transparent);}\n.text-orange{color:color-mix(in oklab, var(--colors-orange-DEFAULT) var(--un-text-opacity), transparent);}\n.text-primary{color:color-mix(in oklab, var(--colors-primary-DEFAULT) var(--un-text-opacity), transparent);}\n.text-primary-600{color:color-mix(in oklab, var(--colors-primary-600) var(--un-text-opacity), transparent);}\n.text-primary-700{color:color-mix(in oklab, var(--colors-primary-700) var(--un-text-opacity), transparent);}\n.text-red{color:color-mix(in oklab, var(--colors-red-DEFAULT) var(--un-text-opacity), transparent);}\n.text-red-500{color:color-mix(in oklab, var(--colors-red-500) var(--un-text-opacity), transparent);}\n.text-red-600{color:color-mix(in oklab, var(--colors-red-600) var(--un-text-opacity), transparent);}\n.text-white{color:color-mix(in oklab, var(--colors-white) var(--un-text-opacity), transparent);}\n.caret-primary-500{caret-color:color-mix(in oklab, var(--colors-primary-500) var(--un-caret-opacity), transparent);}\n.border-amber\\/20{border-color:color-mix(in oklab, var(--colors-amber-DEFAULT) 20%, transparent);}\n.border-primary-400\\/60{border-color:color-mix(in oklab, var(--colors-primary-400) 60%, transparent);}\n.border-primary\\/30{border-color:color-mix(in oklab, var(--colors-primary-DEFAULT) 30%, transparent);}\n.border-red-500\\/70{border-color:color-mix(in oklab, var(--colors-red-500) 70%, transparent);}\n.dark .dark\\:border-primary-400\\/50{border-color:color-mix(in oklab, var(--colors-primary-400) 50%, transparent);}\n.focus-within\\:border-gray\\/15:focus-within{border-color:color-mix(in oklab, var(--colors-gray-DEFAULT) 15%, transparent);}\n.hover\\:border-gray\\/10:hover{border-color:color-mix(in oklab, var(--colors-gray-DEFAULT) 10%, transparent);}\n.focus\\:border-primary-500:focus{border-color:color-mix(in oklab, var(--colors-primary-500) var(--un-border-opacity), transparent);}\n.focus\\:border-primary\\/40:focus{border-color:color-mix(in oklab, var(--colors-primary-DEFAULT) 40%, transparent);}\n.bg-amber{background-color:color-mix(in oklab, var(--colors-amber-DEFAULT) var(--un-bg-opacity), transparent);}\n.bg-amber\\/10{background-color:color-mix(in oklab, var(--colors-amber-DEFAULT) 10%, transparent);}\n.bg-black\\/30{background-color:color-mix(in oklab, var(--colors-black) 30%, transparent);}\n.bg-blue{background-color:color-mix(in oklab, var(--colors-blue-DEFAULT) var(--un-bg-opacity), transparent);}\n.bg-blue\\/10{background-color:color-mix(in oklab, var(--colors-blue-DEFAULT) 10%, transparent);}\n.bg-gray{background-color:color-mix(in oklab, var(--colors-gray-DEFAULT) var(--un-bg-opacity), transparent);}\n.bg-gray\\/10{background-color:color-mix(in oklab, var(--colors-gray-DEFAULT) 10%, transparent);}\n.bg-gray\\/20{background-color:color-mix(in oklab, var(--colors-gray-DEFAULT) 20%, transparent);}\n.bg-gray\\/3{background-color:color-mix(in oklab, var(--colors-gray-DEFAULT) 3%, transparent);}\n.bg-gray\\/30{background-color:color-mix(in oklab, var(--colors-gray-DEFAULT) 30%, transparent);}\n.bg-gray\\/5{background-color:color-mix(in oklab, var(--colors-gray-DEFAULT) 5%, transparent);}\n.bg-green{background-color:color-mix(in oklab, var(--colors-green-DEFAULT) var(--un-bg-opacity), transparent);}\n.bg-orange\\/10{background-color:color-mix(in oklab, var(--colors-orange-DEFAULT) 10%, transparent);}\n.bg-primary{background-color:color-mix(in oklab, var(--colors-primary-DEFAULT) var(--un-bg-opacity), transparent);}\n.bg-primary-500\\/20{background-color:color-mix(in oklab, var(--colors-primary-500) 20%, transparent);}\n.bg-primary-600{background-color:color-mix(in oklab, var(--colors-primary-600) var(--un-bg-opacity), transparent);}\n.bg-primary\\/10{background-color:color-mix(in oklab, var(--colors-primary-DEFAULT) 10%, transparent);}\n.bg-primary\\/15{background-color:color-mix(in oklab, var(--colors-primary-DEFAULT) 15%, transparent);}\n.bg-primary\\/20{background-color:color-mix(in oklab, var(--colors-primary-DEFAULT) 20%, transparent);}\n.bg-red{background-color:color-mix(in oklab, var(--colors-red-DEFAULT) var(--un-bg-opacity), transparent);}\n.bg-red-500\\/12{background-color:color-mix(in oklab, var(--colors-red-500) 12%, transparent);}\n.bg-red\\/10{background-color:color-mix(in oklab, var(--colors-red-DEFAULT) 10%, transparent);}\n.bg-white{background-color:color-mix(in oklab, var(--colors-white) var(--un-bg-opacity), transparent);}\n.bg-white\\/50{background-color:color-mix(in oklab, var(--colors-white) 50%, transparent);}\n.dark .dark\\:bg-black\\/30{background-color:color-mix(in oklab, var(--colors-black) 30%, transparent);}\n.dark .dark\\:bg-black\\/45{background-color:color-mix(in oklab, var(--colors-black) 45%, transparent);}\n.hover\\:bg-gray\\/10:hover{background-color:color-mix(in oklab, var(--colors-gray-DEFAULT) 10%, transparent);}\n.hover\\:bg-gray\\/15:hover{background-color:color-mix(in oklab, var(--colors-gray-DEFAULT) 15%, transparent);}\n.hover\\:bg-gray\\/20:hover{background-color:color-mix(in oklab, var(--colors-gray-DEFAULT) 20%, transparent);}\n.hover\\:bg-gray\\/5:hover{background-color:color-mix(in oklab, var(--colors-gray-DEFAULT) 5%, transparent);}\n.hover\\:bg-orange\\/20:hover{background-color:color-mix(in oklab, var(--colors-orange-DEFAULT) 20%, transparent);}\n.hover\\:bg-primary-700:hover{background-color:color-mix(in oklab, var(--colors-primary-700) var(--un-bg-opacity), transparent);}\n.hover\\:bg-primary\\/20:hover{background-color:color-mix(in oklab, var(--colors-primary-DEFAULT) 20%, transparent);}\n.hover\\:bg-primary\\/25:hover{background-color:color-mix(in oklab, var(--colors-primary-DEFAULT) 25%, transparent);}\n.hover\\:bg-red-500\\/20:hover{background-color:color-mix(in oklab, var(--colors-red-500) 20%, transparent);}\n.hover\\:bg-red\\/20:hover{background-color:color-mix(in oklab, var(--colors-red-DEFAULT) 20%, transparent);}\n.ring-purple\\/50{--un-ring-color:color-mix(in oklab, var(--colors-purple-DEFAULT) 50%, transparent);}\n.focus\\:ring-primary-500\\/25:focus{--un-ring-color:color-mix(in oklab, var(--colors-primary-500) 25%, transparent);}\n.focus\\:ring-primary\\/30:focus{--un-ring-color:color-mix(in oklab, var(--colors-primary-DEFAULT) 30%, transparent);}\n.focus-visible\\:ring-primary-500\\/30:focus-visible{--un-ring-color:color-mix(in oklab, var(--colors-primary-500) 30%, transparent);}\n.focus-visible\\:ring-primary-500\\/40:focus-visible{--un-ring-color:color-mix(in oklab, var(--colors-primary-500) 40%, transparent);}\n.focus-visible\\:ring-red-500\\/30:focus-visible{--un-ring-color:color-mix(in oklab, var(--colors-red-500) 30%, transparent);}\n.dark .dark\\:fill-white{fill:color-mix(in oklab, var(--colors-white) var(--un-fill-opacity), transparent);}\n.fill-black{fill:color-mix(in oklab, var(--colors-black) var(--un-fill-opacity), transparent);}\n}\n@media (min-width: 40rem){\n.sm\\:gap-2\\.5{gap:calc(var(--spacing) * 2.5);}\n}" +export default "/*\n1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)\n2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)\n2. [UnoCSS]: allow to override the default border color with css var `--un-default-border-color`\n*/\n\n*,\n::before,\n::after {\n box-sizing: border-box; /* 1 */\n border-width: 0; /* 2 */\n border-style: solid; /* 2 */\n border-color: var(--un-default-border-color, #e5e7eb); /* 2 */\n}\n\n::before,\n::after {\n --un-content: '';\n}\n\n/*\n1. Use a consistent sensible line-height in all browsers.\n2. Prevent adjustments of font size after orientation changes in iOS.\n3. Use a more readable tab size.\n4. Use the user's configured `sans` font-family by default.\n5. Use the user's configured `sans` font-feature-settings by default.\n6. Use the user's configured `sans` font-variation-settings by default.\n7. Disable tap highlights on iOS.\n*/\n\nhtml,\n:host {\n line-height: 1.5; /* 1 */\n -webkit-text-size-adjust: 100%; /* 2 */\n -moz-tab-size: 4; /* 3 */\n tab-size: 4; /* 3 */\n font-family: ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\"; /* 4 */\n font-feature-settings: normal; /* 5 */\n font-variation-settings: normal; /* 6 */\n -webkit-tap-highlight-color: transparent; /* 7 */\n}\n\n/*\n1. Remove the margin in all browsers.\n2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.\n*/\n\nbody {\n margin: 0; /* 1 */\n line-height: inherit; /* 2 */\n}\n\n/*\n1. Add the correct height in Firefox.\n2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)\n3. Ensure horizontal rules are visible by default.\n*/\n\nhr {\n height: 0; /* 1 */\n color: inherit; /* 2 */\n border-top-width: 1px; /* 3 */\n}\n\n/*\nAdd the correct text decoration in Chrome, Edge, and Safari.\n*/\n\nabbr:where([title]) {\n text-decoration: underline dotted;\n}\n\n/*\nRemove the default font size and weight for headings.\n*/\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n font-size: inherit;\n font-weight: inherit;\n}\n\n/*\nReset links to optimize for opt-in styling instead of opt-out.\n*/\n\na {\n color: inherit;\n text-decoration: inherit;\n}\n\n/*\nAdd the correct font weight in Edge and Safari.\n*/\n\nb,\nstrong {\n font-weight: bolder;\n}\n\n/*\n1. Use the user's configured `mono` font-family by default.\n2. Use the user's configured `mono` font-feature-settings by default.\n3. Use the user's configured `mono` font-variation-settings by default.\n4. Correct the odd `em` font sizing in all browsers.\n*/\n\ncode,\nkbd,\nsamp,\npre {\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace; /* 1 */\n font-feature-settings: normal; /* 2 */\n font-variation-settings: normal; /* 3 */\n font-size: 1em; /* 4 */\n}\n\n/*\nAdd the correct font size in all browsers.\n*/\n\nsmall {\n font-size: 80%;\n}\n\n/*\nPrevent `sub` and `sup` elements from affecting the line height in all browsers.\n*/\n\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -0.25em;\n}\n\nsup {\n top: -0.5em;\n}\n\n/*\n1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)\n2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)\n3. Remove gaps between table borders by default.\n*/\n\ntable {\n text-indent: 0; /* 1 */\n border-color: inherit; /* 2 */\n border-collapse: collapse; /* 3 */\n}\n\n/*\n1. Change the font styles in all browsers.\n2. Remove the margin in Firefox and Safari.\n3. Remove default padding in all browsers.\n*/\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n font-family: inherit; /* 1 */\n font-feature-settings: inherit; /* 1 */\n font-variation-settings: inherit; /* 1 */\n font-size: 100%; /* 1 */\n font-weight: inherit; /* 1 */\n line-height: inherit; /* 1 */\n color: inherit; /* 1 */\n margin: 0; /* 2 */\n padding: 0; /* 3 */\n}\n\n/*\nRemove the inheritance of text transform in Edge and Firefox.\n*/\n\nbutton,\nselect {\n text-transform: none;\n}\n\n/*\n1. Correct the inability to style clickable types in iOS and Safari.\n2. Remove default button styles.\n*/\n\nbutton,\n[type='button'],\n[type='reset'],\n[type='submit'] {\n -webkit-appearance: button; /* 1 */\n background-color: transparent; /* 2 */\n background-image: none; /* 2 */\n}\n\n/*\nUse the modern Firefox focus style for all focusable elements.\n*/\n\n:-moz-focusring {\n outline: auto;\n}\n\n/*\nRemove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)\n*/\n\n:-moz-ui-invalid {\n box-shadow: none;\n}\n\n/*\nAdd the correct vertical alignment in Chrome and Firefox.\n*/\n\nprogress {\n vertical-align: baseline;\n}\n\n/*\nCorrect the cursor style of increment and decrement buttons in Safari.\n*/\n\n::-webkit-inner-spin-button,\n::-webkit-outer-spin-button {\n height: auto;\n}\n\n/*\n1. Correct the odd appearance in Chrome and Safari.\n2. Correct the outline style in Safari.\n*/\n\n[type='search'] {\n -webkit-appearance: textfield; /* 1 */\n outline-offset: -2px; /* 2 */\n}\n\n/*\nRemove the inner padding in Chrome and Safari on macOS.\n*/\n\n::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n/*\n1. Correct the inability to style clickable types in iOS and Safari.\n2. Change font properties to `inherit` in Safari.\n*/\n\n::-webkit-file-upload-button {\n -webkit-appearance: button; /* 1 */\n font: inherit; /* 2 */\n}\n\n/*\nAdd the correct display in Chrome and Safari.\n*/\n\nsummary {\n display: list-item;\n}\n\n/*\nRemoves the default spacing for appropriate elements.\n*/\n\nblockquote,\ndl,\ndd,\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\nhr,\nfigure,\np,\npre {\n margin: 0;\n}\n\nfieldset {\n margin: 0;\n padding: 0;\n}\n\nlegend {\n padding: 0;\n}\n\nol,\nul,\nmenu {\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\ndialog {\n padding: 0;\n}\n\n/*\nPrevent resizing textareas horizontally by default.\n*/\n\ntextarea {\n resize: vertical;\n}\n\n/*\n1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)\n2. Set the default placeholder color to the user's configured gray 400 color.\n*/\n\ninput::placeholder,\ntextarea::placeholder {\n opacity: 1; /* 1 */\n color: #9ca3af; /* 2 */\n}\n\n/*\nSet the default cursor for buttons.\n*/\n\nbutton,\n[role=\"button\"] {\n cursor: pointer;\n}\n\n/*\nMake sure disabled buttons don't get the pointer cursor.\n*/\n\n:disabled {\n cursor: default;\n}\n\n/*\n1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)\n2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)\n This can trigger a poorly considered lint error in some tools but is included by design.\n*/\n\nimg,\nsvg,\nvideo,\ncanvas,\naudio,\niframe,\nembed,\nobject {\n display: block; /* 1 */\n vertical-align: middle; /* 2 */\n}\n\n/*\nConstrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)\n*/\n\nimg,\nvideo {\n max-width: 100%;\n height: auto;\n}\n\n/*\nMake elements with the HTML hidden attribute stay hidden by default.\n*/\n\n[hidden]:where(:not([hidden=\"until-found\"])) {\n display: none;\n}\n\n@property --un-ease{syntax:\"*\";inherits:false;}@property --un-bg-opacity{syntax:\"\";inherits:false;initial-value:100%;}@property --un-backdrop-blur{syntax:\"*\";inherits:false;}@property --un-backdrop-brightness{syntax:\"*\";inherits:false;}@property --un-backdrop-contrast{syntax:\"*\";inherits:false;}@property --un-backdrop-grayscale{syntax:\"*\";inherits:false;}@property --un-backdrop-hue-rotate{syntax:\"*\";inherits:false;}@property --un-backdrop-invert{syntax:\"*\";inherits:false;}@property --un-backdrop-opacity{syntax:\"*\";inherits:false;}@property --un-backdrop-saturate{syntax:\"*\";inherits:false;}@property --un-backdrop-sepia{syntax:\"*\";inherits:false;}@property --un-bg-opacity{syntax:\"\";inherits:false;initial-value:100%;}@property --un-inset-ring-color{syntax:\"*\";inherits:false;}@property --un-inset-ring-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000;}@property --un-inset-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000;}@property --un-inset-shadow-color{syntax:\"*\";inherits:false;}@property --un-ring-color{syntax:\"*\";inherits:false;}@property --un-ring-inset{syntax:\"*\";inherits:false;}@property --un-ring-offset-color{syntax:\"*\";inherits:false;}@property --un-ring-offset-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000;}@property --un-ring-offset-width{syntax:\"\";inherits:false;initial-value:0px;}@property --un-ring-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000;}@property --un-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000;}@property --un-shadow-color{syntax:\"*\";inherits:false;}@property --un-text-opacity{syntax:\"\";inherits:false;initial-value:100%;}:root {\n --un-text-opacity: 100%;\n}\n\n/*\n * Thin/transparent scrollbar, matching packages/ui/src/styles/global.css —\n * that stylesheet only reaches the Nuxt plugin apps, not this injected\n * webcomponents shell, which otherwise falls back to the browser default\n * (thick, opaque-gutter) scrollbar clashing with the translucent dock.\n * `:host`, not `:root` — this stylesheet is adopted into the dock's shadow\n * root (`defineCustomElement(..., { shadowRoot: true, styles: [css] })`),\n * where `:root` only ever matches the top-level document's .\n * Rules below are unscoped (not `#devframes-anchor ...`) since the\n * shadow boundary already keeps them from leaking onto the host page, and\n * scoping under the anchor's id would miss edge-docked mode entirely\n * (`DockEmbedded.vue` renders `DockEdge`/`#devframes-edge-panel` and\n * `Dock`/`#devframes-anchor` as mutually exclusive siblings).\n */\n:host {\n --app-scrollbar-size: 6px;\n --app-scrollbar-radius: 1px;\n --app-scrollbar-thumb: #8884;\n --app-scrollbar-thumb-hover: #8885;\n}\n\n::-webkit-scrollbar {\n width: var(--app-scrollbar-size);\n}\n\n::-webkit-scrollbar:horizontal {\n height: var(--app-scrollbar-size);\n}\n\n::-webkit-scrollbar-corner {\n background: transparent;\n}\n\n::-webkit-scrollbar-thumb {\n background-color: var(--app-scrollbar-thumb);\n transition: background 0.2s ease;\n border-radius: var(--app-scrollbar-radius);\n}\n\n::-webkit-scrollbar-thumb:hover {\n background-color: var(--app-scrollbar-thumb-hover);\n}\n\n::-webkit-scrollbar-track {\n border-radius: var(--app-scrollbar-radius);\n background: transparent;\n}\n\n#devframes-anchor {\n font-size:15px;font-family:var(--font-sans);width:calc(var(--spacing) * 0);transform-origin:center;box-sizing:border-box;position:fixed;z-index:2147483644;\n transform: translate(-50%, -50%) rotate(0);\n}\n\n#devframes-anchor #devframes-dock-container {\n display:flex;width:max-content;left:calc(var(--spacing) * 0);top:calc(var(--spacing) * 0);position:absolute;\n height: var(--devframes-dock-height, 40px);\n min-width: var(--devframes-dock-min-width, 100px);\n transform: translate(-50%, -50%);\n}\n\n#devframes-anchor.devframes-vertical #devframes-dock-container {\n transform: translate(-50%, -50%) rotate(90deg);\n transition-property:all;transition-timing-function:var(--un-ease, var(--default-transition-timingFunction));transition-duration:var(--un-duration, var(--default-transition-duration));--un-duration:500ms;transition-duration:500ms;\n}\n\n#devframes-anchor #devframes-dock {\n margin:auto;border-radius:calc(infinity * 1px);height:100%;-webkit-user-select:none;user-select:none;touch-action:none;\n ;color:color-mix(in oklab, #333 var(--un-text-opacity), transparent);background-color:color-mix(in srgb, var(--colors-white) 80%, transparent);--un-backdrop-blur:blur(7px);-webkit-backdrop-filter:var(--un-backdrop-blur,) var(--un-backdrop-brightness,) var(--un-backdrop-contrast,) var(--un-backdrop-grayscale,) var(--un-backdrop-hue-rotate,) var(--un-backdrop-invert,) var(--un-backdrop-opacity,) var(--un-backdrop-saturate,) var(--un-backdrop-sepia,);backdrop-filter:var(--un-backdrop-blur,) var(--un-backdrop-brightness,) var(--un-backdrop-contrast,) var(--un-backdrop-grayscale,) var(--un-backdrop-hue-rotate,) var(--un-backdrop-invert,) var(--un-backdrop-opacity,) var(--un-backdrop-saturate,) var(--un-backdrop-sepia,);--un-shadow:0 1px 3px 0 var(--un-shadow-color, rgb(0 0 0 / 0.1)),0 1px 2px -1px var(--un-shadow-color, rgb(0 0 0 / 0.1));box-shadow:var(--un-inset-shadow), var(--un-inset-ring-shadow), var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);\n transition-property:all;transition-timing-function:var(--un-ease, var(--default-transition-timingFunction));transition-duration:var(--un-duration, var(--default-transition-duration));--un-duration:500ms;transition-duration:500ms;\n height: var(--devframes-dock-height, 40px);\n width: calc-size(max-content, size);\n}.dark #devframes-anchor #devframes-dock{color:color-mix(in srgb, var(--colors-white) var(--un-text-opacity), transparent);background-color:color-mix(in oklab, #111 80%, transparent);}@supports (color: color-mix(in lab, red, red)){.dark #devframes-anchor #devframes-dock{color:color-mix(in oklab, var(--colors-white) var(--un-text-opacity), transparent);}}@supports (color: color-mix(in lab, red, red)){#devframes-anchor #devframes-dock{background-color:color-mix(in oklab, var(--colors-white) 80%, transparent);}}\n\n#devframes-anchor.devframes-minimized #devframes-dock {\n padding-inline:calc(var(--spacing) * 0);padding-block:2px;\n width: var(--devframes-dock-minimized-size, 22px);\n height: var(--devframes-dock-minimized-size, 22px);\n}\n\n#devframes-anchor.devframes-minimized .devframes-dock-bracket {\n opacity:50%;width:calc(var(--spacing) * 1.5);\n}\n\n#devframes-anchor:hover #devframes-glowing {\n opacity:60%;\n}\n\n#devframes-anchor #devframes-glowing {\n border-radius:calc(infinity * 1px);opacity:0%;pointer-events:none;transition-property:all;transition-timing-function:var(--un-ease, var(--default-transition-timingFunction));transition-duration:var(--un-duration, var(--default-transition-duration));--un-duration:1000ms;transition-duration:1000ms;--un-ease:var(--ease-out);transition-timing-function:var(--ease-out);left:calc(var(--spacing) * 0);top:calc(var(--spacing) * 0);position:absolute;\n width: var(--devframes-dock-glow-size, 160px);\n height: var(--devframes-dock-glow-size, 160px);\n filter: blur(var(--devframes-dock-glow-blur, 60px));\n transform: translate(-50%, -50%);\n background-image: linear-gradient(45deg, #61d9ff, #6b84fd, #715ebd);\n}\n\n@media print {\n #devframes-anchor {\n display:none;\n }\n}\n\n.devframes-resize-handle-horizontal {\n margin-block:-5px;border-radius:var(--radius-md);height:10px;cursor:ns-resize;left:6px;right:6px;position:absolute;\n}\n.devframes-resize-handle-vertical {\n margin-inline:-5px;border-radius:var(--radius-md);width:10px;cursor:ew-resize;top:6px;bottom:calc(var(--spacing) * 0);position:absolute;\n}\n.devframes-resize-handle-corner {\n margin:-6px;border-radius:var(--radius-md);width:14px;height:14px;position:absolute;\n}\n.devframes-resize-handle {\n z-index:30;\n}\n.devframes-resize-handle:hover {\n ;background-color:color-mix(in srgb, var(--colors-gray-400) 10%, transparent);\n}@supports (color: color-mix(in lab, red, red)){.devframes-resize-handle:hover{background-color:color-mix(in oklab, var(--colors-gray-400) 10%, transparent);}}\n\n@keyframes devframes-shake {\n 0%, 100% { transform: translateX(0); }\n 20%, 60% { transform: translateX(-5px); }\n 40%, 80% { transform: translateX(5px); }\n}\n.devframes-shake {\n animation: devframes-shake 0.4s cubic-bezier(0.36, 0.07, 0.19, 0.97);\n}\n@media (prefers-reduced-motion: reduce) {\n .devframes-shake {\n animation: none;\n }\n}\n\n/* layer: properties */\n@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{--un-bg-opacity:100%;--un-border-opacity:100%;--un-caret-opacity:100%;--un-ease:initial;--un-fill-opacity:100%;--un-leading:initial;--un-ring-opacity:100%;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-space-y-reverse:initial;--un-text-opacity:100%;--un-translate-x:initial;--un-translate-y:initial;--un-translate-z:initial;}}\n@property --un-text-opacity{syntax:\"\";inherits:false;initial-value:100%;}\n@property --un-leading{syntax:\"*\";inherits:false;}\n@property --un-outline-style{syntax:\"*\";inherits:false;initial-value:solid;}\n@property --un-caret-opacity{syntax:\"\";inherits:false;initial-value:100%;}\n@property --un-border-opacity{syntax:\"\";inherits:false;initial-value:100%;}\n@property --un-bg-opacity{syntax:\"\";inherits:false;initial-value:100%;}\n@property --un-ring-opacity{syntax:\"\";inherits:false;initial-value:100%;}\n@property --un-inset-ring-color{syntax:\"*\";inherits:false;}\n@property --un-inset-ring-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000;}\n@property --un-inset-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000;}\n@property --un-inset-shadow-color{syntax:\"*\";inherits:false;}\n@property --un-ring-color{syntax:\"*\";inherits:false;}\n@property --un-ring-inset{syntax:\"*\";inherits:false;}\n@property --un-ring-offset-color{syntax:\"*\";inherits:false;}\n@property --un-ring-offset-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000;}\n@property --un-ring-offset-width{syntax:\"\";inherits:false;initial-value:0px;}\n@property --un-ring-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000;}\n@property --un-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000;}\n@property --un-shadow-color{syntax:\"*\";inherits:false;}\n@property --un-translate-x{syntax:\"*\";inherits:false;initial-value:0;}\n@property --un-translate-y{syntax:\"*\";inherits:false;initial-value:0;}\n@property --un-translate-z{syntax:\"*\";inherits:false;initial-value:0;}\n@property --un-scale-x{syntax:\"*\";inherits:false;initial-value:1;}\n@property --un-scale-y{syntax:\"*\";inherits:false;initial-value:1;}\n@property --un-scale-z{syntax:\"*\";inherits:false;initial-value:1;}\n@property --un-ease{syntax:\"*\";inherits:false;}\n@property --un-fill-opacity{syntax:\"\";inherits:false;initial-value:100%;}\n@property --un-backdrop-blur{syntax:\"*\";inherits:false;}\n@property --un-backdrop-brightness{syntax:\"*\";inherits:false;}\n@property --un-backdrop-contrast{syntax:\"*\";inherits:false;}\n@property --un-backdrop-grayscale{syntax:\"*\";inherits:false;}\n@property --un-backdrop-hue-rotate{syntax:\"*\";inherits:false;}\n@property --un-backdrop-invert{syntax:\"*\";inherits:false;}\n@property --un-backdrop-opacity{syntax:\"*\";inherits:false;}\n@property --un-backdrop-saturate{syntax:\"*\";inherits:false;}\n@property --un-backdrop-sepia{syntax:\"*\";inherits:false;}\n@property --un-blur{syntax:\"*\";inherits:false;}\n@property --un-brightness{syntax:\"*\";inherits:false;}\n@property --un-contrast{syntax:\"*\";inherits:false;}\n@property --un-drop-shadow{syntax:\"*\";inherits:false;}\n@property --un-grayscale{syntax:\"*\";inherits:false;}\n@property --un-hue-rotate{syntax:\"*\";inherits:false;}\n@property --un-invert{syntax:\"*\";inherits:false;}\n@property --un-saturate{syntax:\"*\";inherits:false;}\n@property --un-sepia{syntax:\"*\";inherits:false;}\n@property --un-numeric-figure{syntax:\"*\";inherits:false;}\n@property --un-numeric-fraction{syntax:\"*\";inherits:false;}\n@property --un-numeric-spacing{syntax:\"*\";inherits:false;}\n@property --un-ordinal{syntax:\"*\";inherits:false;}\n@property --un-slashed-zero{syntax:\"*\";inherits:false;}\n@property --un-space-y-reverse{syntax:\"*\";inherits:false;initial-value:0;}\n/* layer: theme */\n:root, :host { --spacing: 0.25rem; --font-sans: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"Helvetica Neue\",Arial,\"Noto Sans\",sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\",\"Noto Color Emoji\"; --default-transition-timingFunction: cubic-bezier(0.4, 0, 0.2, 1); --default-transition-duration: 150ms; --ease-out: cubic-bezier(0, 0, 0.2, 1); --radius-md: 0.375rem; --colors-gray-400: oklch(70.7% 0.022 261.325); --colors-white: #fff; --container-lg: 32rem; --radius-lg: 0.5rem; --radius-DEFAULT: 0.25rem; --font-mono: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,\"Liberation Mono\",\"Courier New\",monospace; --colors-primary-DEFAULT: #3a6a45; --colors-blue-DEFAULT: oklch(70.7% 0.165 254.624); --colors-red-500: oklch(63.7% 0.237 25.331); --radius-xl: 0.75rem; --fontWeight-medium: 500; --colors-black: #000; --colors-primary-500: #578861; --ease-in: cubic-bezier(0.4, 0, 1, 1); --colors-primary-600: #40704b; --colors-primary-700: #295935; --colors-red-600: oklch(57.7% 0.245 27.325); --colors-primary-400: #75a67e; --colors-amber-DEFAULT: oklch(82.8% 0.189 84.429); --colors-orange-DEFAULT: oklch(75% 0.183 55.934); --text-xs-fontSize: 0.75rem; --text-xs-lineHeight: 1rem; --text-sm-fontSize: 0.875rem; --text-sm-lineHeight: 1.25rem; --text-2xl-fontSize: 1.5rem; --text-2xl-lineHeight: 2rem; --text-base-fontSize: 1rem; --text-base-lineHeight: 1.5rem; --colors-primary-300: #93c69d; --colors-red-400: oklch(70.4% 0.191 22.216); --leading-none: 1; --colors-neutral-800: oklch(26.9% 0 0); --colors-neutral-200: oklch(92.2% 0 0); --fontWeight-bold: 700; --tracking-wide: 0.025em; --tracking-tight: -0.025em; --colors-gray-DEFAULT: oklch(70.7% 0.022 261.325); --colors-red-DEFAULT: oklch(70.4% 0.191 22.216); --colors-purple-DEFAULT: oklch(71.4% 0.203 305.504); --leading-relaxed: 1.625; --colors-green-DEFAULT: oklch(79.2% 0.209 151.711); --text-xl-fontSize: 1.25rem; --text-xl-lineHeight: 1.75rem; --default-font-family: var(--font-sans); --default-monoFont-family: var(--font-mono); }\n/* layer: base */\n *, ::after, ::before, ::backdrop, ::file-selector-button { box-sizing: border-box; margin: 0; padding: 0; border: 0 solid; } html, :host { line-height: 1.5; -webkit-text-size-adjust: 100%; tab-size: 4; 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-featureSettings, normal); font-variation-settings: var(--default-font-variationSettings, 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; text-decoration: inherit; } b, strong { font-weight: bolder; } code, kbd, samp, pre { font-family: var( --default-monoFont-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace ); font-feature-settings: var(--default-monoFont-featureSettings, normal); font-variation-settings: var(--default-monoFont-variationSettings, normal); font-size: 1em; } small { font-size: 80%; } sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } sub { bottom: -0.25em; } sup { top: -0.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 { display: block; vertical-align: middle; } img, video { max-width: 100%; height: auto; } button, input, select, optgroup, textarea, ::file-selector-button { font: inherit; font-feature-settings: inherit; font-variation-settings: inherit; letter-spacing: inherit; color: inherit; border-radius: 0; background-color: transparent; opacity: 1; } :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: 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, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-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']), ::file-selector-button { appearance: button; } ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { height: auto; } [hidden]:where(:not([hidden~='until-found'])) { display: none !important; }\n/* layer: preflights */\n*,::before,::after{border-color:#8882}\n/* layer: icons */\n.i-ph-arrow-clockwise{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M240 56v48a8 8 0 0 1-8 8h-48a8 8 0 0 1 0-16h27.4l-26.59-24.36l-.25-.24a80 80 0 1 0-1.67 114.78a8 8 0 0 1 11 11.63A95.44 95.44 0 0 1 128 224h-1.32a96 96 0 1 1 69.07-164L224 85.8V56a8 8 0 1 1 16 0'/%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-arrow-clockwise-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M216 128a88 88 0 1 1-88-88a88 88 0 0 1 88 88' opacity='.2'/%3E%3Cpath d='M240 56v48a8 8 0 0 1-8 8h-48a8 8 0 0 1 0-16h27.4l-26.59-24.36l-.25-.24a80 80 0 1 0-1.67 114.78a8 8 0 0 1 11 11.63A95.44 95.44 0 0 1 128 224h-1.32a96 96 0 1 1 69.07-164L224 85.8V56a8 8 0 1 1 16 0'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-arrow-counter-clockwise{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M224 128a96 96 0 0 1-94.71 96H128a95.38 95.38 0 0 1-65.9-26.2a8 8 0 0 1 11-11.63a80 80 0 1 0-1.67-114.78a3 3 0 0 1-.26.25L44.59 96H72a8 8 0 0 1 0 16H24a8 8 0 0 1-8-8V56a8 8 0 0 1 16 0v29.8L60.25 60A96 96 0 0 1 224 128'/%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-arrow-square-out-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M184 80v128a8 8 0 0 1-8 8H48a8 8 0 0 1-8-8V80a8 8 0 0 1 8-8h128a8 8 0 0 1 8 8' opacity='.2'/%3E%3Cpath d='M224 104a8 8 0 0 1-16 0V59.32l-66.33 66.34a8 8 0 0 1-11.32-11.32L196.68 48H152a8 8 0 0 1 0-16h64a8 8 0 0 1 8 8Zm-40 24a8 8 0 0 0-8 8v72H48V80h72a8 8 0 0 0 0-16H48a16 16 0 0 0-16 16v128a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-72a8 8 0 0 0-8-8'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-arrows-counter-clockwise-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M216 128a88 88 0 1 1-88-88a88 88 0 0 1 88 88' opacity='.2'/%3E%3Cpath d='M88 104H40a8 8 0 0 1-8-8V48a8 8 0 0 1 16 0v28.69l14.63-14.63A95.43 95.43 0 0 1 130 33.94h.53a95.36 95.36 0 0 1 67.07 27.33a8 8 0 0 1-11.18 11.44a79.52 79.52 0 0 0-55.89-22.77h-.45a79.56 79.56 0 0 0-56.14 23.43L59.31 88H88a8 8 0 0 1 0 16m128 48h-48a8 8 0 0 0 0 16h28.69l-14.63 14.63a79.56 79.56 0 0 1-56.13 23.43h-.45a79.52 79.52 0 0 1-55.89-22.77a8 8 0 1 0-11.18 11.44a95.36 95.36 0 0 0 67.07 27.33h.52a95.43 95.43 0 0 0 67.36-28.12L208 179.31V208a8 8 0 0 0 16 0v-48a8 8 0 0 0-8-8'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-arrows-out-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M208 48v160H48V48Z' opacity='.2'/%3E%3Cpath d='M216 48v48a8 8 0 0 1-16 0V67.31l-42.34 42.35a8 8 0 0 1-11.32-11.32L188.69 56H160a8 8 0 0 1 0-16h48a8 8 0 0 1 8 8M98.34 146.34L56 188.69V160a8 8 0 0 0-16 0v48a8 8 0 0 0 8 8h48a8 8 0 0 0 0-16H67.31l42.35-42.34a8 8 0 0 0-11.32-11.32M208 152a8 8 0 0 0-8 8v28.69l-42.34-42.35a8 8 0 0 0-11.32 11.32L188.69 200H160a8 8 0 0 0 0 16h48a8 8 0 0 0 8-8v-48a8 8 0 0 0-8-8M67.31 56H96a8 8 0 0 0 0-16H48a8 8 0 0 0-8 8v48a8 8 0 0 0 16 0V67.31l42.34 42.35a8 8 0 0 0 11.32-11.32Z'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-cards-three-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M216 104v96a8 8 0 0 1-8 8H48a8 8 0 0 1-8-8v-96a8 8 0 0 1 8-8h160a8 8 0 0 1 8 8' opacity='.2'/%3E%3Cpath d='M208 88H48a16 16 0 0 0-16 16v96a16 16 0 0 0 16 16h160a16 16 0 0 0 16-16v-96a16 16 0 0 0-16-16m0 112H48v-96h160zM48 64a8 8 0 0 1 8-8h144a8 8 0 0 1 0 16H56a8 8 0 0 1-8-8m16-32a8 8 0 0 1 8-8h112a8 8 0 0 1 0 16H72a8 8 0 0 1-8-8'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-caret-down{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='m213.66 101.66l-80 80a8 8 0 0 1-11.32 0l-80-80a8 8 0 0 1 11.32-11.32L128 164.69l74.34-74.35a8 8 0 0 1 11.32 11.32'/%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-caret-left{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M165.66 202.34a8 8 0 0 1-11.32 11.32l-80-80a8 8 0 0 1 0-11.32l80-80a8 8 0 0 1 11.32 11.32L91.31 128Z'/%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-caret-right{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='m181.66 133.66l-80 80a8 8 0 0 1-11.32-11.32L164.69 128L90.34 53.66a8 8 0 0 1 11.32-11.32l80 80a8 8 0 0 1 0 11.32'/%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-caret-up{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M213.66 165.66a8 8 0 0 1-11.32 0L128 91.31l-74.34 74.35a8 8 0 0 1-11.32-11.32l80-80a8 8 0 0 1 11.32 0l80 80a8 8 0 0 1 0 11.32'/%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-check-bold{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='m232.49 80.49l-128 128a12 12 0 0 1-17 0l-56-56a12 12 0 1 1 17-17L96 183L215.51 63.51a12 12 0 0 1 17 17Z'/%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-circle-notch{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M232 128a104 104 0 0 1-208 0c0-41 23.81-78.36 60.66-95.27a8 8 0 0 1 6.68 14.54C60.15 61.59 40 93.27 40 128a88 88 0 0 0 176 0c0-34.73-20.15-66.41-51.34-80.73a8 8 0 0 1 6.68-14.54C208.19 49.64 232 87 232 128'/%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-dots-six-vertical{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M104 60a12 12 0 1 1-12-12a12 12 0 0 1 12 12m60 12a12 12 0 1 0-12-12a12 12 0 0 0 12 12m-72 44a12 12 0 1 0 12 12a12 12 0 0 0-12-12m72 0a12 12 0 1 0 12 12a12 12 0 0 0-12-12m-72 68a12 12 0 1 0 12 12a12 12 0 0 0-12-12m72 0a12 12 0 1 0 12 12a12 12 0 0 0-12-12'/%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-dots-three-outline-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M152 128a24 24 0 1 1-24-24a24 24 0 0 1 24 24M48 104a24 24 0 1 0 24 24a24 24 0 0 0-24-24m160 0a24 24 0 1 0 24 24a24 24 0 0 0-24-24' opacity='.2'/%3E%3Cpath d='M128 96a32 32 0 1 0 32 32a32 32 0 0 0-32-32m0 48a16 16 0 1 1 16-16a16 16 0 0 1-16 16M48 96a32 32 0 1 0 32 32a32 32 0 0 0-32-32m0 48a16 16 0 1 1 16-16a16 16 0 0 1-16 16m160-48a32 32 0 1 0 32 32a32 32 0 0 0-32-32m0 48a16 16 0 1 1 16-16a16 16 0 0 1-16 16'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-eye-slash{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M53.92 34.62a8 8 0 1 0-11.84 10.76l19.24 21.17C25 88.84 9.38 123.2 8.69 124.76a8 8 0 0 0 0 6.5c.35.79 8.82 19.57 27.65 38.4C61.43 194.74 93.12 208 128 208a127.1 127.1 0 0 0 52.07-10.83l22 24.21a8 8 0 1 0 11.84-10.76Zm47.33 75.84l41.67 45.85a32 32 0 0 1-41.67-45.85M128 192c-30.78 0-57.67-11.19-79.93-33.25A133.2 133.2 0 0 1 25 128c4.69-8.79 19.66-33.39 47.35-49.38l18 19.75a48 48 0 0 0 63.66 70l14.73 16.2A112 112 0 0 1 128 192m6-95.43a8 8 0 0 1 3-15.72a48.16 48.16 0 0 1 38.77 42.64a8 8 0 0 1-7.22 8.71a6 6 0 0 1-.75 0a8 8 0 0 1-8-7.26A32.09 32.09 0 0 0 134 96.57m113.28 34.69c-.42.94-10.55 23.37-33.36 43.8a8 8 0 1 1-10.67-11.92a132.8 132.8 0 0 0 27.8-35.14a133.2 133.2 0 0 0-23.12-30.77C185.67 75.19 158.78 64 128 64a118.4 118.4 0 0 0-19.36 1.57A8 8 0 1 1 106 49.79A134 134 0 0 1 128 48c34.88 0 66.57 13.26 91.66 38.35c18.83 18.83 27.3 37.62 27.65 38.41a8 8 0 0 1 0 6.5Z'/%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-eye-slash-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M128 56c-80 0-112 72-112 72s32 72 112 72s112-72 112-72s-32-72-112-72m0 112a40 40 0 1 1 40-40a40 40 0 0 1-40 40' opacity='.2'/%3E%3Cpath d='M53.92 34.62a8 8 0 1 0-11.84 10.76l19.24 21.17C25 88.84 9.38 123.2 8.69 124.76a8 8 0 0 0 0 6.5c.35.79 8.82 19.57 27.65 38.4C61.43 194.74 93.12 208 128 208a127.1 127.1 0 0 0 52.07-10.83l22 24.21a8 8 0 1 0 11.84-10.76Zm47.33 75.84l41.67 45.85a32 32 0 0 1-41.67-45.85M128 192c-30.78 0-57.67-11.19-79.93-33.25A133.2 133.2 0 0 1 25 128c4.69-8.79 19.66-33.39 47.35-49.38l18 19.75a48 48 0 0 0 63.66 70l14.73 16.2A112 112 0 0 1 128 192m6-95.43a8 8 0 0 1 3-15.72a48.16 48.16 0 0 1 38.77 42.64a8 8 0 0 1-7.22 8.71a6 6 0 0 1-.75 0a8 8 0 0 1-8-7.26A32.09 32.09 0 0 0 134 96.57m113.28 34.69c-.42.94-10.55 23.37-33.36 43.8a8 8 0 1 1-10.67-11.92a132.8 132.8 0 0 0 27.8-35.14a133.2 133.2 0 0 0-23.12-30.77C185.67 75.19 158.78 64 128 64a118.4 118.4 0 0 0-19.36 1.57A8 8 0 1 1 106 49.79A134 134 0 0 1 128 48c34.88 0 66.57 13.26 91.66 38.35c18.83 18.83 27.3 37.62 27.65 38.41a8 8 0 0 1 0 6.5Z'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-globe{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M128 24a104 104 0 1 0 104 104A104.12 104.12 0 0 0 128 24m88 104a87.6 87.6 0 0 1-3.33 24h-38.51a157.4 157.4 0 0 0 0-48h38.51a87.6 87.6 0 0 1 3.33 24m-114 40h52a115.1 115.1 0 0 1-26 45a115.3 115.3 0 0 1-26-45m-3.9-16a140.8 140.8 0 0 1 0-48h59.88a140.8 140.8 0 0 1 0 48ZM40 128a87.6 87.6 0 0 1 3.33-24h38.51a157.4 157.4 0 0 0 0 48H43.33A87.6 87.6 0 0 1 40 128m114-40h-52a115.1 115.1 0 0 1 26-45a115.3 115.3 0 0 1 26 45m52.33 0h-35.62a135.3 135.3 0 0 0-22.3-45.6A88.29 88.29 0 0 1 206.37 88Zm-98.74-45.6A135.3 135.3 0 0 0 85.29 88H49.63a88.29 88.29 0 0 1 57.96-45.6M49.63 168h35.66a135.3 135.3 0 0 0 22.3 45.6A88.29 88.29 0 0 1 49.63 168m98.78 45.6a135.3 135.3 0 0 0 22.3-45.6h35.66a88.29 88.29 0 0 1-57.96 45.6'/%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-keyboard-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M232 64v128a8 8 0 0 1-8 8H32a8 8 0 0 1-8-8V64a8 8 0 0 1 8-8h192a8 8 0 0 1 8 8' opacity='.2'/%3E%3Cpath d='M224 48H32a16 16 0 0 0-16 16v128a16 16 0 0 0 16 16h192a16 16 0 0 0 16-16V64a16 16 0 0 0-16-16m0 144H32V64h192zm-16-64a8 8 0 0 1-8 8H56a8 8 0 0 1 0-16h144a8 8 0 0 1 8 8m0-32a8 8 0 0 1-8 8H56a8 8 0 0 1 0-16h144a8 8 0 0 1 8 8M72 160a8 8 0 0 1-8 8h-8a8 8 0 0 1 0-16h8a8 8 0 0 1 8 8m96 0a8 8 0 0 1-8 8H96a8 8 0 0 1 0-16h64a8 8 0 0 1 8 8m40 0a8 8 0 0 1-8 8h-8a8 8 0 0 1 0-16h8a8 8 0 0 1 8 8'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-laptop-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M216 72v104H40V72a16 16 0 0 1 16-16h144a16 16 0 0 1 16 16' opacity='.2'/%3E%3Cpath d='M232 168h-8V72a24 24 0 0 0-24-24H56a24 24 0 0 0-24 24v96h-8a8 8 0 0 0-8 8v16a24 24 0 0 0 24 24h176a24 24 0 0 0 24-24v-16a8 8 0 0 0-8-8M48 72a8 8 0 0 1 8-8h144a8 8 0 0 1 8 8v96H48Zm176 120a8 8 0 0 1-8 8H40a8 8 0 0 1-8-8v-8h192ZM152 88a8 8 0 0 1-8 8h-32a8 8 0 0 1 0-16h32a8 8 0 0 1 8 8'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-layout-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M104 104v104H40a8 8 0 0 1-8-8v-96Z' opacity='.2'/%3E%3Cpath d='M216 40H40a16 16 0 0 0-16 16v144a16 16 0 0 0 16 16h176a16 16 0 0 0 16-16V56a16 16 0 0 0-16-16m0 16v40H40V56ZM40 112h56v88H40Zm176 88H112v-88h104z'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-magnifying-glass-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M192 112a80 80 0 1 1-80-80a80 80 0 0 1 80 80' opacity='.2'/%3E%3Cpath d='m229.66 218.34l-50.06-50.06a88.21 88.21 0 1 0-11.32 11.31l50.06 50.07a8 8 0 0 0 11.32-11.32M40 112a72 72 0 1 1 72 72a72.08 72.08 0 0 1-72-72'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-moon-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M227.89 147.89A96 96 0 1 1 108.11 28.11a96.09 96.09 0 0 0 119.78 119.78' opacity='.2'/%3E%3Cpath d='M233.54 142.23a8 8 0 0 0-8-2a88.08 88.08 0 0 1-109.8-109.8a8 8 0 0 0-10-10a104.84 104.84 0 0 0-52.91 37A104 104 0 0 0 136 224a103.1 103.1 0 0 0 62.52-20.88a104.84 104.84 0 0 0 37-52.91a8 8 0 0 0-1.98-7.98m-44.64 48.11A88 88 0 0 1 65.66 67.11a89 89 0 0 1 31.4-26A106 106 0 0 0 96 56a104.11 104.11 0 0 0 104 104a106 106 0 0 0 14.92-1.06a89 89 0 0 1-26.02 31.4'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-paint-brush-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M224 32c0 32.81-31.64 67.43-58.64 91.05A84.4 84.4 0 0 0 133 90.64c23.57-27 58.19-58.64 91-58.64' opacity='.2'/%3E%3Cpath d='M232 32a8 8 0 0 0-8-8c-44.08 0-89.31 49.71-114.43 82.63A60 60 0 0 0 32 164c0 30.88-19.54 44.73-20.47 45.37A8 8 0 0 0 16 224h76a60 60 0 0 0 57.37-77.57C182.3 121.31 232 76.08 232 32M92 208H34.63C41.38 198.41 48 183.92 48 164a44 44 0 1 1 44 44m32.42-94.45q5.14-6.66 10.09-12.55A76.2 76.2 0 0 1 155 121.49q-5.9 4.94-12.55 10.09a60.5 60.5 0 0 0-18.03-18.03m42.7-2.68a92.6 92.6 0 0 0-22-22c31.78-34.53 55.75-45 69.9-47.91c-2.85 14.16-13.37 38.13-47.9 69.91'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-push-pin{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='m235.32 81.37l-60.69-60.68a16 16 0 0 0-22.63 0l-53.63 53.8c-10.66-3.34-35-7.37-60.4 13.14a16 16 0 0 0-1.29 23.78L85 159.71l-42.66 42.63a8 8 0 0 0 11.32 11.32L96.29 171l48.29 48.29A16 16 0 0 0 155.9 224h1.13a15.93 15.93 0 0 0 11.64-6.33c19.64-26.1 17.75-47.32 13.19-60L235.33 104a16 16 0 0 0-.01-22.63M224 92.69l-57.27 57.46a8 8 0 0 0-1.49 9.22c9.46 18.93-1.8 38.59-9.34 48.62L48 100.08c12.08-9.74 23.64-12.31 32.48-12.31A40.1 40.1 0 0 1 96.81 91a8 8 0 0 0 9.25-1.51L163.32 32L224 92.68Z'/%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-push-pin-fill{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='m235.33 104l-53.47 53.65c4.56 12.67 6.45 33.89-13.19 60A15.93 15.93 0 0 1 157 224h-1.13a16 16 0 0 1-11.32-4.69L96.29 171l-42.63 42.66a8 8 0 0 1-11.32-11.32L85 159.71l-48.3-48.3A16 16 0 0 1 38 87.63c25.42-20.51 49.75-16.48 60.4-13.14L152 20.7a16 16 0 0 1 22.63 0l60.69 60.68a16 16 0 0 1 .01 22.62'/%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-rocket-launch-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M184 120v61.65a8 8 0 0 1-2.34 5.65l-34.35 34.35a8 8 0 0 1-13.57-4.53L128 176Zm-48-48H74.35a8 8 0 0 0-5.65 2.34l-34.35 34.35a8 8 0 0 0 4.53 13.57L80 128ZM40 216c37.65 0 50.69-19.69 54.56-28.18l-26.38-26.38C59.69 165.31 40 178.35 40 216' opacity='.2'/%3E%3Cpath d='M223.85 47.12a16 16 0 0 0-15-15c-12.58-.75-44.73.4-71.41 27.07L132.69 64H74.36A15.9 15.9 0 0 0 63 68.68L28.7 103a16 16 0 0 0 9.07 27.16l38.47 5.37l44.21 44.21l5.37 38.49a15.94 15.94 0 0 0 10.78 12.92a16.1 16.1 0 0 0 5.1.83a15.9 15.9 0 0 0 11.3-4.68l34.32-34.3a15.9 15.9 0 0 0 4.68-11.36v-58.33l4.77-4.77c26.68-26.68 27.83-58.83 27.08-71.42M74.36 80h42.33l-39.53 39.52L40 114.34Zm74.41-9.45a76.65 76.65 0 0 1 59.11-22.47a76.46 76.46 0 0 1-22.42 59.16L128 164.68L91.32 128ZM176 181.64L141.67 216l-5.19-37.17L176 139.31Zm-74.16 9.5C97.34 201 82.29 224 40 224a8 8 0 0 1-8-8c0-42.29 23-57.34 32.86-61.85a8 8 0 0 1 6.64 14.56c-6.43 2.93-20.62 12.36-23.12 38.91c26.55-2.5 36-16.69 38.91-23.12a8 8 0 1 1 14.56 6.64Z'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-shield-check-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M216 56v56c0 96-88 120-88 120s-88-24-88-120V56a8 8 0 0 1 8-8h160a8 8 0 0 1 8 8' opacity='.2'/%3E%3Cpath d='M208 40H48a16 16 0 0 0-16 16v56c0 52.72 25.52 84.67 46.93 102.19c23.06 18.86 46 25.26 47 25.53a8 8 0 0 0 4.2 0c1-.27 23.91-6.67 47-25.53C198.48 196.67 224 164.72 224 112V56a16 16 0 0 0-16-16m0 72c0 37.07-13.66 67.16-40.6 89.42a129.3 129.3 0 0 1-39.4 22.2a128.3 128.3 0 0 1-38.92-21.81C61.82 179.51 48 149.3 48 112V56h160ZM82.34 141.66a8 8 0 0 1 11.32-11.32L112 148.69l50.34-50.35a8 8 0 0 1 11.32 11.32l-56 56a8 8 0 0 1-11.32 0Z'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-sign-out-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M224 56v144a16 16 0 0 1-16 16H48V40h160a16 16 0 0 1 16 16' opacity='.2'/%3E%3Cpath d='M120 216a8 8 0 0 1-8 8H48a8 8 0 0 1-8-8V40a8 8 0 0 1 8-8h64a8 8 0 0 1 0 16H56v160h56a8 8 0 0 1 8 8m109.66-93.66l-40-40a8 8 0 0 0-11.32 11.32L204.69 120H112a8 8 0 0 0 0 16h92.69l-26.35 26.34a8 8 0 0 0 11.32 11.32l40-40a8 8 0 0 0 0-11.32'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-spinner-gap-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96' opacity='.2'/%3E%3Cpath d='M136 32v32a8 8 0 0 1-16 0V32a8 8 0 0 1 16 0m88 88h-32a8 8 0 0 0 0 16h32a8 8 0 0 0 0-16m-45.09 47.6a8 8 0 0 0-11.31 11.31l22.62 22.63a8 8 0 0 0 11.32-11.32ZM128 184a8 8 0 0 0-8 8v32a8 8 0 0 0 16 0v-32a8 8 0 0 0-8-8m-50.91-16.4l-22.63 22.62a8 8 0 0 0 11.32 11.32l22.62-22.63a8 8 0 0 0-11.31-11.31M72 128a8 8 0 0 0-8-8H32a8 8 0 0 0 0 16h32a8 8 0 0 0 8-8m-6.22-73.54a8 8 0 0 0-11.32 11.32L77.09 88.4A8 8 0 0 0 88.4 77.09Z'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-square-half-bottom-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M208 128v72a8 8 0 0 1-8 8H56a8 8 0 0 1-8-8v-72Z' opacity='.2'/%3E%3Cpath d='M200 40H56a16 16 0 0 0-16 16v144a16 16 0 0 0 16 16h144a16 16 0 0 0 16-16V56a16 16 0 0 0-16-16m0 16v64H56V56Zm0 144H56v-64h144z'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-sun-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M184 128a56 56 0 1 1-56-56a56 56 0 0 1 56 56' opacity='.2'/%3E%3Cpath d='M120 40V16a8 8 0 0 1 16 0v24a8 8 0 0 1-16 0m72 88a64 64 0 1 1-64-64a64.07 64.07 0 0 1 64 64m-16 0a48 48 0 1 0-48 48a48.05 48.05 0 0 0 48-48M58.34 69.66a8 8 0 0 0 11.32-11.32l-16-16a8 8 0 0 0-11.32 11.32Zm0 116.68l-16 16a8 8 0 0 0 11.32 11.32l16-16a8 8 0 0 0-11.32-11.32M192 72a8 8 0 0 0 5.66-2.34l16-16a8 8 0 0 0-11.32-11.32l-16 16A8 8 0 0 0 192 72m5.66 114.34a8 8 0 0 0-11.32 11.32l16 16a8 8 0 0 0 11.32-11.32ZM48 128a8 8 0 0 0-8-8H16a8 8 0 0 0 0 16h24a8 8 0 0 0 8-8m80 80a8 8 0 0 0-8 8v24a8 8 0 0 0 16 0v-24a8 8 0 0 0-8-8m112-88h-24a8 8 0 0 0 0 16h24a8 8 0 0 0 0-16'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-warning-duotone,\n.i-ph\\:warning-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M215.46 216H40.54c-12.62 0-20.54-13.21-14.41-23.91l87.46-151.87c6.3-11 22.52-11 28.82 0l87.46 151.87c6.13 10.7-1.79 23.91-14.41 23.91' opacity='.2'/%3E%3Cpath d='M236.8 188.09L149.35 36.22a24.76 24.76 0 0 0-42.7 0L19.2 188.09a23.51 23.51 0 0 0 0 23.72A24.35 24.35 0 0 0 40.55 224h174.9a24.35 24.35 0 0 0 21.33-12.19a23.51 23.51 0 0 0 .02-23.72m-13.87 15.71a8.5 8.5 0 0 1-7.48 4.2H40.55a8.5 8.5 0 0 1-7.48-4.2a7.59 7.59 0 0 1 0-7.72l87.45-151.87a8.75 8.75 0 0 1 15 0l87.45 151.87a7.59 7.59 0 0 1-.04 7.72M120 144v-40a8 8 0 0 1 16 0v40a8 8 0 0 1-16 0m20 36a12 12 0 1 1-12-12a12 12 0 0 1 12 12'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-warning-fill{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M236.8 188.09L149.35 36.22a24.76 24.76 0 0 0-42.7 0L19.2 188.09a23.51 23.51 0 0 0 0 23.72A24.35 24.35 0 0 0 40.55 224h174.9a24.35 24.35 0 0 0 21.33-12.19a23.51 23.51 0 0 0 .02-23.72M120 104a8 8 0 0 1 16 0v40a8 8 0 0 1-16 0Zm8 88a12 12 0 1 1 12-12a12 12 0 0 1-12 12'/%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-wrench-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M224 96a64 64 0 0 1-94.94 56L73 217a24 24 0 0 1-34-34l65-56.06a64 64 0 0 1 80-90.29L144 80l5.66 26.34L176 112l43.35-40A63.8 63.8 0 0 1 224 96' opacity='.2'/%3E%3Cpath d='M226.76 69a8 8 0 0 0-12.84-2.88l-40.3 37.19l-17.23-3.7l-3.7-17.23l37.19-40.3A8 8 0 0 0 187 29.24A72 72 0 0 0 88 96a72.3 72.3 0 0 0 6 28.94L33.79 177c-.15.12-.29.26-.43.39a32 32 0 0 0 45.26 45.26c.13-.13.27-.28.39-.42L131.06 162A72 72 0 0 0 232 96a71.6 71.6 0 0 0-5.24-27M160 152a56.14 56.14 0 0 1-27.07-7a8 8 0 0 0-9.92 1.77l-55.9 64.74a16 16 0 0 1-22.62-22.62L109.18 133a8 8 0 0 0 1.77-9.93a56 56 0 0 1 58.36-82.31l-31.2 33.81a8 8 0 0 0-1.94 7.1l5.66 26.33a8 8 0 0 0 6.14 6.14l26.35 5.66a8 8 0 0 0 7.1-1.94l33.81-31.2A56.06 56.06 0 0 1 160 152'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph-x{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M205.66 194.34a8 8 0 0 1-11.32 11.32L128 139.31l-66.34 66.35a8 8 0 0 1-11.32-11.32L116.69 128L50.34 61.66a8 8 0 0 1 11.32-11.32L128 116.69l66.34-66.35a8 8 0 0 1 11.32 11.32L139.31 128Z'/%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph\\:bug-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M208 128v16a80 80 0 0 1-160 0v-16Z' opacity='.2'/%3E%3Cpath d='M144 92a12 12 0 1 1 12 12a12 12 0 0 1-12-12m-44-12a12 12 0 1 0 12 12a12 12 0 0 0-12-12m116 64a87.8 87.8 0 0 1-3 23l22.24 9.72A8 8 0 0 1 232 192a7.9 7.9 0 0 1-3.2-.67L207.38 182a88 88 0 0 1-158.76 0l-21.42 9.33a7.9 7.9 0 0 1-3.2.67a8 8 0 0 1-3.2-15.33L43 167a87.8 87.8 0 0 1-3-23v-8H16a8 8 0 0 1 0-16h24v-8a87.8 87.8 0 0 1 3-23l-22.2-9.67a8 8 0 1 1 6.4-14.66L48.62 74a88 88 0 0 1 158.76 0l21.42-9.36a8 8 0 0 1 6.4 14.66L213 89.05a87.8 87.8 0 0 1 3 23v8h24a8 8 0 0 1 0 16h-24ZM56 120h144v-8a72 72 0 0 0-144 0Zm64 95.54V136H56v8a72.08 72.08 0 0 0 64 71.54M200 144v-8h-64v79.54A72.08 72.08 0 0 0 200 144'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph\\:check{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='m229.66 77.66l-128 128a8 8 0 0 1-11.32 0l-56-56a8 8 0 0 1 11.32-11.32L96 188.69L218.34 66.34a8 8 0 0 1 11.32 11.32'/%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph\\:check-circle-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96' opacity='.2'/%3E%3Cpath d='M173.66 98.34a8 8 0 0 1 0 11.32l-56 56a8 8 0 0 1-11.32 0l-24-24a8 8 0 0 1 11.32-11.32L112 148.69l50.34-50.35a8 8 0 0 1 11.32 0M232 128A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph\\:info-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96' opacity='.2'/%3E%3Cpath d='M144 176a8 8 0 0 1-8 8a16 16 0 0 1-16-16v-40a8 8 0 0 1 0-16a16 16 0 0 1 16 16v40a8 8 0 0 1 8 8m88-48A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88m-92-32a12 12 0 1 0-12-12a12 12 0 0 0 12 12'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph\\:plug-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='m212 132l-58.63 58.63a32 32 0 0 1-45.25 0l-42.75-42.75a32 32 0 0 1 0-45.25L124 44Z' opacity='.2'/%3E%3Cpath d='M237.66 66.34a8 8 0 0 0-11.32 0L192 100.69L155.31 64l34.35-34.34a8 8 0 1 0-11.32-11.32L144 52.69l-26.34-26.35a8 8 0 0 0-11.32 11.32l6.35 6.34l-53 53a40 40 0 0 0 0 56.57l15.71 15.71l-49.06 49.06a8 8 0 0 0 11.32 11.32l49.09-49.09l15.71 15.71a40 40 0 0 0 56.57 0l53-53l6.34 6.35a8 8 0 0 0 11.32-11.32L203.31 112l34.35-34.34a8 8 0 0 0 0-11.32M147.72 185a24 24 0 0 1-33.95 0L71 142.23a24 24 0 0 1 0-33.95l53-53L200.69 132Z'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n.i-ph\\:x-circle-duotone{--un-icon:url(\"data:image/svg+xml;utf8,%3Csvg viewBox='0 0 256 256' width='1.11em' height='1.11em' xmlns='http://www.w3.org/2000/svg' %3E%3Cg fill='currentColor'%3E%3Cpath d='M224 128a96 96 0 1 1-96-96a96 96 0 0 1 96 96' opacity='.2'/%3E%3Cpath d='M165.66 101.66L139.31 128l26.35 26.34a8 8 0 0 1-11.32 11.32L128 139.31l-26.34 26.35a8 8 0 0 1-11.32-11.32L116.69 128l-26.35-26.34a8 8 0 0 1 11.32-11.32L128 116.69l26.34-26.35a8 8 0 0 1 11.32 11.32M232 128A104 104 0 1 1 128 24a104.11 104.11 0 0 1 104 104m-16 0a88 88 0 1 0-88 88a88.1 88.1 0 0 0 88-88'/%3E%3C/g%3E%3C/svg%3E\");-webkit-mask:var(--un-icon) no-repeat;mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;mask-size:100% 100%;background-color:currentColor;color:inherit;width:1.11em;height:1.11em;}\n/* layer: shortcuts */\n.badge{font-size:var(--text-xs-fontSize);line-height:var(--un-leading, var(--text-xs-lineHeight));--un-leading:var(--leading-none);line-height:var(--leading-none);--un-font-weight:var(--fontWeight-medium);font-weight:var(--fontWeight-medium);padding-inline:calc(var(--spacing) * 1.5);padding-block:calc(var(--spacing) * 0.5);border-radius:var(--radius-md);display:inline-flex;gap:calc(var(--spacing) * 1);align-items:center;}\n.color-base{color:color-mix(in srgb, var(--colors-neutral-800) var(--un-text-opacity), transparent);}\n.dark .color-base{color:color-mix(in srgb, var(--colors-neutral-200) var(--un-text-opacity), transparent);}\n.border-base{border-color:color-mix(in oklab, #8882 var(--un-border-opacity), transparent);}\n.hover\\:border-base:hover{border-color:color-mix(in oklab, #8882 var(--un-border-opacity), transparent);}\n.bg-active{background-color:color-mix(in srgb, var(--colors-primary-DEFAULT) 10%, transparent);}\n.dark .bg-active{background-color:color-mix(in srgb, var(--colors-primary-DEFAULT) 15%, transparent);}\n.bg-base{background-color:color-mix(in srgb, var(--colors-white) var(--un-bg-opacity), transparent);}\n.dark .bg-base{background-color:color-mix(in oklab, #111 var(--un-bg-opacity), transparent);}\n.bg-glass\\:80{background-color:color-mix(in srgb, var(--colors-white) 80%, transparent);--un-backdrop-blur:blur(7px);-webkit-backdrop-filter:var(--un-backdrop-blur,) var(--un-backdrop-brightness,) var(--un-backdrop-contrast,) var(--un-backdrop-grayscale,) var(--un-backdrop-hue-rotate,) var(--un-backdrop-invert,) var(--un-backdrop-opacity,) var(--un-backdrop-saturate,) var(--un-backdrop-sepia,);backdrop-filter:var(--un-backdrop-blur,) var(--un-backdrop-brightness,) var(--un-backdrop-contrast,) var(--un-backdrop-grayscale,) var(--un-backdrop-hue-rotate,) var(--un-backdrop-invert,) var(--un-backdrop-opacity,) var(--un-backdrop-saturate,) var(--un-backdrop-sepia,);}\n.dark .bg-glass\\:80{background-color:color-mix(in oklab, #111 80%, transparent);}\n.bg-secondary{background-color:color-mix(in oklab, #f6f6f6 var(--un-bg-opacity), transparent);}\n.dark .bg-secondary{background-color:color-mix(in oklab, #101010 var(--un-bg-opacity), transparent);}\n.dark .hover\\:bg-active:hover{background-color:color-mix(in srgb, var(--colors-primary-DEFAULT) 15%, transparent);}\n.hover\\:bg-active:hover{background-color:color-mix(in srgb, var(--colors-primary-DEFAULT) 10%, transparent);}\n.dark .op-fade{opacity:55%;}\n.op-fade{opacity:65%;}\n.dark .op-mute{opacity:25%;}\n.op-mute{opacity:30%;}\n.container{width:100%;}\n.z-command-palette{z-index:2147483646;}\n.z-dock-confirm,\n.z-dock-toast{z-index:2147483647;}\n.z-floating-anchor{z-index:2147483644;}\n.z-floating-tooltip{z-index:2147483645;}\n@supports (color: color-mix(in lab, red, red)){\n.color-base{color:color-mix(in oklab, var(--colors-neutral-800) var(--un-text-opacity), transparent);}\n.dark .color-base{color:color-mix(in oklab, var(--colors-neutral-200) var(--un-text-opacity), transparent);}\n.bg-active{background-color:color-mix(in oklab, var(--colors-primary-DEFAULT) 10%, transparent);}\n.dark .bg-active{background-color:color-mix(in oklab, var(--colors-primary-DEFAULT) 15%, transparent);}\n.bg-base{background-color:color-mix(in oklab, var(--colors-white) var(--un-bg-opacity), transparent);}\n.bg-glass\\:80{background-color:color-mix(in oklab, var(--colors-white) 80%, transparent);}\n.dark .hover\\:bg-active:hover{background-color:color-mix(in oklab, var(--colors-primary-DEFAULT) 15%, transparent);}\n.hover\\:bg-active:hover{background-color:color-mix(in oklab, var(--colors-primary-DEFAULT) 10%, transparent);}\n}\n@media (min-width: 40rem){\n.container{max-width:40rem;}\n}\n@media (min-width: 48rem){\n.container{max-width:48rem;}\n}\n@media (min-width: 64rem){\n.container{max-width:64rem;}\n}\n@media (min-width: 80rem){\n.container{max-width:80rem;}\n}\n@media (min-width: 96rem){\n.container{max-width:96rem;}\n}\n/* layer: default */\n@property --scroll-fade-t{syntax:\"\";inherits:false;initial-value:0px}@property --scroll-fade-b{syntax:\"\";inherits:false;initial-value:0px}@property --scroll-fade-s{syntax:\"\";inherits:false;initial-value:0px}@property --scroll-fade-e{syntax:\"\";inherits:false;initial-value:0px}@property --scroll-fade-mask{syntax:\"*\";inherits:false}\n@property --shimmer-angle{syntax:\"\";inherits:true;initial-value:20deg}@property --shimmer-image{syntax:\"*\";inherits:false}@property --shimmer-text-fill{syntax:\"*\";inherits:false}\n.text-\\[10px\\]{font-size:10px;}\n.text-\\[15px\\]{font-size:15px;}\n.text-0\\.6em{font-size:0.6em;}\n.text-2\\.75{font-size:0.6875rem;}\n.text-2xl{font-size:var(--text-2xl-fontSize);line-height:var(--un-leading, var(--text-2xl-lineHeight));}\n.text-base{font-size:var(--text-base-fontSize);line-height:var(--un-leading, var(--text-base-lineHeight));}\n.text-sm{font-size:var(--text-sm-fontSize);line-height:var(--un-leading, var(--text-sm-lineHeight));}\n.text-xl{font-size:var(--text-xl-fontSize);line-height:var(--un-leading, var(--text-xl-lineHeight));}\n.text-xs{font-size:var(--text-xs-fontSize);line-height:var(--un-leading, var(--text-xs-lineHeight));}\n.dark .dark\\:text-primary-300{color:color-mix(in srgb, var(--colors-primary-300) var(--un-text-opacity), transparent);}\n.dark .dark\\:text-red-400{color:color-mix(in srgb, var(--colors-red-400) var(--un-text-opacity), transparent);}\n.text-amber{color:color-mix(in srgb, var(--colors-amber-DEFAULT) var(--un-text-opacity), transparent);}\n.text-blue{color:color-mix(in srgb, var(--colors-blue-DEFAULT) var(--un-text-opacity), transparent);}\n.text-gray{color:color-mix(in srgb, var(--colors-gray-DEFAULT) var(--un-text-opacity), transparent);}\n.text-green{color:color-mix(in srgb, var(--colors-green-DEFAULT) var(--un-text-opacity), transparent);}\n.text-orange{color:color-mix(in srgb, var(--colors-orange-DEFAULT) var(--un-text-opacity), transparent);}\n.text-primary{color:color-mix(in srgb, var(--colors-primary-DEFAULT) var(--un-text-opacity), transparent);}\n.text-primary-600{color:color-mix(in srgb, var(--colors-primary-600) var(--un-text-opacity), transparent);}\n.text-primary-700{color:color-mix(in srgb, var(--colors-primary-700) var(--un-text-opacity), transparent);}\n.text-red{color:color-mix(in srgb, var(--colors-red-DEFAULT) var(--un-text-opacity), transparent);}\n.text-red-500{color:color-mix(in srgb, var(--colors-red-500) var(--un-text-opacity), transparent);}\n.text-red-600{color:color-mix(in srgb, var(--colors-red-600) var(--un-text-opacity), transparent);}\n.text-white{color:color-mix(in srgb, var(--colors-white) var(--un-text-opacity), transparent);}\n.leading-4{--un-leading:calc(var(--spacing) * 4);line-height:calc(var(--spacing) * 4);}\n.leading-5{--un-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5);}\n.leading-none{--un-leading:var(--leading-none);line-height:var(--leading-none);}\n.leading-relaxed{--un-leading:var(--leading-relaxed);line-height:var(--leading-relaxed);}\n.tracking-tight{--un-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight);}\n.tracking-wide{--un-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide);}\n.font-bold{--un-font-weight:var(--fontWeight-bold);font-weight:var(--fontWeight-bold);}\n.font-medium{--un-font-weight:var(--fontWeight-medium);font-weight:var(--fontWeight-medium);}\n.font-mono{font-family:var(--font-mono);}\n.font-sans{font-family:var(--font-sans);}\n.tab{-moz-tab-size:4;-o-tab-size:4;tab-size:4;}\n.m-auto,\n.ma{margin:auto;}\n.m1{margin:calc(var(--spacing) * 1);}\n.mx--1{margin-inline:calc(var(--spacing) * -1);}\n.mx--2{margin-inline:calc(var(--spacing) * -2);}\n.mx-auto{margin-inline:auto;}\n.my-2{margin-block:calc(var(--spacing) * 2);}\n.my0\\.5{margin-block:calc(var(--spacing) * 0.5);}\n.my1{margin-block:calc(var(--spacing) * 1);}\n.mb-1\\.5{margin-bottom:calc(var(--spacing) * 1.5);}\n.mb-3{margin-bottom:calc(var(--spacing) * 3);}\n.mb-4{margin-bottom:calc(var(--spacing) * 4);}\n.ml-2{margin-left:calc(var(--spacing) * 2);}\n.ml-6{margin-left:calc(var(--spacing) * 6);}\n.mr-1{margin-right:calc(var(--spacing) * 1);}\n.mt-0\\.5{margin-top:calc(var(--spacing) * 0.5);}\n.mt-1\\.5{margin-top:calc(var(--spacing) * 1.5);}\n.mt-6,\n.mt6{margin-top:calc(var(--spacing) * 6);}\n.mt-auto{margin-top:auto;}\n.mt2{margin-top:calc(var(--spacing) * 2);}\n.mt4{margin-top:calc(var(--spacing) * 4);}\n.p-0\\.5{padding:calc(var(--spacing) * 0.5);}\n.p-1\\.5,\n.p1\\.5{padding:calc(var(--spacing) * 1.5);}\n.p-4{padding:calc(var(--spacing) * 4);}\n.p-5{padding:calc(var(--spacing) * 5);}\n.p1{padding:calc(var(--spacing) * 1);}\n.p2{padding:calc(var(--spacing) * 2);}\n.p6{padding:calc(var(--spacing) * 6);}\n.p8{padding:calc(var(--spacing) * 8);}\n.px,\n.px-4,\n.px4{padding-inline:calc(var(--spacing) * 4);}\n.px-0\\.5{padding-inline:calc(var(--spacing) * 0.5);}\n.px-1,\n.px1{padding-inline:calc(var(--spacing) * 1);}\n.px-1\\.5{padding-inline:calc(var(--spacing) * 1.5);}\n.px-2,\n.px2{padding-inline:calc(var(--spacing) * 2);}\n.px-2\\.5{padding-inline:calc(var(--spacing) * 2.5);}\n.px-3,\n.px3{padding-inline:calc(var(--spacing) * 3);}\n.px-6{padding-inline:calc(var(--spacing) * 6);}\n.py-0{padding-block:calc(var(--spacing) * 0);}\n.py-0\\.5{padding-block:calc(var(--spacing) * 0.5);}\n.py-1,\n.py1{padding-block:calc(var(--spacing) * 1);}\n.py-1\\.5,\n.py1\\.5{padding-block:calc(var(--spacing) * 1.5);}\n.py-2,\n.py2{padding-block:calc(var(--spacing) * 2);}\n.py-2\\.5,\n.py2\\.5{padding-block:calc(var(--spacing) * 2.5);}\n.py-3{padding-block:calc(var(--spacing) * 3);}\n.py-6{padding-block:calc(var(--spacing) * 6);}\n.py-8{padding-block:calc(var(--spacing) * 8);}\n.pb2\\.5{padding-bottom:calc(var(--spacing) * 2.5);}\n.pt-\\[20vh\\]{padding-top:20vh;}\n.pt-6{padding-top:calc(var(--spacing) * 6);}\n.pt2{padding-top:calc(var(--spacing) * 2);}\n.text-center{text-align:center;}\n.text-left{text-align:left;}\n.outline{outline-style:var(--un-outline-style);outline-width:1px;}\n.outline-none{--un-outline-style:none;outline-style:none;}\n.caret-primary-500{caret-color:color-mix(in srgb, var(--colors-primary-500) var(--un-caret-opacity), transparent);}\n.border{border-width:1px;}\n.border-2{border-width:2px;}\n.border-b{border-bottom-width:1px;}\n.border-b-0{border-bottom-width:0px;}\n.border-b-1\\.5{border-bottom-width:1.5px;}\n.border-l{border-left-width:1px;}\n.border-r{border-right-width:1px;}\n.border-r-1\\.5{border-right-width:1.5px;}\n.border-t{border-top-width:1px;}\n.border-t-0{border-top-width:0px;}\n.last\\:border-b-0:last-child{border-bottom-width:0px;}\n.border-amber\\/20{border-color:color-mix(in srgb, var(--colors-amber-DEFAULT) 20%, transparent);}\n.border-current{border-color:currentColor;}\n.border-primary-400\\/60{border-color:color-mix(in srgb, var(--colors-primary-400) 60%, transparent);}\n.border-primary\\/30{border-color:color-mix(in srgb, var(--colors-primary-DEFAULT) 30%, transparent);}\n.border-red-500\\/70{border-color:color-mix(in srgb, var(--colors-red-500) 70%, transparent);}\n.border-transparent{border-color:transparent;}\n.dark .dark\\:border-primary-400\\/50{border-color:color-mix(in srgb, var(--colors-primary-400) 50%, transparent);}\n.focus-within\\:border-gray\\/15:focus-within{border-color:color-mix(in srgb, var(--colors-gray-DEFAULT) 15%, transparent);}\n.hover\\:border-gray\\/10:hover{border-color:color-mix(in srgb, var(--colors-gray-DEFAULT) 10%, transparent);}\n.focus\\:border-primary-500:focus{border-color:color-mix(in srgb, var(--colors-primary-500) var(--un-border-opacity), transparent);}\n.focus\\:border-primary\\/40:focus{border-color:color-mix(in srgb, var(--colors-primary-DEFAULT) 40%, transparent);}\n.border-t-transparent{border-top-color:transparent;}\n.rounded{border-radius:var(--radius-DEFAULT);}\n.rounded-full{border-radius:calc(infinity * 1px);}\n.rounded-lg{border-radius:var(--radius-lg);}\n.rounded-md{border-radius:var(--radius-md);}\n.rounded-xl{border-radius:var(--radius-xl);}\n.rounded-r{border-top-right-radius:var(--radius-DEFAULT);border-bottom-right-radius:var(--radius-DEFAULT);}\n.rounded-t-md{border-top-left-radius:var(--radius-md);border-top-right-radius:var(--radius-md);}\n.rounded-tr-md{border-top-right-radius:var(--radius-md);}\n.bg-\\[\\#8881\\]{background-color:color-mix(in oklab, #8881 var(--un-bg-opacity), transparent);}\n.bg-amber{background-color:color-mix(in srgb, var(--colors-amber-DEFAULT) var(--un-bg-opacity), transparent);}\n.bg-amber\\/10{background-color:color-mix(in srgb, var(--colors-amber-DEFAULT) 10%, transparent);}\n.bg-black\\/30,\n.dark .dark\\:bg-black\\/30{background-color:color-mix(in srgb, var(--colors-black) 30%, transparent);}\n.bg-blue{background-color:color-mix(in srgb, var(--colors-blue-DEFAULT) var(--un-bg-opacity), transparent);}\n.bg-blue\\/10{background-color:color-mix(in srgb, var(--colors-blue-DEFAULT) 10%, transparent);}\n.bg-gray{background-color:color-mix(in srgb, var(--colors-gray-DEFAULT) var(--un-bg-opacity), transparent);}\n.bg-gray\\/10{background-color:color-mix(in srgb, var(--colors-gray-DEFAULT) 10%, transparent);}\n.bg-gray\\/20{background-color:color-mix(in srgb, var(--colors-gray-DEFAULT) 20%, transparent);}\n.bg-gray\\/3{background-color:color-mix(in srgb, var(--colors-gray-DEFAULT) 3%, transparent);}\n.bg-gray\\/30{background-color:color-mix(in srgb, var(--colors-gray-DEFAULT) 30%, transparent);}\n.bg-gray\\/5{background-color:color-mix(in srgb, var(--colors-gray-DEFAULT) 5%, transparent);}\n.bg-green{background-color:color-mix(in srgb, var(--colors-green-DEFAULT) var(--un-bg-opacity), transparent);}\n.bg-orange\\/10{background-color:color-mix(in srgb, var(--colors-orange-DEFAULT) 10%, transparent);}\n.bg-primary{background-color:color-mix(in srgb, var(--colors-primary-DEFAULT) var(--un-bg-opacity), transparent);}\n.bg-primary-500\\/20{background-color:color-mix(in srgb, var(--colors-primary-500) 20%, transparent);}\n.bg-primary-600{background-color:color-mix(in srgb, var(--colors-primary-600) var(--un-bg-opacity), transparent);}\n.bg-primary\\/10{background-color:color-mix(in srgb, var(--colors-primary-DEFAULT) 10%, transparent);}\n.bg-primary\\/15{background-color:color-mix(in srgb, var(--colors-primary-DEFAULT) 15%, transparent);}\n.bg-primary\\/20{background-color:color-mix(in srgb, var(--colors-primary-DEFAULT) 20%, transparent);}\n.bg-red{background-color:color-mix(in srgb, var(--colors-red-DEFAULT) var(--un-bg-opacity), transparent);}\n.bg-red-500\\/12{background-color:color-mix(in srgb, var(--colors-red-500) 12%, transparent);}\n.bg-red\\/10{background-color:color-mix(in srgb, var(--colors-red-DEFAULT) 10%, transparent);}\n.bg-transparent{background-color:transparent;}\n.bg-white{background-color:color-mix(in srgb, var(--colors-white) var(--un-bg-opacity), transparent);}\n.bg-white\\/50{background-color:color-mix(in srgb, var(--colors-white) 50%, transparent);}\n.dark .dark\\:bg-black\\/45{background-color:color-mix(in srgb, var(--colors-black) 45%, transparent);}\n.hover\\:bg-\\[\\#8881\\]:hover{background-color:color-mix(in oklab, #8881 var(--un-bg-opacity), transparent);}\n.hover\\:bg-\\[\\#8882\\]:hover{background-color:color-mix(in oklab, #8882 var(--un-bg-opacity), transparent);}\n.hover\\:bg-\\[\\#8883\\]:hover{background-color:color-mix(in oklab, #8883 var(--un-bg-opacity), transparent);}\n.hover\\:bg-gray\\/10:hover{background-color:color-mix(in srgb, var(--colors-gray-DEFAULT) 10%, transparent);}\n.hover\\:bg-gray\\/15:hover{background-color:color-mix(in srgb, var(--colors-gray-DEFAULT) 15%, transparent);}\n.hover\\:bg-gray\\/20:hover{background-color:color-mix(in srgb, var(--colors-gray-DEFAULT) 20%, transparent);}\n.hover\\:bg-gray\\/5:hover{background-color:color-mix(in srgb, var(--colors-gray-DEFAULT) 5%, transparent);}\n.hover\\:bg-orange\\/20:hover{background-color:color-mix(in srgb, var(--colors-orange-DEFAULT) 20%, transparent);}\n.hover\\:bg-primary-700:hover{background-color:color-mix(in srgb, var(--colors-primary-700) var(--un-bg-opacity), transparent);}\n.hover\\:bg-primary\\/20:hover{background-color:color-mix(in srgb, var(--colors-primary-DEFAULT) 20%, transparent);}\n.hover\\:bg-primary\\/25:hover{background-color:color-mix(in srgb, var(--colors-primary-DEFAULT) 25%, transparent);}\n.hover\\:bg-red-500\\/20:hover{background-color:color-mix(in srgb, var(--colors-red-500) 20%, transparent);}\n.hover\\:bg-red\\/20:hover{background-color:color-mix(in srgb, var(--colors-red-DEFAULT) 20%, transparent);}\n.op0,\n.opacity-0{opacity:0%;}\n.op100,\n.opacity-100,\n.group:hover .group-hover\\:opacity-100{opacity:100%;}\n.op25{opacity:25%;}\n.op30{opacity:30%;}\n.op40{opacity:40%;}\n.op50,\n.group:hover .group-hover\\:op50{opacity:50%;}\n.op60{opacity:60%;}\n.op60\\!{opacity:60% !important;}\n.op70{opacity:70%;}\n.op75{opacity:75%;}\n.op80{opacity:80%;}\n.hover\\:op100:hover{opacity:100%;}\n.hover\\:op60:hover{opacity:60%;}\n.hover\\:op70:hover{opacity:70%;}\n.hover\\:op80:hover{opacity:80%;}\n.disabled\\:op40:disabled{opacity:40%;}\n.disabled\\:op50:disabled{opacity:50%;}\n.line-through{text-decoration-line:line-through;}\n.flex{display:flex;}\n.inline-flex{display:inline-flex;}\n.flex-1{flex:1 1 0%;}\n.flex-auto{flex:1 1 auto;}\n.flex-none{flex:none;}\n.shrink-0{flex-shrink:0;}\n.flex-row{flex-direction:row;}\n.flex-row-reverse{flex-direction:row-reverse;}\n.flex-col{flex-direction:column;}\n.flex-col-reverse{flex-direction:column-reverse;}\n.flex-wrap{flex-wrap:wrap;}\n.gap-0{gap:calc(var(--spacing) * 0);}\n.gap-0\\.5{gap:calc(var(--spacing) * 0.5);}\n.gap-1{gap:calc(var(--spacing) * 1);}\n.gap-1\\.5{gap:calc(var(--spacing) * 1.5);}\n.gap-2{gap:calc(var(--spacing) * 2);}\n.gap-3{gap:calc(var(--spacing) * 3);}\n.gap-4{gap:calc(var(--spacing) * 4);}\n.gap-6{gap:calc(var(--spacing) * 6);}\n.gap-x-2{column-gap:calc(var(--spacing) * 2);}\n.gap-y-0\\.5{row-gap:calc(var(--spacing) * 0.5);}\n.grid{display:grid;}\n.cols-\\[max-content_1fr\\]{grid-template-columns:max-content 1fr;}\n.h-0\\.5{height:calc(var(--spacing) * 0.5);}\n.h-1\\.5{height:calc(var(--spacing) * 1.5);}\n.h-10{height:calc(var(--spacing) * 10);}\n.h-14{height:calc(var(--spacing) * 14);}\n.h-16{height:calc(var(--spacing) * 16);}\n.h-20px{height:20px;}\n.h-24{height:calc(var(--spacing) * 24);}\n.h-3{height:calc(var(--spacing) * 3);}\n.h-3\\.5{height:calc(var(--spacing) * 3.5);}\n.h-4{height:calc(var(--spacing) * 4);}\n.h-4\\.5{height:calc(var(--spacing) * 4.5);}\n.h-5{height:calc(var(--spacing) * 5);}\n.h-6{height:calc(var(--spacing) * 6);}\n.h-7{height:calc(var(--spacing) * 7);}\n.h-8{height:calc(var(--spacing) * 8);}\n.h-full{height:100%;}\n.h-px{height:1px;}\n.h-screen{height:100vh;}\n.max-h-\\[60vh\\]{max-height:60vh;}\n.max-w-108{max-width:calc(var(--spacing) * 108);}\n.max-w-200{max-width:calc(var(--spacing) * 200);}\n.max-w-220px{max-width:220px;}\n.max-w-64{max-width:calc(var(--spacing) * 64);}\n.max-w-92{max-width:calc(var(--spacing) * 92);}\n.max-w-96{max-width:calc(var(--spacing) * 96);}\n.max-w-full{max-width:100%;}\n.min-h-0{min-height:calc(var(--spacing) * 0);}\n.min-h-16{min-height:calc(var(--spacing) * 16);}\n.min-h-5{min-height:calc(var(--spacing) * 5);}\n.min-w-0{min-width:calc(var(--spacing) * 0);}\n.min-w-28{min-width:calc(var(--spacing) * 28);}\n.min-w-36{min-width:calc(var(--spacing) * 36);}\n.min-w-40{min-width:calc(var(--spacing) * 40);}\n.min-w-44{min-width:calc(var(--spacing) * 44);}\n.w-\\[40px\\]{width:40px;}\n.w-1\\.5{width:calc(var(--spacing) * 1.5);}\n.w-10{width:calc(var(--spacing) * 10);}\n.w-11{width:calc(var(--spacing) * 11);}\n.w-12{width:calc(var(--spacing) * 12);}\n.w-16{width:calc(var(--spacing) * 16);}\n.w-2\\.5{width:calc(var(--spacing) * 2.5);}\n.w-20px{width:20px;}\n.w-24{width:calc(var(--spacing) * 24);}\n.w-2px{width:2px;}\n.w-3{width:calc(var(--spacing) * 3);}\n.w-3\\.5{width:calc(var(--spacing) * 3.5);}\n.w-4{width:calc(var(--spacing) * 4);}\n.w-4\\.5{width:calc(var(--spacing) * 4.5);}\n.w-5{width:calc(var(--spacing) * 5);}\n.w-6{width:calc(var(--spacing) * 6);}\n.w-64{width:calc(var(--spacing) * 64);}\n.w-7{width:calc(var(--spacing) * 7);}\n.w-72{width:calc(var(--spacing) * 72);}\n.w-8{width:calc(var(--spacing) * 8);}\n.w-96{width:calc(var(--spacing) * 96);}\n.w-fit{width:fit-content;}\n.w-full{width:100%;}\n.w-lg{width:var(--container-lg);}\n.w-max{width:max-content;}\n.w-px{width:1px;}\n.w-screen{width:100vw;}\n.inline{display:inline;}\n.block{display:block;}\n.contents{display:contents;}\n.dark .dark-hidden,\n.hidden,\n.light .light-hidden{display:none;}\n.visible{visibility:visible;}\n.invisible{visibility:hidden;}\n.collapse{visibility:collapse;}\n.cursor-default{cursor:default;}\n.cursor-pointer{cursor:pointer;}\n.cursor-not-allowed{cursor:not-allowed;}\n.cursor-grab{cursor:grab;}\n.pointer-events-auto{pointer-events:auto;}\n.pointer-events-none{pointer-events:none;}\n.disabled\\:pointer-events-none:disabled{pointer-events:none;}\n.resize{resize:both;}\n.select-none{-webkit-user-select:none;user-select:none;}\n.whitespace-pre-wrap{white-space:pre-wrap;}\n.ws-nowrap{white-space:nowrap;}\n.text-balance{text-wrap:balance;}\n.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}\n.uppercase{text-transform:uppercase;}\n.capitalize{text-transform:capitalize;}\n.italic{font-style:italic;}\n.ring-1\\.5{--un-ring-shadow:var(--un-ring-inset,) 0 0 0 calc(1.5px + var(--un-ring-offset-width)) var(--un-ring-color, currentColor);box-shadow:var(--un-inset-shadow), var(--un-inset-ring-shadow), var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);}\n.focus\\:ring-1:focus{--un-ring-shadow:var(--un-ring-inset,) 0 0 0 calc(1px + var(--un-ring-offset-width)) var(--un-ring-color, currentColor);box-shadow:var(--un-inset-shadow), var(--un-inset-ring-shadow), var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);}\n.focus\\:ring-3:focus{--un-ring-shadow:var(--un-ring-inset,) 0 0 0 calc(3px + var(--un-ring-offset-width)) var(--un-ring-color, currentColor);box-shadow:var(--un-inset-shadow), var(--un-inset-ring-shadow), var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);}\n.focus-visible\\:ring-3:focus-visible{--un-ring-shadow:var(--un-ring-inset,) 0 0 0 calc(3px + var(--un-ring-offset-width)) var(--un-ring-color, currentColor);box-shadow:var(--un-inset-shadow), var(--un-inset-ring-shadow), var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);}\n.ring-purple\\/50{--un-ring-color:color-mix(in srgb, var(--colors-purple-DEFAULT) 50%, transparent);}\n.focus\\:ring-primary-500\\/25:focus{--un-ring-color:color-mix(in srgb, var(--colors-primary-500) 25%, transparent);}\n.focus\\:ring-primary\\/30:focus{--un-ring-color:color-mix(in srgb, var(--colors-primary-DEFAULT) 30%, transparent);}\n.focus-visible\\:ring-primary-500\\/30:focus-visible{--un-ring-color:color-mix(in srgb, var(--colors-primary-500) 30%, transparent);}\n.focus-visible\\:ring-primary-500\\/40:focus-visible{--un-ring-color:color-mix(in srgb, var(--colors-primary-500) 40%, transparent);}\n.focus-visible\\:ring-red-500\\/30:focus-visible{--un-ring-color:color-mix(in srgb, var(--colors-red-500) 30%, transparent);}\n.shadow,\n.shadow-sm{--un-shadow:0 1px 3px 0 var(--un-shadow-color, rgb(0 0 0 / 0.1)),0 1px 2px -1px var(--un-shadow-color, rgb(0 0 0 / 0.1));box-shadow:var(--un-inset-shadow), var(--un-inset-ring-shadow), var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);}\n.shadow-xl{--un-shadow:0 20px 25px -5px var(--un-shadow-color, rgb(0 0 0 / 0.1)),0 8px 10px -6px var(--un-shadow-color, rgb(0 0 0 / 0.1));box-shadow:var(--un-inset-shadow), var(--un-inset-ring-shadow), var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);}\n.-translate-y-2{--un-translate-y:calc(var(--spacing) * -2);translate:var(--un-translate-x) var(--un-translate-y);}\n.translate-x--1\\/2{--un-translate-x:-50%;translate:var(--un-translate-x) var(--un-translate-y);}\n.translate-x-1{--un-translate-x:calc(var(--spacing) * 1);translate:var(--un-translate-x) var(--un-translate-y);}\n.translate-x-4{--un-translate-x:calc(var(--spacing) * 4);translate:var(--un-translate-x) var(--un-translate-y);}\n.translate-x-5{--un-translate-x:calc(var(--spacing) * 5);translate:var(--un-translate-x) var(--un-translate-y);}\n.translate-y--1\\/2{--un-translate-y:-50%;translate:var(--un-translate-x) var(--un-translate-y);}\n.translate-y-0{--un-translate-y:calc(var(--spacing) * 0);translate:var(--un-translate-x) var(--un-translate-y);}\n.rotate--45{rotate:-45deg;}\n.rotate-0{rotate:0deg;}\n.rotate-180{rotate:180deg;}\n.rotate-270{rotate:270deg;}\n.rotate-90{rotate:90deg;}\n.scale-100{--un-scale-x:100%;--un-scale-y:100%;scale:var(--un-scale-x) var(--un-scale-y);}\n.scale-120{--un-scale-x:120%;--un-scale-y:120%;scale:var(--un-scale-x) var(--un-scale-y);}\n.scale-98{--un-scale-x:98%;--un-scale-y:98%;scale:var(--un-scale-x) var(--un-scale-y);}\n.hover\\:scale-110:hover{--un-scale-x:110%;--un-scale-y:110%;scale:var(--un-scale-x) var(--un-scale-y);}\n.scale-y--100{--un-scale-y:-100%;scale:var(--un-scale-x) var(--un-scale-y);}\n.transform{transform:var(--un-rotate-x) var(--un-rotate-y) var(--un-rotate-z) var(--un-skew-x) var(--un-skew-y);}\n.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,--un-gradient-from,--un-gradient-via,--un-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter;transition-timing-function:var(--un-ease, var(--default-transition-timingFunction));transition-duration:var(--un-duration, var(--default-transition-duration));}\n.transition-all{transition-property:all;transition-timing-function:var(--un-ease, var(--default-transition-timingFunction));transition-duration:var(--un-duration, var(--default-transition-duration));}\n.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,--un-gradient-from,--un-gradient-via,--un-gradient-to;transition-timing-function:var(--un-ease, var(--default-transition-timingFunction));transition-duration:var(--un-duration, var(--default-transition-duration));}\n.transition-opacity{transition-property:opacity;transition-timing-function:var(--un-ease, var(--default-transition-timingFunction));transition-duration:var(--un-duration, var(--default-transition-duration));}\n.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--un-ease, var(--default-transition-timingFunction));transition-duration:var(--un-duration, var(--default-transition-duration));}\n.duration-150{--un-duration:150ms;transition-duration:150ms;}\n.duration-200{--un-duration:200ms;transition-duration:200ms;}\n.duration-300{--un-duration:300ms;transition-duration:300ms;}\n.delay-200{transition-delay:200ms;}\n.ease-in{--un-ease:var(--ease-in);transition-timing-function:var(--ease-in);}\n.ease-out{--un-ease:var(--ease-out);transition-timing-function:var(--ease-out);}\n.items-start{align-items:flex-start;}\n.items-center{align-items:center;}\n.box-border{box-sizing:border-box;}\n.inset-0{inset:calc(var(--spacing) * 0);}\n.bottom-0{bottom:calc(var(--spacing) * 0);}\n.bottom-4{bottom:calc(var(--spacing) * 4);}\n.bottom-4px{bottom:4px;}\n.left--1{left:calc(var(--spacing) * -1);}\n.left-0{left:calc(var(--spacing) * 0);}\n.left-1\\/2{left:50%;}\n.left-2{left:calc(var(--spacing) * 2);}\n.left-5px{left:5px;}\n.right--1{right:calc(var(--spacing) * -1);}\n.right--1px{right:-1px;}\n.right-0{right:calc(var(--spacing) * 0);}\n.right-0\\.5{right:calc(var(--spacing) * 0.5);}\n.right-2{right:calc(var(--spacing) * 2);}\n.right-4{right:calc(var(--spacing) * 4);}\n.top--32px{top:-32px;}\n.top-0\\.5{top:calc(var(--spacing) * 0.5);}\n.top-1{top:calc(var(--spacing) * 1);}\n.top-1\\/2{top:50%;}\n.top-4px{top:4px;}\n.justify-end{justify-content:flex-end;}\n.justify-center{justify-content:center;}\n.justify-between{justify-content:space-between;}\n.absolute{position:absolute;}\n.fixed{position:fixed;}\n.relative{position:relative;}\n.static{position:static;}\n.z--1{z-index:-1;}\n.of-hidden,\n.overflow-hidden{overflow:hidden;}\n.overflow-auto{overflow:auto;}\n.of-x-hidden{overflow-x:hidden;}\n.of-y-auto{overflow-y:auto;}\n.of-y-hidden{overflow-y:hidden;}\n.dark .dark\\:fill-white{fill:color-mix(in srgb, var(--colors-white) var(--un-fill-opacity), transparent);}\n.fill-black{fill:color-mix(in srgb, var(--colors-black) var(--un-fill-opacity), transparent);}\n@keyframes pulse{0%, 100% {opacity:1} 50% {opacity:.5}}\n@keyframes spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}\n.animate-pulse{animation:pulse 2s cubic-bezier(0.4,0,.6,1) infinite;}\n.animate-spin{animation:spin 1s linear infinite;}\n.backdrop-blur-0{--un-backdrop-blur:blur(0px);-webkit-backdrop-filter:var(--un-backdrop-blur,) var(--un-backdrop-brightness,) var(--un-backdrop-contrast,) var(--un-backdrop-grayscale,) var(--un-backdrop-hue-rotate,) var(--un-backdrop-invert,) var(--un-backdrop-opacity,) var(--un-backdrop-saturate,) var(--un-backdrop-sepia,);backdrop-filter:var(--un-backdrop-blur,) var(--un-backdrop-brightness,) var(--un-backdrop-contrast,) var(--un-backdrop-grayscale,) var(--un-backdrop-hue-rotate,) var(--un-backdrop-invert,) var(--un-backdrop-opacity,) var(--un-backdrop-saturate,) var(--un-backdrop-sepia,);}\n.backdrop-blur-1{--un-backdrop-blur:blur(1px);-webkit-backdrop-filter:var(--un-backdrop-blur,) var(--un-backdrop-brightness,) var(--un-backdrop-contrast,) var(--un-backdrop-grayscale,) var(--un-backdrop-hue-rotate,) var(--un-backdrop-invert,) var(--un-backdrop-opacity,) var(--un-backdrop-saturate,) var(--un-backdrop-sepia,);backdrop-filter:var(--un-backdrop-blur,) var(--un-backdrop-brightness,) var(--un-backdrop-contrast,) var(--un-backdrop-grayscale,) var(--un-backdrop-hue-rotate,) var(--un-backdrop-invert,) var(--un-backdrop-opacity,) var(--un-backdrop-saturate,) var(--un-backdrop-sepia,);}\n.blur-2xl{--un-blur:blur(40px);filter:var(--un-blur,) var(--un-brightness,) var(--un-contrast,) var(--un-grayscale,) var(--un-hue-rotate,) var(--un-invert,) var(--un-saturate,) var(--un-sepia,) var(--un-drop-shadow,);}\n.saturate-0{--un-saturate:saturate(0%);filter:var(--un-blur,) var(--un-brightness,) var(--un-contrast,) var(--un-grayscale,) var(--un-hue-rotate,) var(--un-invert,) var(--un-saturate,) var(--un-sepia,) var(--un-drop-shadow,);}\n.table{display:table;}\n.tabular-nums{--un-numeric-spacing:tabular-nums;font-variant-numeric:var(--un-ordinal,) var(--un-slashed-zero,) var(--un-numeric-figure,) var(--un-numeric-spacing,) var(--un-numeric-fraction,);}\n.space-y-0\\.5{\n:where(&>:not(:last-child)){--un-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 0.5) * var(--un-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 0.5) * calc(1 - var(--un-space-y-reverse)));}\n}\n@supports (color: color-mix(in lab, red, red)){\n.dark .dark\\:text-primary-300{color:color-mix(in oklab, var(--colors-primary-300) var(--un-text-opacity), transparent);}\n.dark .dark\\:text-red-400{color:color-mix(in oklab, var(--colors-red-400) var(--un-text-opacity), transparent);}\n.text-amber{color:color-mix(in oklab, var(--colors-amber-DEFAULT) var(--un-text-opacity), transparent);}\n.text-blue{color:color-mix(in oklab, var(--colors-blue-DEFAULT) var(--un-text-opacity), transparent);}\n.text-gray{color:color-mix(in oklab, var(--colors-gray-DEFAULT) var(--un-text-opacity), transparent);}\n.text-green{color:color-mix(in oklab, var(--colors-green-DEFAULT) var(--un-text-opacity), transparent);}\n.text-orange{color:color-mix(in oklab, var(--colors-orange-DEFAULT) var(--un-text-opacity), transparent);}\n.text-primary{color:color-mix(in oklab, var(--colors-primary-DEFAULT) var(--un-text-opacity), transparent);}\n.text-primary-600{color:color-mix(in oklab, var(--colors-primary-600) var(--un-text-opacity), transparent);}\n.text-primary-700{color:color-mix(in oklab, var(--colors-primary-700) var(--un-text-opacity), transparent);}\n.text-red{color:color-mix(in oklab, var(--colors-red-DEFAULT) var(--un-text-opacity), transparent);}\n.text-red-500{color:color-mix(in oklab, var(--colors-red-500) var(--un-text-opacity), transparent);}\n.text-red-600{color:color-mix(in oklab, var(--colors-red-600) var(--un-text-opacity), transparent);}\n.text-white{color:color-mix(in oklab, var(--colors-white) var(--un-text-opacity), transparent);}\n.caret-primary-500{caret-color:color-mix(in oklab, var(--colors-primary-500) var(--un-caret-opacity), transparent);}\n.border-amber\\/20{border-color:color-mix(in oklab, var(--colors-amber-DEFAULT) 20%, transparent);}\n.border-primary-400\\/60{border-color:color-mix(in oklab, var(--colors-primary-400) 60%, transparent);}\n.border-primary\\/30{border-color:color-mix(in oklab, var(--colors-primary-DEFAULT) 30%, transparent);}\n.border-red-500\\/70{border-color:color-mix(in oklab, var(--colors-red-500) 70%, transparent);}\n.dark .dark\\:border-primary-400\\/50{border-color:color-mix(in oklab, var(--colors-primary-400) 50%, transparent);}\n.focus-within\\:border-gray\\/15:focus-within{border-color:color-mix(in oklab, var(--colors-gray-DEFAULT) 15%, transparent);}\n.hover\\:border-gray\\/10:hover{border-color:color-mix(in oklab, var(--colors-gray-DEFAULT) 10%, transparent);}\n.focus\\:border-primary-500:focus{border-color:color-mix(in oklab, var(--colors-primary-500) var(--un-border-opacity), transparent);}\n.focus\\:border-primary\\/40:focus{border-color:color-mix(in oklab, var(--colors-primary-DEFAULT) 40%, transparent);}\n.bg-amber{background-color:color-mix(in oklab, var(--colors-amber-DEFAULT) var(--un-bg-opacity), transparent);}\n.bg-amber\\/10{background-color:color-mix(in oklab, var(--colors-amber-DEFAULT) 10%, transparent);}\n.bg-black\\/30{background-color:color-mix(in oklab, var(--colors-black) 30%, transparent);}\n.bg-blue{background-color:color-mix(in oklab, var(--colors-blue-DEFAULT) var(--un-bg-opacity), transparent);}\n.bg-blue\\/10{background-color:color-mix(in oklab, var(--colors-blue-DEFAULT) 10%, transparent);}\n.bg-gray{background-color:color-mix(in oklab, var(--colors-gray-DEFAULT) var(--un-bg-opacity), transparent);}\n.bg-gray\\/10{background-color:color-mix(in oklab, var(--colors-gray-DEFAULT) 10%, transparent);}\n.bg-gray\\/20{background-color:color-mix(in oklab, var(--colors-gray-DEFAULT) 20%, transparent);}\n.bg-gray\\/3{background-color:color-mix(in oklab, var(--colors-gray-DEFAULT) 3%, transparent);}\n.bg-gray\\/30{background-color:color-mix(in oklab, var(--colors-gray-DEFAULT) 30%, transparent);}\n.bg-gray\\/5{background-color:color-mix(in oklab, var(--colors-gray-DEFAULT) 5%, transparent);}\n.bg-green{background-color:color-mix(in oklab, var(--colors-green-DEFAULT) var(--un-bg-opacity), transparent);}\n.bg-orange\\/10{background-color:color-mix(in oklab, var(--colors-orange-DEFAULT) 10%, transparent);}\n.bg-primary{background-color:color-mix(in oklab, var(--colors-primary-DEFAULT) var(--un-bg-opacity), transparent);}\n.bg-primary-500\\/20{background-color:color-mix(in oklab, var(--colors-primary-500) 20%, transparent);}\n.bg-primary-600{background-color:color-mix(in oklab, var(--colors-primary-600) var(--un-bg-opacity), transparent);}\n.bg-primary\\/10{background-color:color-mix(in oklab, var(--colors-primary-DEFAULT) 10%, transparent);}\n.bg-primary\\/15{background-color:color-mix(in oklab, var(--colors-primary-DEFAULT) 15%, transparent);}\n.bg-primary\\/20{background-color:color-mix(in oklab, var(--colors-primary-DEFAULT) 20%, transparent);}\n.bg-red{background-color:color-mix(in oklab, var(--colors-red-DEFAULT) var(--un-bg-opacity), transparent);}\n.bg-red-500\\/12{background-color:color-mix(in oklab, var(--colors-red-500) 12%, transparent);}\n.bg-red\\/10{background-color:color-mix(in oklab, var(--colors-red-DEFAULT) 10%, transparent);}\n.bg-white{background-color:color-mix(in oklab, var(--colors-white) var(--un-bg-opacity), transparent);}\n.bg-white\\/50{background-color:color-mix(in oklab, var(--colors-white) 50%, transparent);}\n.dark .dark\\:bg-black\\/30{background-color:color-mix(in oklab, var(--colors-black) 30%, transparent);}\n.dark .dark\\:bg-black\\/45{background-color:color-mix(in oklab, var(--colors-black) 45%, transparent);}\n.hover\\:bg-gray\\/10:hover{background-color:color-mix(in oklab, var(--colors-gray-DEFAULT) 10%, transparent);}\n.hover\\:bg-gray\\/15:hover{background-color:color-mix(in oklab, var(--colors-gray-DEFAULT) 15%, transparent);}\n.hover\\:bg-gray\\/20:hover{background-color:color-mix(in oklab, var(--colors-gray-DEFAULT) 20%, transparent);}\n.hover\\:bg-gray\\/5:hover{background-color:color-mix(in oklab, var(--colors-gray-DEFAULT) 5%, transparent);}\n.hover\\:bg-orange\\/20:hover{background-color:color-mix(in oklab, var(--colors-orange-DEFAULT) 20%, transparent);}\n.hover\\:bg-primary-700:hover{background-color:color-mix(in oklab, var(--colors-primary-700) var(--un-bg-opacity), transparent);}\n.hover\\:bg-primary\\/20:hover{background-color:color-mix(in oklab, var(--colors-primary-DEFAULT) 20%, transparent);}\n.hover\\:bg-primary\\/25:hover{background-color:color-mix(in oklab, var(--colors-primary-DEFAULT) 25%, transparent);}\n.hover\\:bg-red-500\\/20:hover{background-color:color-mix(in oklab, var(--colors-red-500) 20%, transparent);}\n.hover\\:bg-red\\/20:hover{background-color:color-mix(in oklab, var(--colors-red-DEFAULT) 20%, transparent);}\n.ring-purple\\/50{--un-ring-color:color-mix(in oklab, var(--colors-purple-DEFAULT) 50%, transparent);}\n.focus\\:ring-primary-500\\/25:focus{--un-ring-color:color-mix(in oklab, var(--colors-primary-500) 25%, transparent);}\n.focus\\:ring-primary\\/30:focus{--un-ring-color:color-mix(in oklab, var(--colors-primary-DEFAULT) 30%, transparent);}\n.focus-visible\\:ring-primary-500\\/30:focus-visible{--un-ring-color:color-mix(in oklab, var(--colors-primary-500) 30%, transparent);}\n.focus-visible\\:ring-primary-500\\/40:focus-visible{--un-ring-color:color-mix(in oklab, var(--colors-primary-500) 40%, transparent);}\n.focus-visible\\:ring-red-500\\/30:focus-visible{--un-ring-color:color-mix(in oklab, var(--colors-red-500) 30%, transparent);}\n.dark .dark\\:fill-white{fill:color-mix(in oklab, var(--colors-white) var(--un-fill-opacity), transparent);}\n.fill-black{fill:color-mix(in oklab, var(--colors-black) var(--un-fill-opacity), transparent);}\n}\n@media (min-width: 40rem){\n.sm\\:gap-2\\.5{gap:calc(var(--spacing) * 2.5);}\n}" diff --git a/packages/hub/src/node/diagnostics.ts b/packages/hub/src/node/diagnostics.ts index eaf01246..b09efe79 100644 --- a/packages/hub/src/node/diagnostics.ts +++ b/packages/hub/src/node/diagnostics.ts @@ -30,6 +30,10 @@ export const diagnostics = defineDiagnostics({ why: 'connectionMeta() was called before initHub finished initializing.', fix: 'Await `instance.ready` (or any request through `instance.handler`) before reading `connectionMeta()` — the WebSocket binding it describes is only known once initialization completes.', }, + DF8004: { + why: (p: { id: string }) => `Devframe id "${p.id}" is not a mountable URL segment — the hub mounts each frame at \`/\`.`, + fix: 'Ids become route segments, so they may only contain letters, digits, `_`, `-`, and `.` — `:` and `*` are route-pattern markers to the underlying router, and `/` would escape the segment. Set a route-safe `id` on the definition (e.g. `my_plugin` instead of `my:plugin`).', + }, DF8100: { why: (p: { id: string }) => `Dock with id "${p.id}" is already registered`, fix: 'Use the `force` parameter to overwrite an existing registration.', diff --git a/packages/hub/src/node/initiate.ts b/packages/hub/src/node/initiate.ts index 66f95c41..6feced74 100644 --- a/packages/hub/src/node/initiate.ts +++ b/packages/hub/src/node/initiate.ts @@ -5,10 +5,10 @@ import type { WsOriginRegistry } from 'devframe/rpc/transports/ws-server' import type { ConnectionMeta, DevframeDefinition, DevframeStorageScope, DevframeWsOptions, McpRouteOptions } from 'devframe/types' import type { IncomingMessage, Server as NodeHttpServer, ServerResponse } from 'node:http' import type { ClientScriptEntry } from '../types/docks' -import type { DevframeHubContext } from './context' -import { createReadStream } from 'node:fs' +import type { CreateHubContextOptions, DevframeHubContext } from './context' +import type { MountDevframeOptions } from './mount-devframe' +import { readFile } from 'node:fs/promises' import process from 'node:process' -import { Readable } from 'node:stream' import { DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_DOCK_IMPORTS_FILENAME, DEVFRAME_MCP_ROUTE, DEVFRAME_WS_ROUTE } from 'devframe/constants' import { createH3DevframeHost, startHttpAndWs } from 'devframe/node' import { createInteractiveAuth } from 'devframe/recipes/interactive-auth' @@ -21,6 +21,17 @@ import { createHubContext } from './context' import { diagnostics } from './diagnostics' import { mountDevframe } from './mount-devframe' +/** A `devframes` entry with per-mount dock customization. */ +export interface HubDevframeEntry { + devframe: DevframeDefinition + /** Per-mount overrides for the auto-synthesized iframe dock entry. */ + dock?: MountDevframeOptions['dock'] +} + +function normalizeDevframeEntry(entry: DevframeDefinition | HubDevframeEntry): HubDevframeEntry { + return 'devframe' in entry ? entry : { devframe: entry } +} + /** Default mount base for a hub instance — one namespace, one catch-all. */ export const DEVFRAMES_HUB_BASE = '/__devframes/' @@ -73,8 +84,17 @@ export interface InitHubOptions { * Devframes to mount: each runs its `setup()` against the shared hub * context (one merged RPC registry, one WebSocket, one auth gate), serves * its SPA at `/`, and is auto-registered as an iframe dock. + * Wrap an entry in `{ devframe, dock }` to customize its synthesized dock + * (category, icon, a `clientScript` to run in the host page, …). */ - devframes?: DevframeDefinition[] + devframes?: (DevframeDefinition | HubDevframeEntry)[] + /** + * Extra RPC declarations registered at context creation, alongside the + * hub built-ins — forwarded to `createHubContext`'s + * `builtinRpcDeclarations`. Declarative mode only (a pre-built `context` + * already made this choice). + */ + rpcDeclarations?: CreateHubContextOptions['builtinRpcDeclarations'] /** * Bring your own hub context instead of `devframes` — for hosts that * assemble `createHubContext` + `mountDevframe` themselves (with their own @@ -185,7 +205,7 @@ function hubRegistry(): Map { function optionsHash(options: InitHubOptions): string { return JSON.stringify({ base: options.base, - devframes: options.devframes?.map(d => d.id), + devframes: options.devframes?.map(entry => normalizeDevframeEntry(entry).devframe.id), context: options.context != null, ui: options.ui && { viewer: options.ui.viewer?.distDir, @@ -327,17 +347,29 @@ function instantiateHub(options: InitHubOptions): HubInstance { app.use(joinURL(frameBase, DEVFRAME_CONNECTION_META_FILENAME), () => meta) }, } - ctx = await createHubContext({ cwd, workspaceRoot: cwd, mode: 'dev', host }) + ctx = await createHubContext({ + cwd, + workspaceRoot: cwd, + mode: 'dev', + host, + ...(options.rpcDeclarations ? { builtinRpcDeclarations: options.rpcDeclarations } : {}), + }) } // Mount each devframe under `/` — its SPA, its meta, and its // auto-registered iframe dock — after guarding the id against the // reserved hub filenames that live directly under the base. - for (const def of options.devframes ?? []) { + for (const entry of options.devframes ?? []) { + const { devframe: def, dock } = normalizeDevframeEntry(entry) if ((RESERVED_HUB_PATHS as readonly string[]).includes(def.id)) throw diagnostics.DF8000({ id: def.id }) + // The id becomes a URL segment (`/`) routed by h3 — `:` and + // `*` are route-pattern markers there, and separators would escape the + // segment entirely. + if (!/^[\w.-]+$/.test(def.id)) + throw diagnostics.DF8004({ id: def.id }) const frameBase = withTrailingSlash(joinURL(base, def.id)) - await mountDevframe(ctx, def, { base: frameBase }) + await mountDevframe(ctx, def, { base: frameBase, ...(dock ? { dock } : {}) }) frames.push({ id: def.id, base: frameBase, title: def.name }) } @@ -413,15 +445,31 @@ function instantiateHub(options: InitHubOptions): HubInstance { websocketMeta = ws.url } else if (typeof (globalThis as any).Bun === 'undefined') { - const port = await getPort({ port: 9777, portRange: [9777, 9877], host: options.host ?? 'localhost' }) - started = await startHttpAndWs({ - context: ctx, - host: options.host ?? 'localhost', - port, - path: withLeadingSlash(route), - auth: resolvedAuth, - allowedOrigins: options.allowedOrigins, - }) + // Eager auto side-car. `getPort` probes and the bind can still race + // (or disagree across the v4/v6 duals of `localhost`), so a failed + // bind retries on a fresh random port instead of failing init. + const sidecarHost = options.host ?? 'localhost' + let lastError: unknown + for (let attempt = 0; attempt < 3 && !started; attempt++) { + const port = attempt === 0 + ? await getPort({ port: 9777, portRange: [9777, 9877], host: sidecarHost }) + : await getPort({ random: true, host: sidecarHost }) + try { + started = await startHttpAndWs({ + context: ctx, + host: sidecarHost, + port, + path: withLeadingSlash(route), + auth: resolvedAuth, + allowedOrigins: options.allowedOrigins, + }) + } + catch (error) { + lastError = error + } + } + if (!started) + throw lastError websocketMeta = { port: started.port, path: route } } else { @@ -467,10 +515,13 @@ function instantiateHub(options: InitHubOptions): HubInstance { if (options.ui?.embedded) { const entry = resolve(options.ui.embedded.entry) - app.use(joinURL(base, 'embedded.js'), (event) => { + // Buffered rather than streamed: the bootstrap is a single ~1 MB file + // read per request in dev, and a buffered body survives every host's + // request bridging (dev-server worker proxies included). + app.use(joinURL(base, 'embedded.js'), async (event) => { event.res.headers.set('Content-Type', 'text/javascript; charset=utf-8') event.res.headers.set('Cache-Control', 'no-store') - return Readable.toWeb(createReadStream(entry)) as ReadableStream + return await readFile(entry) }) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e0f2bca3..c0c61012 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,6 +40,9 @@ catalogs: specifier: ^2.11.14 version: 2.11.14 deps: + '@hono/node-server': + specifier: ^1.19.9 + version: 1.19.17 '@json-render/core': specifier: ^0.19.0 version: 0.19.0 @@ -70,6 +73,9 @@ catalogs: h3: specifier: ^2.0.1-rc.26 version: 2.0.1-rc.26 + hono: + specifier: ^4.11.9 + version: 4.13.0 image-meta: specifier: ^0.2.2 version: 0.2.2 @@ -85,6 +91,9 @@ catalogs: mrmime: specifier: ^2.0.1 version: 2.0.1 + nitro: + specifier: ^3.0.260610-beta + version: 3.0.260610-beta nostics: specifier: ^1.2.0 version: 1.2.0 @@ -532,6 +541,37 @@ importers: specifier: catalog:deps version: 8.21.2 + examples/hono-devframe-hub: + dependencies: + '@devframes/hub': + specifier: workspace:* + version: link:../../packages/hub + '@devframes/hub-ui': + specifier: workspace:* + version: link:../../packages/hub-ui + '@devframes/plugin-inspect': + specifier: workspace:* + version: link:../../plugins/inspect + '@devframes/plugin-messages': + specifier: workspace:* + version: link:../../plugins/messages + '@hono/node-server': + specifier: catalog:deps + version: 1.19.17(hono@4.13.0) + devframe: + specifier: workspace:* + version: link:../../packages/devframe + hono: + specifier: catalog:deps + version: 4.13.0 + devDependencies: + '@types/node': + specifier: catalog:types + version: 26.1.2 + tsx: + specifier: catalog:build + version: 4.23.6 + examples/json-render: dependencies: '@devframes/json-render': @@ -690,6 +730,31 @@ importers: specifier: catalog:deps version: 8.21.2 + examples/nitro-devframe-hub: + dependencies: + '@devframes/hub': + specifier: workspace:* + version: link:../../packages/hub + '@devframes/hub-ui': + specifier: workspace:* + version: link:../../packages/hub-ui + '@devframes/plugin-inspect': + specifier: workspace:* + version: link:../../plugins/inspect + '@devframes/plugin-messages': + specifier: workspace:* + version: link:../../plugins/messages + devframe: + specifier: workspace:* + version: link:../../packages/devframe + nitro: + specifier: catalog:deps + version: 3.0.260610-beta(chokidar@5.0.0)(dotenv@17.4.2)(giget@3.3.0)(ioredis@5.10.1(supports-color@10.2.2))(jiti@2.7.0)(lru-cache@11.3.6)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.6)(yaml@2.9.0)) + devDependencies: + '@types/node': + specifier: catalog:types + version: 26.1.2 + examples/streaming-chat: dependencies: '@antfu/design': @@ -796,9 +861,6 @@ importers: '@vitejs/plugin-vue': specifier: catalog:build version: 6.0.8(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.6)(yaml@2.9.0))(vue@3.5.40(typescript@6.0.3)) - get-port-please: - specifier: catalog:deps - version: 3.2.0 pathe: specifier: catalog:deps version: 2.0.3 @@ -2450,6 +2512,12 @@ packages: '@floating-ui/vue@1.1.11': resolution: {integrity: sha512-HzHKCNVxnGS35r9fCHBc3+uCnjw9IWIlCPL683cGgM9Kgj2BiAl8x1mS7vtvP6F9S/e/q4O6MApwSHj8hNLGfw==} + '@hono/node-server@1.19.17': + resolution: {integrity: sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} @@ -6426,6 +6494,24 @@ packages: resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} engines: {node: '>=20.19.0'} + env-runner@0.1.16: + resolution: {integrity: sha512-2LRJM4P2KLX6J83QZZrMqvgCDt/D5ea7wPcI3yYiy5cG/9rX5QwdwZFx0D7ktWnjdRyZxYjttGGorb5nFqb1CA==} + hasBin: true + peerDependencies: + '@netlify/runtime': ^4.1.23 + '@vercel/queue': '>=0.2.0' + miniflare: ^4.20260515.0 + wrangler: ^4.0.0 + peerDependenciesMeta: + '@netlify/runtime': + optional: true + '@vercel/queue': + optional: true + miniflare: + optional: true + wrangler: + optional: true + error-stack-parser-es@1.0.5: resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} @@ -6964,6 +7050,16 @@ packages: h3@1.15.11: resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==} + h3@2.0.1-rc.22: + resolution: {integrity: sha512-Esv0DMIuPkCTSWCA0vO73vcTqwzH1wjSrAO1TXNu/K3up1sZHa9EKMapbmxCDYBeymC3fVTk4qxp7ogQWQ+KgA==} + engines: {node: '>=20.11.1'} + hasBin: true + peerDependencies: + crossws: ^0.4.10 + peerDependenciesMeta: + crossws: + optional: true + h3@2.0.1-rc.26: resolution: {integrity: sha512-GDxlvDsKxgjRvG5UBRJYyGJTMWLV30CJ4cV+e7QCTgftDHihrvio1fVPbNembhEr6J4WNm8IWy3fookgyTweLw==} engines: {node: '>=20.11.1'} @@ -7006,6 +7102,10 @@ packages: resolution: {integrity: sha512-H7ph5MF3lrc8BE47hhyHi+ZgoRiGi5kqxnA5Ou6rg9dVHtPnmUU8/Man19X/H3Cj1C/DF+jsN/ch9eV7nfpzMQ==} engines: {node: '>=8.0.0'} + hono@4.13.0: + resolution: {integrity: sha512-jhunvfHWxd7J5EFfSgH4xsYJzSe/lfqbUCxiyyeaQasUsXeEHXtzVid+7EOGByc5JnFa23SSFL3Y2RV/z1T+eQ==} + engines: {node: '>=16.9.0'} + hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} @@ -7036,6 +7136,9 @@ packages: httpxy@0.5.1: resolution: {integrity: sha512-JPhqYiixe1A1I+MXDewWDZqeudBGU8Q9jCHYN8ML+779RQzLjTi78HBvWz4jMxUD6h2/vUL12g4q/mFM0OUw1A==} + httpxy@0.5.5: + resolution: {integrity: sha512-uDjmnPyp1q4Sgzf3w+J/Fc6UqcCEj0x4Wjp7OqK5dGhNeDgpyrAmnS6ey8QWrX3SWDon2DMKf9sBa5X9+CVyMA==} + human-signals@5.0.0: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} @@ -7797,6 +7900,40 @@ packages: sass: optional: true + nf3@0.3.23: + resolution: {integrity: sha512-RWVLAWozmVD3AaDmaU3qMGB3v+yNlH5d9qqStI4e/WLlNQVnJ4YErGDbYCIrGFyrHdbF6I6Baf0Ae6c7tFYmSg==} + + nitro@3.0.260610-beta: + resolution: {integrity: sha512-KPb4L5yaF/Rx/xoGMpgHRJvZhbhGiqbRKOwwPLCH9jKTKTsEUHLjnJas85AeCzaswqa8Wi52eQBtRsODC4PS0Q==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@vercel/queue': ^0.3.0 + dotenv: '*' + giget: '*' + jiti: ^2.7.0 + rollup: ^4.61.1 + vite: ^7 || ^8 + xml2js: ^0.6.2 + zephyr-agent: ^0.2.0 + peerDependenciesMeta: + '@vercel/queue': + optional: true + dotenv: + optional: true + giget: + optional: true + jiti: + optional: true + rollup: + optional: true + vite: + optional: true + xml2js: + optional: true + zephyr-agent: + optional: true + nitropack@2.13.4: resolution: {integrity: sha512-tX7bT6zxNeMwkc6hxHiZeUoTOjVrcjoh1Z3cmxOlodIqjl4HISgqfGOmkWSayky3Nv9Z5+KQH52F8nmXJY5AAA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -7898,6 +8035,9 @@ packages: resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} engines: {node: '>=12.20.0'} + ocache@0.1.5: + resolution: {integrity: sha512-kNNnkkVQup/QDvmTz8Q84wc2ntiyoVHDxa6eHWKt5qdGAmFRBIxy83rxgCYEjW0x06UJ9E3P6VgM2yY4rOBH4w==} + ofetch@1.5.1: resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} @@ -8551,6 +8691,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rou3@0.8.1: + resolution: {integrity: sha512-ePa+XGk00/3HuCqrEnK3LxJW7I0SdNg6EFzKUJG73hMAdDcOUC/i/aSz7LSDwLrGr33kal/rqOGydzwl6U7zBA==} + rou3@0.9.1: resolution: {integrity: sha512-z/sSmzvtwMDDnxsPVhfWMuG6F6mbmhFDXoVqLmMfbpDD9qfV3GDmSQpf0+W296/ZDIpW2wcMmBfpVFzcnOi/nA==} @@ -9337,6 +9480,80 @@ packages: uploadthing: optional: true + unstorage@2.0.0-alpha.7: + resolution: {integrity: sha512-ELPztchk2zgFJnakyodVY3vJWGW9jy//keJ32IOJVGUMyaPydwcA1FtVvWqT0TNRch9H+cMNEGllfVFfScImog==} + peerDependencies: + '@azure/app-configuration': ^1.11.0 + '@azure/cosmos': ^4.9.1 + '@azure/data-tables': ^13.3.2 + '@azure/identity': ^4.13.0 + '@azure/keyvault-secrets': ^4.10.0 + '@azure/storage-blob': ^12.31.0 + '@capacitor/preferences': ^6 || ^7 || ^8 + '@deno/kv': '>=0.13.0' + '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 + '@planetscale/database': ^1.19.0 + '@upstash/redis': ^1.36.2 + '@vercel/blob': '>=0.27.3' + '@vercel/functions': ^2.2.12 || ^3.0.0 + '@vercel/kv': ^1.0.1 + aws4fetch: ^1.0.20 + chokidar: ^5.0.0 + db0: '>=0.3.4' + idb-keyval: ^6.2.2 + ioredis: ^5.9.3 + lru-cache: ^11.2.6 + mongodb: ^6 || ^7 + ofetch: '*' + uploadthing: ^7.7.4 + peerDependenciesMeta: + '@azure/app-configuration': + optional: true + '@azure/cosmos': + optional: true + '@azure/data-tables': + optional: true + '@azure/identity': + optional: true + '@azure/keyvault-secrets': + optional: true + '@azure/storage-blob': + optional: true + '@capacitor/preferences': + optional: true + '@deno/kv': + optional: true + '@netlify/blobs': + optional: true + '@planetscale/database': + optional: true + '@upstash/redis': + optional: true + '@vercel/blob': + optional: true + '@vercel/functions': + optional: true + '@vercel/kv': + optional: true + aws4fetch: + optional: true + chokidar: + optional: true + db0: + optional: true + idb-keyval: + optional: true + ioredis: + optional: true + lru-cache: + optional: true + mongodb: + optional: true + ofetch: + optional: true + uploadthing: + optional: true + untun@0.1.3: resolution: {integrity: sha512-4luGP9LMYszMRZwsvyUd9MrxgEGZdZuZgpVQHEEX0lCYFESasVRvZd0EYpCkOIbJKHMuv0LskpXc/8Un+MJzEQ==} hasBin: true @@ -10485,6 +10702,10 @@ snapshots: - '@vue/composition-api' - vue + '@hono/node-server@1.19.17(hono@4.13.0)': + dependencies: + hono: 4.13.0 + '@humanfs/core@0.19.2': dependencies: '@humanfs/types': 0.15.0 @@ -14275,6 +14496,13 @@ snapshots: entities@8.0.0: {} + env-runner@0.1.16: + dependencies: + crossws: 0.4.10(srvx@0.11.22) + exsolve: 1.1.0 + httpxy: 0.5.5 + srvx: 0.11.22 + error-stack-parser-es@1.0.5: {} errx@0.1.0: {} @@ -14886,6 +15114,13 @@ snapshots: transitivePeerDependencies: - srvx + h3@2.0.1-rc.22(crossws@0.4.10(srvx@0.11.22)): + dependencies: + rou3: 0.8.1 + srvx: 0.11.22 + optionalDependencies: + crossws: 0.4.10(srvx@0.11.22) + h3@2.0.1-rc.26(crossws@0.4.10(srvx@0.12.4)): dependencies: rou3: 0.9.1 @@ -14931,6 +15166,8 @@ snapshots: dependencies: ansi-styles: 3.2.1 + hono@4.13.0: {} + hookable@5.5.3: {} hookable@6.1.1: {} @@ -14960,6 +15197,8 @@ snapshots: httpxy@0.5.1: {} + httpxy@0.5.5: {} + human-signals@5.0.0: {} iconv-lite@0.6.3: @@ -15901,6 +16140,61 @@ snapshots: - '@types/node' - babel-plugin-macros + nf3@0.3.23: {} + + nitro@3.0.260610-beta(chokidar@5.0.0)(dotenv@17.4.2)(giget@3.3.0)(ioredis@5.10.1(supports-color@10.2.2))(jiti@2.7.0)(lru-cache@11.3.6)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.6)(yaml@2.9.0)): + dependencies: + consola: 3.4.2 + crossws: 0.4.10(srvx@0.11.22) + db0: 0.3.4 + env-runner: 0.1.16 + h3: 2.0.1-rc.22(crossws@0.4.10(srvx@0.11.22)) + hookable: 6.1.1 + nf3: 0.3.23 + ocache: 0.1.5 + ofetch: 2.0.0-alpha.3 + ohash: 2.0.11 + rolldown: 1.2.0 + srvx: 0.11.22 + unenv: 2.0.0-rc.24 + unstorage: 2.0.0-alpha.7(chokidar@5.0.0)(db0@0.3.4)(ioredis@5.10.1(supports-color@10.2.2))(lru-cache@11.3.6)(ofetch@2.0.0-alpha.3) + optionalDependencies: + dotenv: 17.4.2 + giget: 3.3.0 + jiti: 2.7.0 + vite: 8.2.0(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.6)(yaml@2.9.0) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@electric-sql/pglite' + - '@libsql/client' + - '@netlify/blobs' + - '@netlify/runtime' + - '@planetscale/database' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - better-sqlite3 + - chokidar + - drizzle-orm + - idb-keyval + - ioredis + - lru-cache + - miniflare + - mongodb + - mysql2 + - sqlite3 + - uploadthing + - wrangler + nitropack@2.13.4(oxc-parser@0.140.0)(rolldown@1.2.0)(srvx@0.11.22)(supports-color@10.2.2)(vite@8.2.0(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.6)(yaml@2.9.0)): dependencies: '@cloudflare/kv-asset-handler': 0.4.2 @@ -16210,6 +16504,10 @@ snapshots: obug@2.1.4: {} + ocache@0.1.5: + dependencies: + ohash: 2.0.11 + ofetch@1.5.1: dependencies: destr: 2.0.5 @@ -17062,6 +17360,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.60.3 fsevents: 2.3.3 + rou3@0.8.1: {} + rou3@0.9.1: {} roughjs@4.6.6: @@ -17926,6 +18226,14 @@ snapshots: transitivePeerDependencies: - srvx + unstorage@2.0.0-alpha.7(chokidar@5.0.0)(db0@0.3.4)(ioredis@5.10.1(supports-color@10.2.2))(lru-cache@11.3.6)(ofetch@2.0.0-alpha.3): + optionalDependencies: + chokidar: 5.0.0 + db0: 0.3.4 + ioredis: 5.10.1(supports-color@10.2.2) + lru-cache: 11.3.6 + ofetch: 2.0.0-alpha.3 + untun@0.1.3: dependencies: citty: 0.1.6 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2ebb215a..99fdb994 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -71,6 +71,7 @@ catalogs: vite: ^8.2.0 vite-plugin-solid: ^2.11.14 deps: + '@hono/node-server': ^1.19.9 '@json-render/core': ^0.19.0 '@modelcontextprotocol/client': ^2.0.0 '@modelcontextprotocol/server': ^2.0.0 @@ -83,11 +84,13 @@ catalogs: destr: ^2.0.5 get-port-please: ^3.2.0 h3: ^2.0.1-rc.26 + hono: ^4.11.9 image-meta: ^0.2.2 immer: ^11.1.15 jora: ^1.0.0-beta.16 launch-editor: ^2.14.1 mrmime: ^2.0.1 + nitro: ^3.0.260610-beta nostics: ^1.2.0 obug: ^2.1.4 ohash: ^2.0.11 diff --git a/scripts/smoke-bun.ts b/scripts/smoke-bun.ts new file mode 100644 index 00000000..3cac24bc --- /dev/null +++ b/scripts/smoke-bun.ts @@ -0,0 +1,75 @@ +/** + * Bun smoke test for the fetch-upgrade WebSocket tier — run locally with: + * + * bun scripts/smoke-bun.ts + * + * Boots the Hono example's hub under `Bun.serve` and exercises the three + * surfaces end to end on Bun: HTTP through the catch-all handler, the + * embedded bootstrap, and an RPC round-trip over a same-origin WebSocket + * upgrade (no side-car port anywhere). + * + * Prerequisites: `pnpm install && pnpm build` (the hub serves built dists). + */ +import process from 'node:process' +import { app, hub } from '../examples/hono-devframe-hub/src/app' + +function fail(step: string, detail: unknown): never { + console.error(`✗ ${step}:`, detail) + process.exit(1) +} + +const server = Bun.serve({ + port: 0, + fetch: app.fetch, + websocket: hub.websocket as never, +}) +await hub.ready +const origin = `http://localhost:${server.port}` +console.log(`serving on ${origin}`) + +// 1. Discovery through the fetch handler — the Bun tier advertises a +// same-origin relative path (no side-car port). +const meta = await (await fetch(`${origin}/__devframes/__connection.json`)).json() as { + backend: string + websocket: { path?: string, port?: number } +} +if (meta.backend !== 'websocket' || !meta.websocket?.path || meta.websocket.port) + fail('connection meta', meta) +console.log('✓ __connection.json advertises the same-origin socket:', JSON.stringify(meta.websocket)) + +// 2. The index document and a frame SPA. +const index = await (await fetch(`${origin}/__devframes/__index.json`)).json() as { frames: { id: string, base: string }[] } +if (!index.frames.some(frame => frame.id === 'devframes_plugin_inspect')) + fail('__index.json', index) +console.log('✓ __index.json lists', index.frames.map(frame => frame.id).join(', ')) +const frame = await fetch(`${origin}${index.frames[0]!.base}`) +if (!frame.ok) + fail('frame SPA', frame.status) +console.log('✓ frame SPA serves:', index.frames[0]!.base, frame.status) + +// 3. The embedded bootstrap from the ui slot. +const embedded = await fetch(`${origin}/__devframes/embedded.js`) +const embeddedJs = await embedded.text() +if (!embedded.ok || !embeddedJs.includes('devframes-dock-embedded')) + fail('embedded.js', embedded.status) +console.log(`✓ embedded.js serves (${(embeddedJs.length / 1024).toFixed(0)} kB)`) + +// 4. RPC round-trip over a same-origin WebSocket upgrade. +const { createRpcClient } = await import('devframe/rpc/client') +const { createWsRpcChannel } = await import('devframe/rpc/transports/ws-client') +const rpc = createRpcClient({}, { + channel: createWsRpcChannel({ url: `ws://localhost:${server.port}${meta.websocket.path}` }), +}) +const handshake = await rpc.$call('anonymous:devframe:auth', { authToken: '', ua: 'smoke', origin }) as { isTrusted: boolean } +if (!handshake.isTrusted) + fail('handshake', handshake) +const pong = await rpc.$call('example:hono-devframe-hub:probe') +if (pong !== 'pong') + fail('rpc probe', pong) +console.log('✓ WS RPC round-trip over the fetch-upgrade tier: probe →', pong) +rpc.$close?.() + +await hub.close() +server.stop(true) +console.log('\nBun smoke test passed.') +process.exit(0) diff --git a/tests/__snapshots__/tsnapi/@devframes/hub/initiate.snapshot.d.ts b/tests/__snapshots__/tsnapi/@devframes/hub/initiate.snapshot.d.ts index afed50f3..8e7d6008 100644 --- a/tests/__snapshots__/tsnapi/@devframes/hub/initiate.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/@devframes/hub/initiate.snapshot.d.ts @@ -10,6 +10,10 @@ export interface DevframeHubUi { entry: string; }; } +export interface HubDevframeEntry { + devframe: DevframeDefinition; + dock?: MountDevframeOptions['dock']; +} export interface HubInstance { handler: (_: Request, _?: unknown) => Promise; nodeMiddleware: (_: IncomingMessage, _: ServerResponse, _?: (_?: unknown) => void) => void; @@ -26,7 +30,8 @@ export interface HubInstance { } export interface InitHubOptions { base?: string; - devframes?: DevframeDefinition[]; + devframes?: (DevframeDefinition | HubDevframeEntry)[]; + rpcDeclarations?: CreateHubContextOptions['builtinRpcDeclarations']; context?: DevframeHubContext; configure?: (_: DevframeHubContext) => void | Promise; ui?: DevframeHubUi; diff --git a/tests/__snapshots__/tsnapi/@devframes/hub/node.snapshot.d.ts b/tests/__snapshots__/tsnapi/@devframes/hub/node.snapshot.d.ts index 9279a3e3..f0601f65 100644 --- a/tests/__snapshots__/tsnapi/@devframes/hub/node.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/@devframes/hub/node.snapshot.d.ts @@ -1,13 +1,6 @@ /** * Generated by tsnapi — public API snapshot of `@devframes/hub/node` */ -// #region Interfaces -export interface MountDevframeOptions { - base?: string; - dock?: Partial>; -} -// #endregion - // #region Classes export declare class DevframeCommandsHost implements DevframeCommandsHost$1 { readonly context: DevframeHubContext; @@ -89,7 +82,6 @@ export declare class DevframeTerminalsHost implements DevframeTerminalsHost$1 { // #region Functions export declare function createSimpleClientScript(_: string | ((_: any) => void)): ClientScriptEntry; -export declare function mountDevframe(_: DevframeHubContext, _: DevframeDefinition, _?: MountDevframeOptions): Promise; // #endregion // #region Variables @@ -281,4 +273,6 @@ export { createHubContext } export { CreateHubContextOptions } export { DEFAULT_CATEGORIES_ORDER } export { DevframeHubContext } +export { mountDevframe } +export { MountDevframeOptions } // #endregion \ No newline at end of file