react-doctor-cli-dev 1.0.6 → 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 +38 -9
  6. package/backend/dist/index.js +29 -3
  7. package/backend/dist/routes/reports.js +105 -22
  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 +46 -14
  48. package/backend/src/index.ts +31 -3
  49. package/backend/src/routes/reports.ts +140 -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
@@ -3,24 +3,53 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.screenshotsDir = void 0;
6
7
  const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
7
8
  const path_1 = __importDefault(require("path"));
8
9
  const fs_1 = __importDefault(require("fs"));
9
- const os_1 = __importDefault(require("os"));
10
- // Store DB in user's home directory — works regardless of where process spawns from
11
- const dbDir = path_1.default.join(os_1.default.homedir(), ".react-doctor");
12
- const dbPath = process.env.DB_PATH || path_1.default.join(dbDir, "reports.db");
13
- fs_1.default.mkdirSync(path_1.default.dirname(dbPath), { recursive: true });
10
+ // ── Initialize database ──────────────────────────────────────
11
+ const dbPath = process.env.DB_PATH || path_1.default.join(__dirname, '../../reports.db');
14
12
  const db = new better_sqlite3_1.default(dbPath);
13
+ // ── Enable foreign keys ──────────────────────────────────────
14
+ db.pragma('foreign_keys = ON');
15
+ // ── Create reports table if it doesn't exist ──────────────
15
16
  db.exec(`
16
- CREATE TABLE IF NOT EXISTS reports (
17
+ CREATE TABLE IF NOT EXISTS reports (
17
18
  id INTEGER PRIMARY KEY AUTOINCREMENT,
18
19
  project TEXT NOT NULL,
19
20
  score INTEGER NOT NULL,
20
21
  grade TEXT NOT NULL,
21
22
  analyzed_at TEXT NOT NULL,
22
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
23
- payload TEXT NOT NULL
24
- );
23
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
24
+ static_json TEXT NOT NULL,
25
+ runtime_json TEXT NOT NULL,
26
+ suggestions TEXT NOT NULL
27
+ )
25
28
  `);
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 ──────
26
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
  //
@@ -39,8 +42,8 @@ const express_1 = require("express");
39
42
  const path_1 = __importDefault(require("path"));
40
43
  const fs_1 = __importDefault(require("fs"));
41
44
  const db_1 = __importDefault(require("../db"));
45
+ const db_2 = require("../db");
42
46
  const auth_1 = require("../middleware/auth");
43
- const screenshotsDir = "data/screenshots";
44
47
  const router = (0, express_1.Router)();
45
48
  // ── GET /api/reports ─────────────────────────────────────────
46
49
  // Summary list — no blobs, just the columns the history page needs.
@@ -76,6 +79,87 @@ router.get("/project/:name", (req, res) => {
76
79
  res.status(500).json({ error: "Internal server error" });
77
80
  }
78
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
+ });
79
163
  // ── POST /api/reports/upload ──────────────────────────────────
80
164
  // Receives a FinalReport from the CLI, strips screenshots to disk,
81
165
  // and stores the three JSON blobs in separate columns.
@@ -99,9 +183,6 @@ router.post("/upload", auth_1.requireApiKey, (req, res) => {
99
183
  // ── Extract grade from static report ────────────────────
100
184
  const grade = body.static?.grade ?? "N/A";
101
185
  // ── Strip screenshots from runtime, save as .png files ──
102
- // We do this BEFORE inserting so the DB never holds base64.
103
- // The row ID isn't known yet — we'll rename after insert.
104
- // For now we use a temp prefix and rename below.
105
186
  const { cleanedRuntime, pendingScreenshots } = extractScreenshots(body.runtime);
106
187
  // ── Insert the row ───────────────────────────────────────
107
188
  const stmt = db_1.default.prepare(`
@@ -115,7 +196,6 @@ router.post("/upload", auth_1.requireApiKey, (req, res) => {
115
196
  // ── Save screenshots with final filenames ────────────────
116
197
  const savedScreenshots = saveScreenshots(reportId, pendingScreenshots);
117
198
  // ── Patch runtime_json with final screenshot paths ───────
118
- // Now that we have the reportId we can write the correct paths.
119
199
  if (savedScreenshots.length > 0) {
120
200
  const patchedRuntime = patchScreenshotPaths(cleanedRuntime, savedScreenshots);
121
201
  db_1.default.prepare("UPDATE reports SET runtime_json = ? WHERE id = ?").run(JSON.stringify(patchedRuntime), reportId);
@@ -148,7 +228,6 @@ router.get("/:id", (req, res) => {
148
228
  grade: row.grade,
149
229
  analyzedAt: row.analyzed_at,
150
230
  createdAt: row.created_at,
151
- // Parse the three JSON blobs back into objects
152
231
  static: JSON.parse(row.static_json),
153
232
  runtime: JSON.parse(row.runtime_json),
154
233
  suggestions: JSON.parse(row.suggestions),
@@ -170,10 +249,6 @@ function getMissingFields(body) {
170
249
  /**
171
250
  * Walk the runtime map, strip every screenshot.dataUrl,
172
251
  * and collect them as Buffers ready to write to disk.
173
- *
174
- * Returns:
175
- * cleanedRuntime — runtime map with dataUrls replaced by tempPath markers
176
- * pendingScreenshots — list of screenshots to save once we have a reportId
177
252
  */
178
253
  function extractScreenshots(runtime) {
179
254
  const pending = [];
@@ -182,17 +257,24 @@ function extractScreenshots(runtime) {
182
257
  const routeClone = { ...routeData };
183
258
  if (Array.isArray(routeClone.screenshots)) {
184
259
  routeClone.screenshots = routeClone.screenshots.map((shot) => {
185
- 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
186
272
  return shot;
187
273
  }
188
- const base64 = shot.dataUrl.replace("data:image/png;base64,", "");
189
- const buffer = Buffer.from(base64, "base64");
190
- // Sanitise routeKey for use in a filename — replace "/" and ":" with "-"
191
- const safeRoute = routeKey.replace(/[/:]/g, "-").replace(/^-+/, "");
192
- const safeLabel = shot.label.replace(/[^a-z0-9]/gi, "-");
193
- const tempPath = `__PENDING__${safeRoute}__${safeLabel}`;
194
- pending.push({ routeKey, label: shot.label, buffer, tempPath });
195
- return { ...shot, dataUrl: tempPath };
274
+ else {
275
+ // No valid dataUrl - keep as is or set to null
276
+ return { ...shot, dataUrl: null };
277
+ }
196
278
  });
197
279
  }
198
280
  cleaned[routeKey] = routeClone;
@@ -201,15 +283,17 @@ function extractScreenshots(runtime) {
201
283
  }
202
284
  /**
203
285
  * Write each screenshot buffer to data/screenshots/<reportId>-<route>-<label>.png
204
- * Returns the list of saved files with their final URL paths.
205
286
  */
206
287
  function saveScreenshots(reportId, pending) {
207
288
  const saved = [];
289
+ if (!fs_1.default.existsSync(db_2.screenshotsDir)) {
290
+ fs_1.default.mkdirSync(db_2.screenshotsDir, { recursive: true });
291
+ }
208
292
  for (const shot of pending) {
209
293
  const safeRoute = shot.routeKey.replace(/[/:]/g, "-").replace(/^-+/, "");
210
294
  const safeLabel = shot.label.replace(/[^a-z0-9]/gi, "-");
211
295
  const filename = `${reportId}-${safeRoute}-${safeLabel}.png`;
212
- const fullPath = path_1.default.join(screenshotsDir, filename);
296
+ const fullPath = path_1.default.join(db_2.screenshotsDir, filename);
213
297
  try {
214
298
  fs_1.default.writeFileSync(fullPath, shot.buffer);
215
299
  saved.push({
@@ -230,7 +314,6 @@ function saveScreenshots(reportId, pending) {
230
314
  * the final /screenshots/<file> URL paths.
231
315
  */
232
316
  function patchScreenshotPaths(runtime, saved) {
233
- // Build a lookup from tempPath → filePath
234
317
  const lookup = {};
235
318
  for (const s of saved)
236
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,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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: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}