ldrouter 1.17.5 → 1.17.7
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/CHANGELOG.md +17 -0
- package/dist/server/gateway/runner.js +24 -3
- package/dist/server/providers/codex-refresh.js +62 -22
- package/dist/server/providers/codex.js +12 -1
- package/dist/server/providers/qoder/client.js +23 -0
- package/dist/server/providers/qoder/sse.js +66 -7
- package/dist/server/routes/admin/requests.js +1 -0
- package/dist/server/routes/admin/stats.js +32 -2
- package/dist/web/assets/{index-DQd2y4FN.css → index-CbKRlw3a.css} +1 -1
- package/dist/web/assets/index-D-rviQ3G.js +391 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-f8A3f3uE.js +0 -391
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ All notable changes to this project are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/) and the project adheres to
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [1.17.7] - 2026-09-20
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **Codex accounts were refreshed at the last minute, in the window where a rotating grant is most fragile.** The refresh lead is a threshold on the *remaining* lifetime, not a token lifetime — `needsCodexRefresh` fires once `expires - now` drops below it — and it sat at 5 minutes, so the exchange ran as the access token expired. Because OpenAI rotates the refresh token on every exchange, that is exactly when another client holding the same grant can invalidate it, and the observed fleet showed every account at `consecutive_failures = 0`: the first failure was already being treated as final. Codex access tokens last 10 days (measured from the fleet: a rotation at 13:39:00 expired at 13:39:00 ten days later), so the lead is now 5 days — half a lifetime of margin, refreshing every 5 days instead of every 10. This matches the 5-day `refreshLeadMs` 9router ships for Codex, and it surfaces a dead grant on day 5 rather than day 10. The lead is deliberately far below the lifetime: any value above it makes `needsCodexRefresh` permanently true and refreshes on *every* request, which multiplies refresh-token rotation by the request count.
|
|
12
|
+
|
|
13
|
+
## [1.17.6] - 2026-09-20
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **Every Qoder request logged `Tokens in 0 · out 0 · cache 0`.** Two independent causes. Streaming: the Qoder branch pipes OpenAI-shaped chunks through the runner's shared chunk handler, which only parsed usage under `cfg.type === 'openai'`, so the accumulator stayed at zero for Qoder — live data confirmed 128 of 128 streaming Qoder requests carried no tokens. Non-streaming: a call asking for no stream can be answered with the whole completion as one plain JSON body, which the SSE-only envelope reader left in its unparsed buffer, so `usage` came back `undefined`. The reader now keeps non-`data:` lines rather than dropping them and the non-streaming path folds a plain completion in (a no-op when the origin did frame its answer as SSE). The usage shapes Qoder actually sends are read too: the object nested one level down as `{statusCodeValue, body}`, and OpenAI-compatible `prompt_tokens_details.cached_tokens`.
|
|
18
|
+
- **A fallback that succeeded was logged as a failure.** The failed first attempt's error was never cleared once a later candidate answered, so the row kept the first attempt's status: 17 requests were recorded `success=0` with `http_status` 502/504/429 while the client had received a 200 answer. The success rate and every per-model figure derived from the request log inherited that error.
|
|
19
|
+
- **The cache-hit rate double-counted the cache.** OpenAI-compatible providers report `cached_tokens` as a *subset* of `prompt_tokens`, so `input` already contains the cached prefix and the blanket `cacheRead / (input + cacheRead)` inflated the denominator — measured at 21.75% where the truth was 27.80% over real traffic. Anthropic is the exception (`input_tokens` *excludes* the cached prefix), so each request is now normalised on its own provider's semantics, on the server and in the live feed alike. The live feed also derives its rates from running totals instead of incrementing a ratio, which is meaningless.
|
|
20
|
+
- **Codex reported zero cached and zero reasoning tokens.** The Responses API nests them under `usage.input_tokens_details.cached_tokens` and `usage.output_tokens_details.reasoning_tokens`; only the flat names were read, so 1,381 requests (46% of traffic) contributed nothing to the cache-hit rate. Flat names still work for upstreams that send them.
|
|
21
|
+
- **A Codex account was retired on a single flaky refresh.** Every account kept turning `degraded` with `last_error=oauth_refresh_failed`, and because the gateway reads that code as "these credentials are dead" it also evicted the account (`enabled=0`). One failure code covered five unrelated things — a request timeout, a DNS error, a token-endpoint 5xx, a 200 carrying no usable token, and a rolled-back database write — and only a grant the endpoint actually rejected is a verdict. The outcomes are now split the way 9router splits them: `oauth_refresh_failed`, `credential_unavailable` and `account_not_found` are final, while `oauth_refresh_unavailable` and `invalid_refresh_response` are transient. A transient failure still moves the request to a sibling account but leaves this one enabled, keeps its health, and writes no `last_error`, so a status the operator reads always means what it says. The rejection is read out of the response body and only on a 4xx — a 5xx or a gateway page echoing `invalid_grant` is still transient — and the body is read whole, because OpenAI nests the code under `error.code` where a top-level read sees nothing. Live data: every account held `consecutive_failures = 0`, the counter meant to require a pattern before a verdict was never reached.
|
|
22
|
+
- **`+ Add` in the New combo dialog never added the model.** One handler served both dialogs and wrote to the *edit* form, so adding in the create dialog mutated the edit state while the create member list stayed empty — the button read as dead. The edit dialog worked, which is why the report was "sometimes". Each dialog now owns its member list.
|
|
23
|
+
|
|
7
24
|
## [1.17.5] - 2026-09-19
|
|
8
25
|
|
|
9
26
|
### Fixed
|
|
@@ -261,6 +261,9 @@ export class GatewayRunner {
|
|
|
261
261
|
});
|
|
262
262
|
attempt.statusCode = out.statusCode ?? null;
|
|
263
263
|
attempt.success = true;
|
|
264
|
+
// A fallback that succeeded answered the client 200: the earlier failed
|
|
265
|
+
// attempt must not keep the request logged (and reported) as a failure.
|
|
266
|
+
lastError = null;
|
|
264
267
|
attempt.latencyMs = Date.now() - attemptStart;
|
|
265
268
|
attempt.ttftMs = out.ttftMs;
|
|
266
269
|
attempt.usage = out.usage;
|
|
@@ -296,7 +299,9 @@ export class GatewayRunner {
|
|
|
296
299
|
catch (e) {
|
|
297
300
|
const err = e instanceof GatewayError ? e : new GatewayError('upstream_error', e.message, { cause: e });
|
|
298
301
|
const quotaFailure = isQuotaFailure(err);
|
|
299
|
-
|
|
302
|
+
// Only a final verdict retires the account (below); a transient refresh failure still moves
|
|
303
|
+
// the request to a sibling account but must leave this one enabled.
|
|
304
|
+
const credentialFailure = isPermanentCredentialFailure(err);
|
|
300
305
|
debugUpstream(ctx.requestId, 'ATTEMPT ERROR', [
|
|
301
306
|
`attempt=${i + 1}`,
|
|
302
307
|
`provider=${provider.name}`,
|
|
@@ -617,7 +622,9 @@ export class GatewayRunner {
|
|
|
617
622
|
}
|
|
618
623
|
try {
|
|
619
624
|
const obj = JSON.parse(chunk.data);
|
|
620
|
-
|
|
625
|
+
// Qoder emits OpenAI-shaped chunks and pipes them through this same handler
|
|
626
|
+
// (see the qoder branch below), so its usage block is parsed here too.
|
|
627
|
+
if (cfg.type === 'openai' || cfg.type === 'qoder') {
|
|
621
628
|
const choice = obj.choices?.[0];
|
|
622
629
|
if (choice?.delta?.content)
|
|
623
630
|
textBuf += choice.delta.content;
|
|
@@ -1059,7 +1066,21 @@ export function markQuotaExhausted(candidate, message) {
|
|
|
1059
1066
|
* inside the gateway) or wrapped by `codexCredentialError`, which now keeps it as `cause`.
|
|
1060
1067
|
*/
|
|
1061
1068
|
export function isCredentialFailure(err) {
|
|
1062
|
-
const codes = /^(oauth_refresh_failed|invalid_refresh_response|credential_unavailable|account_not_found)$/;
|
|
1069
|
+
const codes = /^(oauth_refresh_failed|oauth_refresh_unavailable|invalid_refresh_response|credential_unavailable|account_not_found)$/;
|
|
1070
|
+
const cause = err.cause;
|
|
1071
|
+
return codes.test(err.message) || (typeof cause?.message === 'string' && codes.test(cause.message));
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Whether the verdict is final, i.e. the account cannot serve again until an operator re-imports it.
|
|
1075
|
+
*
|
|
1076
|
+
* A transient refresh failure (timeout, network error, token-endpoint 5xx, rolled-back write) is
|
|
1077
|
+
* still a credential failure for routing — the request should move to a sibling account — but it
|
|
1078
|
+
* must NOT retire this one. Treating both classes as final is what degraded healthy accounts and
|
|
1079
|
+
* disabled them (`enabled=0`) on a single flaky moment; every live account carried
|
|
1080
|
+
* `consecutive_failures = 0`, so no pattern was ever required before the verdict.
|
|
1081
|
+
*/
|
|
1082
|
+
export function isPermanentCredentialFailure(err) {
|
|
1083
|
+
const codes = /^(oauth_refresh_failed|credential_unavailable|account_not_found)$/;
|
|
1063
1084
|
const cause = err.cause;
|
|
1064
1085
|
return codes.test(err.message) || (typeof cause?.message === 'string' && codes.test(cause.message));
|
|
1065
1086
|
}
|
|
@@ -10,14 +10,30 @@ export function codexCredentialError(error) {
|
|
|
10
10
|
const code = error instanceof Error ? error.message : '';
|
|
11
11
|
if (code === 'account_not_found')
|
|
12
12
|
return new GatewayError('invalid_request_error', 'Codex account not found', { status: 404 });
|
|
13
|
-
if (code === '
|
|
13
|
+
if (code === 'oauth_refresh_unavailable' || code === 'invalid_refresh_response') {
|
|
14
|
+
// Transient: the token is probably still good, so the operator should retry, not re-import.
|
|
15
|
+
// Same wrapping as the final verdict (the routing layer sees one credential class either way)
|
|
16
|
+
// but the message must not send them to the import dialog over one flaky attempt.
|
|
17
|
+
return new GatewayError('authentication_error', 'Codex credentials could not be refreshed — try again', { status: 401, cause: error });
|
|
18
|
+
}
|
|
19
|
+
if (code === 'oauth_refresh_failed' || code === 'credential_unavailable') {
|
|
14
20
|
// `cause` carries the raw credential code: the wrapping message is deliberately generic, so
|
|
15
21
|
// without it the routing layer cannot tell a dead account from any other authentication_error.
|
|
16
22
|
return new GatewayError('authentication_error', 'Codex credentials could not be refreshed — re-import the account', { status: 401, cause: error });
|
|
17
23
|
}
|
|
18
24
|
return error;
|
|
19
25
|
}
|
|
20
|
-
|
|
26
|
+
/** Only a final verdict may retire an account; a transient failure leaves it in the pool. */
|
|
27
|
+
const PERMANENT_REFRESH_FAILURES = new Set(['oauth_refresh_failed', 'credential_unavailable', 'account_not_found']);
|
|
28
|
+
export function isPermanentRefreshFailure(error) {
|
|
29
|
+
return PERMANENT_REFRESH_FAILURES.has(error);
|
|
30
|
+
}
|
|
31
|
+
// Lead is measured against the remaining lifetime, and must stay well under it: a refresh that fires
|
|
32
|
+
// at the last minute races the expiry, and because OpenAI rotates the refresh token on every
|
|
33
|
+
// exchange, that is exactly the moment another client holding the same grant can invalidate it.
|
|
34
|
+
// Codex access tokens last 10 days, so a 5-day lead keeps half the lifetime as margin and refreshes
|
|
35
|
+
// every 5 days. Matches the 5-day `refreshLeadMs` 9router ships for Codex.
|
|
36
|
+
const REFRESH_LEAD_MS = 5 * 24 * 60 * 60 * 1000;
|
|
21
37
|
const REFRESH_TIMEOUT_MS = 10_000;
|
|
22
38
|
const flights = new Map();
|
|
23
39
|
let refreshClient = defaultRefreshClient;
|
|
@@ -39,8 +55,12 @@ async function defaultRefreshClient(input) {
|
|
|
39
55
|
body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: input.refreshToken, client_id: CODEX_OAUTH.clientId }),
|
|
40
56
|
signal: input.signal,
|
|
41
57
|
});
|
|
42
|
-
if (!response.ok)
|
|
43
|
-
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
// The RFC 6749 error body is what separates a dead grant from a bad moment; `safeError` classifies
|
|
60
|
+
// on it. Only the resulting code is ever persisted, never this text.
|
|
61
|
+
const detail = await response.text().catch(() => '');
|
|
62
|
+
throw Object.assign(new Error(`oauth refresh http ${response.status}`), { status: response.status, oauthError: detail });
|
|
63
|
+
}
|
|
44
64
|
const value = await response.json();
|
|
45
65
|
if (typeof value.access_token !== 'string' || !value.access_token)
|
|
46
66
|
throw new Error('invalid refresh response');
|
|
@@ -52,9 +72,25 @@ async function defaultRefreshClient(input) {
|
|
|
52
72
|
expiresAt: typeof value.expires_at === 'string' ? value.expires_at : undefined,
|
|
53
73
|
};
|
|
54
74
|
}
|
|
55
|
-
|
|
56
|
-
|
|
75
|
+
/** Grant rejections the endpoint answers with; only these mean the refresh token is dead. */
|
|
76
|
+
const REJECTED_GRANT_MARKERS = ['invalid_grant', 'refresh_token_invalidated', 'refresh_token_reused', 'refresh_token_expired', 'session has ended'];
|
|
77
|
+
/**
|
|
78
|
+
* Decides whether a failed refresh is a verdict or just a failed attempt.
|
|
79
|
+
*
|
|
80
|
+
* A marker only counts when the endpoint itself rejected the grant (4xx): a 5xx or a gateway error
|
|
81
|
+
* page that happens to echo `invalid_grant` is still transient. Anything without a marker — a
|
|
82
|
+
* timeout, a DNS error, an aborted request, a rolled-back write — is transient by default, so an
|
|
83
|
+
* account is never retired on evidence this layer does not actually have.
|
|
84
|
+
*/
|
|
85
|
+
function safeError(error) {
|
|
86
|
+
const status = error?.status;
|
|
87
|
+
if (typeof status === 'number' && status >= 500)
|
|
88
|
+
return 'oauth_refresh_unavailable';
|
|
89
|
+
const oauthError = error?.oauthError;
|
|
90
|
+
const text = `${error?.message ?? ''} ${typeof oauthError === 'string' ? oauthError : ''}`.toLowerCase();
|
|
91
|
+
return REJECTED_GRANT_MARKERS.some((marker) => text.includes(marker)) ? 'oauth_refresh_failed' : 'oauth_refresh_unavailable';
|
|
57
92
|
}
|
|
93
|
+
/** Degrades an account for a final verdict only — a transient failure must leave health alone. */
|
|
58
94
|
function markDegraded(accountId, error) {
|
|
59
95
|
try {
|
|
60
96
|
setCodexAccountHealth(accountId, 'degraded', error);
|
|
@@ -66,9 +102,11 @@ async function refreshOnce(accountId, now, force) {
|
|
|
66
102
|
try {
|
|
67
103
|
state = getCodexAccountRefreshState(accountId);
|
|
68
104
|
}
|
|
69
|
-
catch
|
|
70
|
-
|
|
71
|
-
|
|
105
|
+
catch {
|
|
106
|
+
// A row that cannot be read is an operator problem, not a failed refresh: it needs a re-import,
|
|
107
|
+
// so the account leaves the pool instead of burning an attempt on every request.
|
|
108
|
+
markDegraded(accountId, 'credential_unavailable');
|
|
109
|
+
return { ok: false, error: 'credential_unavailable' };
|
|
72
110
|
}
|
|
73
111
|
if (!state)
|
|
74
112
|
return { ok: false, error: 'account_not_found' };
|
|
@@ -79,7 +117,8 @@ async function refreshOnce(accountId, now, force) {
|
|
|
79
117
|
try {
|
|
80
118
|
const result = await refreshClient({ refreshToken: state.refreshToken, signal: controller.signal });
|
|
81
119
|
if (typeof result.accessToken !== 'string' || !result.accessToken) {
|
|
82
|
-
|
|
120
|
+
// A 200 with nothing usable is suspicious, not proof the grant is dead: the next attempt may
|
|
121
|
+
// succeed, so the account keeps its health and stays in the pool.
|
|
83
122
|
return { ok: false, error: 'invalid_refresh_response' };
|
|
84
123
|
}
|
|
85
124
|
const expiresAt = result.expiresAt ?? (typeof result.expiresIn === 'number' && Number.isFinite(result.expiresIn) && result.expiresIn > 0
|
|
@@ -87,7 +126,6 @@ async function refreshOnce(accountId, now, force) {
|
|
|
87
126
|
: '');
|
|
88
127
|
const expiresMs = Date.parse(expiresAt);
|
|
89
128
|
if (!Number.isFinite(expiresMs) || expiresMs <= now.getTime()) {
|
|
90
|
-
markDegraded(accountId, 'invalid_refresh_response');
|
|
91
129
|
return { ok: false, error: 'invalid_refresh_response' };
|
|
92
130
|
}
|
|
93
131
|
try {
|
|
@@ -98,15 +136,17 @@ async function refreshOnce(accountId, now, force) {
|
|
|
98
136
|
expiresAt,
|
|
99
137
|
});
|
|
100
138
|
}
|
|
101
|
-
catch
|
|
102
|
-
|
|
103
|
-
return { ok: false, error: '
|
|
139
|
+
catch {
|
|
140
|
+
// The transaction rolled back, so the stored token is unchanged and still usable.
|
|
141
|
+
return { ok: false, error: 'oauth_refresh_unavailable' };
|
|
104
142
|
}
|
|
105
143
|
return { ok: true, expiresAt };
|
|
106
144
|
}
|
|
107
145
|
catch (error) {
|
|
108
|
-
|
|
109
|
-
|
|
146
|
+
const failure = safeError(error);
|
|
147
|
+
if (isPermanentRefreshFailure(failure))
|
|
148
|
+
markDegraded(accountId, failure);
|
|
149
|
+
return { ok: false, error: failure };
|
|
110
150
|
}
|
|
111
151
|
finally {
|
|
112
152
|
clearTimeout(timer);
|
|
@@ -129,8 +169,8 @@ export async function withCodexCredentials(accountId, fn, now = new Date()) {
|
|
|
129
169
|
try {
|
|
130
170
|
state = getCodexAccountRefreshState(accountId);
|
|
131
171
|
}
|
|
132
|
-
catch
|
|
133
|
-
markDegraded(accountId,
|
|
172
|
+
catch {
|
|
173
|
+
markDegraded(accountId, 'credential_unavailable');
|
|
134
174
|
throw new Error('credential_unavailable', { cause: new Error('credential_unavailable') });
|
|
135
175
|
}
|
|
136
176
|
if (!state)
|
|
@@ -144,8 +184,8 @@ export async function withCodexCredentials(accountId, fn, now = new Date()) {
|
|
|
144
184
|
try {
|
|
145
185
|
credentials = getCodexCredentials(accountId);
|
|
146
186
|
}
|
|
147
|
-
catch
|
|
148
|
-
markDegraded(accountId,
|
|
187
|
+
catch {
|
|
188
|
+
markDegraded(accountId, 'credential_unavailable');
|
|
149
189
|
throw new Error('credential_unavailable', { cause: new Error('credential_unavailable') });
|
|
150
190
|
}
|
|
151
191
|
try {
|
|
@@ -160,8 +200,8 @@ export async function withCodexCredentials(accountId, fn, now = new Date()) {
|
|
|
160
200
|
try {
|
|
161
201
|
credentials = getCodexCredentials(accountId);
|
|
162
202
|
}
|
|
163
|
-
catch
|
|
164
|
-
markDegraded(accountId,
|
|
203
|
+
catch {
|
|
204
|
+
markDegraded(accountId, 'credential_unavailable');
|
|
165
205
|
throw new Error('credential_unavailable', { cause: new Error('credential_unavailable') });
|
|
166
206
|
}
|
|
167
207
|
return fn(credentials);
|
|
@@ -151,7 +151,18 @@ function usage(value) {
|
|
|
151
151
|
const u = (value && typeof value === 'object' ? value : {});
|
|
152
152
|
const input = typeof u.input_tokens === 'number' ? u.input_tokens : 0;
|
|
153
153
|
const output = typeof u.output_tokens === 'number' ? u.output_tokens : 0;
|
|
154
|
-
|
|
154
|
+
// The Responses API nests both of these (`usage.input_tokens_details.cached_tokens`,
|
|
155
|
+
// `usage.output_tokens_details.reasoning_tokens`). Reading only the flat names returned
|
|
156
|
+
// 0 for every Codex request — 1,381 of them, 46% of all traffic — so the statistics page
|
|
157
|
+
// reported a cache-hit rate missing its largest contributor. Flat names stay supported
|
|
158
|
+
// for compatibility with OpenAI-compatible upstreams that use them.
|
|
159
|
+
const inDetails = (u.input_tokens_details && typeof u.input_tokens_details === 'object' ? u.input_tokens_details : {});
|
|
160
|
+
const outDetails = (u.output_tokens_details && typeof u.output_tokens_details === 'object' ? u.output_tokens_details : {});
|
|
161
|
+
const cacheRead = typeof inDetails.cached_tokens === 'number' ? inDetails.cached_tokens
|
|
162
|
+
: typeof u.cached_input_tokens === 'number' ? u.cached_input_tokens : 0;
|
|
163
|
+
const reasoning = typeof outDetails.reasoning_tokens === 'number' ? outDetails.reasoning_tokens
|
|
164
|
+
: typeof u.reasoning_tokens === 'number' ? u.reasoning_tokens : 0;
|
|
165
|
+
return { input, output, total: input + output, cacheRead, cacheWrite: 0, reasoning };
|
|
155
166
|
}
|
|
156
167
|
export function codexResponseToCanonical(body, requestedModel) {
|
|
157
168
|
let text = '';
|
|
@@ -209,6 +209,23 @@ async function drainEnvelope(reader, response, onChunk) {
|
|
|
209
209
|
await upstream.cancel().catch(() => { });
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* Interpret a non-SSE body as a plain OpenAI chat completion and fold it into the reader.
|
|
214
|
+
* A no-op when the origin framed its answer as SSE frames, which is the usual case.
|
|
215
|
+
*/
|
|
216
|
+
function applyPlainCompletion(reader) {
|
|
217
|
+
const raw = reader.takeUnparsed();
|
|
218
|
+
if (!raw)
|
|
219
|
+
return;
|
|
220
|
+
let body;
|
|
221
|
+
try {
|
|
222
|
+
body = JSON.parse(raw);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
reader.applyCompletion(body);
|
|
228
|
+
}
|
|
212
229
|
/** Streaming: emit OpenAI-shaped chunks as they arrive, stop at the terminal frame. */
|
|
213
230
|
export async function callQoderStreaming(cfg, req, onChunk, deps = {}) {
|
|
214
231
|
const { response, reader, status, upstreamRequestId } = await openSignedStream(cfg, { ...req, stream: true }, deps);
|
|
@@ -219,6 +236,12 @@ export async function callQoderStreaming(cfg, req, onChunk, deps = {}) {
|
|
|
219
236
|
export async function callQoderNonStreaming(cfg, req, deps = {}) {
|
|
220
237
|
const { response, reader, status, upstreamRequestId } = await openSignedStream(cfg, { ...req, stream: false }, deps);
|
|
221
238
|
await drainEnvelope(reader, response);
|
|
239
|
+
// The caller invoked this with stream disabled, so the origin may honour the stream
|
|
240
|
+
// preference and answer with the full completion in one body rather than SSE frames.
|
|
241
|
+
// The reader buffers those bytes as an unparsed trailing line (it only understands
|
|
242
|
+
// `data:` framing), which is why a non-streaming Qoder call logged zero tokens:
|
|
243
|
+
// `usage` came back `undefined` and the runner reported `in 0 · out 0 · cache 0`.
|
|
244
|
+
applyPlainCompletion(reader);
|
|
222
245
|
return { status, upstreamRequestId, text: reader.text, toolCalls: reader.toolCalls, finishReason: reader.finishReason, usage: reader.usage ?? emptyUsage() };
|
|
223
246
|
}
|
|
224
247
|
export async function probeQoder(cfg) {
|
|
@@ -8,13 +8,18 @@ function numberOr(value, fallback) {
|
|
|
8
8
|
}
|
|
9
9
|
export function qoderUsageOf(value) {
|
|
10
10
|
const usage = (value && typeof value === 'object' ? value : {});
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
11
|
+
// The envelope nests the real OpenAI usage object one level down under `body`, so a
|
|
12
|
+
// few shapes are worth reading; the flat one stays authoritative when present.
|
|
13
|
+
const nested = (usage.usage && typeof usage.usage === 'object' ? usage.usage : {});
|
|
14
|
+
const asObject = (v) => (v && typeof v === 'object' ? v : {});
|
|
15
|
+
const details = asObject(usage.prompt_tokens_details ?? nested.prompt_tokens_details);
|
|
16
|
+
const input = numberOr(usage.prompt_tokens ?? usage.input_tokens ?? nested.prompt_tokens ?? nested.input_tokens, 0);
|
|
17
|
+
const output = numberOr(usage.completion_tokens ?? usage.output_tokens ?? nested.completion_tokens ?? nested.output_tokens, 0);
|
|
14
18
|
return {
|
|
15
19
|
input, output,
|
|
16
|
-
total: numberOr(usage.total_tokens, input + output),
|
|
17
|
-
cacheRead: numberOr(details.cached_tokens ?? usage.cached_tokens ?? usage.cache_read_input_tokens
|
|
20
|
+
total: numberOr(usage.total_tokens ?? nested.total_tokens, input + output),
|
|
21
|
+
cacheRead: numberOr(details.cached_tokens ?? usage.cached_tokens ?? usage.cache_read_input_tokens
|
|
22
|
+
?? nested.cached_tokens, 0),
|
|
18
23
|
cacheWrite: numberOr(details.cache_creation_tokens ?? usage.cache_creation_input_tokens, 0),
|
|
19
24
|
reasoning: numberOr(usage.reasoning_tokens ?? usage.completion_tokens_details?.reasoning_tokens, 0),
|
|
20
25
|
};
|
|
@@ -28,6 +33,8 @@ export class QoderEnvelopeReader {
|
|
|
28
33
|
usage = null;
|
|
29
34
|
error = null;
|
|
30
35
|
buffer = '';
|
|
36
|
+
// Non-`data:` lines, in arrival order: a plain (unframed) JSON body. See takeUnparsed().
|
|
37
|
+
plain = [];
|
|
31
38
|
ended = false;
|
|
32
39
|
pendingFinish = null;
|
|
33
40
|
pendingUsage = null;
|
|
@@ -39,6 +46,50 @@ export class QoderEnvelopeReader {
|
|
|
39
46
|
}
|
|
40
47
|
terminal() { return this.ended; }
|
|
41
48
|
errorEnvelope() { return this.error; }
|
|
49
|
+
/**
|
|
50
|
+
* Take the buffered, never-`data:`-framed bytes (if any) and clear them. An origin
|
|
51
|
+
* answering a non-streaming call with a plain JSON completion leaves the whole
|
|
52
|
+
* response here — the SSE line scanner only recognises `data:` frames.
|
|
53
|
+
*/
|
|
54
|
+
takeUnparsed() {
|
|
55
|
+
const raw = [...this.plain, this.buffer].join('\n').trim();
|
|
56
|
+
this.plain = [];
|
|
57
|
+
this.buffer = '';
|
|
58
|
+
return raw;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Fold an OpenAI-shaped non-streaming completion into this reader, so the caller
|
|
62
|
+
* gets text/toolCalls/finishReason/usage from one accessor set regardless of how
|
|
63
|
+
* the origin framed the answer. `usage` is only written when the body carries one:
|
|
64
|
+
* a zero would be indistinguishable from "the provider reported nothing".
|
|
65
|
+
*/
|
|
66
|
+
applyCompletion(body) {
|
|
67
|
+
const choice = (Array.isArray(body.choices) ? body.choices[0] : null);
|
|
68
|
+
const message = (choice?.message ?? {});
|
|
69
|
+
if (typeof message.content === 'string')
|
|
70
|
+
this.text += message.content;
|
|
71
|
+
const calls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
72
|
+
for (const call of calls) {
|
|
73
|
+
let input = {};
|
|
74
|
+
if (typeof call.function?.arguments === 'string' && call.function.arguments) {
|
|
75
|
+
try {
|
|
76
|
+
input = JSON.parse(call.function.arguments);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
input = {};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
this.toolCalls.push({ id: call.id ?? `call-${this.toolCalls.length}`, name: call.function?.name ?? 'unknown', input });
|
|
83
|
+
}
|
|
84
|
+
// Assigned directly, not via the pending machinery: this body is complete, and the
|
|
85
|
+
// stream has already ended (finish() set `ended`), which those paths return early on.
|
|
86
|
+
const finish = (choice?.finish_reason ?? (typeof body.status === 'string' ? body.status : null));
|
|
87
|
+
if (finish)
|
|
88
|
+
this.finishReason = finish;
|
|
89
|
+
if (body.usage)
|
|
90
|
+
this.usage = qoderUsageOf(body.usage);
|
|
91
|
+
this.ended = true;
|
|
92
|
+
}
|
|
42
93
|
push(text) {
|
|
43
94
|
if (this.ended)
|
|
44
95
|
return;
|
|
@@ -63,9 +114,17 @@ export class QoderEnvelopeReader {
|
|
|
63
114
|
}
|
|
64
115
|
cancel() { this.ended = true; this.buffer = ''; }
|
|
65
116
|
line(raw) {
|
|
66
|
-
|
|
67
|
-
|
|
117
|
+
// trim() also drops the trailing CR of a CRLF frame, so no separate strip is needed.
|
|
118
|
+
const trimmed = raw.trim();
|
|
119
|
+
if (!trimmed.startsWith('data:')) {
|
|
120
|
+
// An origin answering a non-streaming call may send the whole completion as one
|
|
121
|
+
// plain JSON body instead of SSE frames. Hold those lines rather than dropping
|
|
122
|
+
// them: finish() consumes the buffer, so this is the only place they survive.
|
|
123
|
+
// Blank lines and `:comment` keepalives are not content.
|
|
124
|
+
if (trimmed && !trimmed.startsWith(':'))
|
|
125
|
+
this.plain.push(trimmed);
|
|
68
126
|
return;
|
|
127
|
+
}
|
|
69
128
|
const data = trimmed.slice(5).trimStart();
|
|
70
129
|
if (data === '[DONE]') {
|
|
71
130
|
this.flushPending();
|
|
@@ -35,6 +35,7 @@ export function toSummary(r, maps) {
|
|
|
35
35
|
finalModelPublicId: finalModel?.publicModelId ?? null,
|
|
36
36
|
providerId: provider ? finalModel.providerId : null,
|
|
37
37
|
providerName: provider?.name ?? null,
|
|
38
|
+
providerType: provider?.type ?? null,
|
|
38
39
|
streaming: Boolean(r.streaming),
|
|
39
40
|
httpStatus: r.httpStatus,
|
|
40
41
|
success: Boolean(r.success),
|
|
@@ -18,6 +18,34 @@ const PRESETS = {
|
|
|
18
18
|
return { from: new Date(now.getTime() - 30 * 24 * 3600 * 1000), to: now, bucket: 'day' };
|
|
19
19
|
},
|
|
20
20
|
};
|
|
21
|
+
/**
|
|
22
|
+
* Provider-cache hit rate for a window.
|
|
23
|
+
*
|
|
24
|
+
* The blanket `cacheRead / (input + cacheRead)` double-counts the cache: OpenAI-compatible
|
|
25
|
+
* providers report `cached_tokens` as a SUBSET of `prompt_tokens`, so the cached prefix is
|
|
26
|
+
* already inside `input` and adding it again inflates the denominator — understating the
|
|
27
|
+
* rate (measured on real traffic: 21.75% instead of 27.80%).
|
|
28
|
+
*
|
|
29
|
+
* Anthropic is the exception: its `input_tokens` EXCLUDES the cached prefix, so there the
|
|
30
|
+
* prompt really is `input + cache`. Each row is weighed by its provider's own `type` in a
|
|
31
|
+
* single pass.
|
|
32
|
+
*/
|
|
33
|
+
function cacheHitRateFor(db, conds) {
|
|
34
|
+
const anthropic = sql `${schema.providers.type} = 'anthropic'`;
|
|
35
|
+
const r = db
|
|
36
|
+
.select({
|
|
37
|
+
cacheRead: sql `COALESCE(SUM(${schema.requests.cacheReadTokens}),0)`,
|
|
38
|
+
anthropicPrompt: sql `COALESCE(SUM(CASE WHEN ${anthropic} THEN ${schema.requests.inputTokens} + ${schema.requests.cacheReadTokens} ELSE 0 END),0)`,
|
|
39
|
+
otherPrompt: sql `COALESCE(SUM(CASE WHEN ${anthropic} THEN 0 ELSE ${schema.requests.inputTokens} END),0)`,
|
|
40
|
+
})
|
|
41
|
+
.from(schema.requests)
|
|
42
|
+
.leftJoin(schema.models, eq(schema.models.id, schema.requests.finalModelId))
|
|
43
|
+
.leftJoin(schema.providers, eq(schema.providers.id, schema.models.providerId))
|
|
44
|
+
.where(and(...conds))
|
|
45
|
+
.get();
|
|
46
|
+
const prompt = Number(r?.anthropicPrompt ?? 0) + Number(r?.otherPrompt ?? 0);
|
|
47
|
+
return prompt > 0 ? Number(r?.cacheRead ?? 0) / prompt : 0;
|
|
48
|
+
}
|
|
21
49
|
function rangeFromQuery(q) {
|
|
22
50
|
if (q.preset && PRESETS[q.preset])
|
|
23
51
|
return PRESETS[q.preset]();
|
|
@@ -50,6 +78,7 @@ export async function registerStatsRoutes(app) {
|
|
|
50
78
|
.from(schema.requests)
|
|
51
79
|
.where(and(...conds))
|
|
52
80
|
.get();
|
|
81
|
+
const cacheHitRate = cacheHitRateFor(db, conds);
|
|
53
82
|
const latRows = db
|
|
54
83
|
.select({ v: schema.requests.totalLatencyMs })
|
|
55
84
|
.from(schema.requests)
|
|
@@ -84,7 +113,7 @@ export async function registerStatsRoutes(app) {
|
|
|
84
113
|
p95LatencyMs: p95,
|
|
85
114
|
averageTtftMs: avgTtft,
|
|
86
115
|
p95TtftMs: p95Ttft,
|
|
87
|
-
cacheHitRate
|
|
116
|
+
cacheHitRate,
|
|
88
117
|
gatewayCacheHitRate: total ? Number(summary?.gatewayCacheHits ?? 0) / total : 0,
|
|
89
118
|
fallbackRate: total ? Number(summary?.fallbacks ?? 0) / total : 0,
|
|
90
119
|
};
|
|
@@ -241,6 +270,7 @@ function buildSummary(db, fromIso, toIso) {
|
|
|
241
270
|
const success = Number(r?.success ?? 0);
|
|
242
271
|
const cacheRead = Number(r?.cacheRead ?? 0);
|
|
243
272
|
const inputTokens = Number(r?.inputTokens ?? 0);
|
|
273
|
+
const cacheHitRate = cacheHitRateFor(db, conds);
|
|
244
274
|
return {
|
|
245
275
|
totalRequests: total,
|
|
246
276
|
successfulRequests: success,
|
|
@@ -256,7 +286,7 @@ function buildSummary(db, fromIso, toIso) {
|
|
|
256
286
|
p95LatencyMs: 0, // caller fills via percentile query
|
|
257
287
|
averageTtftMs: (r?.avgTtft ?? null),
|
|
258
288
|
p95TtftMs: null,
|
|
259
|
-
cacheHitRate
|
|
289
|
+
cacheHitRate,
|
|
260
290
|
gatewayCacheHitRate: total ? Number(r?.gatewayCacheHits ?? 0) / total : 0,
|
|
261
291
|
fallbackRate: total ? Number(r?.fallbacks ?? 0) / total : 0,
|
|
262
292
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}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;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--background: 0 0% 100%;--foreground: 240 10% 3.9%;--card: 0 0% 100%;--card-foreground: 240 10% 3.9%;--popover: 0 0% 100%;--popover-foreground: 240 10% 3.9%;--primary: 341 86% 41%;--primary-foreground: 0 0% 100%;--secondary: 240 4.8% 95.9%;--secondary-foreground: 240 5.9% 10%;--muted: 240 4.8% 95.9%;--muted-foreground: 240 3.8% 46.1%;--accent: 240 4.8% 95.9%;--accent-foreground: 240 5.9% 10%;--destructive: 0 84.2% 60.2%;--destructive-foreground: 0 0% 98%;--border: 240 5.9% 90%;--input: 240 5.9% 90%;--ring: 341 86% 41%;--sidebar: 240 5.9% 98%;--sidebar-foreground: 240 10% 3.9%;--radius: .5rem}.dark{--background: 240 10% 3.9%;--foreground: 0 0% 98%;--card: 240 10% 6%;--card-foreground: 0 0% 98%;--popover: 240 10% 5%;--popover-foreground: 0 0% 98%;--primary: 341 86% 55%;--primary-foreground: 0 0% 100%;--secondary: 240 3.7% 15.9%;--secondary-foreground: 0 0% 98%;--muted: 240 3.7% 15.9%;--muted-foreground: 240 5% 64.9%;--accent: 240 3.7% 15.9%;--accent-foreground: 0 0% 98%;--destructive: 0 72% 51%;--destructive-foreground: 0 0% 98%;--border: 240 3.7% 15.9%;--input: 240 3.7% 15.9%;--ring: 341 86% 55%;--sidebar: 240 10% 5%;--sidebar-foreground: 0 0% 98%}*{border-color:hsl(var(--border))}body{background-color:hsl(var(--background));color:hsl(var(--foreground));font-feature-settings:"rlig" 1,"calt" 1}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.left-1\/2{left:50%}.left-2{left:.5rem}.left-2\.5{left:.625rem}.left-\[50\%\]{left:50%}.right-2{right:.5rem}.right-4{right:1rem}.top-1\/2{top:50%}.top-16{top:4rem}.top-2\.5{top:.625rem}.top-4{top:1rem}.top-\[50\%\]{top:50%}.z-10{z-index:10}.z-50{z-index:50}.-mx-1{margin-left:-.25rem;margin-right:-.25rem}.my-1{margin-top:.25rem;margin-bottom:.25rem}.mb-1{margin-bottom:.25rem}.mb-3{margin-bottom:.75rem}.mb-6{margin-bottom:1.5rem}.ml-0\.5{margin-left:.125rem}.ml-1{margin-left:.25rem}.ml-1\.5{margin-left:.375rem}.ml-2{margin-left:.5rem}.mr-0\.5{margin-right:.125rem}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-14{height:3.5rem}.h-2{height:.5rem}.h-20{height:5rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-40{height:10rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-auto{height:auto}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.max-h-40{max-height:10rem}.max-h-48{max-height:12rem}.max-h-60{max-height:15rem}.max-h-80{max-height:20rem}.max-h-96{max-height:24rem}.max-h-\[calc\(100vh-8rem\)\]{max-height:calc(100vh - 8rem)}.min-h-32{min-height:8rem}.min-h-\[60px\]{min-height:60px}.min-h-screen{min-height:100vh}.w-1\.5{width:.375rem}.w-16{width:4rem}.w-2{width:.5rem}.w-20{width:5rem}.w-28{width:7rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-32{width:8rem}.w-36{width:9rem}.w-4{width:1rem}.w-40{width:10rem}.w-5{width:1.25rem}.w-56{width:14rem}.w-60{width:15rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-80{width:20rem}.w-9{width:2.25rem}.w-full{width:100%}.min-w-0{min-width:0px}.min-w-\[10rem\]{min-width:10rem}.min-w-\[8rem\]{min-width:8rem}.min-w-\[9rem\]{min-width:9rem}.min-w-\[var\(--radix-select-trigger-width\)\]{min-width:var(--radix-select-trigger-width)}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-64{max-width:16rem}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.caption-bottom{caption-side:bottom}.translate-x-\[-50\%\]{--tw-translate-x: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-\[-50\%\]{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-rotate-90{--tw-rotate: -90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-default{cursor:default}.cursor-grab{cursor:grab}.cursor-pointer{cursor:pointer}.touch-none{touch-action:none}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.items-stretch{align-items:stretch}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.25rem * var(--tw-space-x-reverse));margin-left:calc(.25rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:var(--radius)}.rounded-md{border-radius:calc(var(--radius) - 2px)}.rounded-sm{border-radius:calc(var(--radius) - 4px)}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-amber-200{--tw-border-opacity: 1;border-color:rgb(253 230 138 / var(--tw-border-opacity, 1))}.border-amber-500\/40{border-color:#f59e0b66}.border-amber-600\/50{border-color:#d9770680}.border-border{border-color:hsl(var(--border))}.border-destructive{border-color:hsl(var(--destructive))}.border-destructive\/40{border-color:hsl(var(--destructive) / .4)}.border-input{border-color:hsl(var(--input))}.border-primary{border-color:hsl(var(--primary))}.border-transparent{border-color:transparent}.bg-amber-400{--tw-bg-opacity: 1;background-color:rgb(251 191 36 / var(--tw-bg-opacity, 1))}.bg-amber-50{--tw-bg-opacity: 1;background-color:rgb(255 251 235 / var(--tw-bg-opacity, 1))}.bg-amber-500{--tw-bg-opacity: 1;background-color:rgb(245 158 11 / var(--tw-bg-opacity, 1))}.bg-amber-500\/90{background-color:#f59e0be6}.bg-background{background-color:hsl(var(--background))}.bg-black\/15{background-color:#00000026}.bg-black\/80{background-color:#000c}.bg-card{background-color:hsl(var(--card))}.bg-destructive{background-color:hsl(var(--destructive))}.bg-destructive\/10{background-color:hsl(var(--destructive) / .1)}.bg-emerald-100{--tw-bg-opacity: 1;background-color:rgb(209 250 229 / var(--tw-bg-opacity, 1))}.bg-emerald-400{--tw-bg-opacity: 1;background-color:rgb(52 211 153 / var(--tw-bg-opacity, 1))}.bg-emerald-500{--tw-bg-opacity: 1;background-color:rgb(16 185 129 / var(--tw-bg-opacity, 1))}.bg-emerald-600{--tw-bg-opacity: 1;background-color:rgb(5 150 105 / var(--tw-bg-opacity, 1))}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity, 1))}.bg-muted{background-color:hsl(var(--muted))}.bg-muted\/30{background-color:hsl(var(--muted) / .3)}.bg-muted\/60{background-color:hsl(var(--muted) / .6)}.bg-popover{background-color:hsl(var(--popover))}.bg-primary{background-color:hsl(var(--primary))}.bg-primary\/10{background-color:hsl(var(--primary) / .1)}.bg-primary\/70{background-color:hsl(var(--primary) / .7)}.bg-red-100{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity, 1))}.bg-red-400{--tw-bg-opacity: 1;background-color:rgb(248 113 113 / var(--tw-bg-opacity, 1))}.bg-secondary{background-color:hsl(var(--secondary))}.bg-sidebar{background-color:hsl(var(--sidebar))}.bg-transparent{background-color:transparent}.fill-amber-400{fill:#fbbf24}.fill-amber-500\/10{fill:#f59e0b1a}.fill-emerald-400{fill:#34d399}.fill-emerald-500{fill:#10b981}.fill-foreground{fill:hsl(var(--foreground))}.fill-muted-foreground{fill:hsl(var(--muted-foreground))}.fill-primary\/10{fill:hsl(var(--primary) / .1)}.fill-red-400{fill:#f87171}.fill-red-500{fill:#ef4444}.stroke-amber-500{stroke:#f59e0b}.stroke-emerald-500{stroke:#10b981}.stroke-primary{stroke:hsl(var(--primary))}.object-contain{-o-object-fit:contain;object-fit:contain}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pb-1{padding-bottom:.25rem}.pb-2{padding-bottom:.5rem}.pb-6{padding-bottom:1.5rem}.pl-8{padding-left:2rem}.pr-0{padding-right:0}.pr-2{padding-right:.5rem}.pt-0{padding-top:0}.pt-2{padding-top:.5rem}.pt-4{padding-top:1rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-middle{vertical-align:middle}.align-text-bottom{vertical-align:text-bottom}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.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)}.leading-none{line-height:1}.tracking-tight{letter-spacing:-.025em}.text-amber-500{--tw-text-opacity: 1;color:rgb(245 158 11 / var(--tw-text-opacity, 1))}.text-amber-600{--tw-text-opacity: 1;color:rgb(217 119 6 / var(--tw-text-opacity, 1))}.text-amber-800{--tw-text-opacity: 1;color:rgb(146 64 14 / var(--tw-text-opacity, 1))}.text-amber-900{--tw-text-opacity: 1;color:rgb(120 53 15 / var(--tw-text-opacity, 1))}.text-amber-950{--tw-text-opacity: 1;color:rgb(69 26 3 / var(--tw-text-opacity, 1))}.text-blue-500{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity, 1))}.text-card-foreground{color:hsl(var(--card-foreground))}.text-current{color:currentColor}.text-destructive{color:hsl(var(--destructive))}.text-destructive-foreground{color:hsl(var(--destructive-foreground))}.text-emerald-500{--tw-text-opacity: 1;color:rgb(16 185 129 / var(--tw-text-opacity, 1))}.text-emerald-600{--tw-text-opacity: 1;color:rgb(5 150 105 / var(--tw-text-opacity, 1))}.text-emerald-700{--tw-text-opacity: 1;color:rgb(4 120 87 / var(--tw-text-opacity, 1))}.text-foreground{color:hsl(var(--foreground))}.text-green-800{--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity, 1))}.text-muted-foreground{color:hsl(var(--muted-foreground))}.text-muted-foreground\/60{color:hsl(var(--muted-foreground) / .6)}.text-popover-foreground{color:hsl(var(--popover-foreground))}.text-primary{color:hsl(var(--primary))}.text-primary-foreground{color:hsl(var(--primary-foreground))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.text-red-700{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity, 1))}.text-secondary-foreground{color:hsl(var(--secondary-foreground))}.text-sidebar-foreground{color:hsl(var(--sidebar-foreground))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.underline-offset-4{text-underline-offset:4px}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-90{opacity:.9}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.ring-0{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-offset-background{--tw-ring-offset-color: hsl(var(--background))}.blur{--tw-blur: blur(8px);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)}.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{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}@keyframes route-active-pulse{0%{opacity:.35}50%{opacity:1}to{opacity:.35}}.route-active{animation:route-active-pulse 1.4s ease-in-out infinite}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-thumb{background:hsl(var(--border));border-radius:4px}::-webkit-scrollbar-track{background:transparent}.file\:border-0::file-selector-button{border-width:0px}.file\:bg-transparent::file-selector-button{background-color:transparent}.file\:text-sm::file-selector-button{font-size:.875rem;line-height:1.25rem}.file\:font-medium::file-selector-button{font-weight:500}.placeholder\:text-muted-foreground::-moz-placeholder{color:hsl(var(--muted-foreground))}.placeholder\:text-muted-foreground::placeholder{color:hsl(var(--muted-foreground))}.last\:border-0:last-child{border-width:0px}.hover\:bg-accent:hover{background-color:hsl(var(--accent))}.hover\:bg-destructive\/90:hover{background-color:hsl(var(--destructive) / .9)}.hover\:bg-muted:hover{background-color:hsl(var(--muted))}.hover\:bg-muted\/50:hover{background-color:hsl(var(--muted) / .5)}.hover\:bg-primary\/90:hover{background-color:hsl(var(--primary) / .9)}.hover\:bg-secondary\/80:hover{background-color:hsl(var(--secondary) / .8)}.hover\:text-accent-foreground:hover{color:hsl(var(--accent-foreground))}.hover\:text-foreground:hover{color:hsl(var(--foreground))}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-100:hover{opacity:1}.hover\:opacity-95:hover{opacity:.95}.focus\:bg-accent:focus{background-color:hsl(var(--accent))}.focus\:text-accent-foreground:focus{color:hsl(var(--accent-foreground))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-1:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-ring:focus{--tw-ring-color: hsl(var(--ring))}.focus-visible\:outline-none:focus-visible{outline:2px solid transparent;outline-offset:2px}.focus-visible\:ring-1:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-2:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-ring:focus-visible{--tw-ring-color: hsl(var(--ring))}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width: 2px}.focus-visible\:ring-offset-background:focus-visible{--tw-ring-offset-color: hsl(var(--background))}.active\:cursor-grabbing:active{cursor:grabbing}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.peer:disabled~.peer-disabled\:cursor-not-allowed{cursor:not-allowed}.peer:disabled~.peer-disabled\:opacity-70{opacity:.7}.data-\[disabled\]\:pointer-events-none[data-disabled]{pointer-events:none}.data-\[state\=checked\]\:translate-x-4[data-state=checked]{--tw-translate-x: 1rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[state\=unchecked\]\:translate-x-0[data-state=unchecked]{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[state\=active\]\:bg-background[data-state=active]{background-color:hsl(var(--background))}.data-\[state\=checked\]\:bg-primary[data-state=checked]{background-color:hsl(var(--primary))}.data-\[state\=selected\]\:bg-muted[data-state=selected]{background-color:hsl(var(--muted))}.data-\[state\=unchecked\]\:bg-input[data-state=unchecked]{background-color:hsl(var(--input))}.data-\[state\=active\]\:text-foreground[data-state=active]{color:hsl(var(--foreground))}.data-\[state\=checked\]\:text-primary-foreground[data-state=checked]{color:hsl(var(--primary-foreground))}.data-\[disabled\]\:opacity-50[data-disabled]{opacity:.5}.data-\[state\=active\]\:shadow[data-state=active]{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.dark\:border-amber-800:is(.dark *){--tw-border-opacity: 1;border-color:rgb(146 64 14 / var(--tw-border-opacity, 1))}.dark\:bg-amber-950\/20:is(.dark *){background-color:#451a0333}.dark\:bg-emerald-900\/40:is(.dark *){background-color:#064e3b66}.dark\:bg-green-900:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(20 83 45 / var(--tw-bg-opacity, 1))}.dark\:bg-red-900\/40:is(.dark *){background-color:#7f1d1d66}.dark\:text-amber-200:is(.dark *){--tw-text-opacity: 1;color:rgb(253 230 138 / var(--tw-text-opacity, 1))}.dark\:text-amber-300:is(.dark *){--tw-text-opacity: 1;color:rgb(252 211 77 / var(--tw-text-opacity, 1))}.dark\:text-emerald-300:is(.dark *){--tw-text-opacity: 1;color:rgb(110 231 183 / var(--tw-text-opacity, 1))}.dark\:text-green-200:is(.dark *){--tw-text-opacity: 1;color:rgb(187 247 208 / var(--tw-text-opacity, 1))}.dark\:text-red-300:is(.dark *){--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}@media(min-width:640px){.sm\:max-w-\[425px\]{max-width:425px}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:justify-end{justify-content:flex-end}.sm\:space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.sm\:rounded-lg{border-radius:var(--radius)}.sm\:text-left{text-align:left}}@media(min-width:768px){.md\:flex{display:flex}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:flex-col{flex-direction:column}}@media(min-width:1024px){.lg\:col-span-2{grid-column:span 2 / span 2}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}.\[\&_tr\:last-child\]\:border-0 tr:last-child{border-width:0px}.\[\&_tr\]\:border-b tr{border-bottom-width:1px}
|
|
1
|
+
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}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;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--background: 0 0% 100%;--foreground: 240 10% 3.9%;--card: 0 0% 100%;--card-foreground: 240 10% 3.9%;--popover: 0 0% 100%;--popover-foreground: 240 10% 3.9%;--primary: 341 86% 41%;--primary-foreground: 0 0% 100%;--secondary: 240 4.8% 95.9%;--secondary-foreground: 240 5.9% 10%;--muted: 240 4.8% 95.9%;--muted-foreground: 240 3.8% 46.1%;--accent: 240 4.8% 95.9%;--accent-foreground: 240 5.9% 10%;--destructive: 0 84.2% 60.2%;--destructive-foreground: 0 0% 98%;--border: 240 5.9% 90%;--input: 240 5.9% 90%;--ring: 341 86% 41%;--sidebar: 240 5.9% 98%;--sidebar-foreground: 240 10% 3.9%;--radius: .5rem}.dark{--background: 240 10% 3.9%;--foreground: 0 0% 98%;--card: 240 10% 6%;--card-foreground: 0 0% 98%;--popover: 240 10% 5%;--popover-foreground: 0 0% 98%;--primary: 341 86% 55%;--primary-foreground: 0 0% 100%;--secondary: 240 3.7% 15.9%;--secondary-foreground: 0 0% 98%;--muted: 240 3.7% 15.9%;--muted-foreground: 240 5% 64.9%;--accent: 240 3.7% 15.9%;--accent-foreground: 0 0% 98%;--destructive: 0 72% 51%;--destructive-foreground: 0 0% 98%;--border: 240 3.7% 15.9%;--input: 240 3.7% 15.9%;--ring: 341 86% 55%;--sidebar: 240 10% 5%;--sidebar-foreground: 0 0% 98%}*{border-color:hsl(var(--border))}body{background-color:hsl(var(--background));color:hsl(var(--foreground));font-feature-settings:"rlig" 1,"calt" 1}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.left-1\/2{left:50%}.left-2{left:.5rem}.left-2\.5{left:.625rem}.left-\[50\%\]{left:50%}.right-2{right:.5rem}.right-4{right:1rem}.top-1\/2{top:50%}.top-16{top:4rem}.top-2\.5{top:.625rem}.top-4{top:1rem}.top-\[50\%\]{top:50%}.z-10{z-index:10}.z-50{z-index:50}.-mx-1{margin-left:-.25rem;margin-right:-.25rem}.my-1{margin-top:.25rem;margin-bottom:.25rem}.mb-1{margin-bottom:.25rem}.mb-3{margin-bottom:.75rem}.mb-6{margin-bottom:1.5rem}.ml-0\.5{margin-left:.125rem}.ml-1{margin-left:.25rem}.ml-1\.5{margin-left:.375rem}.ml-2{margin-left:.5rem}.mr-0\.5{margin-right:.125rem}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-14{height:3.5rem}.h-2{height:.5rem}.h-20{height:5rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-40{height:10rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-auto{height:auto}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.max-h-40{max-height:10rem}.max-h-48{max-height:12rem}.max-h-60{max-height:15rem}.max-h-80{max-height:20rem}.max-h-96{max-height:24rem}.max-h-\[calc\(100vh-8rem\)\]{max-height:calc(100vh - 8rem)}.min-h-32{min-height:8rem}.min-h-\[60px\]{min-height:60px}.min-h-screen{min-height:100vh}.w-1\.5{width:.375rem}.w-16{width:4rem}.w-2{width:.5rem}.w-20{width:5rem}.w-28{width:7rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-32{width:8rem}.w-36{width:9rem}.w-4{width:1rem}.w-40{width:10rem}.w-5{width:1.25rem}.w-56{width:14rem}.w-60{width:15rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-80{width:20rem}.w-9{width:2.25rem}.w-full{width:100%}.min-w-0{min-width:0px}.min-w-\[10rem\]{min-width:10rem}.min-w-\[8rem\]{min-width:8rem}.min-w-\[9rem\]{min-width:9rem}.min-w-\[var\(--radix-select-trigger-width\)\]{min-width:var(--radix-select-trigger-width)}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-64{max-width:16rem}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.caption-bottom{caption-side:bottom}.translate-x-\[-50\%\]{--tw-translate-x: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-\[-50\%\]{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-rotate-90{--tw-rotate: -90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-default{cursor:default}.cursor-grab{cursor:grab}.cursor-pointer{cursor:pointer}.touch-none{touch-action:none}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.items-stretch{align-items:stretch}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.25rem * var(--tw-space-x-reverse));margin-left:calc(.25rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:var(--radius)}.rounded-md{border-radius:calc(var(--radius) - 2px)}.rounded-sm{border-radius:calc(var(--radius) - 4px)}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-amber-200{--tw-border-opacity: 1;border-color:rgb(253 230 138 / var(--tw-border-opacity, 1))}.border-amber-500\/40{border-color:#f59e0b66}.border-amber-600\/50{border-color:#d9770680}.border-border{border-color:hsl(var(--border))}.border-destructive{border-color:hsl(var(--destructive))}.border-destructive\/40{border-color:hsl(var(--destructive) / .4)}.border-input{border-color:hsl(var(--input))}.border-primary{border-color:hsl(var(--primary))}.border-transparent{border-color:transparent}.bg-amber-400{--tw-bg-opacity: 1;background-color:rgb(251 191 36 / var(--tw-bg-opacity, 1))}.bg-amber-50{--tw-bg-opacity: 1;background-color:rgb(255 251 235 / var(--tw-bg-opacity, 1))}.bg-amber-500{--tw-bg-opacity: 1;background-color:rgb(245 158 11 / var(--tw-bg-opacity, 1))}.bg-amber-500\/90{background-color:#f59e0be6}.bg-background{background-color:hsl(var(--background))}.bg-black\/15{background-color:#00000026}.bg-black\/80{background-color:#000c}.bg-card{background-color:hsl(var(--card))}.bg-destructive{background-color:hsl(var(--destructive))}.bg-destructive\/10{background-color:hsl(var(--destructive) / .1)}.bg-emerald-100{--tw-bg-opacity: 1;background-color:rgb(209 250 229 / var(--tw-bg-opacity, 1))}.bg-emerald-400{--tw-bg-opacity: 1;background-color:rgb(52 211 153 / var(--tw-bg-opacity, 1))}.bg-emerald-500{--tw-bg-opacity: 1;background-color:rgb(16 185 129 / var(--tw-bg-opacity, 1))}.bg-emerald-600{--tw-bg-opacity: 1;background-color:rgb(5 150 105 / var(--tw-bg-opacity, 1))}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity, 1))}.bg-muted{background-color:hsl(var(--muted))}.bg-muted\/30{background-color:hsl(var(--muted) / .3)}.bg-muted\/60{background-color:hsl(var(--muted) / .6)}.bg-popover{background-color:hsl(var(--popover))}.bg-primary{background-color:hsl(var(--primary))}.bg-primary\/10{background-color:hsl(var(--primary) / .1)}.bg-primary\/70{background-color:hsl(var(--primary) / .7)}.bg-red-100{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity, 1))}.bg-red-400{--tw-bg-opacity: 1;background-color:rgb(248 113 113 / var(--tw-bg-opacity, 1))}.bg-secondary{background-color:hsl(var(--secondary))}.bg-sidebar{background-color:hsl(var(--sidebar))}.bg-transparent{background-color:transparent}.fill-amber-400{fill:#fbbf24}.fill-amber-500\/10{fill:#f59e0b1a}.fill-emerald-400{fill:#34d399}.fill-emerald-500{fill:#10b981}.fill-foreground{fill:hsl(var(--foreground))}.fill-muted-foreground{fill:hsl(var(--muted-foreground))}.fill-primary\/10{fill:hsl(var(--primary) / .1)}.fill-red-400{fill:#f87171}.fill-red-500{fill:#ef4444}.stroke-amber-500{stroke:#f59e0b}.stroke-emerald-500{stroke:#10b981}.stroke-primary{stroke:hsl(var(--primary))}.object-contain{-o-object-fit:contain;object-fit:contain}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.pb-1{padding-bottom:.25rem}.pb-2{padding-bottom:.5rem}.pb-6{padding-bottom:1.5rem}.pl-8{padding-left:2rem}.pr-0{padding-right:0}.pr-2{padding-right:.5rem}.pt-0{padding-top:0}.pt-2{padding-top:.5rem}.pt-4{padding-top:1rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-middle{vertical-align:middle}.align-text-bottom{vertical-align:text-bottom}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.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)}.leading-none{line-height:1}.tracking-tight{letter-spacing:-.025em}.text-amber-500{--tw-text-opacity: 1;color:rgb(245 158 11 / var(--tw-text-opacity, 1))}.text-amber-600{--tw-text-opacity: 1;color:rgb(217 119 6 / var(--tw-text-opacity, 1))}.text-amber-800{--tw-text-opacity: 1;color:rgb(146 64 14 / var(--tw-text-opacity, 1))}.text-amber-900{--tw-text-opacity: 1;color:rgb(120 53 15 / var(--tw-text-opacity, 1))}.text-amber-950{--tw-text-opacity: 1;color:rgb(69 26 3 / var(--tw-text-opacity, 1))}.text-blue-500{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity, 1))}.text-card-foreground{color:hsl(var(--card-foreground))}.text-current{color:currentColor}.text-destructive{color:hsl(var(--destructive))}.text-destructive-foreground{color:hsl(var(--destructive-foreground))}.text-emerald-500{--tw-text-opacity: 1;color:rgb(16 185 129 / var(--tw-text-opacity, 1))}.text-emerald-600{--tw-text-opacity: 1;color:rgb(5 150 105 / var(--tw-text-opacity, 1))}.text-emerald-700{--tw-text-opacity: 1;color:rgb(4 120 87 / var(--tw-text-opacity, 1))}.text-foreground{color:hsl(var(--foreground))}.text-green-800{--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity, 1))}.text-muted-foreground{color:hsl(var(--muted-foreground))}.text-muted-foreground\/60{color:hsl(var(--muted-foreground) / .6)}.text-popover-foreground{color:hsl(var(--popover-foreground))}.text-primary{color:hsl(var(--primary))}.text-primary-foreground{color:hsl(var(--primary-foreground))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.text-red-700{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity, 1))}.text-secondary-foreground{color:hsl(var(--secondary-foreground))}.text-sidebar-foreground{color:hsl(var(--sidebar-foreground))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.underline-offset-4{text-underline-offset:4px}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-90{opacity:.9}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.ring-0{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-offset-background{--tw-ring-offset-color: hsl(var(--background))}.blur{--tw-blur: blur(8px);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)}.invert{--tw-invert: invert(100%);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)}.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{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}@keyframes route-active-pulse{0%{opacity:.35}50%{opacity:1}to{opacity:.35}}.route-active{animation:route-active-pulse 1.4s ease-in-out infinite}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-thumb{background:hsl(var(--border));border-radius:4px}::-webkit-scrollbar-track{background:transparent}.file\:border-0::file-selector-button{border-width:0px}.file\:bg-transparent::file-selector-button{background-color:transparent}.file\:text-sm::file-selector-button{font-size:.875rem;line-height:1.25rem}.file\:font-medium::file-selector-button{font-weight:500}.placeholder\:text-muted-foreground::-moz-placeholder{color:hsl(var(--muted-foreground))}.placeholder\:text-muted-foreground::placeholder{color:hsl(var(--muted-foreground))}.last\:border-0:last-child{border-width:0px}.hover\:bg-accent:hover{background-color:hsl(var(--accent))}.hover\:bg-destructive\/90:hover{background-color:hsl(var(--destructive) / .9)}.hover\:bg-muted:hover{background-color:hsl(var(--muted))}.hover\:bg-muted\/50:hover{background-color:hsl(var(--muted) / .5)}.hover\:bg-primary\/90:hover{background-color:hsl(var(--primary) / .9)}.hover\:bg-secondary\/80:hover{background-color:hsl(var(--secondary) / .8)}.hover\:text-accent-foreground:hover{color:hsl(var(--accent-foreground))}.hover\:text-foreground:hover{color:hsl(var(--foreground))}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-100:hover{opacity:1}.hover\:opacity-95:hover{opacity:.95}.focus\:bg-accent:focus{background-color:hsl(var(--accent))}.focus\:text-accent-foreground:focus{color:hsl(var(--accent-foreground))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-1:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-ring:focus{--tw-ring-color: hsl(var(--ring))}.focus-visible\:outline-none:focus-visible{outline:2px solid transparent;outline-offset:2px}.focus-visible\:ring-1:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-2:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-ring:focus-visible{--tw-ring-color: hsl(var(--ring))}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width: 2px}.focus-visible\:ring-offset-background:focus-visible{--tw-ring-offset-color: hsl(var(--background))}.active\:cursor-grabbing:active{cursor:grabbing}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.peer:disabled~.peer-disabled\:cursor-not-allowed{cursor:not-allowed}.peer:disabled~.peer-disabled\:opacity-70{opacity:.7}.data-\[disabled\]\:pointer-events-none[data-disabled]{pointer-events:none}.data-\[state\=checked\]\:translate-x-4[data-state=checked]{--tw-translate-x: 1rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[state\=unchecked\]\:translate-x-0[data-state=unchecked]{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[state\=active\]\:bg-background[data-state=active]{background-color:hsl(var(--background))}.data-\[state\=checked\]\:bg-primary[data-state=checked]{background-color:hsl(var(--primary))}.data-\[state\=selected\]\:bg-muted[data-state=selected]{background-color:hsl(var(--muted))}.data-\[state\=unchecked\]\:bg-input[data-state=unchecked]{background-color:hsl(var(--input))}.data-\[state\=active\]\:text-foreground[data-state=active]{color:hsl(var(--foreground))}.data-\[state\=checked\]\:text-primary-foreground[data-state=checked]{color:hsl(var(--primary-foreground))}.data-\[disabled\]\:opacity-50[data-disabled]{opacity:.5}.data-\[state\=active\]\:shadow[data-state=active]{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.dark\:border-amber-800:is(.dark *){--tw-border-opacity: 1;border-color:rgb(146 64 14 / var(--tw-border-opacity, 1))}.dark\:bg-amber-950\/20:is(.dark *){background-color:#451a0333}.dark\:bg-emerald-900\/40:is(.dark *){background-color:#064e3b66}.dark\:bg-green-900:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(20 83 45 / var(--tw-bg-opacity, 1))}.dark\:bg-red-900\/40:is(.dark *){background-color:#7f1d1d66}.dark\:text-amber-200:is(.dark *){--tw-text-opacity: 1;color:rgb(253 230 138 / var(--tw-text-opacity, 1))}.dark\:text-amber-300:is(.dark *){--tw-text-opacity: 1;color:rgb(252 211 77 / var(--tw-text-opacity, 1))}.dark\:text-emerald-300:is(.dark *){--tw-text-opacity: 1;color:rgb(110 231 183 / var(--tw-text-opacity, 1))}.dark\:text-green-200:is(.dark *){--tw-text-opacity: 1;color:rgb(187 247 208 / var(--tw-text-opacity, 1))}.dark\:text-red-300:is(.dark *){--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}@media(min-width:640px){.sm\:max-w-\[425px\]{max-width:425px}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:justify-end{justify-content:flex-end}.sm\:space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.sm\:rounded-lg{border-radius:var(--radius)}.sm\:text-left{text-align:left}}@media(min-width:768px){.md\:flex{display:flex}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:flex-col{flex-direction:column}}@media(min-width:1024px){.lg\:col-span-2{grid-column:span 2 / span 2}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}.\[\&_tr\:last-child\]\:border-0 tr:last-child{border-width:0px}.\[\&_tr\]\:border-b tr{border-bottom-width:1px}
|