react-doctor-cli-dev 1.0.7 → 1.0.9

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 (65) hide show
  1. package/backend/data/screenshots/4--fcp.png +0 -0
  2. package/backend/data/screenshots/4--fullLoad.png +0 -0
  3. package/backend/data/screenshots/4-docs-fullLoad.png +0 -0
  4. package/backend/data/screenshots/4-white-fullLoad.png +0 -0
  5. package/backend/dist/db.js +33 -11
  6. package/backend/dist/index.js +29 -3
  7. package/backend/dist/routes/reports.js +106 -55
  8. package/backend/public/assets/index-BpODc0fS.css +1 -0
  9. package/backend/public/assets/index-zKyZPsv1.js +118 -0
  10. package/backend/public/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  11. package/backend/public/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  12. package/backend/public/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
  13. package/backend/public/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
  14. package/backend/public/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  15. package/backend/public/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  16. package/backend/public/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
  17. package/backend/public/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
  18. package/backend/public/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  19. package/backend/public/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  20. package/backend/public/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
  21. package/backend/public/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
  22. package/backend/public/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  23. package/backend/public/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  24. package/backend/public/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
  25. package/backend/public/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
  26. package/backend/public/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  27. package/backend/public/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
  28. package/backend/public/assets/tajawal-arabic-300-normal-Bq0yWa0Z.woff +0 -0
  29. package/backend/public/assets/tajawal-arabic-300-normal-By07C9pa.woff2 +0 -0
  30. package/backend/public/assets/tajawal-arabic-400-normal-CyCXRvzh.woff2 +0 -0
  31. package/backend/public/assets/tajawal-arabic-400-normal-DCQxawbB.woff +0 -0
  32. package/backend/public/assets/tajawal-arabic-500-normal-BZ8ojJNu.woff2 +0 -0
  33. package/backend/public/assets/tajawal-arabic-500-normal-CbVEaYEW.woff +0 -0
  34. package/backend/public/assets/tajawal-arabic-700-normal-9L7Zusdl.woff +0 -0
  35. package/backend/public/assets/tajawal-arabic-700-normal-D2-eand5.woff2 +0 -0
  36. package/backend/public/assets/tajawal-latin-300-normal-C0-xR3ms.woff +0 -0
  37. package/backend/public/assets/tajawal-latin-300-normal-CeEKeOxZ.woff2 +0 -0
  38. package/backend/public/assets/tajawal-latin-400-normal-BVNSOH3d.woff2 +0 -0
  39. package/backend/public/assets/tajawal-latin-400-normal-BdYcZznU.woff +0 -0
  40. package/backend/public/assets/tajawal-latin-500-normal-CoYeBiSI.woff2 +0 -0
  41. package/backend/public/assets/tajawal-latin-500-normal-DU9v6xgj.woff +0 -0
  42. package/backend/public/assets/tajawal-latin-700-normal-BypgxfGb.woff2 +0 -0
  43. package/backend/public/assets/tajawal-latin-700-normal-CV3bxpHe.woff +0 -0
  44. package/backend/public/favicon.svg +1 -0
  45. package/backend/public/icons.svg +24 -0
  46. package/backend/public/index.html +254 -0
  47. package/backend/src/db.ts +42 -18
  48. package/backend/src/index.ts +31 -3
  49. package/backend/src/routes/reports.ts +141 -52
  50. package/cli/dist/commands/full.js +82 -48
  51. package/cli/src/commands/full.ts +161 -115
  52. package/package.json +25 -4
  53. package/shared/dist/index.d.ts +0 -2
  54. package/shared/dist/index.js +0 -19
  55. package/shared/dist/schemas.d.ts +0 -91
  56. package/shared/dist/schemas.js +0 -82
  57. package/shared/dist/types.d.ts +0 -44
  58. package/shared/dist/types.js +0 -2
  59. package/shared/package-lock.json +0 -47
  60. package/shared/package.json +0 -21
  61. package/shared/src/index.ts +0 -4
  62. package/shared/src/schemas.ts +0 -136
  63. package/shared/src/types.ts +0 -137
  64. package/shared/tsconfig.json +0 -15
  65. package/tsconfig.json +0 -25
@@ -7,15 +7,12 @@ exports.screenshotsDir = void 0;
7
7
  const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const fs_1 = __importDefault(require("fs"));
10
- const dotenv_1 = __importDefault(require("dotenv"));
11
- dotenv_1.default.config({ path: path_1.default.join(__dirname, "..", ".env") });
12
- const PACKAGE_ROOT = path_1.default.resolve(__dirname, "..", "..");
13
- const DEFAULT_DB = path_1.default.join(PACKAGE_ROOT, "backend", "data", "reports.db");
14
- const dbPath = process.env.DB_PATH || DEFAULT_DB;
15
- fs_1.default.mkdirSync(path_1.default.dirname(dbPath), { recursive: true });
16
- fs_1.default.mkdirSync(path_1.default.join(path_1.default.dirname(dbPath), "screenshots"), { recursive: true });
10
+ // ── Initialize database ──────────────────────────────────────
11
+ const dbPath = process.env.DB_PATH || path_1.default.join(__dirname, '../../reports.db');
17
12
  const db = new better_sqlite3_1.default(dbPath);
18
- // NEW SCHEMA WITH SPLIT COLUMNS
13
+ // ── Enable foreign keys ──────────────────────────────────────
14
+ db.pragma('foreign_keys = ON');
15
+ // ── Create reports table if it doesn't exist ──────────────
19
16
  db.exec(`
20
17
  CREATE TABLE IF NOT EXISTS reports (
21
18
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -23,11 +20,36 @@ db.exec(`
23
20
  score INTEGER NOT NULL,
24
21
  grade TEXT NOT NULL,
25
22
  analyzed_at TEXT NOT NULL,
26
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
23
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
27
24
  static_json TEXT NOT NULL,
28
25
  runtime_json TEXT NOT NULL,
29
26
  suggestions TEXT NOT NULL
30
- );
27
+ )
31
28
  `);
32
- exports.screenshotsDir = path_1.default.join(path_1.default.dirname(dbPath), "screenshots");
29
+ // ── Create screenshots table if it doesn't exist ───────────
30
+ db.exec(`
31
+ CREATE TABLE IF NOT EXISTS screenshots (
32
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
33
+ report_id INTEGER NOT NULL,
34
+ route TEXT NOT NULL,
35
+ label TEXT NOT NULL,
36
+ taken_at INTEGER NOT NULL,
37
+ data_url TEXT NOT NULL,
38
+ FOREIGN KEY (report_id) REFERENCES reports(id) ON DELETE CASCADE
39
+ )
40
+ `);
41
+ // ── Create indexes for performance ──────────────────────────
42
+ db.exec(`
43
+ CREATE INDEX IF NOT EXISTS idx_reports_project ON reports(project);
44
+ CREATE INDEX IF NOT EXISTS idx_reports_created_at ON reports(created_at DESC);
45
+ CREATE INDEX IF NOT EXISTS idx_screenshots_report_id ON screenshots(report_id);
46
+ `);
47
+ // ── Define screenshots directory ────────────────────────────
48
+ const screenshotsDir = path_1.default.join(__dirname, '../../data/screenshots');
49
+ exports.screenshotsDir = screenshotsDir;
50
+ // ── Ensure screenshots directory exists ──────────────────────
51
+ if (!fs_1.default.existsSync(screenshotsDir)) {
52
+ fs_1.default.mkdirSync(screenshotsDir, { recursive: true });
53
+ }
54
+ // ── Export both the database and the screenshots dir ──────
33
55
  exports.default = db;
@@ -16,20 +16,46 @@ const PORT = process.env.PORT || 3000;
16
16
  const API_KEY = process.env.API_KEY || "react-doctor-secret-key-change-this";
17
17
  // Make API_KEY available globally so auth middleware can use it
18
18
  process.env.API_KEY = API_KEY;
19
- app.use((0, helmet_1.default)());
19
+ // ── Security headers ────────────────────────────────────────────
20
+ // CSP disabled so the bundled dashboard JS/CSS can load without
21
+ // Express blocking inline scripts or the Vite-built bundle.
22
+ app.use((0, helmet_1.default)({ contentSecurityPolicy: false }));
20
23
  app.use((0, cors_1.default)());
21
24
  app.use(express_1.default.json({ limit: '50mb' }));
25
+ // ── API routes ───────────────────────────────────────────────────
22
26
  app.use('/api/reports', reports_1.default);
23
27
  app.get('/health', (_req, res) => {
24
28
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
25
29
  });
26
- app.use((req, res) => {
27
- res.status(404).json({ message: 'Route not found' });
30
+ // ── Serve the built dashboard (frontend/ built via `npm run build`
31
+ // into backend/public/) ───────────────────────────────────────
32
+ const publicDir = path_1.default.join(__dirname, '..', 'public');
33
+ app.use(express_1.default.static(publicDir));
34
+ // ── SPA fallback ─────────────────────────────────────────────────
35
+ // Any route not matched above (e.g. /report/7, /history, /overview)
36
+ // gets index.html so the dashboard's client-side router can take
37
+ // over and render the right page based on the URL.
38
+ //
39
+ // IMPORTANT: this must come AFTER /api/reports and /health so API
40
+ // calls are never accidentally swallowed by this fallback — but
41
+ // BEFORE the 404 handler below, since it's the last real route.
42
+ app.get('/{*path}', (req, res) => {
43
+ const indexPath = path_1.default.join(publicDir, 'index.html');
44
+ res.sendFile(indexPath, (err) => {
45
+ if (err) {
46
+ res.status(404).json({
47
+ message: 'Dashboard not built yet.',
48
+ hint: 'Run "npm run build" inside the frontend/ folder, then restart the backend.',
49
+ });
50
+ }
51
+ });
28
52
  });
53
+ // ── Error handler ────────────────────────────────────────────────
29
54
  app.use((err, _req, res, _next) => {
30
55
  console.error(err.stack);
31
56
  res.status(500).json({ message: 'Internal Server Error' });
32
57
  });
58
+ // ── Start ─────────────────────────────────────────────────────────
33
59
  app.listen(PORT, () => {
34
60
  console.log(`🩺 React Doctor backend running on http://localhost:${PORT}`);
35
61
  });
@@ -14,6 +14,9 @@
14
14
  // Returns the full report for one run — static + runtime +
15
15
  // suggestions all parsed back to objects.
16
16
  //
17
+ // GET /api/reports/:id/screenshots
18
+ // Returns all screenshots for a report as base64 data URLs.
19
+ //
17
20
  // GET /api/reports/project/:name
18
21
  // All runs for a named project, summary only.
19
22
  //
@@ -31,39 +34,6 @@
31
34
  // /screenshots/<filename>
32
35
  // so the dashboard can load them as normal <img> tags.
33
36
  // ─────────────────────────────────────────────────────────────
34
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
35
- if (k2 === undefined) k2 = k;
36
- var desc = Object.getOwnPropertyDescriptor(m, k);
37
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
38
- desc = { enumerable: true, get: function() { return m[k]; } };
39
- }
40
- Object.defineProperty(o, k2, desc);
41
- }) : (function(o, m, k, k2) {
42
- if (k2 === undefined) k2 = k;
43
- o[k2] = m[k];
44
- }));
45
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
46
- Object.defineProperty(o, "default", { enumerable: true, value: v });
47
- }) : function(o, v) {
48
- o["default"] = v;
49
- });
50
- var __importStar = (this && this.__importStar) || (function () {
51
- var ownKeys = function(o) {
52
- ownKeys = Object.getOwnPropertyNames || function (o) {
53
- var ar = [];
54
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
55
- return ar;
56
- };
57
- return ownKeys(o);
58
- };
59
- return function (mod) {
60
- if (mod && mod.__esModule) return mod;
61
- var result = {};
62
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
63
- __setModuleDefault(result, mod);
64
- return result;
65
- };
66
- })();
67
37
  var __importDefault = (this && this.__importDefault) || function (mod) {
68
38
  return (mod && mod.__esModule) ? mod : { "default": mod };
69
39
  };
@@ -71,7 +41,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
71
41
  const express_1 = require("express");
72
42
  const path_1 = __importDefault(require("path"));
73
43
  const fs_1 = __importDefault(require("fs"));
74
- const db_1 = __importStar(require("../db"));
44
+ const db_1 = __importDefault(require("../db"));
45
+ const db_2 = require("../db");
75
46
  const auth_1 = require("../middleware/auth");
76
47
  const router = (0, express_1.Router)();
77
48
  // ── GET /api/reports ─────────────────────────────────────────
@@ -108,6 +79,87 @@ router.get("/project/:name", (req, res) => {
108
79
  res.status(500).json({ error: "Internal server error" });
109
80
  }
110
81
  });
82
+ // ── GET /api/reports/:id/screenshots ──────────────────────────
83
+ // Returns all screenshots for a report as base64 data URLs.
84
+ // This is used by the dashboard to display screenshots.
85
+ router.get("/:id/screenshots", (req, res) => {
86
+ try {
87
+ const reportId = req.params.id;
88
+ // First, check if screenshots table exists
89
+ const tableCheck = db_1.default.prepare(`
90
+ SELECT name FROM sqlite_master
91
+ WHERE type='table' AND name='screenshots'
92
+ `).get();
93
+ if (!tableCheck) {
94
+ return res.json({ screenshots: [] });
95
+ }
96
+ // Get screenshots from the database
97
+ const stmt = db_1.default.prepare(`
98
+ SELECT route, label, taken_at, data_url
99
+ FROM screenshots
100
+ WHERE report_id = ?
101
+ ORDER BY taken_at ASC
102
+ `);
103
+ const dbScreenshots = stmt.all(reportId);
104
+ if (dbScreenshots.length === 0) {
105
+ // Fallback: try to extract from runtime_json
106
+ const report = db_1.default.prepare(`
107
+ SELECT runtime_json FROM reports WHERE id = ?
108
+ `).get(reportId);
109
+ if (report) {
110
+ const runtime = JSON.parse(report.runtime_json || '{}');
111
+ const screenshots = [];
112
+ for (const [routeKey, routeData] of Object.entries(runtime)) {
113
+ const route = routeData;
114
+ if (route.screenshots && Array.isArray(route.screenshots)) {
115
+ for (const screenshot of route.screenshots) {
116
+ if (screenshot.dataUrl && screenshot.dataUrl.startsWith('/screenshots/')) {
117
+ const filename = screenshot.dataUrl.replace('/screenshots/', '');
118
+ const filePath = path_1.default.join(db_2.screenshotsDir, filename);
119
+ if (fs_1.default.existsSync(filePath)) {
120
+ try {
121
+ const imageBuffer = fs_1.default.readFileSync(filePath);
122
+ const base64Image = imageBuffer.toString('base64');
123
+ screenshots.push({
124
+ route: routeKey,
125
+ label: screenshot.label || 'screenshot',
126
+ taken_at: screenshot.takenAt || 0,
127
+ data_url: `data:image/png;base64,${base64Image}`,
128
+ });
129
+ }
130
+ catch (err) {
131
+ console.warn(`Could not read screenshot ${filename}: ${err}`);
132
+ }
133
+ }
134
+ }
135
+ else if (screenshot.dataUrl && screenshot.dataUrl.startsWith('data:image')) {
136
+ screenshots.push({
137
+ route: routeKey,
138
+ label: screenshot.label || 'screenshot',
139
+ taken_at: screenshot.takenAt || 0,
140
+ data_url: screenshot.dataUrl,
141
+ });
142
+ }
143
+ }
144
+ }
145
+ }
146
+ return res.json({ screenshots });
147
+ }
148
+ }
149
+ // Return screenshots from database
150
+ const screenshots = dbScreenshots.map((s) => ({
151
+ route: s.route,
152
+ label: s.label,
153
+ taken_at: s.taken_at,
154
+ data_url: s.data_url,
155
+ }));
156
+ res.json({ screenshots });
157
+ }
158
+ catch (err) {
159
+ console.error("GET /:id/screenshots error:", err.message);
160
+ res.status(500).json({ error: "Internal server error" });
161
+ }
162
+ });
111
163
  // ── POST /api/reports/upload ──────────────────────────────────
112
164
  // Receives a FinalReport from the CLI, strips screenshots to disk,
113
165
  // and stores the three JSON blobs in separate columns.
@@ -131,9 +183,6 @@ router.post("/upload", auth_1.requireApiKey, (req, res) => {
131
183
  // ── Extract grade from static report ────────────────────
132
184
  const grade = body.static?.grade ?? "N/A";
133
185
  // ── Strip screenshots from runtime, save as .png files ──
134
- // We do this BEFORE inserting so the DB never holds base64.
135
- // The row ID isn't known yet — we'll rename after insert.
136
- // For now we use a temp prefix and rename below.
137
186
  const { cleanedRuntime, pendingScreenshots } = extractScreenshots(body.runtime);
138
187
  // ── Insert the row ───────────────────────────────────────
139
188
  const stmt = db_1.default.prepare(`
@@ -147,7 +196,6 @@ router.post("/upload", auth_1.requireApiKey, (req, res) => {
147
196
  // ── Save screenshots with final filenames ────────────────
148
197
  const savedScreenshots = saveScreenshots(reportId, pendingScreenshots);
149
198
  // ── Patch runtime_json with final screenshot paths ───────
150
- // Now that we have the reportId we can write the correct paths.
151
199
  if (savedScreenshots.length > 0) {
152
200
  const patchedRuntime = patchScreenshotPaths(cleanedRuntime, savedScreenshots);
153
201
  db_1.default.prepare("UPDATE reports SET runtime_json = ? WHERE id = ?").run(JSON.stringify(patchedRuntime), reportId);
@@ -180,7 +228,6 @@ router.get("/:id", (req, res) => {
180
228
  grade: row.grade,
181
229
  analyzedAt: row.analyzed_at,
182
230
  createdAt: row.created_at,
183
- // Parse the three JSON blobs back into objects
184
231
  static: JSON.parse(row.static_json),
185
232
  runtime: JSON.parse(row.runtime_json),
186
233
  suggestions: JSON.parse(row.suggestions),
@@ -202,10 +249,6 @@ function getMissingFields(body) {
202
249
  /**
203
250
  * Walk the runtime map, strip every screenshot.dataUrl,
204
251
  * and collect them as Buffers ready to write to disk.
205
- *
206
- * Returns:
207
- * cleanedRuntime — runtime map with dataUrls replaced by tempPath markers
208
- * pendingScreenshots — list of screenshots to save once we have a reportId
209
252
  */
210
253
  function extractScreenshots(runtime) {
211
254
  const pending = [];
@@ -214,17 +257,24 @@ function extractScreenshots(runtime) {
214
257
  const routeClone = { ...routeData };
215
258
  if (Array.isArray(routeClone.screenshots)) {
216
259
  routeClone.screenshots = routeClone.screenshots.map((shot) => {
217
- if (!shot.dataUrl || !shot.dataUrl.startsWith("data:image/png;base64,")) {
260
+ // Check if it's a base64 data URL
261
+ if (shot.dataUrl && shot.dataUrl.startsWith("data:image/png;base64,")) {
262
+ const base64 = shot.dataUrl.replace("data:image/png;base64,", "");
263
+ const buffer = Buffer.from(base64, "base64");
264
+ const safeRoute = routeKey.replace(/[/:]/g, "-").replace(/^-+/, "");
265
+ const safeLabel = shot.label.replace(/[^a-z0-9]/gi, "-");
266
+ const tempPath = `__PENDING__${safeRoute}__${safeLabel}`;
267
+ pending.push({ routeKey, label: shot.label, buffer, tempPath });
268
+ return { ...shot, dataUrl: tempPath };
269
+ }
270
+ else if (shot.dataUrl && shot.dataUrl.startsWith('/screenshots/')) {
271
+ // Already a path - keep it
218
272
  return shot;
219
273
  }
220
- const base64 = shot.dataUrl.replace("data:image/png;base64,", "");
221
- const buffer = Buffer.from(base64, "base64");
222
- // Sanitise routeKey for use in a filename — replace "/" and ":" with "-"
223
- const safeRoute = routeKey.replace(/[/:]/g, "-").replace(/^-+/, "");
224
- const safeLabel = shot.label.replace(/[^a-z0-9]/gi, "-");
225
- const tempPath = `__PENDING__${safeRoute}__${safeLabel}`;
226
- pending.push({ routeKey, label: shot.label, buffer, tempPath });
227
- return { ...shot, dataUrl: tempPath };
274
+ else {
275
+ // No valid dataUrl - keep as is or set to null
276
+ return { ...shot, dataUrl: null };
277
+ }
228
278
  });
229
279
  }
230
280
  cleaned[routeKey] = routeClone;
@@ -233,15 +283,17 @@ function extractScreenshots(runtime) {
233
283
  }
234
284
  /**
235
285
  * Write each screenshot buffer to data/screenshots/<reportId>-<route>-<label>.png
236
- * Returns the list of saved files with their final URL paths.
237
286
  */
238
287
  function saveScreenshots(reportId, pending) {
239
288
  const saved = [];
289
+ if (!fs_1.default.existsSync(db_2.screenshotsDir)) {
290
+ fs_1.default.mkdirSync(db_2.screenshotsDir, { recursive: true });
291
+ }
240
292
  for (const shot of pending) {
241
293
  const safeRoute = shot.routeKey.replace(/[/:]/g, "-").replace(/^-+/, "");
242
294
  const safeLabel = shot.label.replace(/[^a-z0-9]/gi, "-");
243
295
  const filename = `${reportId}-${safeRoute}-${safeLabel}.png`;
244
- const fullPath = path_1.default.join(db_1.screenshotsDir, filename);
296
+ const fullPath = path_1.default.join(db_2.screenshotsDir, filename);
245
297
  try {
246
298
  fs_1.default.writeFileSync(fullPath, shot.buffer);
247
299
  saved.push({
@@ -262,7 +314,6 @@ function saveScreenshots(reportId, pending) {
262
314
  * the final /screenshots/<file> URL paths.
263
315
  */
264
316
  function patchScreenshotPaths(runtime, saved) {
265
- // Build a lookup from tempPath → filePath
266
317
  const lookup = {};
267
318
  for (const s of saved)
268
319
  lookup[s.tempPath] = s.filePath;
@@ -0,0 +1 @@
1
+ @font-face{font-family:Tajawal;font-style:normal;font-display:swap;font-weight:300;src:url(/assets/tajawal-arabic-300-normal-By07C9pa.woff2)format("woff2"),url(/assets/tajawal-arabic-300-normal-Bq0yWa0Z.woff)format("woff");unicode-range:U+6??,U+750-77F,U+870-88E,U+890-891,U+897-8E1,U+8E3-8FF,U+200C-200E,U+2010-2011,U+204F,U+2E41,U+FB50-FDFF,U+FE70-FE74,U+FE76-FEFC,U+102E0-102FB,U+10E60-10E7E,U+10EC2-10EC4,U+10EFC-10EFF,U+1EE00-1EE03,U+1EE05-1EE1F,U+1EE21-1EE22,U+1EE24,U+1EE27,U+1EE29-1EE32,U+1EE34-1EE37,U+1EE39,U+1EE3B,U+1EE42,U+1EE47,U+1EE49,U+1EE4B,U+1EE4D-1EE4F,U+1EE51-1EE52,U+1EE54,U+1EE57,U+1EE59,U+1EE5B,U+1EE5D,U+1EE5F,U+1EE61-1EE62,U+1EE64,U+1EE67-1EE6A,U+1EE6C-1EE72,U+1EE74-1EE77,U+1EE79-1EE7C,U+1EE7E,U+1EE80-1EE89,U+1EE8B-1EE9B,U+1EEA1-1EEA3,U+1EEA5-1EEA9,U+1EEAB-1EEBB,U+1EEF0-1EEF1}@font-face{font-family:Tajawal;font-style:normal;font-display:swap;font-weight:300;src:url(/assets/tajawal-latin-300-normal-CeEKeOxZ.woff2)format("woff2"),url(/assets/tajawal-latin-300-normal-C0-xR3ms.woff)format("woff");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:Tajawal;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/tajawal-arabic-400-normal-CyCXRvzh.woff2)format("woff2"),url(/assets/tajawal-arabic-400-normal-DCQxawbB.woff)format("woff");unicode-range:U+6??,U+750-77F,U+870-88E,U+890-891,U+897-8E1,U+8E3-8FF,U+200C-200E,U+2010-2011,U+204F,U+2E41,U+FB50-FDFF,U+FE70-FE74,U+FE76-FEFC,U+102E0-102FB,U+10E60-10E7E,U+10EC2-10EC4,U+10EFC-10EFF,U+1EE00-1EE03,U+1EE05-1EE1F,U+1EE21-1EE22,U+1EE24,U+1EE27,U+1EE29-1EE32,U+1EE34-1EE37,U+1EE39,U+1EE3B,U+1EE42,U+1EE47,U+1EE49,U+1EE4B,U+1EE4D-1EE4F,U+1EE51-1EE52,U+1EE54,U+1EE57,U+1EE59,U+1EE5B,U+1EE5D,U+1EE5F,U+1EE61-1EE62,U+1EE64,U+1EE67-1EE6A,U+1EE6C-1EE72,U+1EE74-1EE77,U+1EE79-1EE7C,U+1EE7E,U+1EE80-1EE89,U+1EE8B-1EE9B,U+1EEA1-1EEA3,U+1EEA5-1EEA9,U+1EEAB-1EEBB,U+1EEF0-1EEF1}@font-face{font-family:Tajawal;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/tajawal-latin-400-normal-BVNSOH3d.woff2)format("woff2"),url(/assets/tajawal-latin-400-normal-BdYcZznU.woff)format("woff");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:Tajawal;font-style:normal;font-display:swap;font-weight:500;src:url(/assets/tajawal-arabic-500-normal-BZ8ojJNu.woff2)format("woff2"),url(/assets/tajawal-arabic-500-normal-CbVEaYEW.woff)format("woff");unicode-range:U+6??,U+750-77F,U+870-88E,U+890-891,U+897-8E1,U+8E3-8FF,U+200C-200E,U+2010-2011,U+204F,U+2E41,U+FB50-FDFF,U+FE70-FE74,U+FE76-FEFC,U+102E0-102FB,U+10E60-10E7E,U+10EC2-10EC4,U+10EFC-10EFF,U+1EE00-1EE03,U+1EE05-1EE1F,U+1EE21-1EE22,U+1EE24,U+1EE27,U+1EE29-1EE32,U+1EE34-1EE37,U+1EE39,U+1EE3B,U+1EE42,U+1EE47,U+1EE49,U+1EE4B,U+1EE4D-1EE4F,U+1EE51-1EE52,U+1EE54,U+1EE57,U+1EE59,U+1EE5B,U+1EE5D,U+1EE5F,U+1EE61-1EE62,U+1EE64,U+1EE67-1EE6A,U+1EE6C-1EE72,U+1EE74-1EE77,U+1EE79-1EE7C,U+1EE7E,U+1EE80-1EE89,U+1EE8B-1EE9B,U+1EEA1-1EEA3,U+1EEA5-1EEA9,U+1EEAB-1EEBB,U+1EEF0-1EEF1}@font-face{font-family:Tajawal;font-style:normal;font-display:swap;font-weight:500;src:url(/assets/tajawal-latin-500-normal-CoYeBiSI.woff2)format("woff2"),url(/assets/tajawal-latin-500-normal-DU9v6xgj.woff)format("woff");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:Tajawal;font-style:normal;font-display:swap;font-weight:700;src:url(/assets/tajawal-arabic-700-normal-D2-eand5.woff2)format("woff2"),url(/assets/tajawal-arabic-700-normal-9L7Zusdl.woff)format("woff");unicode-range:U+6??,U+750-77F,U+870-88E,U+890-891,U+897-8E1,U+8E3-8FF,U+200C-200E,U+2010-2011,U+204F,U+2E41,U+FB50-FDFF,U+FE70-FE74,U+FE76-FEFC,U+102E0-102FB,U+10E60-10E7E,U+10EC2-10EC4,U+10EFC-10EFF,U+1EE00-1EE03,U+1EE05-1EE1F,U+1EE21-1EE22,U+1EE24,U+1EE27,U+1EE29-1EE32,U+1EE34-1EE37,U+1EE39,U+1EE3B,U+1EE42,U+1EE47,U+1EE49,U+1EE4B,U+1EE4D-1EE4F,U+1EE51-1EE52,U+1EE54,U+1EE57,U+1EE59,U+1EE5B,U+1EE5D,U+1EE5F,U+1EE61-1EE62,U+1EE64,U+1EE67-1EE6A,U+1EE6C-1EE72,U+1EE74-1EE77,U+1EE79-1EE7C,U+1EE7E,U+1EE80-1EE89,U+1EE8B-1EE9B,U+1EEA1-1EEA3,U+1EEA5-1EEA9,U+1EEAB-1EEBB,U+1EEF0-1EEF1}@font-face{font-family:Tajawal;font-style:normal;font-display:swap;font-weight:700;src:url(/assets/tajawal-latin-700-normal-BypgxfGb.woff2)format("woff2"),url(/assets/tajawal-latin-700-normal-CV3bxpHe.woff)format("woff");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:JetBrains Mono;font-style:normal;font-display:swap;font-weight:400;src:url(data:font/woff2;base64,d09GMgABAAAAAASIABAAAAAACQQAAAQuAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhwbHhwoBmA/U1RBVF4AdBEICoRkhAALIAABNgIkAzoEIAWFAAeBFAwHG2oHKJ6DceNW4lJEFHn9tEOE37LNsvkjHr7f739rn3u+OSrSxLQytE91Dw2PYtNVq3Qaw/vJ7fUBMdOj/u2ImB98cT5WUx9F13ZKt06mU7tG1sAYcM26yCRX4f/0904bmwq8hwkUUMTRX61pa9C3xlpAGYw1vOu8C2SBZZFlld7DAW82RJWmwYMI1AJCKBNCIASqdZGFmtPSNQRhMpR0EKjWTwm6z6sJ+4jqhxjovTinVdRpZaQjaQzAYjI8NgAKDJCokgunCn9oUQE8VZd/F//+v6P4F1U9gLr58yNI/dJX9BAshEoJbTDogz7ocKoUsPvES8UK/aQIUQgpGgECBVLkOEV6iASTYgNAPKdNXayhDPL7IJuAZnEYSZ0eOLc9i5Rv5/+lEi3sW/kfnCf/+fTe0sxS7CKLx5erRJkKOixEqQaoqrvx5HN4iz4VhX0gS6DjFIWUzn/fIXJRVFY1NFRVNVeFt+SYmrK6vCU6eJsnKnJj5HTESMTQZOTI3Y/tzr3rUcOsiRy/ciP50s3Mycgxmhz9GMXhjzVH30ut796NHP0UUa31/Zoa6vCH6iPvdIM0IRANaT60FablmZlZd+UKNFy9NrvnFGxb5NC2CRxR0/rIyF/WNn+35sv9r+tHVhkseuWGKkCPPh96+GXt8Lc/Sh24ujfjf+tNw1lZWreiPuHb/PSpyjdv2rf/++ZE5TSKjFdZxYsBvr1sEHl5STuHZqYR7jGYGvGBBdjcaB5bODY1GAxMjsd0wDMIzXAXODRd74DxNMtO4YuYHclgLQRzp9KlUmxptCwv9bYgvWGD2xBW7r9413fdJu+mxzvzAUYeeONQvZP4kgfqZle4jhH/MWh+UbxgtJdrLdXLymNL53PRU0D3Q6FcXTfGqGcul6vrqhgLGAVSv+IVIfZOHpu+mlLqSlnj903j8mazXDsba/wbxhS/4Oubh1c5/uXDgt4j5KtnGEy5BIJHo0ur+jD+r2qCkuP1aRVM8EpUoRYSJyqNRA38uwDvHBNo0AejO2Z+ZYLLal1QiFyFh1EDQOgSCSCpjwwKU8yDbIjroEwne0G5Qca49cYp4AlqtQNZfWTQoFb1tRpFYAmRD+HXTkV4uQhG2bg4OKmyoypAsUGtWgQXyFxYrkpODeUxvGqIZA61erXo0sFG1UBGuPgpuon8RNUG2DhovAjZCBuZwtXrU3mQGki9+pm0eVZAXoWxLWTQoYHBPrJR4WunNxIFbCHfkTyPK+sqxjxDmGArZvf79JGJ3GwEqjwc7a7NIrl/7bJ7Nzu4+n1Ow6shEPnUrtVo7cnd5Wi5qCUbBIQbNKCP95FMCSZylEV5VCiiMqqiOmrKZ/I/C0gI1fyuulVcM2E1r4MhtcV/fbCD+HwEvVzH1dGNAzFE0FTbKzyQz3gootrNbN2a4PuG0j0JOgE=)format("woff2"),url(data:font/woff;base64,d09GRgABAAAAAAbUAA8AAAAACOgAAQABAAAAAAAAAAAAAAAAAAAAAAAAAABHREVGAAABWAAAABgAAAAcABQABEdQT1MAAAFwAAAAHgAAAB5EdEx1R1NVQgAAAZAAAAAnAAAAKLj8uOpPUy8yAAABuAAAAFEAAABgFwRca1NUQVQAAAIMAAAARgAAAF7mY9MfY21hcAAAAlQAAABRAAAAdAyHCodnYXNwAAACqAAAAAgAAAAIAAAAEGdseWYAAAKwAAAB6QAAAl7g5OpvaGVhZAAABJwAAAA2AAAANhSQ8UNoaGVhAAAE1AAAAB8AAAAkAcoBImhtdHgAAAT0AAAAHgAAADofBAM2bG9jYQAABRQAAAAgAAAAIATcBZ9tYXhwAAAFNAAAABwAAAAgAIcCb25hbWUAAAVQAAABEgAAAoA4pV3kcG9zdAAABmQAAABwAAAAlNdzMTB42mNgZGBg4GGAAEYgZAWTjCAeAAG0ABkAAQAAAAoAHAAcAAFERkxUAAgABAAAAAD//wAAAAAAAHjaY2BkYGDgYlADQiYXN58QBqGcxJI8Bj4GEGABEf//g0gAWkkFVQB42mNgYYpinMDAysDA1MUUwcDA4A2hGeMYjBgdgKIMDJwMUMDEzoAEvJyAxAEGXpaXzH/+XWFgYP7DqKPAwDj//nWgSjWmW0BZBQZWABNDDdYAAAB42kXIIQIBARQFwHl/QQIkWXInUAANXMOBJCcTd+KIGCjMNViq9/H0CM6P/S0j0bOwUqh08iEb8kPTXn3RiMoYhD90Nwc3AAB42lXIRwHCQBAF0JceehWAlTjhjikUgAVQA4mJLbdk5neUqHBVKWSv3XF2VCnt3AyevkVZv+sxBBbbq/6HfFP4hEcYQgEK82v1aHQ5RTs3FRQAAAAAAQAB//8AD3jabZA1tNRAFEDfTGDdfXHI2QQnZJJvK5ngDs13Welwt/78CtcS7xvcrcHKbXCtceh+wgtu437nXRCgC4BY9AGOXOAFYNHR0dzoKCGju8gJ6xiZaF3DWqcPBiSV5i2geaCgAlAJ77ghgjfiLC7KojvOZEmSXS63emXr5W7lsTLtXFMkRmks4ly+cYOEnvO2YLDNANv+9oJwhkpAAJAcHPG/1RC2BERc1ZGGMxYVWDyVSjc0NMaZELUf9e4JhCgNB3b2PiJRctuqZ0WPR8zilxutN/ji97vUBYMAnGjtzxjtK3wrDaMABo+R9KjWwNRUMppwuWS1QdfFZCIlMF2TxDGuJJlYW7OmVlvD+sx86b1hvC/l8yXf2hqZ66xbZ80yWzyD9/fzGaRzA58+nSOzYC+gw9AP+vyNqUAUhoD4lSnputbgUFPJ5O9Y+ReWlxkrc96naX08b3zk/KORb+E+VjYPmH1M6+UHeVlDtCzzGZ9FY8YMA8gPMsQA0jqaGkHT6cYSdVwl7Zs9Wzx+DyWUugPujd03T52ikrUvM26kF9PIcRmycuDBKcd3XRhPDgtnQACINzJ3fVl1tTB++3bc2S7kyD3cCQIQNMcSKaY2YOciE+d0zFbGjps6u0PILTFlRZHNJfAFnfeHtgAAAAABAAAAAjYEro52gl8PPPUAAwPoAAAAANvSppoAAAAA29rQ8vk5/tQEkgP8AAAABgACAAAAAAAAeNpjYGRgYP7z7woDA8uGn5Z/+1kmAUVQAR8Ar8AG6gB42mOKYIgCYiMkrAzCUHEbJJoBikHgCkMPAPDwCFUAAAAAABoAOwBGAFEAbwB3AKkAsQDmAQkBCQEJAQkBFgEveNpjYGRgYOBn3MGQx7CFgQ3MQwBmBiYALRoB+HjajJADbgVQEEXPZ20bQW3bthvXbr8Vp4to1KV0AV1Wb9JvZzJvzhvcB6CQL0wYzHnAj6E/yAYaDeVBNlJsMAfZxCy/QTbTy3eQLdTzGWQr3VwGuZhx2v/ZAIVUB9kc0TSUKJ8f5FIRHGPjnWs+2MKt+MYzt1xwr/jIk3JymR0X0wzIXKo7VbXjFvfLn3lTtCn7qPoB6+yyzb3qy8pdq/6hrj11fMj7OFbtEY+mrlU/516rVFCdVobplw/JEjVaCaqIYlUiUzOaW9aa/Q1idmlPXZHbCagW/B10XwaRi06VuVdMf+9DnIov6rlF8yzhUXxC/4c66Uz48UepqEN9N/Tzt2RwjOljsUUfj581AbaEXsIAAHjaY2BiAIP/qQxGQIqRAR3wA7EKw0lGJgYbRmZGFkZWRjZGdgZmRg5GTkYuRm42x6Si1LJU9tK8TAMTR1cI7WoB5btB+Zas6Ym5uYlgnoGjAZNzEF9icmlJanJ+bpJecmJxKg9IxtjADMwBAKDtHJg=)format("woff");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:JetBrains Mono;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2)format("woff2"),url(/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff)format("woff");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:JetBrains Mono;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2)format("woff2"),url(/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff)format("woff");unicode-range:U+370-377,U+37A-37F,U+384-38A,U+38C,U+38E-3A1,U+3A3-3FF}@font-face{font-family:JetBrains Mono;font-style:normal;font-display:swap;font-weight:400;src:url(data:font/woff2;base64,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)format("woff2"),url(/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff)format("woff");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:JetBrains Mono;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2)format("woff2"),url(/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff)format("woff");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:JetBrains Mono;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2)format("woff2"),url(/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff)format("woff");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:JetBrains Mono;font-style:normal;font-display:swap;font-weight:500;src:url(data:font/woff2;base64,d09GMgABAAAAAASUABAAAAAACRAAAAQ3AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhwbHhwoBmA/U1RBVEwAdBEICoRkg3oLIAABNgIkAzoEIAWFHgeBFAwHG3AHKB4HzukrRjyF5Gb4kQ/B873d17mv8TWkoqUUGaHodJCtoqntw7m8tiPWyGkfWDU/IMpzVk4zAs9Czv3c7Os5NvKFG1Ht9vBVXySRZpJc4tTrbM3sTRFcLa3zVboKWRIoAksKSKgqUQWEwlSoqjp44V5J9/kjd0+Fi5xaFjwJArgBAIJgIAgCCAI4CdNMqGuYXYUKdADLAkAAJwl6iX0+bZqEm1AH0Hu9rvrJHwxQG1Q/gFYwWYbgTRWgwEESVzbmtjQAt3DXO7Tu/+9Q/KNKWhTCgZ8fQSWoL0CUUgTBjgL8iEAcce7YHBeA8ZvogieCnxI0BCVeACCAAhBD/oDcAdUPABSgAX4kBPBTwjCAjwYSCneiqF65LUo/qXdYFrRwTujtMrKm07plTbSqLQFAiHBN7E4DG5wkiGXhCYBNETh3K/AciHN69iFfQjFOr9EXQQAAQXRNszs8PR0OH4fDV/dwGe76CHdj1MQa11DXYGcfZ6+B1X0OfwjY+i7KpQ1w9d+1z3/HydCB1f3U8A0fRFv+0bXxPWrZu3d9Nn4UcbLs/QCXLH/j3PCKblKOiZxLOR78W9brPo7eGTLz/F27ZPmePQdCB1YP4nJU91o4gU9cy64L3c3SPu8GfDn6ZXD2cGmllVqSxtD5vCXHm5xaeHlnpffx7DcPvXog5NnjA//cPz2btsz3yf+0MI8ri33tuv7upNbYmW5STbosW8bYItV/SPzxvXC0KnpWnFdC91oojit5Lyo6WcJQ1YzwBYbi52ohJVpcxfcHBqsv0nFz8U74rzS6M7r0RVlXZ3jJC4G92O//7Lm78VOtnUx+aQxAzsqPnrRr1t9zDrezKePvZ027x+fGte4s4NXu7/jEp3G9cM1bICYgACbuETUUD5wmenGXA/IBpAKVrl0AEJ5cqr6Q89tGq+SpqkDfX2DtG+pZ9teuae0AX98c301CX179InyErnSG8WCiABB4HFbVrw/lpTdBIOk82nXbpHKoQGyBpUovAAD4XQA8ZTUCeBJHIpI0v0FRrgcnckV3ANckrwYJKXEihQfdSKPER6XeExmEeEMmSe6QBwWmkZdmSOSthhibgIkBlUYreBKd/0NeVAE9WAyEaQpH6KikDWlUVg43Jy2FOmaYlsZhDEGZXO3mWEyWHI6zuaJkMlcSQ9k4F+KidIiFyZItGDc7r1pUj6wP6zrWB0/fbeeFeHl4YOvETMNiM8Ny+7OO6Qh2wur20YVxFts5dlORdl5Kwd5utR7SRcxqX8RA5Ehacha76hg7c6yzNm/Xf6vOLNrv544lIEkWg7xWo/ItBAeIJEmt0XgBwgM8gTifoiiLJroYYooNTeziEKe4zOoR/wvIKp89hSr+6jFAqw/LaonjmZHSscMNg4iPlWpFrO1RN3zkMU+/xUP+JQ99TbvTl1Vg0gtirT6lbwYAAAA=)format("woff2"),url(data:font/woff;base64,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)format("woff");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:JetBrains Mono;font-style:normal;font-display:swap;font-weight:500;src:url(/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2)format("woff2"),url(/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff)format("woff");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:JetBrains Mono;font-style:normal;font-display:swap;font-weight:500;src:url(/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2)format("woff2"),url(/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff)format("woff");unicode-range:U+370-377,U+37A-37F,U+384-38A,U+38C,U+38E-3A1,U+3A3-3FF}@font-face{font-family:JetBrains Mono;font-style:normal;font-display:swap;font-weight:500;src:url(data:font/woff2;base64,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)format("woff2"),url(/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff)format("woff");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:JetBrains Mono;font-style:normal;font-display:swap;font-weight:500;src:url(/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2)format("woff2"),url(/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff)format("woff");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:JetBrains Mono;font-style:normal;font-display:swap;font-weight:500;src:url(/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2)format("woff2"),url(/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff)format("woff");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}:root{--bg:#0b0c10;--bg2:#0f1117;--bg3:#161b22;--bg4:#1c2333;--border:#21262d;--border2:#30363d;--text:#e6edf3;--text2:#8b949e;--text3:#6e7681;--accent:#c778dd;--accent2:#b05fc9;--accent-rgb:199, 120, 221;--teal:#00ffc2;--teal-rgb:0, 255, 194;--purple:#c778dd;--blue:#58a6ff;--orange:#f0883e;--red:#ff7b72;--green:#3fb950;--yellow:#d29922;--sidebar-w:240px;--radius:10px;--radius-sm:6px;--shadow:0 4px 24px #0006;--transition:.18s ease}*,:before,:after{box-sizing:border-box;margin:0;padding:0}html{scroll-behavior:smooth;font-size:15px}body{background:var(--bg);color:var(--text);min-height:100vh;font-family:Tajawal,system-ui,sans-serif;display:flex;overflow-x:hidden}#sidebar{width:var(--sidebar-w);background:var(--bg2);border-right:1px solid var(--border);z-index:100;flex-direction:column;min-height:100vh;display:flex;position:fixed;top:0;bottom:0;left:0}.sidebar-logo{border-bottom:1px solid var(--border);align-items:center;gap:10px;padding:22px 20px 18px;display:flex}.logo-icon{background:linear-gradient(135deg, var(--accent), var(--teal));border-radius:8px;flex-shrink:0;justify-content:center;align-items:center;width:34px;height:34px;font-size:18px;display:flex}.logo-text{letter-spacing:-.01em;font-size:.95rem;font-weight:700;line-height:1.2}.logo-sub{color:var(--text3);letter-spacing:.05em;text-transform:uppercase;font-size:.65rem;font-weight:400}.sidebar-section{letter-spacing:.12em;text-transform:uppercase;color:var(--text3);padding:18px 16px 6px;font-size:.6rem;font-weight:700}.nav-item{border-radius:var(--radius-sm);cursor:pointer;color:var(--text2);transition:background var(--transition), color var(--transition);text-align:left;background:0 0;border:none;align-items:center;gap:10px;width:calc(100% - 16px);margin:1px 8px;padding:9px 14px;font-size:.85rem;font-weight:500;display:flex}.nav-item:hover{background:var(--bg4);color:var(--text)}.nav-item.active{background:rgba(var(--accent-rgb),.1);color:var(--accent)}.nav-icon{text-align:center;flex-shrink:0;width:20px;font-size:1rem}.nav-badge{background:var(--bg4);color:var(--text3);border-radius:20px;margin-left:auto;padding:2px 7px;font-size:.64rem;font-weight:700}.nav-item.active .nav-badge{background:rgba(var(--accent-rgb),.15);color:var(--accent)}.sidebar-footer{border-top:1px solid var(--border);margin-top:auto;padding:16px}.project-pill{background:var(--bg3);border:1px solid var(--border);border-radius:var(--radius-sm);align-items:center;gap:8px;padding:9px 12px;display:flex}.status-dot{background:var(--accent);width:7px;height:7px;box-shadow:0 0 6px var(--accent);border-radius:50%;flex-shrink:0}.proj-name{color:var(--text);font-size:.8rem;font-weight:600}.proj-sub{color:var(--text3);margin-top:1px;font-size:.68rem}#main{margin-left:var(--sidebar-w);flex-direction:column;flex:1;min-height:100vh;display:flex}.topbar{border-bottom:1px solid var(--border);background:var(--bg2);z-index:50;align-items:center;gap:12px;height:54px;padding:0 28px;display:flex;position:sticky;top:0}.topbar-title{font-size:.95rem;font-weight:600}.topbar-sub{color:var(--text3);font-size:.75rem}.topbar-right{align-items:center;gap:12px;margin-left:auto;display:flex}.topbar-time{color:var(--text3);font-family:JetBrains Mono,monospace;font-size:.7rem}.page-content{flex:1;padding:26px 28px;display:none}.page-content.active{display:block}.section-gap{margin-bottom:20px}.card{background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);padding:20px}.card-title{text-transform:uppercase;letter-spacing:.1em;color:var(--text3);align-items:center;gap:8px;margin-bottom:16px;font-size:.72rem;font-weight:700;display:flex}.grid-2{grid-template-columns:1fr 1fr;gap:16px;display:grid}.grid-3{grid-template-columns:repeat(3,1fr);gap:16px;display:grid}.grid-4{grid-template-columns:repeat(4,1fr);gap:16px;display:grid}.grid-5{grid-template-columns:repeat(5,1fr);gap:16px;display:grid}.stat-tile{background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);padding:18px 20px;position:relative;overflow:hidden}.stat-tile:before{content:"";height:2px;position:absolute;top:0;left:0;right:0}.stat-tile.good:before{background:var(--green)}.stat-tile.warn:before{background:var(--orange)}.stat-tile.bad:before{background:var(--red)}.stat-tile.blue:before{background:var(--blue)}.stat-tile.purple:before{background:var(--teal)}.stat-label{text-transform:uppercase;letter-spacing:.1em;color:var(--text3);margin-bottom:8px;font-size:.68rem;font-weight:700}.stat-value{letter-spacing:-.02em;font-family:JetBrains Mono,monospace;font-size:1.85rem;font-weight:700;line-height:1}.stat-value.good{color:var(--green)}.stat-value.warn{color:var(--orange)}.stat-value.bad{color:var(--red)}.stat-value.blue{color:var(--blue)}.stat-value.purple{color:var(--teal)}.stat-meta{color:var(--text3);margin-top:6px;font-size:.7rem}.score-ring-wrap{align-items:center;gap:32px;display:flex}.score-ring{flex-shrink:0;width:140px;height:140px;position:relative}.score-ring svg{transform:rotate(-90deg)}.ring-bg{fill:none;stroke:var(--bg4);stroke-width:10px}.ring-fg{fill:none;stroke-width:10px;stroke-linecap:round;transition:stroke-dashoffset 1.4s cubic-bezier(.4,0,.2,1)}.ring-label{text-align:center;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.ring-num{font-family:JetBrains Mono,monospace;font-size:2rem;font-weight:700;line-height:1}.ring-grade{color:var(--text3);margin-top:3px;font-size:.75rem;font-weight:600}.score-details h2{margin-bottom:6px;font-size:1.05rem;font-weight:700}.score-details p{color:var(--text2);font-size:.8rem;line-height:1.6}.badge{letter-spacing:.04em;text-transform:uppercase;border-radius:20px;align-items:center;gap:4px;padding:2px 8px;font-size:.68rem;font-weight:700;display:inline-flex}.badge.critical{color:var(--red);background:#ff7b7226;border:1px solid #ff7b724d}.badge.warning{color:var(--orange);background:#f0883e26;border:1px solid #f0883e4d}.badge.info{color:var(--blue);background:#58a6ff1f;border:1px solid #58a6ff40}.badge.good{color:var(--green);background:#3fb9501f;border:1px solid #3fb95040}.badge.grade-a{background:rgba(var(--accent-rgb),.12);color:var(--accent);border:1px solid rgba(var(--accent-rgb),.25);padding:3px 12px;font-size:.78rem}.badge.grade-b{color:var(--blue);background:#58a6ff1f;border:1px solid #58a6ff40;padding:3px 12px;font-size:.78rem}.badge.grade-c{color:var(--orange);background:#f0883e1f;border:1px solid #f0883e4d;padding:3px 12px;font-size:.78rem}.badge.grade-d{color:var(--red);background:#ff7b721f;border:1px solid #ff7b724d;padding:3px 12px;font-size:.78rem}.rd-table{border-collapse:collapse;width:100%;font-size:.82rem}.rd-table th{text-align:left;text-transform:uppercase;letter-spacing:.1em;color:var(--text3);border-bottom:1px solid var(--border);white-space:nowrap;padding:10px 14px;font-size:.64rem;font-weight:700}.rd-table td{border-bottom:1px solid var(--border);color:var(--text2);vertical-align:middle;padding:11px 14px}.rd-table tr:last-child td{border-bottom:none}.rd-table tbody tr:hover td{background:var(--bg3)}.rd-table .mono{color:var(--blue);font-family:JetBrains Mono,monospace;font-size:.78rem}.rd-table .score-cell{font-family:JetBrains Mono,monospace;font-weight:700}.suggestion-card{border:1px solid var(--border);border-radius:var(--radius);background:var(--bg2);align-items:flex-start;gap:14px;padding:16px 18px;display:flex}.suggestion-card+.suggestion-card{margin-top:10px}.sug-icon{border-radius:8px;flex-shrink:0;justify-content:center;align-items:center;width:36px;height:36px;font-size:1rem;display:flex}.sug-icon.critical{background:#ff7b721f}.sug-icon.warning{background:#f0883e1f}.sug-icon.info{background:#58a6ff1f}.sug-body{flex:1;min-width:0}.sug-title{align-items:center;gap:8px;margin-bottom:4px;font-size:.88rem;font-weight:600;display:flex}.sug-desc{color:var(--text2);margin-bottom:8px;font-size:.78rem;line-height:1.55}.sug-fix{background:var(--bg3);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--accent);word-break:break-word;padding:8px 12px;font-family:JetBrains Mono,monospace;font-size:.76rem;line-height:1.5}.sug-component{color:var(--text3);margin-top:6px;font-family:JetBrains Mono,monospace;font-size:.7rem}.filmstrip{gap:14px;padding-bottom:4px;display:flex;overflow-x:auto}.filmstrip::-webkit-scrollbar{height:4px}.filmstrip::-webkit-scrollbar-thumb{background:var(--border2);border-radius:2px}.film-frame{text-align:center;flex-shrink:0}.film-img{object-fit:cover;border-radius:var(--radius-sm);border:2px solid var(--border);background:var(--bg3);cursor:pointer;width:210px;height:128px;transition:border-color var(--transition), transform var(--transition);display:block}.film-img:hover{border-color:var(--accent);transform:scale(1.02)}.film-label{text-transform:uppercase;letter-spacing:.08em;color:var(--text3);margin-top:6px;font-size:.66rem;font-weight:700}.film-time{color:var(--accent);font-family:JetBrains Mono,monospace;font-size:.7rem}.film-placeholder{border-radius:var(--radius-sm);border:2px dashed var(--border2);background:var(--bg3);width:210px;height:128px;color:var(--text3);flex-direction:column;justify-content:center;align-items:center;gap:6px;font-size:.72rem;display:flex}#lightbox{z-index:9999;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);background:#000000e0;justify-content:center;align-items:center;display:none;position:fixed;inset:0}#lightbox.open{display:flex}#lightbox img{border-radius:var(--radius);border:1px solid var(--border2);max-width:90vw;max-height:86vh;box-shadow:var(--shadow)}#lb-close{background:var(--bg3);border:1px solid var(--border2);color:var(--text);cursor:pointer;border-radius:50%;justify-content:center;align-items:center;width:36px;height:36px;font-size:1.1rem;display:flex;position:fixed;top:20px;right:24px}#lb-caption{color:var(--text2);background:var(--bg3);border:1px solid var(--border);border-radius:20px;padding:6px 16px;font-size:.8rem;position:fixed;bottom:24px;left:50%;transform:translate(-50%)}.vital-row{margin-bottom:14px}.vital-row-head{justify-content:space-between;margin-bottom:5px;font-size:.75rem;font-weight:600;display:flex}.vital-row-head .vname{color:var(--text2)}.vital-row-head .vval{font-family:JetBrains Mono,monospace;font-size:.77rem}.vital-track{background:var(--bg4);border-radius:3px;height:6px;overflow:hidden}.vital-fill{border-radius:3px;height:100%;transition:width 1s cubic-bezier(.4,0,.2,1)}.pagination{flex-flow:row;justify-content:center;align-items:center;gap:6px;margin-top:20px;display:flex;overflow-x:auto}.page-btn{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg2);min-width:32px;height:32px;color:var(--text2);cursor:pointer;transition:all var(--transition);justify-content:center;align-items:center;padding:0 8px;font-size:.78rem;display:flex}.page-btn:hover:not(:disabled){background:var(--bg4);color:var(--text);border-color:var(--border2)}.page-btn.active{background:var(--accent);color:var(--bg);border-color:var(--accent);font-weight:700}.page-btn:disabled{opacity:.35;cursor:not-allowed}.page-info{color:var(--text3);padding:0 6px;font-size:.73rem}.filter-bar{flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:16px;display:flex}.filter-btn{border:1px solid var(--border);background:var(--bg3);color:var(--text2);cursor:pointer;transition:all var(--transition);border-radius:20px;padding:5px 14px;font-size:.73rem;font-weight:600}.filter-btn:hover{border-color:var(--border2);color:var(--text)}.filter-btn.active{background:rgba(var(--accent-rgb),.1);border-color:var(--accent);color:var(--accent)}.filter-btn.fc.active{border-color:var(--red);color:var(--red);background:#ff7b721a}.filter-btn.fw.active{border-color:var(--orange);color:var(--orange);background:#f0883e1a}.filter-btn.fi.active{border-color:var(--blue);color:var(--blue);background:#58a6ff1a}.route-tabs{flex-wrap:wrap;gap:6px;margin-bottom:20px;display:flex}.route-tab{border-radius:var(--radius-sm);border:1px solid var(--border);background:var(--bg3);color:var(--text2);cursor:pointer;transition:all var(--transition);padding:6px 14px;font-family:JetBrains Mono,monospace;font-size:.76rem;font-weight:500}.route-tab:hover{border-color:var(--border2);color:var(--text)}.route-tab.active{background:rgba(var(--accent-rgb),.08);border-color:var(--accent);color:var(--accent)}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:var(--bg)}::-webkit-scrollbar-thumb{background:var(--border2);border-radius:3px}