terra-mcp-google 0.1.14 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,6 +18,7 @@ terra-mcp client codex # print safe (read-only) MCP config
18
18
  | `auth login` / `logout` / `status` | sign in / out / show account, scopes, expiry |
19
19
  | `setup` | check config dir + auth, print MCP config for all clients |
20
20
  | `client [codex\|claude\|copilot\|kiro\|all]` | print MCP config with **mutating tools disabled** (`--include-dangerous` keeps them) |
21
+ | `admin` | open the web console to manage which resources the agent may access (`-p` port, `--no-open`) |
21
22
  | *(no command)* | start the stdio server |
22
23
 
23
24
  ## Configuration
@@ -31,6 +32,9 @@ Set these in your MCP client's `env` block (or the launching shell):
31
32
  | `GOOGLE_OAUTH_TOKEN` | `<TERRA_MCP_DIR>/token.json` | Cached access/refresh token |
32
33
  | `TERRA_MCP_SAFE_MODE` | unset | `1` → register **only read-only tools**; drop every mutating tool |
33
34
  | `TERRA_MCP_LOCAL_FILE_ROOT` | unset | Only directory `local_path`/`save_path` may touch. Unset = local file up/download disabled |
35
+ | `TERRA_MCP_POLICY_DB` | `<TERRA_MCP_DIR>/policy.db` | SQLite database holding the resource allowlist + policy mode |
36
+ | `TERRA_MCP_POLICY_MODE` | `read_open` | Initial mode before one is chosen in the console: `off` \| `read_open` \| `strict` |
37
+ | `TERRA_MCP_ADMIN_PORT` | `4717` | Port the `admin` web console listens on |
34
38
  | `TERRA_MCP_TOKEN_PROXY_URL` | bundled proxy | OAuth token-exchange proxy (advanced; only when self-hosting it) |
35
39
  | `TERRA_MCP_PROXY_KEY` | bundled key | Deterrent key sent to the proxy (ships in the package — not a secret) |
36
40
 
@@ -38,6 +42,33 @@ Set these in your MCP client's `env` block (or the launching shell):
38
42
  per-client with `terra-mcp client <agent>` (pass `--include-dangerous` to keep them on). Tools are
39
43
  also gated by the **OAuth scopes granted at login** — a Sheets-only grant exposes no `drive_*` tools.
40
44
 
45
+ ## Permission gate (per-resource allowlist)
46
+
47
+ Beyond on/off tool gating, the server enforces a **per-resource allowlist**: you decide exactly
48
+ which Drive files/folders and Sheets the agent may touch, and with what power (read / write / delete).
49
+ Every tool call is checked against the allowlist (stored in a small local SQLite database) before it
50
+ runs; created resources are auto-added so the agent can keep working with what it just made.
51
+
52
+ ```bash
53
+ terra-mcp admin # opens the web console at http://localhost:4717
54
+ ```
55
+
56
+ **Three modes** (switch any time in the console):
57
+
58
+ | Mode | The agent can… |
59
+ | --- | --- |
60
+ | `off` | …do anything your account can (gate disabled). |
61
+ | `read_open` *(default)* | …**read** anything and **create** new resources, but only **write/delete existing** resources you've granted. Useful out of the box — nothing to configure to start reading. |
62
+ | `strict` | …only **see and use** resources in the allowlist (and inside granted folders). Everything else is invisible, even to list/search. |
63
+
64
+ **Grants** carry independent read / write / delete flags (new grants default to read-only — the safe
65
+ starting point), and a grant on a **folder cascades** to everything inside it. Manage them — search
66
+ your Drive to pick a file, or paste an ID — in the `admin` console. The console and the running
67
+ server share the same database, so changes take effect on the agent's next call; no restart needed.
68
+
69
+ > Requires **Node 23.4+** (or Node 22.5+ launched with `--experimental-sqlite`) for the gate. On
70
+ > older Node the server still runs, with the gate disabled.
71
+
41
72
  ## Tools
42
73
 
43
74
  **Auth** — sign-in/out are CLI-only (`terra-mcp auth login`/`logout`).
@@ -0,0 +1,35 @@
1
+ import type { drive_v3 } from "googleapis";
2
+ import type { PolicyStore } from "../policy/store.js";
3
+ import type { ResourceKind } from "../policy/types.js";
4
+ /** A Drive resource as surfaced to the admin console's "search & pick" flow. */
5
+ export interface DriveItem {
6
+ id: string;
7
+ name: string;
8
+ mimeType: string;
9
+ kind: ResourceKind;
10
+ }
11
+ /** Collaborators the API routes need, injected so the router stays testable. */
12
+ export interface ApiDeps {
13
+ store: PolicyStore;
14
+ searchDrive: (query: string) => Promise<DriveItem[]>;
15
+ authInfo: () => Promise<{
16
+ signedIn: boolean;
17
+ email: string | null;
18
+ }>;
19
+ }
20
+ /** A plain HTTP-ish response the transport layer serializes. */
21
+ export interface ApiResponse {
22
+ status: number;
23
+ body: unknown;
24
+ }
25
+ /** Classify a Drive mimeType into the grant `kind` the gate understands. */
26
+ export declare function kindOfMime(mimeType: string | null | undefined): ResourceKind;
27
+ /** Build the `searchDrive` dependency backed by a live Drive client. */
28
+ export declare function driveSearcher(drive: drive_v3.Drive): (query: string) => Promise<DriveItem[]>;
29
+ /**
30
+ * The admin console's HTTP surface, as a pure function over (method, path, body)
31
+ * — no `node:http` objects — so it unit-tests against an in-memory store and
32
+ * fake deps. Returns `null` for non-`/api` paths so the caller can fall back to
33
+ * serving the SPA's static assets.
34
+ */
35
+ export declare function routeApi(method: string, path: string, query: URLSearchParams, body: unknown, deps: ApiDeps): Promise<ApiResponse | null>;
@@ -0,0 +1,106 @@
1
+ import { z } from "zod";
2
+ import { DriveFileAdapter } from "../services/drive/adapter.js";
3
+ const kindSchema = z.enum(["file", "folder", "spreadsheet"]);
4
+ const modeSchema = z.enum(["off", "read_open", "strict"]);
5
+ const createGrantSchema = z.object({
6
+ kind: kindSchema,
7
+ googleId: z.string().min(1),
8
+ name: z.string().nullish(),
9
+ canRead: z.boolean(),
10
+ canWrite: z.boolean(),
11
+ canDelete: z.boolean(),
12
+ });
13
+ const patchGrantSchema = z.object({
14
+ name: z.string().nullish(),
15
+ canRead: z.boolean().optional(),
16
+ canWrite: z.boolean().optional(),
17
+ canDelete: z.boolean().optional(),
18
+ });
19
+ /** Classify a Drive mimeType into the grant `kind` the gate understands. */
20
+ export function kindOfMime(mimeType) {
21
+ if (mimeType === "application/vnd.google-apps.folder")
22
+ return "folder";
23
+ if (mimeType === "application/vnd.google-apps.spreadsheet")
24
+ return "spreadsheet";
25
+ return "file";
26
+ }
27
+ /** Build the `searchDrive` dependency backed by a live Drive client. */
28
+ export function driveSearcher(drive) {
29
+ return async (query) => {
30
+ const trimmed = query.trim();
31
+ // Escape single quotes for the Drive `q` string literal.
32
+ const q = trimmed ? `name contains '${trimmed.replace(/'/g, "\\'")}'` : undefined;
33
+ const { files } = await new DriveFileAdapter(drive).listFiles({
34
+ query: q,
35
+ pageSize: 25,
36
+ orderBy: "modifiedTime desc",
37
+ includeTrashed: false,
38
+ });
39
+ return files
40
+ .filter((f) => typeof f.id === "string")
41
+ .map((f) => ({
42
+ id: f.id,
43
+ name: f.name ?? "(untitled)",
44
+ mimeType: f.mimeType ?? "application/octet-stream",
45
+ kind: kindOfMime(f.mimeType),
46
+ }));
47
+ };
48
+ }
49
+ /**
50
+ * The admin console's HTTP surface, as a pure function over (method, path, body)
51
+ * — no `node:http` objects — so it unit-tests against an in-memory store and
52
+ * fake deps. Returns `null` for non-`/api` paths so the caller can fall back to
53
+ * serving the SPA's static assets.
54
+ */
55
+ export async function routeApi(method, path, query, body, deps) {
56
+ if (path !== "/api" && !path.startsWith("/api/"))
57
+ return null;
58
+ try {
59
+ if (path === "/api/health" && method === "GET") {
60
+ const auth = await deps.authInfo();
61
+ return ok({ ok: true, mode: deps.store.getMode(), ...auth });
62
+ }
63
+ if (path === "/api/grants") {
64
+ if (method === "GET")
65
+ return ok({ grants: deps.store.listGrants() });
66
+ if (method === "POST") {
67
+ const input = createGrantSchema.parse(body);
68
+ return ok({ grant: deps.store.upsertGrant(input) }, 201);
69
+ }
70
+ }
71
+ const grantId = path.match(/^\/api\/grants\/(\d+)$/);
72
+ if (grantId) {
73
+ const id = Number(grantId[1]);
74
+ if (method === "PATCH") {
75
+ const patch = patchGrantSchema.parse(body);
76
+ const grant = deps.store.updateGrant(id, patch);
77
+ return grant ? ok({ grant }) : fail(404, "Grant not found");
78
+ }
79
+ if (method === "DELETE") {
80
+ return deps.store.deleteGrant(id) ? ok({ ok: true }) : fail(404, "Grant not found");
81
+ }
82
+ }
83
+ if (path === "/api/mode" && method === "PUT") {
84
+ const { mode } = z.object({ mode: modeSchema }).parse(body);
85
+ deps.store.setMode(mode);
86
+ return ok({ mode });
87
+ }
88
+ if (path === "/api/drive/search" && method === "GET") {
89
+ return ok({ files: await deps.searchDrive(query.get("q") ?? "") });
90
+ }
91
+ return fail(404, "Not found");
92
+ }
93
+ catch (error) {
94
+ if (error instanceof z.ZodError) {
95
+ return fail(400, error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; "));
96
+ }
97
+ return fail(500, error instanceof Error ? error.message : String(error));
98
+ }
99
+ }
100
+ function ok(body, status = 200) {
101
+ return { status, body };
102
+ }
103
+ function fail(status, error) {
104
+ return { status, body: { error } };
105
+ }
106
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/admin/api.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ export interface AdminServerHandle {
2
+ port: number;
3
+ url: string;
4
+ close: () => Promise<void>;
5
+ }
6
+ /**
7
+ * Start the admin web console: a tiny `node:http` server that exposes the policy
8
+ * REST API under `/api` and serves the built SPA for everything else. No
9
+ * framework, no extra runtime dependency.
10
+ */
11
+ export declare function startAdminServer(opts?: {
12
+ port?: number;
13
+ staticDir?: string;
14
+ }): Promise<AdminServerHandle>;
@@ -0,0 +1,138 @@
1
+ import { createServer } from "node:http";
2
+ import { readFile, stat } from "node:fs/promises";
3
+ import { fileURLToPath } from "node:url";
4
+ import { dirname, extname, join, normalize, resolve } from "node:path";
5
+ import { ADMIN_PORT, getAdminStaticDir } from "../config/constants.js";
6
+ import { getGoogleClients } from "../google/client.js";
7
+ import { getAuthStatus } from "../google/auth.js";
8
+ import { getPolicyStoreOrThrow } from "../policy/guard.js";
9
+ import { driveSearcher, routeApi } from "./api.js";
10
+ const HERE = dirname(fileURLToPath(import.meta.url));
11
+ const MIME = {
12
+ ".html": "text/html; charset=utf-8",
13
+ ".js": "text/javascript; charset=utf-8",
14
+ ".css": "text/css; charset=utf-8",
15
+ ".json": "application/json; charset=utf-8",
16
+ ".svg": "image/svg+xml",
17
+ ".png": "image/png",
18
+ ".jpg": "image/jpeg",
19
+ ".ico": "image/x-icon",
20
+ ".woff2": "font/woff2",
21
+ ".woff": "font/woff",
22
+ };
23
+ /** Build the API dependencies from the live policy store + Google clients. */
24
+ function buildDeps() {
25
+ return {
26
+ store: getPolicyStoreOrThrow(),
27
+ searchDrive: async (query) => driveSearcher((await getGoogleClients()).drive)(query),
28
+ authInfo: async () => {
29
+ const status = await getAuthStatus().catch(() => ({ authenticated: false, email: null }));
30
+ return { signedIn: status.authenticated, email: status.email ?? null };
31
+ },
32
+ };
33
+ }
34
+ /** First existing candidate directory holding the built SPA, or undefined. */
35
+ async function resolveStaticDir(override) {
36
+ const candidates = [
37
+ override,
38
+ getAdminStaticDir(),
39
+ join(HERE, "ui"), // production: copied beside the compiled server (dist/admin/ui)
40
+ join(HERE, "../../admin/dist"), // dev via tsx: the repo's built SPA
41
+ ].filter((c) => typeof c === "string");
42
+ const exists = await Promise.all(candidates.map(isDir));
43
+ return candidates.find((_, index) => exists[index]);
44
+ }
45
+ async function isDir(path) {
46
+ try {
47
+ return (await stat(path)).isDirectory();
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ }
53
+ async function readBody(req) {
54
+ const chunks = [];
55
+ for await (const chunk of req)
56
+ chunks.push(chunk);
57
+ if (chunks.length === 0)
58
+ return undefined;
59
+ const raw = Buffer.concat(chunks).toString("utf8");
60
+ return raw ? JSON.parse(raw) : undefined;
61
+ }
62
+ function sendJson(res, status, body) {
63
+ const payload = JSON.stringify(body);
64
+ res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
65
+ res.end(payload);
66
+ }
67
+ async function serveStatic(res, staticDir, urlPath) {
68
+ if (!staticDir) {
69
+ res.writeHead(503, { "content-type": "text/plain; charset=utf-8" });
70
+ res.end("Admin UI is not built. Run `pnpm admin:build` (or set TERRA_MCP_ADMIN_STATIC_DIR).");
71
+ return;
72
+ }
73
+ // Resolve the request to a file inside staticDir; fall back to index.html so
74
+ // the single-page app can handle its own routing. Reject path traversal.
75
+ const relative = normalize(urlPath === "/" ? "/index.html" : urlPath).replace(/^(\.\.[/\\])+/, "");
76
+ let filePath = resolve(staticDir, "." + relative);
77
+ if (!filePath.startsWith(resolve(staticDir)))
78
+ filePath = join(staticDir, "index.html");
79
+ if (!(await fileExists(filePath)))
80
+ filePath = join(staticDir, "index.html");
81
+ try {
82
+ const data = await readFile(filePath);
83
+ res.writeHead(200, { "content-type": MIME[extname(filePath)] ?? "application/octet-stream" });
84
+ res.end(data);
85
+ }
86
+ catch {
87
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
88
+ res.end("Not found");
89
+ }
90
+ }
91
+ async function fileExists(path) {
92
+ try {
93
+ return (await stat(path)).isFile();
94
+ }
95
+ catch {
96
+ return false;
97
+ }
98
+ }
99
+ /**
100
+ * Start the admin web console: a tiny `node:http` server that exposes the policy
101
+ * REST API under `/api` and serves the built SPA for everything else. No
102
+ * framework, no extra runtime dependency.
103
+ */
104
+ export async function startAdminServer(opts = {}) {
105
+ const port = opts.port ?? ADMIN_PORT;
106
+ const deps = buildDeps();
107
+ const staticDir = await resolveStaticDir(opts.staticDir);
108
+ return new Promise((resolvePromise, rejectPromise) => {
109
+ const server = createServer((req, res) => {
110
+ void handle(req, res, deps, staticDir);
111
+ });
112
+ server.on("error", rejectPromise);
113
+ server.listen(port, () => {
114
+ resolvePromise({
115
+ port,
116
+ url: `http://localhost:${port}`,
117
+ close: () => new Promise((done) => server.close(() => done())),
118
+ });
119
+ });
120
+ });
121
+ }
122
+ async function handle(req, res, deps, staticDir) {
123
+ const url = new URL(req.url ?? "/", "http://localhost");
124
+ const method = req.method ?? "GET";
125
+ try {
126
+ if (url.pathname === "/api" || url.pathname.startsWith("/api/")) {
127
+ const body = method === "GET" || method === "DELETE" ? undefined : await readBody(req);
128
+ const result = await routeApi(method, url.pathname, url.searchParams, body, deps);
129
+ if (result)
130
+ return sendJson(res, result.status, result.body);
131
+ }
132
+ await serveStatic(res, staticDir, url.pathname);
133
+ }
134
+ catch (error) {
135
+ sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
136
+ }
137
+ }
138
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/admin/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ /*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-content:""}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--spacing:.25rem;--container-xs:20rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--font-weight-medium:500;--leading-snug:1.375;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-4xl:2rem;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.\@container\/card-header{container:card-header/inline-size}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.absolute{position:absolute}.relative{position:relative}.right-2{right:calc(var(--spacing) * 2)}.z-10{z-index:10}.z-50{z-index:50}.col-start-2{grid-column-start:2}.row-span-2{grid-row:span 2/span 2}.row-start-1{grid-row-start:1}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.-mx-1{margin-inline:calc(var(--spacing) * -1)}.my-1{margin-block:calc(var(--spacing) * 1)}.mt-4{margin-top:calc(var(--spacing) * 4)}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.inline-flex{display:inline-flex}.table{display:table}.table-caption{display:table-caption}.table-cell{display:table-cell}.table-row{display:table-row}.size-2\.5{width:calc(var(--spacing) * 2.5);height:calc(var(--spacing) * 2.5)}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.size-6{width:calc(var(--spacing) * 6);height:calc(var(--spacing) * 6)}.size-7{width:calc(var(--spacing) * 7);height:calc(var(--spacing) * 7)}.size-8{width:calc(var(--spacing) * 8);height:calc(var(--spacing) * 8)}.size-9{width:calc(var(--spacing) * 9);height:calc(var(--spacing) * 9)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-7{height:calc(var(--spacing) * 7)}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-px{height:1px}.max-h-\(--radix-select-content-available-height\){max-height:var(--radix-select-content-available-height)}.w-fit{width:fit-content}.w-full{width:100%}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:calc(var(--spacing) * 0)}.min-w-7{min-width:calc(var(--spacing) * 7)}.min-w-8{min-width:calc(var(--spacing) * 8)}.min-w-9{min-width:calc(var(--spacing) * 9)}.min-w-36{min-width:calc(var(--spacing) * 36)}.shrink-0{flex-shrink:0}.caption-bottom{caption-side:bottom}.origin-\(--radix-select-content-transform-origin\){transform-origin:var(--radix-select-content-transform-origin)}.origin-\(--radix-tooltip-content-transform-origin\){transform-origin:var(--radix-tooltip-content-transform-origin)}.translate-y-\[calc\(-50\%_-_2px\)\]{--tw-translate-y: calc(-50% - 2px) ;translate:var(--tw-translate-x) var(--tw-translate-y)}.rotate-45{rotate:45deg}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-pulse{animation:var(--animate-pulse)}.cursor-default{cursor:default}.scroll-my-1{scroll-margin-block:calc(var(--spacing) * 1)}.auto-rows-min{grid-auto-rows:min-content}.flex-col{flex-direction:column}.flex-row{flex-direction:row}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-\(--card-spacing\){gap:var(--card-spacing)}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-\[--spacing\(var\(--gap\)\)\]{gap:calc(var(--spacing) * var(--gap))}.self-start{align-self:flex-start}.justify-self-end{justify-self:flex-end}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-x-hidden{overflow-x:hidden}.overflow-y-auto{overflow-y:auto}.rounded-4xl{border-radius:var(--radius-4xl)}.rounded-\[2px\]{border-radius:2px}.rounded-\[min\(var\(--radius-md\)\,10px\)\]{border-radius:min(var(--radius-md),10px)}.rounded-\[min\(var\(--radius-md\)\,12px\)\]{border-radius:min(var(--radius-md),12px)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-t-xl{border-top-left-radius:var(--radius-xl);border-top-right-radius:var(--radius-xl)}.rounded-b-xl{border-bottom-right-radius:var(--radius-xl);border-bottom-left-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-transparent{border-color:#0000}.bg-transparent{background-color:#0000}.bg-clip-padding{background-clip:padding-box}.p-\(--card-spacing\){padding:var(--card-spacing)}.p-1{padding:calc(var(--spacing) * 1)}.p-2{padding:calc(var(--spacing) * 2)}.px-\(--card-spacing\){padding-inline:var(--card-spacing)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.py-\(--card-spacing\){padding-block:var(--card-spacing)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.pr-2{padding-right:calc(var(--spacing) * 2)}.pr-8{padding-right:calc(var(--spacing) * 8)}.pl-1\.5{padding-left:calc(var(--spacing) * 1.5)}.pl-2\.5{padding-left:calc(var(--spacing) * 2.5)}.text-left{text-align:left}.align-middle{vertical-align:middle}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.8rem\]{font-size:.8rem}.leading-none{--tw-leading:1;line-height:1}.leading-snug{--tw-leading:var(--leading-snug);line-height:var(--leading-snug)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.whitespace-nowrap{white-space:nowrap}.underline-offset-4{text-underline-offset:4px}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-0{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.outline-hidden{--tw-outline-style:none;outline-style:none}@media(forced-colors:active){.outline-hidden{outline-offset:2px;outline:2px solid #0000}}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-100{--tw-duration:.1s;transition-duration:.1s}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.\[--card-spacing\:--spacing\(4\)\]{--card-spacing:calc(var(--spacing) * 4)}.group-data-\[disabled\=true\]\:pointer-events-none:is(:where(.group)[data-disabled=true] *){pointer-events:none}.group-data-\[disabled\=true\]\:opacity-50:is(:where(.group)[data-disabled=true] *){opacity:.5}.group-data-\[size\=default\]\/switch\:size-4:is(:where(.group\/switch)[data-size=default] *){width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.group-data-\[size\=sm\]\/card\:text-sm:is(:where(.group\/card)[data-size=sm] *){font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.group-data-\[size\=sm\]\/switch\:size-3:is(:where(.group\/switch)[data-size=sm] *){width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.group-data-\[spacing\=0\]\/toggle-group\:rounded-none:is(:where(.group\/toggle-group)[data-spacing="0"] *){border-radius:0}.group-data-\[spacing\=0\]\/toggle-group\:px-2:is(:where(.group\/toggle-group)[data-spacing="0"] *){padding-inline:calc(var(--spacing) * 2)}.peer-disabled\:cursor-not-allowed:is(:where(.peer):disabled~*){cursor:not-allowed}.peer-disabled\:opacity-50:is(:where(.peer):disabled~*){opacity:.5}.file\:inline-flex::file-selector-button{display:inline-flex}.file\:h-6::file-selector-button{height:calc(var(--spacing) * 6)}.file\:border-0::file-selector-button{border-style:var(--tw-border-style);border-width:0}.file\:bg-transparent::file-selector-button{background-color:#0000}.file\:text-sm::file-selector-button{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.file\:font-medium::file-selector-button{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:-inset-x-3:after{content:var(--tw-content);inset-inline:calc(var(--spacing) * -3)}.after\:-inset-y-2:after{content:var(--tw-content);inset-block:calc(var(--spacing) * -2)}@media(hover:hover){.hover\:bg-\[color-mix\(in_oklch\,var\(--secondary\)\,var\(--foreground\)_5\%\)\]:hover{background-color:var(--secondary)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-\[color-mix\(in_oklch\,var\(--secondary\)\,var\(--foreground\)_5\%\)\]:hover{background-color:color-mix(in oklch,var(--secondary),var(--foreground) 5%)}}.hover\:underline:hover{text-decoration-line:underline}}.focus\:z-10:focus,.focus-visible\:z-10:focus-visible{z-index:10}.focus-visible\:ring-3:focus-visible,.focus-visible\:ring-\[3px\]:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.active\:not-aria-\[haspopup\]\:translate-y-px:active:not([aria-haspopup]){--tw-translate-y:1px;translate:var(--tw-translate-x) var(--tw-translate-y)}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}:where([data-slot=button-group]) .in-data-\[slot\=button-group\]\:rounded-lg{border-radius:var(--radius-lg)}.has-data-\[icon\=inline-end\]\:pr-1\.5:has([data-icon=inline-end]){padding-right:calc(var(--spacing) * 1.5)}.has-data-\[icon\=inline-end\]\:pr-2:has([data-icon=inline-end]){padding-right:calc(var(--spacing) * 2)}.group-data-\[spacing\=0\]\/toggle-group\:has-data-\[icon\=inline-end\]\:pr-1\.5:is(:where(.group\/toggle-group)[data-spacing="0"] *):has([data-icon=inline-end]){padding-right:calc(var(--spacing) * 1.5)}.has-data-\[icon\=inline-start\]\:pl-1\.5:has([data-icon=inline-start]){padding-left:calc(var(--spacing) * 1.5)}.has-data-\[icon\=inline-start\]\:pl-2:has([data-icon=inline-start]){padding-left:calc(var(--spacing) * 2)}.group-data-\[spacing\=0\]\/toggle-group\:has-data-\[icon\=inline-start\]\:pl-1\.5:is(:where(.group\/toggle-group)[data-spacing="0"] *):has([data-icon=inline-start]){padding-left:calc(var(--spacing) * 1.5)}.has-data-\[slot\=card-action\]\:grid-cols-\[1fr_auto\]:has([data-slot=card-action]){grid-template-columns:1fr auto}.has-data-\[slot\=card-description\]\:grid-rows-\[auto_auto\]:has([data-slot=card-description]){grid-template-rows:auto auto}.has-data-\[slot\=card-footer\]\:pb-0:has([data-slot=card-footer]){padding-bottom:calc(var(--spacing) * 0)}.has-data-\[slot\=kbd\]\:pr-1\.5:has([data-slot=kbd]){padding-right:calc(var(--spacing) * 1.5)}.has-\[\>img\:first-child\]\:pt-0:has(>img:first-child){padding-top:calc(var(--spacing) * 0)}.aria-invalid\:ring-3[aria-invalid=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.group-data-\[size\=default\]\/switch\:data-checked\:translate-x-\[calc\(100\%-2px\)\]:is(:where(.group\/switch)[data-size=default] *)[data-checked],.group-data-\[size\=sm\]\/switch\:data-checked\:translate-x-\[calc\(100\%-2px\)\]:is(:where(.group\/switch)[data-size=sm] *)[data-checked]{--tw-translate-x: calc(100% - 2px) ;translate:var(--tw-translate-x) var(--tw-translate-y)}.data-disabled\:pointer-events-none[data-disabled]{pointer-events:none}.data-disabled\:cursor-not-allowed[data-disabled]{cursor:not-allowed}.data-disabled\:opacity-50[data-disabled]{opacity:.5}.group-data-\[size\=default\]\/switch\:data-unchecked\:translate-x-0:is(:where(.group\/switch)[data-size=default] *)[data-unchecked],.group-data-\[size\=sm\]\/switch\:data-unchecked\:translate-x-0:is(:where(.group\/switch)[data-size=sm] *)[data-unchecked]{--tw-translate-x:calc(var(--spacing) * 0);translate:var(--tw-translate-x) var(--tw-translate-y)}.data-vertical\:flex-col[data-vertical]{flex-direction:column}.data-vertical\:items-stretch[data-vertical]{align-items:stretch}.data-\[align-trigger\=true\]\:animate-none[data-align-trigger=true]{animation:none}.data-\[position\=popper\]\:h-\(--radix-select-trigger-height\)[data-position=popper]{height:var(--radix-select-trigger-height)}.data-\[position\=popper\]\:w-full[data-position=popper]{width:100%}.data-\[position\=popper\]\:min-w-\(--radix-select-trigger-width\)[data-position=popper]{min-width:var(--radix-select-trigger-width)}.data-\[side\=bottom\]\:translate-y-1[data-side=bottom]{--tw-translate-y:calc(var(--spacing) * 1);translate:var(--tw-translate-x) var(--tw-translate-y)}.data-\[side\=left\]\:-translate-x-1[data-side=left]{--tw-translate-x:calc(var(--spacing) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.data-\[side\=right\]\:translate-x-1[data-side=right]{--tw-translate-x:calc(var(--spacing) * 1);translate:var(--tw-translate-x) var(--tw-translate-y)}.data-\[side\=top\]\:-translate-y-1[data-side=top]{--tw-translate-y:calc(var(--spacing) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.data-\[size\=default\]\:h-8[data-size=default]{height:calc(var(--spacing) * 8)}.data-\[size\=default\]\:h-\[18\.4px\][data-size=default]{height:18.4px}.data-\[size\=default\]\:w-\[32px\][data-size=default]{width:32px}.data-\[size\=sm\]\:h-7[data-size=sm]{height:calc(var(--spacing) * 7)}.data-\[size\=sm\]\:h-\[14px\][data-size=sm]{height:14px}.data-\[size\=sm\]\:w-\[24px\][data-size=sm]{width:24px}.data-\[size\=sm\]\:rounded-\[min\(var\(--radius-md\)\,10px\)\][data-size=sm]{border-radius:min(var(--radius-md),10px)}.data-\[size\=sm\]\:\[--card-spacing\:--spacing\(3\)\][data-size=sm]{--card-spacing:calc(var(--spacing) * 3)}.data-\[size\=sm\]\:has-data-\[slot\=card-footer\]\:pb-0[data-size=sm]:has([data-slot=card-footer]){padding-bottom:calc(var(--spacing) * 0)}:is(.\*\*\:data-\[slot\=kbd\]\:relative *)[data-slot=kbd]{position:relative}:is(.\*\*\:data-\[slot\=kbd\]\:isolate *)[data-slot=kbd]{isolation:isolate}:is(.\*\*\:data-\[slot\=kbd\]\:z-50 *)[data-slot=kbd]{z-index:50}:is(.\*\*\:data-\[slot\=kbd\]\:rounded-sm *)[data-slot=kbd]{border-radius:var(--radius-sm)}:is(.\*\:data-\[slot\=select-value\]\:line-clamp-1>*)[data-slot=select-value]{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}:is(.\*\:data-\[slot\=select-value\]\:flex>*)[data-slot=select-value]{display:flex}:is(.\*\:data-\[slot\=select-value\]\:items-center>*)[data-slot=select-value]{align-items:center}:is(.\*\:data-\[slot\=select-value\]\:gap-1\.5>*)[data-slot=select-value]{gap:calc(var(--spacing) * 1.5)}.group-data-horizontal\/toggle-group\:data-\[spacing\=0\]\:first\:rounded-l-lg:is(:where(.group\/toggle-group)[data-horizontal] *)[data-spacing="0"]:first-child{border-top-left-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.group-data-vertical\/toggle-group\:data-\[spacing\=0\]\:first\:rounded-t-lg:is(:where(.group\/toggle-group)[data-vertical] *)[data-spacing="0"]:first-child{border-top-left-radius:var(--radius-lg);border-top-right-radius:var(--radius-lg)}.group-data-horizontal\/toggle-group\:data-\[spacing\=0\]\:last\:rounded-r-lg:is(:where(.group\/toggle-group)[data-horizontal] *)[data-spacing="0"]:last-child{border-top-right-radius:var(--radius-lg);border-bottom-right-radius:var(--radius-lg)}.group-data-vertical\/toggle-group\:data-\[spacing\=0\]\:last\:rounded-b-lg:is(:where(.group\/toggle-group)[data-vertical] *)[data-spacing="0"]:last-child{border-bottom-right-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.group-data-horizontal\/toggle-group\:data-\[spacing\=0\]\:data-\[variant\=outline\]\:border-l-0:is(:where(.group\/toggle-group)[data-horizontal] *)[data-spacing="0"][data-variant=outline]{border-left-style:var(--tw-border-style);border-left-width:0}.group-data-vertical\/toggle-group\:data-\[spacing\=0\]\:data-\[variant\=outline\]\:border-t-0:is(:where(.group\/toggle-group)[data-vertical] *)[data-spacing="0"][data-variant=outline]{border-top-style:var(--tw-border-style);border-top-width:0}.group-data-horizontal\/toggle-group\:data-\[spacing\=0\]\:data-\[variant\=outline\]\:first\:border-l:is(:where(.group\/toggle-group)[data-horizontal] *)[data-spacing="0"][data-variant=outline]:first-child{border-left-style:var(--tw-border-style);border-left-width:1px}.group-data-vertical\/toggle-group\:data-\[spacing\=0\]\:data-\[variant\=outline\]\:first\:border-t:is(:where(.group\/toggle-group)[data-vertical] *)[data-spacing="0"][data-variant=outline]:first-child{border-top-style:var(--tw-border-style);border-top-width:1px}@media(min-width:48rem){.md\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}.\[\&_svg\]\:pointer-events-none svg{pointer-events:none}.\[\&_svg\]\:shrink-0 svg{flex-shrink:0}.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-3 svg:not([class*=size-]){width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-3\.5 svg:not([class*=size-]){width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5)}.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-4 svg:not([class*=size-]){width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.\[\&_tr\]\:border-b tr{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.\[\&_tr\:last-child\]\:border-0 tr:last-child{border-style:var(--tw-border-style);border-width:0}.\[\&\:has\(\[role\=checkbox\]\)\]\:pr-0:has([role=checkbox]){padding-right:calc(var(--spacing) * 0)}.\[\.border-b\]\:pb-\(--card-spacing\).border-b{padding-bottom:var(--card-spacing)}:is(.\*\:\[img\:first-child\]\:rounded-t-xl>*):is(img:first-child){border-top-left-radius:var(--radius-xl);border-top-right-radius:var(--radius-xl)}:is(.\*\:\[img\:last-child\]\:rounded-b-xl>*):is(img:last-child){border-bottom-right-radius:var(--radius-xl);border-bottom-left-radius:var(--radius-xl)}:is(.\*\:\[span\]\:last\:flex>*):is(span):last-child{display:flex}:is(.\*\:\[span\]\:last\:items-center>*):is(span):last-child{align-items:center}:is(.\*\:\[span\]\:last\:gap-2>*):is(span):last-child{gap:calc(var(--spacing) * 2)}.\[\&\>svg\]\:pointer-events-none>svg{pointer-events:none}.\[\&\>svg\]\:size-3\!>svg{width:calc(var(--spacing) * 3)!important;height:calc(var(--spacing) * 3)!important}.\[\&\>tr\]\:last\:border-b-0>tr:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}}:root{--base:#0c0d0f;--panel:#15171a;--panel-2:#1a1d21;--panel-3:#20242a;--line:#ffffff24;--line-soft:#ffffff14;--text:#f3f4f1;--muted:#a4a8a6;--faint:#737977;--amber:#f5a623;--amber-soft:#f5a62324;--red:#ff5c5c;--red-soft:#ff5c5c24;--green:#69d28e;--blue:#82aef6;--radius:6px;--mono:"IBM Plex Mono", ui-monospace, monospace;--body:"Archivo", system-ui, sans-serif;--ease:cubic-bezier(.2, .8, .2, 1)}*{box-sizing:border-box}html,body,#root{min-height:100%}body{font-family:var(--body);color:var(--text);background:var(--base);-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility;letter-spacing:0;margin:0}button,input{font:inherit}button{cursor:pointer}button:disabled{cursor:not-allowed}code{font-family:var(--mono)}.shell{flex-direction:column;min-height:100vh;display:flex}.topbar{z-index:20;border-bottom:1px solid var(--line);-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px);background:#0c0d0feb;justify-content:space-between;align-items:center;gap:18px;min-height:50px;padding:8px 14px;display:flex;position:sticky;top:0}.masthead,.brand,.top-actions,.account-chip,.mode-pill,.action-btn,.icon-btn{align-items:center;display:inline-flex}.masthead{gap:18px;min-width:0}.brand{gap:10px}.mark{border:1px solid var(--amber);background:linear-gradient(90deg,transparent 8px,var(--amber) 8px 10px,transparent 10px),linear-gradient(0deg,transparent 8px,var(--amber) 8px 10px,transparent 10px);width:20px;height:20px}.brand h1{text-transform:uppercase;letter-spacing:.08em;white-space:nowrap;margin:0;font-size:15px;font-weight:800;line-height:1}.top-actions{gap:8px;margin-left:auto}.account-chip,.mode-pill,.action-btn,.icon-btn,.filter-chip,.risk-chip{border:1px solid var(--line);border-radius:var(--radius);color:var(--text);background:var(--panel)}.account-chip{max-width:270px;color:var(--muted);gap:7px;padding:6px 9px;font-size:12px;overflow:hidden}.account-chip span:last-of-type{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.account-chip em{border:1px solid var(--line-soft);color:var(--faint);text-transform:uppercase;border-radius:99px;flex:none;padding:1px 5px;font-size:10px;font-style:normal}.status-dot{border:1px solid;border-radius:50%;flex:none;width:8px;height:8px}.account-chip[data-state=in] .status-dot{color:var(--green);background:var(--green)}.account-chip[data-state=out] .status-dot{color:var(--red)}.mode-switcher,.settings-wrap{position:relative}.mode-pill,.action-btn,.icon-btn{gap:7px;height:32px;padding:0 10px;font-size:12px;font-weight:700}.mode-pill[data-tone=read]{color:var(--amber)}.mode-pill[data-tone=strict]{color:var(--blue)}.mode-pill[data-tone=off]{color:var(--red)}.action-btn.primary{background:var(--amber);color:#15100a;border-color:#f5a6238c}.icon-btn{justify-content:center;width:32px;padding:0}.mode-popover,.settings-popover{border:1px solid var(--line);border-radius:var(--radius);background:var(--panel);width:320px;padding:8px;position:absolute;top:calc(100% + 8px);right:0;box-shadow:0 18px 50px #0000006b}.mode-option,.menu-row{width:100%;color:var(--text);text-align:left;background:0 0;border:1px solid #0000;border-radius:5px}.mode-option{grid-template-columns:14px 86px 1fr;align-items:center;gap:9px;padding:9px;display:grid}.mode-option:hover,.mode-option[data-active=true],.menu-row:hover{border-color:var(--line);background:var(--panel-2)}.mode-option[data-tone=read] .status-dot{color:var(--amber);background:var(--amber)}.mode-option[data-tone=strict] .status-dot{color:var(--blue);background:var(--blue)}.mode-option[data-tone=off] .status-dot{color:var(--red)}.mode-option strong{font-size:12px}.mode-option .help{color:var(--muted);font-size:12px;line-height:1.35}.settings-popover{width:210px}.menu-kicker,.kicker{color:var(--faint);font-family:var(--mono);text-transform:uppercase;margin:0;font-size:10px;font-weight:600}.menu-row{justify-content:space-between;align-items:center;padding:8px;font-size:12px;display:flex}.menu-row span{color:var(--faint);font-family:var(--mono);font-size:10px}.notice{color:#f8dba7;background:#f5a62317;border-bottom:1px solid #f5a62359;align-items:center;gap:12px;padding:8px 14px;font-size:12px;display:flex}.notice code{color:var(--text);background:#0003;border:1px solid #f5a62366;border-radius:4px;padding:2px 6px}.allowlist{flex:1;min-height:0;padding:14px}.table-toolbar{grid-template-columns:minmax(190px,1fr) auto minmax(220px,360px) auto auto;align-items:end;gap:10px;margin-bottom:10px;display:grid}.table-toolbar h2{margin:2px 0 0;font-size:22px;line-height:1}.table-count{font-family:var(--mono);color:var(--faint);align-self:center;font-size:12px}.table-count strong{color:var(--amber);font-size:16px}.filter-search{border:1px solid var(--line);border-radius:var(--radius);background:var(--panel);height:34px;color:var(--faint);align-items:center;gap:8px;padding:0 10px;display:flex}.filter-search input{width:100%;min-width:0;color:var(--text);background:0 0;border:0;outline:0;font-size:13px}.filter-search input::placeholder{color:var(--faint)}.chip-row{gap:5px;height:34px;display:inline-flex}.filter-chip,.risk-chip{height:34px;color:var(--muted);text-transform:capitalize;padding:0 10px;font-size:12px}.risk-chip{color:var(--red);text-transform:none}.filter-chip[data-active=true],.risk-chip[data-active=true]{border-color:var(--amber);color:var(--amber);background:var(--amber-soft)}.table-shell{border:1px solid var(--line);border-radius:var(--radius);background:var(--panel);min-height:0;overflow:hidden}.grant-head,.grant-row{grid-template-columns:minmax(260px,1fr) 150px 230px 46px;display:grid}.grant-head{border-bottom:1px solid var(--line);background:var(--panel-2);height:34px}.head-cell{border:0;border-right:1px solid var(--line-soft);min-width:0;color:var(--faint);font-family:var(--mono);text-align:left;text-transform:uppercase;background:0 0;align-items:center;gap:6px;padding:0 12px;font-size:10px;font-weight:600;display:flex}.head-cell:disabled{cursor:default}.grant-viewport{scrollbar-color:var(--panel-3) var(--base);overflow:auto}.grant-spacer{position:relative}.grant-row{border:0;border-bottom:1px solid var(--line-soft);height:36px;color:var(--text);text-align:left;background:0 0;position:absolute;left:0;right:0}.grant-row:hover{background:var(--panel-2)}.body-cell{align-items:center;min-width:0;padding:0 12px;display:flex;overflow:hidden}.body-cell[data-col=action]{justify-content:center}.resource-button{align-items:center;gap:9px;min-width:0;font-size:13px;font-weight:600;display:inline-flex}.resource-button svg{width:14px;height:14px;color:var(--amber);flex:none}.resource-button span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.badge{border:1px solid var(--line);height:22px;color:var(--muted);text-transform:uppercase;background:#ffffff08;border-radius:99px;align-items:center;gap:6px;padding:0 8px;font-size:10px;font-weight:700;display:inline-flex}.badge svg{width:12px;height:12px}.badge[data-kind=folder]{color:var(--amber)}.badge[data-kind=spreadsheet]{color:var(--green)}.badge[data-kind=file]{color:var(--blue)}.status-lights{gap:6px;display:inline-flex}.status-badge{min-width:46px;color:var(--faint);font-family:var(--mono);align-items:center;gap:4px;font-size:11px;display:inline-flex}.status-badge[data-on=true]{color:var(--amber)}.status-badge[data-perm=delete][data-on=true]{color:var(--red)}.kebab{border:1px solid var(--line-soft);width:24px;height:24px;color:var(--muted);border-radius:4px;place-items:center;display:grid}.drawer-dialog{width:100vw;max-width:none;height:100vh;max-height:none;color:var(--text);background:0 0;border:0;margin:0;padding:0;overflow:hidden}.drawer-dialog::backdrop{background:#00000061}@keyframes drawer-in{0%{opacity:0;transform:translate(28px)}to{opacity:1;transform:translate(0)}}.drawer{border-left:1px solid var(--line);background:var(--panel);width:min(480px,100vw);height:100%;animation:drawer-in .18s var(--ease);margin-left:auto;box-shadow:-24px 0 60px #00000073}.drawer-head{border-bottom:1px solid var(--line);justify-content:space-between;align-items:center;gap:14px;min-height:64px;padding:14px 16px;display:flex}.drawer-head h2{margin:2px 0 0;font-size:20px;line-height:1.15}.drawer-body{gap:14px;padding:16px;display:grid}.drawer-section{border-bottom:1px solid var(--line-soft);gap:10px;padding-bottom:14px;display:grid}.drawer-section h3{color:var(--muted);text-transform:uppercase;margin:0;font-size:12px;font-weight:700}.field{color:var(--muted);gap:6px;font-size:12px;font-weight:600;display:grid}.search-box{border:1px solid var(--line);border-radius:var(--radius);background:var(--base);align-items:center;gap:8px;height:36px;padding:0 10px;display:flex}.field input,.kind-select{border:1px solid var(--line);border-radius:var(--radius);background:var(--base);width:100%;min-height:36px;color:var(--text);padding:0 10px}.search-box input{border:0;outline:0;min-height:0;padding:0}.results{gap:6px;max-height:180px;margin:0;padding:0;list-style:none;display:grid;overflow:auto}.result{border:1px solid var(--line-soft);border-radius:var(--radius);background:var(--panel-2);width:100%;min-height:36px;color:var(--text);text-align:left;justify-content:space-between;align-items:center;gap:10px;padding:6px 8px;display:flex}.rname,.sname{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.search-hint,.sid{color:var(--faint);font-family:var(--mono);margin:0;font-size:11px}.selected{border:1px solid var(--line);border-radius:var(--radius);background:var(--panel-2);grid-template-columns:22px 1fr 32px;align-items:center;gap:10px;min-height:48px;padding:8px;display:grid}.selected svg{color:var(--amber)}.perm-row,.perms{gap:8px;display:grid}.perm-choice,.perm-pill{border:1px solid var(--line);border-radius:var(--radius);background:var(--base);min-height:38px;color:var(--muted);text-align:left;grid-template-columns:22px 72px 1fr;align-items:center;gap:8px;padding:8px 10px;display:grid}.perm-choice[data-on=true],.perm-pill[data-on=true]{color:var(--text);background:var(--amber-soft);border-color:#f5a6238c}.perm-choice[data-perm=delete][data-on=true],.perm-pill[data-perm=delete][data-on=true]{background:var(--red-soft);border-color:#ff5c5c99}.pc-name,.perm-pill strong{font-size:12px}.pc-desc,.perm-pill small{color:var(--faint);font-size:11px}.perm-pill{grid-template-columns:1fr auto}.perm-pill span{gap:2px;display:grid}.btn{border-radius:var(--radius);background:var(--amber);color:#15100a;border:1px solid #f5a62399;min-height:38px;font-weight:800}.btn:disabled{opacity:.45}.detail-grid{grid-template-columns:92px 1fr;align-items:center;gap:10px 12px;font-size:13px;display:grid}.detail-grid>span{color:var(--faint)}.gid{border:1px solid var(--line);border-radius:var(--radius);background:var(--base);width:fit-content;max-width:100%;min-height:28px;color:var(--muted);font-family:var(--mono);align-items:center;gap:8px;padding:0 8px;font-size:11px;display:inline-flex}.gid:hover{border-color:var(--amber);color:var(--text)}.copied{color:var(--green)}.danger-zone{border-bottom:0}.revoke{border:1px solid var(--line);border-radius:var(--radius);width:fit-content;min-height:34px;color:var(--red);background:0 0;padding:0 12px;font-weight:700}.revoke[data-confirm=true]{border-color:var(--red);background:var(--red);color:#190606}.empty,.grant-skeletons{min-height:360px;color:var(--muted);text-align:center;place-items:center;padding:28px;display:grid}.empty-mark{border:1px solid var(--line);background:linear-gradient(90deg,transparent 18px,var(--amber) 18px 20px,transparent 20px),linear-gradient(0deg,transparent 18px,var(--amber) 18px 20px,transparent 20px);width:42px;height:42px}.empty h3{color:var(--text);margin:12px 0 4px;font-size:18px}.empty p{margin:0;font-size:13px}.grant-skeletons{gap:8px}.grant-skeletons>*{border-radius:var(--radius);background:var(--panel-2);width:min(720px,100%);height:34px}@media(max-width:920px){.topbar,.table-toolbar{align-items:stretch}.topbar{flex-wrap:wrap}.top-actions{width:100%}.account-chip{margin-left:auto}.table-toolbar{grid-template-columns:1fr}.chip-row{overflow-x:auto}.grant-head,.grant-row{grid-template-columns:minmax(190px,1fr) 118px 170px 38px}}@media(max-width:620px){.mode-popover{width:calc(100vw - 28px);left:0;right:auto}.action-btn.primary{flex:1;justify-content:center}.account-chip{max-width:100%}.allowlist{padding:10px}.grant-viewport{overflow:auto}.grant-head,.grant-row{min-width:560px}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@keyframes pulse{50%{opacity:.5}}