server-devtools 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +544 -0
  3. package/dist/dashboard/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  4. package/dist/dashboard/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  5. package/dist/dashboard/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  6. package/dist/dashboard/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  7. package/dist/dashboard/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  8. package/dist/dashboard/assets/index-DvYSUjO3.css +2 -0
  9. package/dist/dashboard/assets/index-vQGCvuXC.js +9 -0
  10. package/dist/dashboard/favicon.svg +9 -0
  11. package/dist/dashboard/icons.svg +24 -0
  12. package/dist/dashboard/index.html +14 -0
  13. package/dist/src/config/index.d.ts +21 -0
  14. package/dist/src/config/index.d.ts.map +1 -0
  15. package/dist/src/config/index.js +30 -0
  16. package/dist/src/config/index.js.map +1 -0
  17. package/dist/src/core/execution-tree-builder.d.ts +6 -0
  18. package/dist/src/core/execution-tree-builder.d.ts.map +1 -0
  19. package/dist/src/core/execution-tree-builder.js +44 -0
  20. package/dist/src/core/execution-tree-builder.js.map +1 -0
  21. package/dist/src/dashboard/dashboard-errors-mapper.d.ts +5 -0
  22. package/dist/src/dashboard/dashboard-errors-mapper.d.ts.map +1 -0
  23. package/dist/src/dashboard/dashboard-errors-mapper.js +19 -0
  24. package/dist/src/dashboard/dashboard-errors-mapper.js.map +1 -0
  25. package/dist/src/dashboard/dashboard-request-detail-mapper.d.ts +16 -0
  26. package/dist/src/dashboard/dashboard-request-detail-mapper.d.ts.map +1 -0
  27. package/dist/src/dashboard/dashboard-request-detail-mapper.js +79 -0
  28. package/dist/src/dashboard/dashboard-request-detail-mapper.js.map +1 -0
  29. package/dist/src/dashboard/dashboard-response-detail-mapper.d.ts +6 -0
  30. package/dist/src/dashboard/dashboard-response-detail-mapper.d.ts.map +1 -0
  31. package/dist/src/dashboard/dashboard-response-detail-mapper.js +39 -0
  32. package/dist/src/dashboard/dashboard-response-detail-mapper.js.map +1 -0
  33. package/dist/src/dashboard/dashboard-server.d.ts +21 -0
  34. package/dist/src/dashboard/dashboard-server.d.ts.map +1 -0
  35. package/dist/src/dashboard/dashboard-server.js +676 -0
  36. package/dist/src/dashboard/dashboard-server.js.map +1 -0
  37. package/dist/src/index.d.ts +16 -0
  38. package/dist/src/index.d.ts.map +1 -0
  39. package/dist/src/index.js +118 -0
  40. package/dist/src/index.js.map +1 -0
  41. package/dist/src/instrumentation/exporter.d.ts +11 -0
  42. package/dist/src/instrumentation/exporter.d.ts.map +1 -0
  43. package/dist/src/instrumentation/exporter.js +46 -0
  44. package/dist/src/instrumentation/exporter.js.map +1 -0
  45. package/dist/src/instrumentation/instrumentation.d.ts +11 -0
  46. package/dist/src/instrumentation/instrumentation.d.ts.map +1 -0
  47. package/dist/src/instrumentation/instrumentation.js +144 -0
  48. package/dist/src/instrumentation/instrumentation.js.map +1 -0
  49. package/dist/src/instrumentation/outbound-http/outbound-http-capture.d.ts +24 -0
  50. package/dist/src/instrumentation/outbound-http/outbound-http-capture.d.ts.map +1 -0
  51. package/dist/src/instrumentation/outbound-http/outbound-http-capture.js +245 -0
  52. package/dist/src/instrumentation/outbound-http/outbound-http-capture.js.map +1 -0
  53. package/dist/src/instrumentation/span-mapper.d.ts +8 -0
  54. package/dist/src/instrumentation/span-mapper.d.ts.map +1 -0
  55. package/dist/src/instrumentation/span-mapper.js +54 -0
  56. package/dist/src/instrumentation/span-mapper.js.map +1 -0
  57. package/dist/src/integrations/nestjs/server-devtools-nest.interceptor.d.ts +6 -0
  58. package/dist/src/integrations/nestjs/server-devtools-nest.interceptor.d.ts.map +1 -0
  59. package/dist/src/integrations/nestjs/server-devtools-nest.interceptor.js +23 -0
  60. package/dist/src/integrations/nestjs/server-devtools-nest.interceptor.js.map +1 -0
  61. package/dist/src/nestjs.d.ts +2 -0
  62. package/dist/src/nestjs.d.ts.map +1 -0
  63. package/dist/src/nestjs.js +9 -0
  64. package/dist/src/nestjs.js.map +1 -0
  65. package/dist/src/security/auth.service.d.ts +16 -0
  66. package/dist/src/security/auth.service.d.ts.map +1 -0
  67. package/dist/src/security/auth.service.js +96 -0
  68. package/dist/src/security/auth.service.js.map +1 -0
  69. package/dist/src/security/encrypt-decrypt.service.d.ts +10 -0
  70. package/dist/src/security/encrypt-decrypt.service.d.ts.map +1 -0
  71. package/dist/src/security/encrypt-decrypt.service.js +93 -0
  72. package/dist/src/security/encrypt-decrypt.service.js.map +1 -0
  73. package/dist/src/storage/sqlite-storage.d.ts +35 -0
  74. package/dist/src/storage/sqlite-storage.d.ts.map +1 -0
  75. package/dist/src/storage/sqlite-storage.js +583 -0
  76. package/dist/src/storage/sqlite-storage.js.map +1 -0
  77. package/dist/src/types/dashboard.d.ts +32 -0
  78. package/dist/src/types/dashboard.d.ts.map +1 -0
  79. package/dist/src/types/dashboard.js +3 -0
  80. package/dist/src/types/dashboard.js.map +1 -0
  81. package/dist/src/types/database.d.ts +16 -0
  82. package/dist/src/types/database.d.ts.map +1 -0
  83. package/dist/src/types/database.js +3 -0
  84. package/dist/src/types/database.js.map +1 -0
  85. package/dist/src/types/http-client.d.ts +8 -0
  86. package/dist/src/types/http-client.d.ts.map +1 -0
  87. package/dist/src/types/http-client.js +3 -0
  88. package/dist/src/types/http-client.js.map +1 -0
  89. package/dist/src/types/index.d.ts +7 -0
  90. package/dist/src/types/index.d.ts.map +1 -0
  91. package/dist/src/types/index.js +23 -0
  92. package/dist/src/types/index.js.map +1 -0
  93. package/dist/src/types/misc.d.ts +25 -0
  94. package/dist/src/types/misc.d.ts.map +1 -0
  95. package/dist/src/types/misc.js +3 -0
  96. package/dist/src/types/misc.js.map +1 -0
  97. package/dist/src/types/span.d.ts +25 -0
  98. package/dist/src/types/span.d.ts.map +1 -0
  99. package/dist/src/types/span.js +3 -0
  100. package/dist/src/types/span.js.map +1 -0
  101. package/dist/src/types/trace.d.ts +32 -0
  102. package/dist/src/types/trace.d.ts.map +1 -0
  103. package/dist/src/types/trace.js +4 -0
  104. package/dist/src/types/trace.js.map +1 -0
  105. package/dist/src/utils/logger.d.ts +2 -0
  106. package/dist/src/utils/logger.d.ts.map +1 -0
  107. package/dist/src/utils/logger.js +19 -0
  108. package/dist/src/utils/logger.js.map +1 -0
  109. package/package.json +82 -0
package/README.md ADDED
@@ -0,0 +1,544 @@
1
+ # ServerDevTools
2
+
3
+ **Chrome DevTools for your backend.**
4
+
5
+ ServerDevTools is an embeddable backend inspector for Node.js
6
+ applications.
7
+
8
+ It gives you a live dashboard for inspecting:
9
+
10
+ - incoming HTTP requests
11
+ - request headers, params, query, and body
12
+ - response status, headers, and body
13
+ - execution flow
14
+ - framework middleware and handlers
15
+ - outgoing HTTP requests
16
+ - database operations
17
+ - errors
18
+ - timing
19
+
20
+ Instead of jumping between logs, traces, and multiple observability
21
+ tools, ServerDevTools lets you inspect what your backend is doing from
22
+ one developer-focused interface.
23
+
24
+ ------------------------------------------------------------------------
25
+
26
+ ## Demo
27
+
28
+ > Add launch GIF/video here.
29
+
30
+ ``` text
31
+ POST /users
32
+ │
33
+ ├── Express middleware
34
+ ├── Request handler
35
+ ├── MongoDB query
36
+ └── External HTTP request
37
+ ```
38
+
39
+ Open:
40
+
41
+ ``` text
42
+ http://localhost:3000/_devtools
43
+ ```
44
+
45
+ and inspect the complete request lifecycle.
46
+
47
+ ------------------------------------------------------------------------
48
+
49
+ ## Why ServerDevTools?
50
+
51
+ Frontend developers have Chrome DevTools.
52
+
53
+ Backend debugging often looks like this:
54
+
55
+ ``` text
56
+ console.log(...)
57
+ tail logs
58
+ search logs
59
+ inspect database
60
+ check external API logs
61
+ open tracing platform
62
+ repeat
63
+ ```
64
+
65
+ ServerDevTools provides a simpler workflow:
66
+
67
+ ``` text
68
+ Run request
69
+ ↓
70
+ Open /_devtools
71
+ ↓
72
+ Inspect what happened
73
+ ```
74
+
75
+ It is designed for **live backend debugging**, not as a replacement for
76
+ full production observability platforms.
77
+
78
+ ------------------------------------------------------------------------
79
+
80
+ ## Installation
81
+
82
+ ``` bash
83
+ npm install server-devtools
84
+ ```
85
+
86
+ ------------------------------------------------------------------------
87
+
88
+ ## Quick Start
89
+
90
+ ServerDevTools uses an authenticated, optionally encrypted initialization file.
91
+ Keep the credentials and encryption key in environment variables.
92
+
93
+ The encryption key must be a base64-encoded 32-byte key. Generate one with:
94
+
95
+ ```sh
96
+ node -e 'console.log(require("node:crypto").randomBytes(32).toString("base64"))'
97
+ ```
98
+
99
+ `fields` is optional. By default, ServerDevTools encrypts these field names:
100
+
101
+ - `password`
102
+ - `accessToken`
103
+ - `refreshToken`
104
+ - `authorization`
105
+ - `cookie`
106
+
107
+ Specifying `fields` replaces the default list; it does not extend it.
108
+
109
+ To customize the encrypted fields:
110
+
111
+ ```ts
112
+ encryption: {
113
+ key: process.env.SERVER_DEVTOOLS_ENCRYPTION_KEY!,
114
+ fields: ["password", "authorization", "apiKey"],
115
+ },
116
+ ```
117
+
118
+ `getCurrentUser` is optional. Use it when your application already exposes the
119
+ authenticated user on the request; ServerDevTools does not require a
120
+ Passport, JWT, or other authentication framework.
121
+
122
+ ### Node.js / Express TypeScript
123
+
124
+ Create `server-devtools.ts`:
125
+
126
+ ```ts
127
+ import ServerDevTools from "server-devtools";
128
+
129
+ export const devtools = new ServerDevTools({
130
+ auth: {
131
+ username: process.env.SERVER_DEVTOOLS_USERNAME!,
132
+ password: process.env.SERVER_DEVTOOLS_PASSWORD!,
133
+ },
134
+ encryption: {
135
+ key: process.env.SERVER_DEVTOOLS_ENCRYPTION_KEY!,
136
+ },
137
+ getCurrentUser: (req) => req.user,
138
+ });
139
+
140
+ await devtools.start();
141
+ ```
142
+
143
+ ```ts
144
+ // src/main.ts
145
+ import { createServer } from "node:http";
146
+ import { devtools } from "../server-devtools";
147
+
148
+ const server = createServer((req, res) => {
149
+ devtools.middleware(req, res, () => {
150
+ res.statusCode = 404;
151
+ res.end("Not found");
152
+ });
153
+ });
154
+
155
+ server.listen(3000);
156
+ ```
157
+
158
+ ```sh
159
+ tsx watch --import ./server-devtools.ts src/main.ts
160
+ ```
161
+
162
+ The application can import the same instance normally and mount it in Express:
163
+
164
+ ```ts
165
+ // src/main.ts
166
+ import express from "express";
167
+ import { devtools } from "../server-devtools";
168
+
169
+ const app = express();
170
+ app.use((req, res, next) => devtools.middleware(req, res, next));
171
+
172
+ app.get("/users/:id", async (req, res) => {
173
+ res.json({ id: req.params.id, name: "John Doe" });
174
+ });
175
+
176
+ app.listen(3000);
177
+ ```
178
+
179
+ ### Node.js / Express JavaScript
180
+
181
+ For JavaScript projects, create `server-devtools.js`:
182
+
183
+ ```js
184
+ const ServerDevTools = require("server-devtools");
185
+
186
+ const devtools = new ServerDevTools({
187
+ auth: {
188
+ username: process.env.SERVER_DEVTOOLS_USERNAME,
189
+ password: process.env.SERVER_DEVTOOLS_PASSWORD,
190
+ },
191
+ encryption: {
192
+ key: process.env.SERVER_DEVTOOLS_ENCRYPTION_KEY,
193
+ },
194
+ getCurrentUser: (req) => req.user,
195
+ });
196
+
197
+ module.exports = { devtools };
198
+
199
+ void devtools.start();
200
+ ```
201
+
202
+ Preload the JavaScript file normally:
203
+
204
+ ```sh
205
+ node --import ./server-devtools.js src/main.js
206
+ ```
207
+
208
+ In the application, import the same instance and mount it with:
209
+
210
+ ```js
211
+ const express = require("express");
212
+ const { devtools } = require("../server-devtools.js");
213
+
214
+ const app = express();
215
+ app.use((req, res, next) => devtools.middleware(req, res, next));
216
+ app.listen(3000);
217
+ ```
218
+
219
+ JavaScript projects do not need `allowJs`.
220
+
221
+ ### NestJS
222
+
223
+ NestJS uses a different preload pattern. Create a plain `server-devtools.js`
224
+ outside `src`:
225
+
226
+ ```js
227
+ const ServerDevTools = require("server-devtools");
228
+
229
+ const devtools = new ServerDevTools({
230
+ auth: {
231
+ username: process.env.SERVER_DEVTOOLS_USERNAME,
232
+ password: process.env.SERVER_DEVTOOLS_PASSWORD,
233
+ },
234
+ encryption: {
235
+ key: process.env.SERVER_DEVTOOLS_ENCRYPTION_KEY,
236
+ },
237
+ getCurrentUser: (req) => req.user,
238
+ });
239
+
240
+ globalThis.__serverDevtools = devtools;
241
+
242
+ void (async () => {
243
+ await devtools.start();
244
+ })();
245
+ ```
246
+
247
+ Enable JavaScript files in the Nest TypeScript configuration:
248
+
249
+ ```json
250
+ {
251
+ "compilerOptions": {
252
+ "allowJs": true
253
+ }
254
+ }
255
+ ```
256
+
257
+ Start Nest with the preload file:
258
+
259
+ ```sh
260
+ NODE_OPTIONS='--import ./server-devtools.js' nest start --watch
261
+ ```
262
+
263
+ Do not create another `ServerDevTools` instance in `main.ts`. Retrieve the
264
+ preloaded instance instead:
265
+
266
+ ```ts
267
+ // src/main.ts
268
+ import { NestFactory } from "@nestjs/core";
269
+ import { AppModule } from "./app.module";
270
+ import { ServerDevToolsNestInterceptor } from "server-devtools/nestjs";
271
+ import ServerDevTools from "server-devtools";
272
+
273
+ declare global {
274
+ var __serverDevtools: ServerDevTools | undefined;
275
+ }
276
+
277
+ async function bootstrap() {
278
+ const devtools = globalThis.__serverDevtools;
279
+ if (!devtools) {
280
+ throw new Error(
281
+ "ServerDevTools was not preloaded. Start Nest with NODE_OPTIONS='--import ./server-devtools.js'.",
282
+ );
283
+ }
284
+
285
+ const app = await NestFactory.create(AppModule);
286
+ app.use((req, res, next) => devtools.middleware(req, res, next));
287
+ app.useGlobalInterceptors(
288
+ new ServerDevToolsNestInterceptor(),
289
+ );
290
+ await app.listen(3000);
291
+ }
292
+
293
+ bootstrap();
294
+ ```
295
+
296
+ This Nest-specific pattern initializes instrumentation before Nest and
297
+ application dependencies load, while avoiding a second ServerDevTools
298
+ initialization during watch mode.
299
+
300
+ Each example imports and mounts the same instance that was started by the
301
+ preloaded initialization file. Its middleware serves the dashboard from the
302
+ application's existing HTTP server at `http://localhost:<your-port>/_devtools`.
303
+
304
+ OpenTelemetry and outbound fetch capture start when `devtools.start()` is
305
+ called; constructing the instance alone does not wrap `globalThis.fetch`.
306
+
307
+ Then open:
308
+
309
+ ``` text
310
+ http://localhost:3000/_devtools
311
+ ```
312
+
313
+ Make requests to your application and they will appear in the dashboard.
314
+
315
+ ------------------------------------------------------------------------
316
+
317
+ ## Request Inspector
318
+
319
+ Inspect the incoming request, including:
320
+
321
+ - method
322
+ - path
323
+ - route
324
+ - headers
325
+ - query parameters
326
+ - route parameters
327
+ - request body
328
+
329
+ Example:
330
+
331
+ ``` json
332
+ {
333
+ "name": "John Doe",
334
+ "email": "john@example.com"
335
+ }
336
+ ```
337
+
338
+ ------------------------------------------------------------------------
339
+
340
+ ## Response Inspector
341
+
342
+ Inspect what your application returned:
343
+
344
+ - status code
345
+ - response headers
346
+ - response body
347
+
348
+ Example:
349
+
350
+ ``` json
351
+ {
352
+ "received": {
353
+ "name": "John Doe",
354
+ "email": "john@example.com"
355
+ }
356
+ }
357
+ ```
358
+
359
+ ------------------------------------------------------------------------
360
+
361
+ ## Execution View
362
+
363
+ ServerDevTools reconstructs the execution hierarchy for each request.
364
+
365
+ ``` text
366
+ POST /users
367
+ │
368
+ ├── Express middleware
369
+ ├── Request handler
370
+ ├── MongoDB query
371
+ └── HTTP request
372
+ └── api.example.com
373
+ ```
374
+
375
+ This makes it easier to understand what your backend executed during a
376
+ request and where time was spent.
377
+
378
+ ------------------------------------------------------------------------
379
+
380
+ ## How It Works
381
+
382
+ ServerDevTools uses OpenTelemetry instrumentation to observe supported
383
+ backend operations.
384
+
385
+ ``` text
386
+ Application request and dependencies
387
+ ↓
388
+ OpenTelemetry captures supported activity
389
+ ↓
390
+ ServerDevTools maps captured spans and details
391
+ ↓
392
+ Historical inspection data in SQLite
393
+ ↓
394
+ Embedded dashboard on the application's HTTP server
395
+ ```
396
+
397
+ OpenTelemetry provides the instrumentation layer. ServerDevTools
398
+ provides the developer-focused inspection experience on top of it.
399
+
400
+ ------------------------------------------------------------------------
401
+
402
+ ## Embedded Dashboard
403
+
404
+ ServerDevTools does not require a separate dashboard server. It mounts
405
+ directly into your existing application:
406
+
407
+ ``` text
408
+ http://localhost:3000
409
+ │
410
+ ├── /api/users
411
+ ├── /api/orders
412
+ ├── /api/...
413
+ └── /_devtools
414
+ ```
415
+
416
+ ------------------------------------------------------------------------
417
+
418
+ ## Current Support
419
+
420
+ Initial support focuses on Node.js applications:
421
+
422
+ - Node.js HTTP
423
+ - Express
424
+ - NestJS middleware and error interception
425
+ - MongoDB operations
426
+ - native HTTP/HTTPS and fetch/Undici requests, including headers and bodies
427
+ - OpenTelemetry-powered activity capture
428
+
429
+ Support will expand as the project develops.
430
+
431
+ ------------------------------------------------------------------------
432
+
433
+ ## What ServerDevTools Is Not
434
+
435
+ ServerDevTools is not intended to replace full observability,
436
+ monitoring, alerting, or log aggregation platforms.
437
+
438
+ Its focus is:
439
+
440
+ > **Interactive inspection of individual backend requests.**
441
+
442
+ ------------------------------------------------------------------------
443
+
444
+ ## Development Status
445
+
446
+ ServerDevTools is currently under active development.
447
+
448
+ The first release focuses on making this workflow excellent:
449
+
450
+ ``` text
451
+ Install ServerDevTools
452
+ ↓
453
+ Initialize + preload ServerDevTools
454
+ ↓
455
+ Start application
456
+ ↓
457
+ Open /_devtools
458
+ ↓
459
+ Run and inspect requests
460
+ ```
461
+
462
+ Expect APIs and configuration to evolve before `1.0`.
463
+
464
+ ------------------------------------------------------------------------
465
+
466
+ ## Roadmap
467
+
468
+ Near-term:
469
+
470
+ - [x] HTTP request capture
471
+ - [x] Request headers
472
+ - [x] Query parameters
473
+ - [x] Route parameters
474
+ - [x] Request body
475
+ - [x] HTTP response status
476
+ - [x] Response headers
477
+ - [x] Response body
478
+ - [x] Execution tree
479
+ - [x] Native HTTP/HTTPS and fetch/Undici inspection
480
+ - [x] MongoDB inspection
481
+ - [x] Error inspection
482
+ - [x] NestJS integration and error interception
483
+ - [x] Encryption at rest for selected fields
484
+ - [ ] Request/response redaction
485
+ - [ ] Body-size limits
486
+ - [ ] Production-safe configuration
487
+ - [ ] Broader NestJS compatibility
488
+ - [x] Static dashboard packaging
489
+
490
+ Future ideas:
491
+
492
+ - Redis
493
+ - PostgreSQL
494
+ - MySQL
495
+ - additional Node frameworks
496
+ - trace filtering
497
+ - request search
498
+ - configurable retention
499
+ - multi-instance support
500
+
501
+ ------------------------------------------------------------------------
502
+
503
+ ## Security
504
+
505
+ ServerDevTools may capture sensitive application data, including request
506
+ headers, request bodies, response bodies, database operations, and
507
+ external request metadata.
508
+
509
+ Optional encryption at rest can protect configured fields in supported
510
+ captured data. It does not redact all captured data or replace application
511
+ security practices. The dashboard has authentication; use strong credentials
512
+ and restrict access to `/_devtools` through your network and deployment setup.
513
+
514
+ Automatic redaction and capture-size limits are not currently implemented.
515
+
516
+ ------------------------------------------------------------------------
517
+
518
+ ## Philosophy
519
+
520
+ ServerDevTools should remain:
521
+
522
+ - easy to install
523
+ - easy to understand
524
+ - framework-friendly
525
+ - developer-focused
526
+ - useful without requiring a full observability stack
527
+
528
+ The goal is simple:
529
+
530
+ > **Make backend debugging feel more like using browser DevTools.**
531
+
532
+ ------------------------------------------------------------------------
533
+
534
+ ## Contributing
535
+
536
+ The project is still early.
537
+
538
+ Issues, ideas, bug reports, and contributions are welcome.
539
+
540
+ ------------------------------------------------------------------------
541
+
542
+ ## License
543
+
544
+ Apache-2.0
@@ -0,0 +1,2 @@
1
+ /*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){:root,:host{--shimmer-angle:20deg}*,: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-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction: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-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0;--scroll-fade-t:0px;--scroll-fade-b:0px;--scroll-fade-s:0px;--scroll-fade-e:0px;--scroll-fade-mask:initial;--shimmer-image:initial;--shimmer-text-fill:initial}}}@layer theme{:root,:host{--font-sans:"Geist Variable", sans-serif;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-green-500:oklch(72.3% .219 149.579);--color-black:#000;--spacing:.25rem;--container-md:28rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-wide:.025em;--radius-md:calc(var(--radius) * .8);--animate-spin:spin 1s linear infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:"Geist Variable", sans-serif;--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%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*{border-color:var(--border);outline-color:var(--ring)}@supports (color:color-mix(in lab, red, red)){*{outline-color:color-mix(in oklab, var(--ring) 50%, transparent)}}body{background-color:var(--background);color:var(--foreground)}html{font-family:Geist Variable,sans-serif}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.inset-0{inset:0}.top-1\/2{top:50%}.right-2{right:calc(var(--spacing) * 2)}.left-3{left:calc(var(--spacing) * 3)}.z-50{z-index:50}.mt-1{margin-top:var(--spacing)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mr-2{margin-right:calc(var(--spacing) * 2)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-7{margin-bottom:calc(var(--spacing) * 7)}.-ml-2{margin-left:calc(var(--spacing) * -2)}.ml-1{margin-left:var(--spacing)}.ml-4{margin-left:calc(var(--spacing) * 4)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.inline-flex{display:inline-flex}.size-2{width:calc(var(--spacing) * 2);height:calc(var(--spacing) * 2)}.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-0\.5{height:calc(var(--spacing) * .5)}.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-14{height:calc(var(--spacing) * 14)}.h-full{height:100%}.max-h-96{max-height:calc(var(--spacing) * 96)}.min-h-screen{min-height:100vh}.w-12{width:calc(var(--spacing) * 12)}.w-fit{width:fit-content}.w-full{width:100%}.max-w-48{max-width:calc(var(--spacing) * 48)}.max-w-md{max-width:var(--container-md)}.min-w-0{min-width:0}.shrink-0{flex-shrink:0}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-spin{animation:var(--animate-spin)}.cursor-pointer{cursor:pointer}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-\[minmax\(8rem\,30\%\)_1fr\]{grid-template-columns:minmax(8rem,30%) 1fr}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.rounded{border-radius:.25rem}.rounded-4xl{border-radius:calc(var(--radius) * 2.6)}.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:2147483647px}.rounded-lg{border-radius:var(--radius)}.rounded-md{border-radius:calc(var(--radius) * .8)}.rounded-xl{border-radius:calc(var(--radius) * 1.4)}.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-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-border{border-color:var(--border)}.border-destructive\/30{border-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.border-destructive\/30{border-color:color-mix(in oklab, var(--destructive) 30%, transparent)}}.border-input{border-color:var(--input)}.border-transparent{border-color:#0000}.bg-background{background-color:var(--background)}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab, red, red)){.bg-black\/50{background-color:color-mix(in oklab, var(--color-black) 50%, transparent)}}.bg-destructive\/5{background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.bg-destructive\/5{background-color:color-mix(in oklab, var(--destructive) 5%, transparent)}}.bg-destructive\/10{background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.bg-destructive\/10{background-color:color-mix(in oklab, var(--destructive) 10%, transparent)}}.bg-green-500{background-color:var(--color-green-500)}.bg-muted{background-color:var(--muted)}.bg-muted-foreground\/60{background-color:var(--muted-foreground)}@supports (color:color-mix(in lab, red, red)){.bg-muted-foreground\/60{background-color:color-mix(in oklab, var(--muted-foreground) 60%, transparent)}}.bg-muted\/20{background-color:var(--muted)}@supports (color:color-mix(in lab, red, red)){.bg-muted\/20{background-color:color-mix(in oklab, var(--muted) 20%, transparent)}}.bg-muted\/30{background-color:var(--muted)}@supports (color:color-mix(in lab, red, red)){.bg-muted\/30{background-color:color-mix(in oklab, var(--muted) 30%, transparent)}}.bg-muted\/40{background-color:var(--muted)}@supports (color:color-mix(in lab, red, red)){.bg-muted\/40{background-color:color-mix(in oklab, var(--muted) 40%, transparent)}}.bg-muted\/70{background-color:var(--muted)}@supports (color:color-mix(in lab, red, red)){.bg-muted\/70{background-color:color-mix(in oklab, var(--muted) 70%, transparent)}}.bg-primary{background-color:var(--primary)}.bg-secondary{background-color:var(--secondary)}.bg-transparent{background-color:#0000}.bg-clip-padding{background-clip:padding-box}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-9{padding-inline:calc(var(--spacing) * 9)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-10{padding-block:calc(var(--spacing) * 10)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pl-3{padding-left:calc(var(--spacing) * 3)}.pl-9{padding-left:calc(var(--spacing) * 9)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.8rem\]{font-size:.8rem}.text-\[10px\]{font-size:10px}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.break-words{overflow-wrap:break-word}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-destructive{color:var(--destructive)}.text-foreground{color:var(--foreground)}.text-muted-foreground{color:var(--muted-foreground)}.text-primary{color:var(--primary)}.text-primary-foreground{color:var(--primary-foreground)}.text-secondary-foreground{color:var(--secondary-foreground)}.uppercase{text-transform:uppercase}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.underline{text-decoration-line:underline}.underline-offset-4{text-underline-offset:4px}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.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))}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.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)}.file\:text-foreground::file-selector-button{color:var(--foreground)}.placeholder\:text-muted-foreground::placeholder{color:var(--muted-foreground)}.last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}@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\:bg-destructive\/20:hover{background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-destructive\/20:hover{background-color:color-mix(in oklab, var(--destructive) 20%, transparent)}}.hover\:bg-muted:hover,.hover\:bg-muted\/40:hover{background-color:var(--muted)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-muted\/40:hover{background-color:color-mix(in oklab, var(--muted) 40%, transparent)}}.hover\:bg-primary\/80:hover{background-color:var(--primary)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-primary\/80:hover{background-color:color-mix(in oklab, var(--primary) 80%, transparent)}}.hover\:text-destructive:hover{color:var(--destructive)}.hover\:text-foreground:hover{color:var(--foreground)}.hover\:text-muted-foreground:hover{color:var(--muted-foreground)}.hover\:underline:hover{text-decoration-line:underline}}.focus-visible\:border-destructive\/40:focus-visible{border-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.focus-visible\:border-destructive\/40:focus-visible{border-color:color-mix(in oklab, var(--destructive) 40%, transparent)}}.focus-visible\:border-ring:focus-visible{border-color:var(--ring)}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus-visible\:ring-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)}.focus-visible\:ring-destructive\/20:focus-visible{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.focus-visible\:ring-destructive\/20:focus-visible{--tw-ring-color:color-mix(in oklab, var(--destructive) 20%, transparent)}}.focus-visible\:ring-ring:focus-visible,.focus-visible\:ring-ring\/50:focus-visible{--tw-ring-color:var(--ring)}@supports (color:color-mix(in lab, red, red)){.focus-visible\:ring-ring\/50:focus-visible{--tw-ring-color:color-mix(in oklab, var(--ring) 50%, transparent)}}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.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\:bg-input\/50:disabled{background-color:var(--input)}@supports (color:color-mix(in lab, red, red)){.disabled\:bg-input\/50:disabled{background-color:color-mix(in oklab, var(--input) 50%, transparent)}}.disabled\:opacity-50:disabled{opacity:.5}:where([data-slot=button-group]) .in-data-\[slot\=button-group\]\:rounded-lg{border-radius:var(--radius)}.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)}.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)}.aria-expanded\:bg-muted[aria-expanded=true]{background-color:var(--muted)}.aria-expanded\:bg-secondary[aria-expanded=true]{background-color:var(--secondary)}.aria-expanded\:text-foreground[aria-expanded=true]{color:var(--foreground)}.aria-expanded\:text-secondary-foreground[aria-expanded=true]{color:var(--secondary-foreground)}.aria-invalid\:border-destructive[aria-invalid=true]{border-color:var(--destructive)}.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)}.aria-invalid\:ring-destructive\/20[aria-invalid=true]{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.aria-invalid\:ring-destructive\/20[aria-invalid=true]{--tw-ring-color:color-mix(in oklab, var(--destructive) 20%, transparent)}}@media (width>=40rem){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:p-8{padding:calc(var(--spacing) * 8)}}@media (width>=48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}@media (width>=64rem){.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}.dark\:border-input:is(.dark *){border-color:var(--input)}.dark\:bg-destructive\/20:is(.dark *){background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.dark\:bg-destructive\/20:is(.dark *){background-color:color-mix(in oklab, var(--destructive) 20%, transparent)}}.dark\:bg-input\/30:is(.dark *){background-color:var(--input)}@supports (color:color-mix(in lab, red, red)){.dark\:bg-input\/30:is(.dark *){background-color:color-mix(in oklab, var(--input) 30%, transparent)}}@media (hover:hover){.dark\:hover\:bg-destructive\/30:is(.dark *):hover{background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.dark\:hover\:bg-destructive\/30:is(.dark *):hover{background-color:color-mix(in oklab, var(--destructive) 30%, transparent)}}.dark\:hover\:bg-input\/50:is(.dark *):hover{background-color:var(--input)}@supports (color:color-mix(in lab, red, red)){.dark\:hover\:bg-input\/50:is(.dark *):hover{background-color:color-mix(in oklab, var(--input) 50%, transparent)}}.dark\:hover\:bg-muted\/50:is(.dark *):hover{background-color:var(--muted)}@supports (color:color-mix(in lab, red, red)){.dark\:hover\:bg-muted\/50:is(.dark *):hover{background-color:color-mix(in oklab, var(--muted) 50%, transparent)}}}.dark\:focus-visible\:ring-destructive\/40:is(.dark *):focus-visible{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.dark\:focus-visible\:ring-destructive\/40:is(.dark *):focus-visible{--tw-ring-color:color-mix(in oklab, var(--destructive) 40%, transparent)}}.dark\:disabled\:bg-input\/80:is(.dark *):disabled{background-color:var(--input)}@supports (color:color-mix(in lab, red, red)){.dark\:disabled\:bg-input\/80:is(.dark *):disabled{background-color:color-mix(in oklab, var(--input) 80%, transparent)}}.dark\:aria-invalid\:border-destructive\/50:is(.dark *)[aria-invalid=true]{border-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.dark\:aria-invalid\:border-destructive\/50:is(.dark *)[aria-invalid=true]{border-color:color-mix(in oklab, var(--destructive) 50%, transparent)}}.dark\:aria-invalid\:ring-destructive\/40:is(.dark *)[aria-invalid=true]{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.dark\:aria-invalid\:ring-destructive\/40:is(.dark *)[aria-invalid=true]{--tw-ring-color:color-mix(in oklab, var(--destructive) 40%, transparent)}}.\[\&_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)}@media (hover:hover){.\[a\]\:hover\:bg-destructive\/20:is(a):hover{background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.\[a\]\:hover\:bg-destructive\/20:is(a):hover{background-color:color-mix(in oklab, var(--destructive) 20%, transparent)}}.\[a\]\:hover\:bg-muted:is(a):hover{background-color:var(--muted)}.\[a\]\:hover\:bg-primary\/80:is(a):hover{background-color:var(--primary)}@supports (color:color-mix(in lab, red, red)){.\[a\]\:hover\:bg-primary\/80:is(a):hover{background-color:color-mix(in oklab, var(--primary) 80%, transparent)}}.\[a\]\:hover\:bg-secondary\/80:is(a):hover{background-color:var(--secondary)}@supports (color:color-mix(in lab, red, red)){.\[a\]\:hover\:bg-secondary\/80:is(a):hover{background-color:color-mix(in oklab, var(--secondary) 80%, transparent)}}.\[a\]\:hover\:text-muted-foreground:is(a):hover{color:var(--muted-foreground)}}.\[\&\>svg\]\:pointer-events-none>svg{pointer-events:none}.\[\&\>svg\]\:size-3\!>svg{width:calc(var(--spacing) * 3)!important;height:calc(var(--spacing) * 3)!important}}@property --tw-animation-delay{syntax:"*";inherits:false;initial-value:0s}@property --tw-animation-direction{syntax:"*";inherits:false;initial-value:normal}@property --tw-animation-duration{syntax:"*";inherits:false}@property --tw-animation-fill-mode{syntax:"*";inherits:false;initial-value:none}@property --tw-animation-iteration-count{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-translate-y{syntax:"*";inherits:false;initial-value:0}@property --scroll-fade-t{syntax:"<length-percentage>";inherits:false;initial-value:0}@property --scroll-fade-b{syntax:"<length-percentage>";inherits:false;initial-value:0}@property --scroll-fade-s{syntax:"<length-percentage>";inherits:false;initial-value:0}@property --scroll-fade-e{syntax:"<length-percentage>";inherits:false;initial-value:0}@property --scroll-fade-mask{syntax:"*";inherits:false}@property --shimmer-angle{syntax:"<angle>";inherits:true;initial-value:20deg}@property --shimmer-image{syntax:"*";inherits:false}@property --shimmer-text-fill{syntax:"*";inherits:false}@media (prefers-reduced-motion:reduce){.shimmer{-webkit-text-fill-color:currentColor;background-image:none;animation:none}}@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/_devtools/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2)format("woff2-variations");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/_devtools/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2)format("woff2-variations");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/_devtools/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2)format("woff2-variations");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/_devtools/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2)format("woff2-variations");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Geist Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/_devtools/assets/geist-latin-wght-normal-BgDaEnEv.woff2)format("woff2-variations");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}:root{--background:oklch(100% 0 0);--foreground:oklch(14.5% 0 0);--card:oklch(100% 0 0);--card-foreground:oklch(14.5% 0 0);--popover:oklch(100% 0 0);--popover-foreground:oklch(14.5% 0 0);--primary:oklch(20.5% 0 0);--primary-foreground:oklch(98.5% 0 0);--secondary:oklch(97% 0 0);--secondary-foreground:oklch(20.5% 0 0);--muted:oklch(97% 0 0);--muted-foreground:oklch(55.6% 0 0);--accent:oklch(97% 0 0);--accent-foreground:oklch(20.5% 0 0);--destructive:oklch(57.7% .245 27.325);--border:oklch(92.2% 0 0);--input:oklch(92.2% 0 0);--ring:oklch(70.8% 0 0);--chart-1:oklch(87% 0 0);--chart-2:oklch(55.6% 0 0);--chart-3:oklch(43.9% 0 0);--chart-4:oklch(37.1% 0 0);--chart-5:oklch(26.9% 0 0);--radius:.625rem;--sidebar:oklch(98.5% 0 0);--sidebar-foreground:oklch(14.5% 0 0);--sidebar-primary:oklch(20.5% 0 0);--sidebar-primary-foreground:oklch(98.5% 0 0);--sidebar-accent:oklch(97% 0 0);--sidebar-accent-foreground:oklch(20.5% 0 0);--sidebar-border:oklch(92.2% 0 0);--sidebar-ring:oklch(70.8% 0 0)}.dark{--background:oklch(14.5% 0 0);--foreground:oklch(98.5% 0 0);--card:oklch(20.5% 0 0);--card-foreground:oklch(98.5% 0 0);--popover:oklch(20.5% 0 0);--popover-foreground:oklch(98.5% 0 0);--primary:oklch(92.2% 0 0);--primary-foreground:oklch(20.5% 0 0);--secondary:oklch(26.9% 0 0);--secondary-foreground:oklch(98.5% 0 0);--muted:oklch(26.9% 0 0);--muted-foreground:oklch(70.8% 0 0);--accent:oklch(26.9% 0 0);--accent-foreground:oklch(98.5% 0 0);--destructive:oklch(70.4% .191 22.216);--border:oklch(100% 0 0/.1);--input:oklch(100% 0 0/.15);--ring:oklch(55.6% 0 0);--chart-1:oklch(87% 0 0);--chart-2:oklch(55.6% 0 0);--chart-3:oklch(43.9% 0 0);--chart-4:oklch(37.1% 0 0);--chart-5:oklch(26.9% 0 0);--sidebar:oklch(20.5% 0 0);--sidebar-foreground:oklch(98.5% 0 0);--sidebar-primary:oklch(48.8% .243 264.376);--sidebar-primary-foreground:oklch(98.5% 0 0);--sidebar-accent:oklch(26.9% 0 0);--sidebar-accent-foreground:oklch(98.5% 0 0);--sidebar-border:oklch(100% 0 0/.1);--sidebar-ring:oklch(55.6% 0 0)}@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-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{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}@keyframes spin{to{transform:rotate(360deg)}}