quadwork 1.11.1 → 1.12.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 (70) hide show
  1. package/README.md +7 -0
  2. package/out/404.html +1 -1
  3. package/out/__next.__PAGE__.txt +1 -1
  4. package/out/__next._full.txt +2 -2
  5. package/out/__next._head.txt +1 -1
  6. package/out/__next._index.txt +2 -2
  7. package/out/__next._tree.txt +2 -2
  8. package/out/_next/static/chunks/0nk3kw~5j75~v.css +2 -0
  9. package/out/_next/static/chunks/{0a5314ra5t9bs.js → 11h7y0f5o9.hx.js} +1 -1
  10. package/out/_next/static/chunks/{0ge87xt6a9j~..js → 13w.n.3zipzvz.js} +8 -8
  11. package/out/_not-found/__next._full.txt +2 -2
  12. package/out/_not-found/__next._head.txt +1 -1
  13. package/out/_not-found/__next._index.txt +2 -2
  14. package/out/_not-found/__next._not-found.__PAGE__.txt +1 -1
  15. package/out/_not-found/__next._not-found.txt +1 -1
  16. package/out/_not-found/__next._tree.txt +2 -2
  17. package/out/_not-found.html +1 -1
  18. package/out/_not-found.txt +2 -2
  19. package/out/app-shell/__next._full.txt +2 -2
  20. package/out/app-shell/__next._head.txt +1 -1
  21. package/out/app-shell/__next._index.txt +2 -2
  22. package/out/app-shell/__next._tree.txt +2 -2
  23. package/out/app-shell/__next.app-shell.__PAGE__.txt +1 -1
  24. package/out/app-shell/__next.app-shell.txt +1 -1
  25. package/out/app-shell.html +1 -1
  26. package/out/app-shell.txt +2 -2
  27. package/out/index.html +1 -1
  28. package/out/index.txt +2 -2
  29. package/out/project/_/__next._full.txt +3 -3
  30. package/out/project/_/__next._head.txt +1 -1
  31. package/out/project/_/__next._index.txt +2 -2
  32. package/out/project/_/__next._tree.txt +2 -2
  33. package/out/project/_/__next.project.$d$id.__PAGE__.txt +2 -2
  34. package/out/project/_/__next.project.$d$id.txt +1 -1
  35. package/out/project/_/__next.project.txt +1 -1
  36. package/out/project/_/queue/__next._full.txt +2 -2
  37. package/out/project/_/queue/__next._head.txt +1 -1
  38. package/out/project/_/queue/__next._index.txt +2 -2
  39. package/out/project/_/queue/__next._tree.txt +2 -2
  40. package/out/project/_/queue/__next.project.$d$id.queue.__PAGE__.txt +1 -1
  41. package/out/project/_/queue/__next.project.$d$id.queue.txt +1 -1
  42. package/out/project/_/queue/__next.project.$d$id.txt +1 -1
  43. package/out/project/_/queue/__next.project.txt +1 -1
  44. package/out/project/_/queue.html +1 -1
  45. package/out/project/_/queue.txt +2 -2
  46. package/out/project/_.html +1 -1
  47. package/out/project/_.txt +3 -3
  48. package/out/settings/__next._full.txt +2 -2
  49. package/out/settings/__next._head.txt +1 -1
  50. package/out/settings/__next._index.txt +2 -2
  51. package/out/settings/__next._tree.txt +2 -2
  52. package/out/settings/__next.settings.__PAGE__.txt +1 -1
  53. package/out/settings/__next.settings.txt +1 -1
  54. package/out/settings.html +1 -1
  55. package/out/settings.txt +2 -2
  56. package/out/setup/__next._full.txt +2 -2
  57. package/out/setup/__next._head.txt +1 -1
  58. package/out/setup/__next._index.txt +2 -2
  59. package/out/setup/__next._tree.txt +2 -2
  60. package/out/setup/__next.setup.__PAGE__.txt +1 -1
  61. package/out/setup/__next.setup.txt +1 -1
  62. package/out/setup.html +1 -1
  63. package/out/setup.txt +2 -2
  64. package/package.json +7 -7
  65. package/server/__tests__/rate-limit-handling.test.js +168 -0
  66. package/server/routes.js +168 -73
  67. package/out/_next/static/chunks/0a4.d381szseh.css +0 -2
  68. /package/out/_next/static/{QmshV04af9o06krSyFHwf → nkNB54Q5aOvoEsUmAlro2}/_buildManifest.js +0 -0
  69. /package/out/_next/static/{QmshV04af9o06krSyFHwf → nkNB54Q5aOvoEsUmAlro2}/_clientMiddlewareManifest.js +0 -0
  70. /package/out/_next/static/{QmshV04af9o06krSyFHwf → nkNB54Q5aOvoEsUmAlro2}/_ssgManifest.js +0 -0
@@ -0,0 +1,168 @@
1
+ /**
2
+ * #554 — Verify rate-limit-aware caching and backoff in server/routes.js.
3
+ *
4
+ * Tests verify:
5
+ * 1. Rate limit state variables exist and are initialised
6
+ * 2. adaptiveTTL returns extended TTLs when rate is low/critical
7
+ * 3. cachedGhEndpoint serves stale data with _rateLimited flag
8
+ * 4. GitHub endpoints use the cached helper
9
+ * 5. /api/github/rate-limit endpoint is registered
10
+ * 6. Batch progress handler has rate-limit guard
11
+ */
12
+
13
+ const fs = require("fs");
14
+ const path = require("path");
15
+
16
+ const ROUTES_PATH = path.join(__dirname, "..", "routes.js");
17
+ const src = fs.readFileSync(ROUTES_PATH, "utf-8");
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // 1. Rate limit state and constants
21
+ // ---------------------------------------------------------------------------
22
+
23
+ describe("#554 rate limit infrastructure (code analysis)", () => {
24
+ test("_rateLimit state object is defined with expected fields", () => {
25
+ expect(src).toContain("const _rateLimit = {");
26
+ expect(src).toContain("remaining:");
27
+ expect(src).toContain("resetAt:");
28
+ });
29
+
30
+ test("RATE_LIMIT_LOW_THRESHOLD and RATE_LIMIT_CRITICAL are defined", () => {
31
+ expect(src).toMatch(/RATE_LIMIT_LOW_THRESHOLD\s*=\s*\d+/);
32
+ expect(src).toMatch(/RATE_LIMIT_CRITICAL\s*=\s*\d+/);
33
+ });
34
+
35
+ test("refreshRateLimit calls gh api rate_limit", () => {
36
+ expect(src).toContain("gh");
37
+ expect(src).toContain("api");
38
+ expect(src).toContain("rate_limit");
39
+ });
40
+
41
+ test("startRateLimitPolling is called at module load", () => {
42
+ expect(src).toContain("startRateLimitPolling()");
43
+ });
44
+ });
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // 2. Adaptive TTL
48
+ // ---------------------------------------------------------------------------
49
+
50
+ describe("#554 adaptive TTL logic", () => {
51
+ test("adaptiveTTL function is defined", () => {
52
+ expect(src).toContain("function adaptiveTTL(baseTTL)");
53
+ });
54
+
55
+ test("adaptiveTTL returns Infinity when critically rate-limited", () => {
56
+ expect(src).toMatch(/isRateLimited\(\).*Infinity/s);
57
+ });
58
+
59
+ test("adaptiveTTL extends TTL when rate is low", () => {
60
+ expect(src).toMatch(/isRateLow\(\).*120[_,]?000/s);
61
+ });
62
+ });
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // 3. Cached endpoint helper
66
+ // ---------------------------------------------------------------------------
67
+
68
+ describe("#554 cachedGhEndpoint helper", () => {
69
+ test("cachedGhEndpoint function is defined", () => {
70
+ expect(src).toContain("function cachedGhEndpoint(");
71
+ });
72
+
73
+ test("serves stale data with _rateLimited flag when critical", () => {
74
+ const fnStart = src.indexOf("function cachedGhEndpoint(");
75
+ const fnBody = src.slice(fnStart, fnStart + 800);
76
+ expect(fnBody).toContain("_rateLimited");
77
+ expect(fnBody).toContain("_stale");
78
+ });
79
+
80
+ test("uses adaptiveTTL for cache checks", () => {
81
+ const fnStart = src.indexOf("function cachedGhEndpoint(");
82
+ const fnBody = src.slice(fnStart, fnStart + 600);
83
+ expect(fnBody).toContain("adaptiveTTL");
84
+ });
85
+ });
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // 4. GitHub endpoints use cached helper
89
+ // ---------------------------------------------------------------------------
90
+
91
+ describe("#554 GitHub endpoints use cachedGhEndpoint", () => {
92
+ test("/api/github/issues uses cachedGhEndpoint", () => {
93
+ const section = src.slice(
94
+ src.indexOf('"/api/github/issues"'),
95
+ src.indexOf('"/api/github/issues"') + 300,
96
+ );
97
+ expect(section).toContain("cachedGhEndpoint");
98
+ });
99
+
100
+ test("/api/github/prs uses cachedGhEndpoint", () => {
101
+ const section = src.slice(
102
+ src.indexOf('"/api/github/prs"'),
103
+ src.indexOf('"/api/github/prs"') + 300,
104
+ );
105
+ expect(section).toContain("cachedGhEndpoint");
106
+ });
107
+
108
+ test("/api/github/closed-issues uses cachedGhEndpoint", () => {
109
+ const section = src.slice(
110
+ src.indexOf('"/api/github/closed-issues"'),
111
+ src.indexOf('"/api/github/closed-issues"') + 500,
112
+ );
113
+ expect(section).toContain("cachedGhEndpoint");
114
+ });
115
+
116
+ test("/api/github/merged-prs uses cachedGhEndpoint", () => {
117
+ const section = src.slice(
118
+ src.indexOf('"/api/github/merged-prs"'),
119
+ src.indexOf('"/api/github/merged-prs"') + 500,
120
+ );
121
+ expect(section).toContain("cachedGhEndpoint");
122
+ });
123
+ });
124
+
125
+ // ---------------------------------------------------------------------------
126
+ // 5. Rate limit API endpoint
127
+ // ---------------------------------------------------------------------------
128
+
129
+ describe("#554 /api/github/rate-limit endpoint", () => {
130
+ test("endpoint is registered", () => {
131
+ expect(src).toContain('"/api/github/rate-limit"');
132
+ });
133
+
134
+ test("returns remaining, limit, resetInMinutes, low, critical fields", () => {
135
+ const idx = src.indexOf('"/api/github/rate-limit"');
136
+ const section = src.slice(idx, idx + 500);
137
+ expect(section).toContain("remaining");
138
+ expect(section).toContain("limit");
139
+ expect(section).toContain("resetInMinutes");
140
+ expect(section).toContain("low");
141
+ expect(section).toContain("critical");
142
+ });
143
+ });
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // 6. Batch progress rate limit guard
147
+ // ---------------------------------------------------------------------------
148
+
149
+ describe("#554 batch progress rate limit awareness", () => {
150
+ test("batch progress handler checks isRateLimited before gh calls", () => {
151
+ const batchStart = src.indexOf('"/api/batch-progress"');
152
+ const batchSection = src.slice(batchStart, batchStart + 600);
153
+ expect(batchSection).toContain("isRateLimited()");
154
+ expect(batchSection).toContain("_rateLimited");
155
+ });
156
+
157
+ test("batch progress uses adaptiveTTL for cache", () => {
158
+ const batchStart = src.indexOf('"/api/batch-progress"');
159
+ const batchSection = src.slice(batchStart, batchStart + 400);
160
+ expect(batchSection).toContain("adaptiveTTL");
161
+ });
162
+
163
+ test("projects endpoint uses adaptiveTTL for cache", () => {
164
+ const projStart = src.indexOf('"/api/projects"');
165
+ const projSection = src.slice(projStart, projStart + 400);
166
+ expect(projSection).toContain("adaptiveTTL");
167
+ });
168
+ });
package/server/routes.js CHANGED
@@ -3,7 +3,8 @@
3
3
  * Routes: config, chat, projects, memory, setup, rename, github/issues, github/prs, telegram
4
4
  */
5
5
  const express = require("express");
6
- const { execFileSync, spawn } = require("child_process");
6
+ const { execFile: _execFileCb, execFileSync, spawn } = require("child_process");
7
+ const _execFileAsync = require("util").promisify(_execFileCb);
7
8
  const fs = require("fs");
8
9
  const path = require("path");
9
10
  const os = require("os");
@@ -18,6 +19,89 @@ const ENV_PATH = path.join(CONFIG_DIR, ".env");
18
19
  const TEMPLATES_DIR = path.join(__dirname, "..", "templates");
19
20
  const REPO_RE = /^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/;
20
21
 
22
+ // ─── GitHub API rate limit tracking (#554) ────────────────────────────────
23
+ // Shared rate-limit state: periodically refreshed via `gh api rate_limit`.
24
+ // Server-side gh calls check this before executing and back off when low.
25
+ const _rateLimit = {
26
+ limit: 5000,
27
+ remaining: 5000,
28
+ resetAt: 0, // epoch ms
29
+ updatedAt: 0, // epoch ms when we last fetched
30
+ error: null, // last fetch error message, if any
31
+ };
32
+ const RATE_LIMIT_POLL_MS = 60_000; // refresh every 60s
33
+ const RATE_LIMIT_LOW_THRESHOLD = 200; // below this → back off
34
+ const RATE_LIMIT_CRITICAL = 50; // below this → stop all infra gh calls
35
+ let _rateLimitTimer = null;
36
+
37
+ async function refreshRateLimit() {
38
+ try {
39
+ const { stdout } = await _execFileAsync("gh", [
40
+ "api", "rate_limit", "--jq", ".resources.core | {limit,remaining,reset}",
41
+ ], { encoding: "utf-8", timeout: 10000 });
42
+ const data = JSON.parse(stdout);
43
+ _rateLimit.limit = data.limit;
44
+ _rateLimit.remaining = data.remaining;
45
+ _rateLimit.resetAt = data.reset * 1000; // seconds → ms
46
+ _rateLimit.updatedAt = Date.now();
47
+ _rateLimit.error = null;
48
+ } catch (err) {
49
+ _rateLimit.error = err.message;
50
+ _rateLimit.updatedAt = Date.now();
51
+ }
52
+ }
53
+
54
+ function startRateLimitPolling() {
55
+ if (_rateLimitTimer) return;
56
+ refreshRateLimit();
57
+ _rateLimitTimer = setInterval(refreshRateLimit, RATE_LIMIT_POLL_MS);
58
+ }
59
+
60
+ function isRateLimited() {
61
+ return _rateLimit.remaining < RATE_LIMIT_CRITICAL;
62
+ }
63
+ function isRateLow() {
64
+ return _rateLimit.remaining < RATE_LIMIT_LOW_THRESHOLD;
65
+ }
66
+
67
+ // Adaptive cache TTL: normal 30s, low 120s, critical ∞ (serve stale)
68
+ function adaptiveTTL(baseTTL) {
69
+ if (isRateLimited()) return Infinity;
70
+ if (isRateLow()) return Math.max(baseTTL, 120_000);
71
+ return baseTTL;
72
+ }
73
+
74
+ // ─── Cached GitHub endpoint helper (#554) ─────────────────────────────────
75
+ // Wraps a synchronous execFileSync gh call with an in-memory cache that
76
+ // serves stale data when rate-limited instead of hammering the API.
77
+ const _ghEndpointCache = new Map(); // key → { ts, data }
78
+ const GH_ENDPOINT_CACHE_TTL = 30_000; // 30s base TTL
79
+
80
+ function cachedGhEndpoint(cacheKey, ghArgs, res, { transform } = {}) {
81
+ const ttl = adaptiveTTL(GH_ENDPOINT_CACHE_TTL);
82
+ const cached = _ghEndpointCache.get(cacheKey);
83
+ if (cached && Date.now() - cached.ts < ttl) {
84
+ return res.json(cached.stale ? { ...cached.data, _stale: true } : cached.data);
85
+ }
86
+ // If critically rate-limited, serve whatever we have (even expired)
87
+ if (isRateLimited() && cached) {
88
+ return res.json({ ...cached.data, _stale: true, _rateLimited: true });
89
+ }
90
+ try {
91
+ const out = execFileSync("gh", ghArgs, { encoding: "utf-8", timeout: 15000 });
92
+ let data = JSON.parse(out);
93
+ if (transform) data = transform(data);
94
+ _ghEndpointCache.set(cacheKey, { ts: Date.now(), data, stale: false });
95
+ res.json(data);
96
+ } catch (err) {
97
+ // On error, try to serve stale cache
98
+ if (cached) {
99
+ return res.json({ ...cached.data, _stale: true });
100
+ }
101
+ res.status(502).json({ error: "gh call failed", detail: err.message });
102
+ }
103
+ }
104
+
21
105
  const DEFAULT_CONFIG = {
22
106
  port: 8400,
23
107
  agentchattr_url: "http://127.0.0.1:8300",
@@ -1076,7 +1160,11 @@ let _projectsCacheTs = 0;
1076
1160
  const PROJECTS_CACHE_TTL = 60_000;
1077
1161
 
1078
1162
  router.get("/api/projects", async (req, res) => {
1079
- if (_projectsCache && Date.now() - _projectsCacheTs < PROJECTS_CACHE_TTL) {
1163
+ if (_projectsCache && Date.now() - _projectsCacheTs < adaptiveTTL(PROJECTS_CACHE_TTL)) {
1164
+ return res.json(_projectsCache);
1165
+ }
1166
+ // #554: serve stale projects cache when critically rate-limited
1167
+ if (isRateLimited() && _projectsCache) {
1080
1168
  return res.json(_projectsCache);
1081
1169
  }
1082
1170
 
@@ -1186,6 +1274,24 @@ router.get("/api/projects", async (req, res) => {
1186
1274
  res.json(result);
1187
1275
  });
1188
1276
 
1277
+ // ─── GitHub Rate Limit (#554) ──────────────────────────────────────────────
1278
+
1279
+ router.get("/api/github/rate-limit", (_req, res) => {
1280
+ const resetIn = _rateLimit.resetAt > Date.now()
1281
+ ? Math.ceil((_rateLimit.resetAt - Date.now()) / 60000)
1282
+ : 0;
1283
+ res.json({
1284
+ limit: _rateLimit.limit,
1285
+ remaining: _rateLimit.remaining,
1286
+ resetAt: _rateLimit.resetAt,
1287
+ resetInMinutes: resetIn,
1288
+ low: isRateLow(),
1289
+ critical: isRateLimited(),
1290
+ updatedAt: _rateLimit.updatedAt,
1291
+ error: _rateLimit.error,
1292
+ });
1293
+ });
1294
+
1189
1295
  // ─── GitHub Issues / PRs ───────────────────────────────────────────────────
1190
1296
 
1191
1297
  function getRepo(projectId) {
@@ -1203,33 +1309,21 @@ function getRepo(projectId) {
1203
1309
  router.get("/api/github/issues", (req, res) => {
1204
1310
  const repo = getRepo(req.query.project || "");
1205
1311
  if (!repo) return res.status(400).json({ error: "No repo configured for project" });
1206
-
1207
- try {
1208
- const out = execFileSync(
1209
- "gh",
1210
- ["issue", "list", "-R", repo, "--json", "number,title,state,assignees,labels,createdAt,url", "--limit", "50"],
1211
- { encoding: "utf-8", timeout: 15000 }
1212
- );
1213
- res.json(JSON.parse(out));
1214
- } catch (err) {
1215
- res.status(502).json({ error: "gh issue list failed", detail: err.message });
1216
- }
1312
+ cachedGhEndpoint(
1313
+ `issues:${repo}`,
1314
+ ["issue", "list", "-R", repo, "--json", "number,title,state,assignees,labels,createdAt,url", "--limit", "50"],
1315
+ res,
1316
+ );
1217
1317
  });
1218
1318
 
1219
1319
  router.get("/api/github/prs", (req, res) => {
1220
1320
  const repo = getRepo(req.query.project || "");
1221
1321
  if (!repo) return res.status(400).json({ error: "No repo configured for project" });
1222
-
1223
- try {
1224
- const out = execFileSync(
1225
- "gh",
1226
- ["pr", "list", "-R", repo, "--json", "number,title,state,author,assignees,reviewDecision,reviews,statusCheckRollup,url,createdAt", "--limit", "50"],
1227
- { encoding: "utf-8", timeout: 15000 }
1228
- );
1229
- res.json(JSON.parse(out));
1230
- } catch (err) {
1231
- res.status(502).json({ error: "gh pr list failed", detail: err.message });
1232
- }
1322
+ cachedGhEndpoint(
1323
+ `prs:${repo}`,
1324
+ ["pr", "list", "-R", repo, "--json", "number,title,state,author,assignees,reviewDecision,reviews,statusCheckRollup,url,createdAt", "--limit", "50"],
1325
+ res,
1326
+ );
1233
1327
  });
1234
1328
 
1235
1329
  // #411 / quadwork#281: recently closed issues + merged PRs for the
@@ -1247,57 +1341,51 @@ const RECENT_DISPLAY_LIMIT = 5;
1247
1341
  router.get("/api/github/closed-issues", (req, res) => {
1248
1342
  const repo = getRepo(req.query.project || "");
1249
1343
  if (!repo) return res.status(400).json({ error: "No repo configured for project" });
1250
- try {
1251
- const out = execFileSync(
1252
- "gh",
1253
- ["issue", "list", "-R", repo, "--state", "closed", "--json", "number,title,state,url,closedAt", "--limit", String(RECENT_FETCH_LIMIT)],
1254
- { encoding: "utf-8", timeout: 15000 },
1255
- );
1256
- const items = JSON.parse(out);
1257
- const sorted = Array.isArray(items)
1258
- ? items
1259
- .slice()
1260
- .sort((a, b) => {
1261
- const ta = a && a.closedAt ? Date.parse(a.closedAt) : 0;
1262
- const tb = b && b.closedAt ? Date.parse(b.closedAt) : 0;
1263
- return tb - ta;
1264
- })
1265
- .slice(0, RECENT_DISPLAY_LIMIT)
1266
- : items;
1267
- res.json(sorted);
1268
- } catch (err) {
1269
- res.status(502).json({ error: "gh issue list (closed) failed", detail: err.message });
1270
- }
1344
+ cachedGhEndpoint(
1345
+ `closed-issues:${repo}`,
1346
+ ["issue", "list", "-R", repo, "--state", "closed", "--json", "number,title,state,url,closedAt", "--limit", String(RECENT_FETCH_LIMIT)],
1347
+ res,
1348
+ {
1349
+ transform: (items) =>
1350
+ Array.isArray(items)
1351
+ ? items
1352
+ .slice()
1353
+ .sort((a, b) => {
1354
+ const ta = a && a.closedAt ? Date.parse(a.closedAt) : 0;
1355
+ const tb = b && b.closedAt ? Date.parse(b.closedAt) : 0;
1356
+ return tb - ta;
1357
+ })
1358
+ .slice(0, RECENT_DISPLAY_LIMIT)
1359
+ : items,
1360
+ },
1361
+ );
1271
1362
  });
1272
1363
 
1273
1364
  router.get("/api/github/merged-prs", (req, res) => {
1274
1365
  const repo = getRepo(req.query.project || "");
1275
1366
  if (!repo) return res.status(400).json({ error: "No repo configured for project" });
1276
- try {
1277
- // gh pr list with `--state merged` filters server-side so we
1278
- // don't have to pull every closed PR and discard the un-merged
1279
- // ones (closed-without-merge). Same fetch-wider-then-sort
1280
- // strategy as closed-issues so the newest merge always wins.
1281
- const out = execFileSync(
1282
- "gh",
1283
- ["pr", "list", "-R", repo, "--state", "merged", "--json", "number,title,state,url,mergedAt,author", "--limit", String(RECENT_FETCH_LIMIT)],
1284
- { encoding: "utf-8", timeout: 15000 },
1285
- );
1286
- const items = JSON.parse(out);
1287
- const sorted = Array.isArray(items)
1288
- ? items
1289
- .slice()
1290
- .sort((a, b) => {
1291
- const ta = a && a.mergedAt ? Date.parse(a.mergedAt) : 0;
1292
- const tb = b && b.mergedAt ? Date.parse(b.mergedAt) : 0;
1293
- return tb - ta;
1294
- })
1295
- .slice(0, RECENT_DISPLAY_LIMIT)
1296
- : items;
1297
- res.json(sorted);
1298
- } catch (err) {
1299
- res.status(502).json({ error: "gh pr list (merged) failed", detail: err.message });
1300
- }
1367
+ // gh pr list with `--state merged` filters server-side so we
1368
+ // don't have to pull every closed PR and discard the un-merged
1369
+ // ones (closed-without-merge). Same fetch-wider-then-sort
1370
+ // strategy as closed-issues so the newest merge always wins.
1371
+ cachedGhEndpoint(
1372
+ `merged-prs:${repo}`,
1373
+ ["pr", "list", "-R", repo, "--state", "merged", "--json", "number,title,state,url,mergedAt,author", "--limit", String(RECENT_FETCH_LIMIT)],
1374
+ res,
1375
+ {
1376
+ transform: (items) =>
1377
+ Array.isArray(items)
1378
+ ? items
1379
+ .slice()
1380
+ .sort((a, b) => {
1381
+ const ta = a && a.mergedAt ? Date.parse(a.mergedAt) : 0;
1382
+ const tb = b && b.mergedAt ? Date.parse(b.mergedAt) : 0;
1383
+ return tb - ta;
1384
+ })
1385
+ .slice(0, RECENT_DISPLAY_LIMIT)
1386
+ : items,
1387
+ },
1388
+ );
1301
1389
  });
1302
1390
 
1303
1391
  // #413 / quadwork#282: Current Batch Progress panel.
@@ -1499,8 +1587,6 @@ function parseActiveBatch(queueText) {
1499
1587
  // catch-all-and-return-null contract collapsed real subprocess
1500
1588
  // errors into the "not found" branch, making the new failure-row
1501
1589
  // fallback unreachable for genuine command failures (t2a review).
1502
- const { execFile: _execFile } = require("child_process");
1503
- const _execFileAsync = require("util").promisify(_execFile);
1504
1590
  async function ghJsonExecAsync(args) {
1505
1591
  const { stdout } = await _execFileAsync("gh", args, { encoding: "utf-8", timeout: 10000 });
1506
1592
  return JSON.parse(stdout);
@@ -1698,9 +1784,15 @@ router.get("/api/batch-progress", async (req, res) => {
1698
1784
  if (!projectId) return res.status(400).json({ error: "Missing project" });
1699
1785
 
1700
1786
  const cached = _batchProgressCache.get(projectId);
1701
- if (cached && Date.now() - cached.ts < BATCH_PROGRESS_TTL_MS) {
1787
+ const batchTTL = adaptiveTTL(BATCH_PROGRESS_TTL_MS);
1788
+ if (cached && Date.now() - cached.ts < batchTTL) {
1702
1789
  return res.json(cached.data);
1703
1790
  }
1791
+ // #554: if critically rate-limited, serve stale cache instead of
1792
+ // firing N gh calls per batch item.
1793
+ if (isRateLimited() && cached) {
1794
+ return res.json({ ...cached.data, _stale: true, _rateLimited: true });
1795
+ }
1704
1796
 
1705
1797
  const repo = getRepo(projectId);
1706
1798
  if (!repo) return res.status(400).json({ error: "No repo configured for project" });
@@ -3349,6 +3441,9 @@ router.put("/api/project/:projectId/agent-models/:agentId", (req, res) => {
3349
3441
  }
3350
3442
  });
3351
3443
 
3444
+ // #554: start rate-limit polling as soon as routes are loaded.
3445
+ startRateLimitPolling();
3446
+
3352
3447
  module.exports = router;
3353
3448
  // #341: export parseActiveBatch for unit tests. No production callers
3354
3449
  // outside this file; the export is strictly for the node:assert
@@ -1,2 +0,0 @@
1
- @font-face{font-family:Geist Mono;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/4fa387ec64143e14-s.0q3udbd2bu5yp.woff2)format("woff2");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Geist Mono;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/bbc41e54d2fcbd21-s.0gw~uztddq1df.woff2)format("woff2");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 Mono;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/797e433ab948586e-s.p.0.q-h669a_dqa.woff2)format("woff2");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}@font-face{font-family:Geist Mono Fallback;src:local(Arial);ascent-override:74.67%;descent-override:21.92%;line-gap-override:0.0%;size-adjust:134.59%}.geist_mono_8d43a2aa-module__8Li5zG__className{font-family:Geist Mono,Geist Mono Fallback;font-style:normal}.geist_mono_8d43a2aa-module__8Li5zG__variable{--font-geist-mono:"Geist Mono", "Geist Mono Fallback"}
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)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--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-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-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--color-red-400:#ff6568;--color-red-500:#fb2c36;--color-red-700:#bf000f;--color-red-900:#82181a;--color-amber-200:#fee685;--color-amber-300:#ffd236;--color-amber-400:#fcbb00;--color-amber-500:#f99c00;--color-green-500:#00c758;--color-blue-300:#90c5ff;--color-blue-400:#54a2ff;--color-neutral-200:#e5e5e5;--color-neutral-300:#d4d4d4;--color-neutral-400:#a1a1a1;--color-neutral-500:#737373;--color-neutral-600:#525252;--color-neutral-700:#404040;--color-neutral-900:#171717;--color-neutral-950:#0a0a0a;--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-md:28rem;--container-lg:32rem;--container-xl:36rem;--container-3xl:48rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-tight:1.25;--leading-snug:1.375;--leading-relaxed:1.625;--radius-sm:.25rem;--radius-lg:.5rem;--ease-in-out:cubic-bezier(.4, 0, .2, 1);--animate-ping:ping 1s cubic-bezier(0, 0, .2, 1) infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--blur-sm:8px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-geist-mono)}@supports (color:lab(0% 0 0)){:root,:host{--color-red-400:lab(63.7053% 60.745 31.3109);--color-red-500:lab(55.4814% 75.0732 48.8528);--color-red-700:lab(40.4273% 67.2623 53.7441);--color-red-900:lab(28.5139% 44.5539 29.0463);--color-amber-200:lab(91.7203% -.505269 49.9084);--color-amber-300:lab(86.4156% 6.13147 78.3961);--color-amber-400:lab(80.1641% 16.6016 99.2089);--color-amber-500:lab(72.7183% 31.8672 97.9407);--color-green-500:lab(70.5521% -66.5147 45.8073);--color-blue-300:lab(77.5052% -6.4629 -36.42);--color-blue-400:lab(65.0361% -1.42065 -56.9802);--color-neutral-200:lab(90.952% 0 -.0000119209);--color-neutral-300:lab(84.92% 0 -.0000119209);--color-neutral-400:lab(66.128% -.0000298023 .0000119209);--color-neutral-500:lab(48.496% 0 0);--color-neutral-600:lab(34.924% 0 0);--color-neutral-700:lab(27.036% 0 0);--color-neutral-900:lab(7.78201% -.0000149012 0);--color-neutral-950:lab(2.75381% 0 0)}}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.invisible{visibility:hidden}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing) * 0)}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.-top-1{top:calc(var(--spacing) * -1)}.-top-1\.5{top:calc(var(--spacing) * -1.5)}.top-0{top:calc(var(--spacing) * 0)}.top-3{top:calc(var(--spacing) * 3)}.top-4{top:calc(var(--spacing) * 4)}.top-5{top:calc(var(--spacing) * 5)}.top-6{top:calc(var(--spacing) * 6)}.-right-1{right:calc(var(--spacing) * -1)}.-right-1\.5{right:calc(var(--spacing) * -1.5)}.right-0{right:calc(var(--spacing) * 0)}.right-3{right:calc(var(--spacing) * 3)}.bottom-3{bottom:calc(var(--spacing) * 3)}.bottom-5{bottom:calc(var(--spacing) * 5)}.bottom-full{bottom:100%}.left-0{left:calc(var(--spacing) * 0)}.left-16{left:calc(var(--spacing) * 16)}.left-\[14px\]{left:14px}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.col-span-2{grid-column:span 2/span 2}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-4{margin-inline:calc(var(--spacing) * 4)}.my-1{margin-block:calc(var(--spacing) * 1)}.my-2{margin-block:calc(var(--spacing) * 2)}.my-4{margin-block:calc(var(--spacing) * 4)}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-5{margin-top:calc(var(--spacing) * 5)}.mb-0\.5{margin-bottom:calc(var(--spacing) * .5)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.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-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.ml-0\.5{margin-left:calc(var(--spacing) * .5)}.ml-1{margin-left:calc(var(--spacing) * 1)}.ml-1\.5{margin-left:calc(var(--spacing) * 1.5)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-3{margin-left:calc(var(--spacing) * 3)}.ml-auto{margin-left:auto}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.list-item{display:list-item}.table{display:table}.h-1{height:calc(var(--spacing) * 1)}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-2{height:calc(var(--spacing) * 2)}.h-3{height:calc(var(--spacing) * 3)}.h-3\.5{height:calc(var(--spacing) * 3.5)}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-7{height:calc(var(--spacing) * 7)}.h-10{height:calc(var(--spacing) * 10)}.h-12{height:calc(var(--spacing) * 12)}.h-16{height:calc(var(--spacing) * 16)}.h-\[12px\]{height:12px}.h-\[80vh\]{height:80vh}.h-\[calc\(100\%-80px\)\]{height:calc(100% - 80px)}.h-full{height:100%}.h-px{height:1px}.max-h-28{max-height:calc(var(--spacing) * 28)}.max-h-40{max-height:calc(var(--spacing) * 40)}.max-h-48{max-height:calc(var(--spacing) * 48)}.max-h-60{max-height:calc(var(--spacing) * 60)}.max-h-\[90vh\]{max-height:90vh}.max-h-\[150px\]{max-height:150px}.min-h-0{min-height:calc(var(--spacing) * 0)}.min-h-\[88px\]{min-height:88px}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-2{width:calc(var(--spacing) * 2)}.w-3{width:calc(var(--spacing) * 3)}.w-3\.5{width:calc(var(--spacing) * 3.5)}.w-4{width:calc(var(--spacing) * 4)}.w-5{width:calc(var(--spacing) * 5)}.w-6{width:calc(var(--spacing) * 6)}.w-7{width:calc(var(--spacing) * 7)}.w-8{width:calc(var(--spacing) * 8)}.w-9{width:calc(var(--spacing) * 9)}.w-10{width:calc(var(--spacing) * 10)}.w-12{width:calc(var(--spacing) * 12)}.w-14{width:calc(var(--spacing) * 14)}.w-16{width:calc(var(--spacing) * 16)}.w-44{width:calc(var(--spacing) * 44)}.w-52{width:calc(var(--spacing) * 52)}.w-64{width:calc(var(--spacing) * 64)}.w-72{width:calc(var(--spacing) * 72)}.w-\[1px\]{width:1px}.w-full{width:100%}.w-px{width:1px}.max-w-3xl{max-width:var(--container-3xl)}.max-w-5xl{max-width:var(--container-5xl)}.max-w-\[60\%\]{max-width:60%}.max-w-\[200px\]{max-width:200px}.max-w-\[520px\]{max-width:520px}.max-w-lg{max-width:var(--container-lg)}.max-w-md{max-width:var(--container-md)}.max-w-none{max-width:none}.max-w-sm{max-width:var(--container-sm)}.max-w-xl{max-width:var(--container-xl)}.min-w-0{min-width:calc(var(--spacing) * 0)}.min-w-\[140px\]{min-width:140px}.min-w-\[220px\]{min-width:220px}.min-w-\[280px\]{min-width:280px}.flex-1{flex:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.-rotate-90{rotate:-90deg}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-ping{animation:var(--animate-ping)}.animate-pulse{animation:var(--animate-pulse)}.cursor-col-resize{cursor:col-resize}.cursor-move{cursor:move}.cursor-pointer{cursor:pointer}.cursor-row-resize{cursor:row-resize}.resize{resize:both}.resize-none{resize:none}.resize-y{resize:vertical}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.grid-flow-col{grid-auto-flow:column}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.grid-rows-2{grid-template-rows:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.items-stretch{align-items:stretch}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-0{gap:calc(var(--spacing) * 0)}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}:where(.space-y-0\.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-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1.5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}.self-center{align-self:center}.self-end{align-self:flex-end}.self-start{align-self:flex-start}.self-stretch{align-self:stretch}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-sm{border-radius:var(--radius-sm)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-\[\#ffcc00\]\/40{border-color:#fc06;border-color:lab(84.7597% 8.24091 84.7906/.4)}.border-accent,.border-accent\/20{border-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.border-accent\/20{border-color:color-mix(in oklab, var(--accent) 20%, transparent)}}.border-accent\/30{border-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.border-accent\/30{border-color:color-mix(in oklab, var(--accent) 30%, transparent)}}.border-accent\/40{border-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.border-accent\/40{border-color:color-mix(in oklab, var(--accent) 40%, transparent)}}.border-accent\/50{border-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.border-accent\/50{border-color:color-mix(in oklab, var(--accent) 50%, transparent)}}.border-amber-500\/40{border-color:#f99c0066}@supports (color:color-mix(in lab, red, red)){.border-amber-500\/40{border-color:color-mix(in oklab, var(--color-amber-500) 40%, transparent)}}.border-border,.border-border\/30{border-color:var(--border)}@supports (color:color-mix(in lab, red, red)){.border-border\/30{border-color:color-mix(in oklab, var(--border) 30%, transparent)}}.border-border\/40{border-color:var(--border)}@supports (color:color-mix(in lab, red, red)){.border-border\/40{border-color:color-mix(in oklab, var(--border) 40%, transparent)}}.border-border\/50{border-color:var(--border)}@supports (color:color-mix(in lab, red, red)){.border-border\/50{border-color:color-mix(in oklab, var(--border) 50%, transparent)}}.border-border\/60{border-color:var(--border)}@supports (color:color-mix(in lab, red, red)){.border-border\/60{border-color:color-mix(in oklab, var(--border) 60%, transparent)}}.border-error,.border-error\/30{border-color:var(--error)}@supports (color:color-mix(in lab, red, red)){.border-error\/30{border-color:color-mix(in oklab, var(--error) 30%, transparent)}}.border-error\/40{border-color:var(--error)}@supports (color:color-mix(in lab, red, red)){.border-error\/40{border-color:color-mix(in oklab, var(--error) 40%, transparent)}}.border-error\/60{border-color:var(--error)}@supports (color:color-mix(in lab, red, red)){.border-error\/60{border-color:color-mix(in oklab, var(--error) 60%, transparent)}}.border-red-700\/40{border-color:#bf000f66}@supports (color:color-mix(in lab, red, red)){.border-red-700\/40{border-color:color-mix(in oklab, var(--color-red-700) 40%, transparent)}}.border-red-700\/50{border-color:#bf000f80}@supports (color:color-mix(in lab, red, red)){.border-red-700\/50{border-color:color-mix(in oklab, var(--color-red-700) 50%, transparent)}}.border-white\/10{border-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.border-white\/10{border-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}.border-white\/15{border-color:#ffffff26}@supports (color:color-mix(in lab, red, red)){.border-white\/15{border-color:color-mix(in oklab, var(--color-white) 15%, transparent)}}.bg-\[\#1a1a1a\]{background-color:#1a1a1a}.bg-\[\#ffcc00\]{background-color:#fc0}.bg-accent,.bg-accent\/5{background-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.bg-accent\/5{background-color:color-mix(in oklab, var(--accent) 5%, transparent)}}.bg-accent\/10{background-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.bg-accent\/10{background-color:color-mix(in oklab, var(--accent) 10%, transparent)}}.bg-accent\/30{background-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.bg-accent\/30{background-color:color-mix(in oklab, var(--accent) 30%, transparent)}}.bg-amber-500\/5{background-color:#f99c000d}@supports (color:color-mix(in lab, red, red)){.bg-amber-500\/5{background-color:color-mix(in oklab, var(--color-amber-500) 5%, transparent)}}.bg-bg{background-color:var(--bg)}.bg-bg-surface,.bg-bg-surface\/50{background-color:var(--bg-surface)}@supports (color:color-mix(in lab, red, red)){.bg-bg-surface\/50{background-color:color-mix(in oklab, var(--bg-surface) 50%, transparent)}}.bg-black\/60{background-color:#0009}@supports (color:color-mix(in lab, red, red)){.bg-black\/60{background-color:color-mix(in oklab, var(--color-black) 60%, transparent)}}.bg-border{background-color:var(--border)}.bg-error,.bg-error\/5{background-color:var(--error)}@supports (color:color-mix(in lab, red, red)){.bg-error\/5{background-color:color-mix(in oklab, var(--error) 5%, transparent)}}.bg-error\/10{background-color:var(--error)}@supports (color:color-mix(in lab, red, red)){.bg-error\/10{background-color:color-mix(in oklab, var(--error) 10%, transparent)}}.bg-green-500{background-color:var(--color-green-500)}.bg-neutral-400{background-color:var(--color-neutral-400)}.bg-neutral-900{background-color:var(--color-neutral-900)}.bg-neutral-950{background-color:var(--color-neutral-950)}.bg-neutral-950\/90{background-color:#0a0a0ae6}@supports (color:color-mix(in lab, red, red)){.bg-neutral-950\/90{background-color:color-mix(in oklab, var(--color-neutral-950) 90%, transparent)}}.bg-red-500{background-color:var(--color-red-500)}.bg-red-900\/20{background-color:#82181a33}@supports (color:color-mix(in lab, red, red)){.bg-red-900\/20{background-color:color-mix(in oklab, var(--color-red-900) 20%, transparent)}}.bg-red-900\/30{background-color:#82181a4d}@supports (color:color-mix(in lab, red, red)){.bg-red-900\/30{background-color:color-mix(in oklab, var(--color-red-900) 30%, transparent)}}.bg-text-muted{background-color:var(--text-muted)}.bg-transparent{background-color:#0000}.bg-white\/5{background-color:#ffffff0d}@supports (color:color-mix(in lab, red, red)){.bg-white\/5{background-color:color-mix(in oklab, var(--color-white) 5%, transparent)}}.object-cover{object-fit:cover}.p-1{padding:calc(var(--spacing) * 1)}.p-1\.5{padding:calc(var(--spacing) * 1.5)}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.px-0\.5{padding-inline:calc(var(--spacing) * .5)}.px-1{padding-inline:calc(var(--spacing) * 1)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0{padding-block:calc(var(--spacing) * 0)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-10{padding-block:calc(var(--spacing) * 10)}.py-12{padding-block:calc(var(--spacing) * 12)}.py-\[1px\]{padding-block:1px}.pt-1{padding-top:calc(var(--spacing) * 1)}.pt-1\.5{padding-top:calc(var(--spacing) * 1.5)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pb-1{padding-bottom:calc(var(--spacing) * 1)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.pb-5{padding-bottom:calc(var(--spacing) * 5)}.pb-6{padding-bottom:calc(var(--spacing) * 6)}.pl-4{padding-left:calc(var(--spacing) * 4)}.pl-6{padding-left:calc(var(--spacing) * 6)}.pl-10{padding-left:calc(var(--spacing) * 10)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-middle{vertical-align:middle}.font-mono{font-family:var(--font-geist-mono)}.font-sans{font-family:var(--font-sans)}.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-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[8px\]{font-size:8px}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[12px\]{font-size:12px}.text-\[13px\]{font-size:13px}.text-\[16px\]{font-size:16px}.leading-5{--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5)}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-snug{--tw-leading:var(--leading-snug);line-height:var(--leading-snug)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-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-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-\[\#ffcc00\]{color:#fc0}.text-accent,.text-accent\/70{color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.text-accent\/70{color:color-mix(in oklab, var(--accent) 70%, transparent)}}.text-amber-200\/90{color:#fee685e6}@supports (color:color-mix(in lab, red, red)){.text-amber-200\/90{color:color-mix(in oklab, var(--color-amber-200) 90%, transparent)}}.text-amber-300{color:var(--color-amber-300)}.text-amber-400{color:var(--color-amber-400)}.text-bg{color:var(--bg)}.text-blue-400{color:var(--color-blue-400)}.text-error{color:var(--error)}.text-neutral-200{color:var(--color-neutral-200)}.text-neutral-300{color:var(--color-neutral-300)}.text-neutral-400{color:var(--color-neutral-400)}.text-neutral-500{color:var(--color-neutral-500)}.text-neutral-600{color:var(--color-neutral-600)}.text-neutral-700{color:var(--color-neutral-700)}.text-red-400{color:var(--color-red-400)}.text-text{color:var(--text)}.text-text-muted,.text-text-muted\/60{color:var(--text-muted)}@supports (color:color-mix(in lab, red, red)){.text-text-muted\/60{color:color-mix(in oklab, var(--text-muted) 60%, transparent)}}.text-text-muted\/80{color:var(--text-muted)}@supports (color:color-mix(in lab, red, red)){.text-text-muted\/80{color:color-mix(in oklab, var(--text-muted) 80%, transparent)}}.text-white{color:var(--color-white)}.capitalize{text-transform:capitalize}.uppercase{text-transform:uppercase}.italic{font-style:italic}.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,)}.line-through{text-decoration-line:line-through}.underline{text-decoration-line:underline}.underline-offset-2{text-underline-offset:2px}.accent-accent{accent-color:var(--accent)}.opacity-0{opacity:0}.opacity-60{opacity:.6}.shadow{--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)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.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)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.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,)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[width\]{transition-property:width;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.outline-none{--tw-outline-style:none;outline-style:none}.select-all{-webkit-user-select:all;user-select:all}.select-none{-webkit-user-select:none;user-select:none}@media (hover:hover){.group-hover\:block:is(:where(.group):hover *){display:block}.group-hover\:text-text:is(:where(.group):hover *){color:var(--text)}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}}.placeholder\:text-text-muted::placeholder{color:var(--text-muted)}.last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.last\:pb-0:last-child{padding-bottom:calc(var(--spacing) * 0)}@media (hover:hover){.hover\:border-accent:hover,.hover\:border-accent\/40:hover{border-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.hover\:border-accent\/40:hover{border-color:color-mix(in oklab, var(--accent) 40%, transparent)}}.hover\:border-accent\/50:hover{border-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.hover\:border-accent\/50:hover{border-color:color-mix(in oklab, var(--accent) 50%, transparent)}}.hover\:border-error\/40:hover{border-color:var(--error)}@supports (color:color-mix(in lab, red, red)){.hover\:border-error\/40:hover{border-color:color-mix(in oklab, var(--error) 40%, transparent)}}.hover\:border-text-muted:hover{border-color:var(--text-muted)}.hover\:border-white\/40:hover{border-color:#fff6}@supports (color:color-mix(in lab, red, red)){.hover\:border-white\/40:hover{border-color:color-mix(in oklab, var(--color-white) 40%, transparent)}}.hover\:bg-\[\#1a1a1a\]:hover{background-color:#1a1a1a}.hover\:bg-accent-dim:hover{background-color:var(--accent-dim)}.hover\:bg-accent\/5:hover{background-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-accent\/5:hover{background-color:color-mix(in oklab, var(--accent) 5%, transparent)}}.hover\:bg-accent\/10:hover{background-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-accent\/10:hover{background-color:color-mix(in oklab, var(--accent) 10%, transparent)}}.hover\:bg-accent\/20:hover{background-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-accent\/20:hover{background-color:color-mix(in oklab, var(--accent) 20%, transparent)}}.hover\:bg-error\/20:hover{background-color:var(--error)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-error\/20:hover{background-color:color-mix(in oklab, var(--error) 20%, transparent)}}.hover\:bg-white\/5:hover{background-color:#ffffff0d}@supports (color:color-mix(in lab, red, red)){.hover\:bg-white\/5:hover{background-color:color-mix(in oklab, var(--color-white) 5%, transparent)}}.hover\:text-accent:hover{color:var(--accent)}.hover\:text-accent-dim:hover{color:var(--accent-dim)}.hover\:text-blue-300:hover{color:var(--color-blue-300)}.hover\:text-blue-400:hover{color:var(--color-blue-400)}.hover\:text-error:hover{color:var(--error)}.hover\:text-text:hover{color:var(--text)}.hover\:text-white:hover{color:var(--color-white)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-100:hover{opacity:1}}.focus\:border-accent:focus{border-color:var(--accent)}.focus\:opacity-100:focus{opacity:1}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-accent:focus{--tw-ring-color:var(--accent)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}@media (min-width:40rem){.sm\:inline{display:inline}.sm\:inline-flex{display:inline-flex}}@media (min-width:48rem){.md\:flex{display:flex}.md\:h-auto{height:auto}.md\:w-px{width:1px}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:items-start{align-items:flex-start}.md\:gap-6{gap:calc(var(--spacing) * 6)}.md\:self-stretch{align-self:stretch}.md\:border-t-0{border-top-style:var(--tw-border-style);border-top-width:0}.md\:bg-border{background-color:var(--border)}}@media (min-width:64rem){.lg\:mb-0{margin-bottom:calc(var(--spacing) * 0)}.lg\:mb-4{margin-bottom:calc(var(--spacing) * 4)}.lg\:block{display:block}.lg\:flex{display:flex}.lg\:grid{display:grid}.lg\:min-h-0{min-height:calc(var(--spacing) * 0)}.lg\:min-w-\[280px\]{min-width:280px}.lg\:flex-1{flex:1}.lg\:grid-cols-\[1fr_340px\]{grid-template-columns:1fr 340px}.lg\:flex-col{flex-direction:column}.lg\:flex-row{flex-direction:row}.lg\:gap-6{gap:calc(var(--spacing) * 6)}.lg\:overflow-hidden{overflow:hidden}.lg\:overflow-y-auto{overflow-y:auto}}@media (min-width:80rem){.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}.\[\&_a\]\:text-accent a{color:var(--accent)}.\[\&_a\]\:underline a{text-decoration-line:underline}.\[\&_blockquote\]\:border-l-2 blockquote{border-left-style:var(--tw-border-style);border-left-width:2px}.\[\&_blockquote\]\:border-border blockquote{border-color:var(--border)}.\[\&_blockquote\]\:pl-2 blockquote{padding-left:calc(var(--spacing) * 2)}.\[\&_blockquote\]\:text-text-muted blockquote{color:var(--text-muted)}.\[\&_code\]\:rounded code{border-radius:.25rem}.\[\&_code\]\:bg-bg-surface code{background-color:var(--bg-surface)}.\[\&_code\]\:px-1 code{padding-inline:calc(var(--spacing) * 1)}.\[\&_code\]\:text-\[11px\] code{font-size:11px}.\[\&_h1\]\:mt-3 h1{margin-top:calc(var(--spacing) * 3)}.\[\&_h1\]\:mb-2 h1{margin-bottom:calc(var(--spacing) * 2)}.\[\&_h1\]\:text-\[14px\] h1{font-size:14px}.\[\&_h1\]\:font-semibold h1{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.\[\&_h2\]\:mt-3 h2{margin-top:calc(var(--spacing) * 3)}.\[\&_h2\]\:mb-1\.5 h2{margin-bottom:calc(var(--spacing) * 1.5)}.\[\&_h2\]\:text-\[13px\] h2{font-size:13px}.\[\&_h2\]\:font-semibold h2{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.\[\&_h3\]\:mt-2 h3{margin-top:calc(var(--spacing) * 2)}.\[\&_h3\]\:mb-1 h3{margin-bottom:calc(var(--spacing) * 1)}.\[\&_h3\]\:text-\[12px\] h3{font-size:12px}.\[\&_h3\]\:font-semibold h3{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.\[\&_hr\]\:my-3 hr{margin-block:calc(var(--spacing) * 3)}.\[\&_hr\]\:border-border hr{border-color:var(--border)}.\[\&_li\]\:my-0\.5 li{margin-block:calc(var(--spacing) * .5)}.\[\&_ol\]\:my-1\.5 ol{margin-block:calc(var(--spacing) * 1.5)}.\[\&_ol\]\:list-decimal ol{list-style-type:decimal}.\[\&_ol\]\:pl-4 ol{padding-left:calc(var(--spacing) * 4)}.\[\&_p\]\:my-1\.5 p{margin-block:calc(var(--spacing) * 1.5)}.\[\&_strong\]\:text-text strong{color:var(--text)}.\[\&_ul\]\:my-1\.5 ul{margin-block:calc(var(--spacing) * 1.5)}.\[\&_ul\]\:list-disc ul{list-style-type:disc}.\[\&_ul\]\:pl-4 ul{padding-left:calc(var(--spacing) * 4)}}:root{--bg:#0a0a0a;--bg-surface:#111;--text:#e0e0e0;--text-muted:#737373;--accent:#0f8;--accent-dim:#00cc6a;--border:#2a2a2a;--error:#f44}::selection{background:var(--accent);color:var(--bg)}body{background:var(--bg);color:var(--text);font-family:var(--font-geist-mono), ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}h1,h2,h3,h4,h5,h6{font-family:var(--font-geist-mono), ui-monospace, monospace;letter-spacing:-.01em}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:var(--bg)}::-webkit-scrollbar-thumb{background:var(--border);border-radius:0}::-webkit-scrollbar-thumb:hover{background:var(--text-muted)}:focus-visible{outline:1px solid var(--accent);outline-offset:1px}@keyframes qw-blink{0%,50%{opacity:1}51%,to{opacity:0}}.animate-qw-blink{animation:1s steps(2,start) infinite qw-blink}@keyframes qw-name-shimmer{0%,to{color:var(--accent)}50%{color:color-mix(in srgb, var(--accent) 55%, #e0e0e0)}}.animate-name-shimmer{animation:1.6s ease-in-out infinite qw-name-shimmer}@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-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-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-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@keyframes ping{75%,to{opacity:0;transform:scale(2)}}@keyframes pulse{50%{opacity:.5}}