argusqa-os 9.7.5 → 9.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,8 +3,11 @@
3
3
  * ARGUS HTML Report Generator — D7.1
4
4
  *
5
5
  * Converts the latest (or a specified) JSON report into a single self-contained
6
- * report.html with screenshots inlined as base64 data URIs. No external
7
- * dependencies the output file opens correctly offline.
6
+ * report.html with screenshots inlined as base64 data URIs. Styled to match the
7
+ * Argus brand (argus-qa.com): #5E0ED7 accent, ring+dot logo, Inter / JetBrains
8
+ * Mono type. Fonts load from Google Fonts when online and fall back to a system
9
+ * stack offline, so the report still renders cleanly with no network — the only
10
+ * hard requirement (inlined screenshots) is met regardless.
8
11
  *
9
12
  * Usage:
10
13
  * node src/utils/html-reporter.js # auto-picks latest report
@@ -24,22 +27,19 @@ const logger = childLogger('html-reporter');
24
27
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
25
28
  const REPORTS_DIR = path.resolve(__dirname, '../../reports');
26
29
 
30
+ // ── Brand ───────────────────────────────────────────────────────────────────
31
+ const ACCENT = '#5E0ED7';
32
+
27
33
  // ── Severity helpers ──────────────────────────────────────────────────────────
28
34
 
29
- const SEV_COLOR = {
30
- critical: { bg: '#fef2f2', border: '#fca5a5', text: '#991b1b', badge: '#dc2626', label: 'CRITICAL' },
31
- warning: { bg: '#fffbeb', border: '#fcd34d', text: '#92400e', badge: '#d97706', label: 'WARNING' },
32
- info: { bg: '#eff6ff', border: '#93c5fd', text: '#1e40af', badge: '#2563eb', label: 'INFO' },
35
+ const SEV = {
36
+ critical: { cls: 'critical', label: 'CRITICAL' },
37
+ warning: { cls: 'warning', label: 'WARNING' },
38
+ info: { cls: 'info', label: 'INFO' },
33
39
  };
34
40
 
35
- function sevStyle(sev) {
36
- const c = SEV_COLOR[sev] ?? SEV_COLOR.info;
37
- return `background:${c.bg};border-left:4px solid ${c.border};color:${c.text}`;
38
- }
39
-
40
- function sevBadge(sev) {
41
- const c = SEV_COLOR[sev] ?? SEV_COLOR.info;
42
- return `<span style="background:${c.badge};color:#fff;border-radius:3px;font-size:11px;font-weight:700;padding:2px 7px;letter-spacing:.5px;white-space:nowrap">${c.label}</span>`;
41
+ function sevOf(sev) {
42
+ return SEV[sev] ? sev : 'info';
43
43
  }
44
44
 
45
45
  function esc(str) {
@@ -55,6 +55,15 @@ function safeHref(url) {
55
55
  return /^https?:\/\//i.test(s) ? esc(s) : '#';
56
56
  }
57
57
 
58
+ // ── Logo (ring + dot — replicates landing/public/favicon.svg) ──────────────────
59
+
60
+ function logoSvg(size = 34, ring = '#7b3fe4', dot = '#9b6cf2') {
61
+ return `<svg class="logo" viewBox="0 0 32 32" width="${size}" height="${size}" aria-hidden="true">
62
+ <circle cx="16" cy="16" r="14" fill="none" stroke="${ring}" stroke-width="2.5"/>
63
+ <circle cx="16" cy="16" r="5" fill="${dot}"/>
64
+ </svg>`;
65
+ }
66
+
58
67
  // ── Screenshot embedding ──────────────────────────────────────────────────────
59
68
 
60
69
  function imgTag(filePath, alt = 'Screenshot', style = '') {
@@ -63,29 +72,29 @@ function imgTag(filePath, alt = 'Screenshot', style = '') {
63
72
  const buf = fs.readFileSync(filePath);
64
73
  const ext = path.extname(filePath).slice(1).toLowerCase() || 'png';
65
74
  const mime = ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg' : 'image/png';
66
- return `<img src="data:${mime};base64,${buf.toString('base64')}" alt="${esc(alt)}" style="max-width:100%;border-radius:6px;border:1px solid #e5e7eb;${style}">`;
75
+ return `<img class="shot" src="data:${mime};base64,${buf.toString('base64')}" alt="${esc(alt)}" style="${style}">`;
67
76
  } catch {
68
- return `<p style="color:#6b7280;font-size:13px">Screenshot not found: ${esc(path.basename(filePath))}</p>`;
77
+ return `<p class="shot-missing">Screenshot not found: ${esc(path.basename(filePath))}</p>`;
69
78
  }
70
79
  }
71
80
 
72
81
  // ── Finding renderer ──────────────────────────────────────────────────────────
73
82
 
74
83
  function renderFinding(e) {
75
- const sev = e.severity ?? 'info';
84
+ const sev = sevOf(e.severity);
76
85
  const type = esc(e.type ?? 'unknown');
77
86
  const msg = esc(e.message ?? e.description ?? (e.requestUrl ? `HTTP ${e.status ?? '?'} ${e.requestUrl}` : ''));
78
- const flaky = e.flaky ? ' <span style="color:#6b7280;font-size:11px">⚡ flaky</span>' : '';
79
- const isNew = e.isNew ? ' <span style="color:#059669;font-size:11px">★ new</span>' : '';
87
+ const flaky = e.flaky ? ' <span class="tag tag-flaky">⚡ flaky</span>' : '';
88
+ const isNew = e.isNew ? ' <span class="tag tag-new">★ new</span>' : '';
80
89
  return `
81
- <div style="${sevStyle(sev)};padding:10px 14px;margin:6px 0;border-radius:0 4px 4px 0;font-size:13px;line-height:1.5">
82
- <div style="display:flex;align-items:flex-start;gap:10px;flex-wrap:wrap">
83
- ${sevBadge(sev)}
84
- <code style="background:rgba(0,0,0,.06);border-radius:3px;padding:1px 6px;font-size:12px;white-space:nowrap">${type}</code>
90
+ <div class="finding ${sev}">
91
+ <div class="finding-head">
92
+ <span class="chip ${sev}">${SEV[sev].label}</span>
93
+ <code class="type">${type}</code>
85
94
  ${flaky}${isNew}
86
95
  </div>
87
- <div style="margin-top:6px;word-break:break-word">${msg}</div>
88
- ${e.requestUrl ? `<div style="margin-top:3px;font-size:11px;opacity:.8">URL: ${esc(e.requestUrl)}</div>` : ''}
96
+ <div class="finding-msg">${msg}</div>
97
+ ${e.requestUrl ? `<div class="finding-url">${esc(e.requestUrl)}</div>` : ''}
89
98
  </div>`;
90
99
  }
91
100
 
@@ -97,19 +106,16 @@ function renderRoute(route) {
97
106
  const warnings = errors.filter(e => e.severity === 'warning');
98
107
  const infos = errors.filter(e => e.severity === 'info');
99
108
 
100
- const headerColor = criticals.length > 0 ? '#dc2626'
101
- : warnings.length > 0 ? '#d97706'
102
- : '#16a34a';
103
- const headerBg = criticals.length > 0 ? '#fef2f2'
104
- : warnings.length > 0 ? '#fffbeb'
105
- : '#f0fdf4';
109
+ const state = criticals.length > 0 ? 'critical'
110
+ : warnings.length > 0 ? 'warning'
111
+ : 'ok';
106
112
 
107
113
  // Summary pill row
108
114
  const pills = [
109
- criticals.length > 0 ? `<span style="background:#dc2626;color:#fff;border-radius:12px;padding:2px 10px;font-size:12px;font-weight:600">${criticals.length} critical</span>` : '',
110
- warnings.length > 0 ? `<span style="background:#d97706;color:#fff;border-radius:12px;padding:2px 10px;font-size:12px;font-weight:600">${warnings.length} warning</span>` : '',
111
- infos.length > 0 ? `<span style="background:#2563eb;color:#fff;border-radius:12px;padding:2px 10px;font-size:12px;font-weight:600">${infos.length} info</span>` : '',
112
- errors.length === 0 ? `<span style="background:#16a34a;color:#fff;border-radius:12px;padding:2px 10px;font-size:12px;font-weight:600">✓ clean</span>` : '',
115
+ criticals.length > 0 ? `<span class="pill critical">${criticals.length} critical</span>` : '',
116
+ warnings.length > 0 ? `<span class="pill warning">${warnings.length} warning</span>` : '',
117
+ infos.length > 0 ? `<span class="pill info">${infos.length} info</span>` : '',
118
+ errors.length === 0 ? `<span class="pill ok">✓ clean</span>` : '',
113
119
  ].filter(Boolean).join(' ');
114
120
 
115
121
  // Screenshot
@@ -121,42 +127,43 @@ function renderRoute(route) {
121
127
  const viewports = Object.entries(route.responsiveScreenshots)
122
128
  .sort(([a], [b]) => Number(a) - Number(b))
123
129
  .map(([vp, fp]) => `
124
- <div style="text-align:center">
125
- <div style="font-size:11px;color:#6b7280;margin-bottom:4px">${esc(String(vp))}px</div>
130
+ <figure class="vp">
131
+ <figcaption>${esc(String(vp))}px</figcaption>
126
132
  ${imgTag(fp, `${route.route} at ${vp}px`, 'width:100%')}
127
- </div>`).join('');
133
+ </figure>`).join('');
128
134
  responsiveGrid = `
129
- <div style="margin-top:16px">
130
- <h4 style="margin:0 0 8px;font-size:13px;color:#374151">Responsive snapshots</h4>
131
- <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px">${viewports}</div>
135
+ <div class="route-block">
136
+ <h4 class="block-title">Responsive snapshots</h4>
137
+ <div class="vp-grid">${viewports}</div>
132
138
  </div>`;
133
139
  }
134
140
 
135
141
  // Findings list
136
142
  const findingRows = errors.length > 0
137
143
  ? errors.map(renderFinding).join('')
138
- : `<p style="color:#16a34a;margin:8px 0;font-size:13px">✓ No issues detected on this route.</p>`;
144
+ : `<p class="all-clear">✓ No issues detected on this route.</p>`;
139
145
 
140
146
  return `
141
- <div style="border:1px solid #e5e7eb;border-radius:8px;overflow:hidden;margin-bottom:24px;box-shadow:0 1px 3px rgba(0,0,0,.06)">
142
- <!-- route header -->
143
- <div style="background:${headerBg};border-bottom:1px solid #e5e7eb;padding:14px 20px;display:flex;align-items:flex-start;justify-content:space-between;flex-wrap:wrap;gap:8px">
144
- <div>
145
- <h3 style="margin:0;font-size:16px;color:${headerColor}">${esc(route.route)}</h3>
146
- <a href="${safeHref(route.url)}" style="font-size:12px;color:#6b7280;word-break:break-all">${esc(route.url)}</a>
147
+ <article class="route ${state}">
148
+ <header class="route-head">
149
+ <div class="route-id">
150
+ <span class="dot ${state}"></span>
151
+ <div>
152
+ <h3 class="route-name">${esc(route.route)}</h3>
153
+ <a class="route-url" href="${safeHref(route.url)}">${esc(route.url)}</a>
154
+ </div>
147
155
  </div>
148
- <div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center">${pills}</div>
149
- </div>
150
- <!-- route body -->
151
- <div style="padding:16px 20px">
152
- ${shot ? `<div style="margin-bottom:16px">${shot}</div>` : ''}
156
+ <div class="pills">${pills}</div>
157
+ </header>
158
+ <div class="route-body">
159
+ ${shot ? `<div class="route-block">${shot}</div>` : ''}
153
160
  ${responsiveGrid}
154
- <div style="margin-top:${shot || responsiveGrid ? '16px' : '0'}">
155
- <h4 style="margin:0 0 8px;font-size:13px;color:#374151">Findings</h4>
161
+ <div class="route-block">
162
+ <h4 class="block-title">Findings</h4>
156
163
  ${findingRows}
157
164
  </div>
158
165
  </div>
159
- </div>`;
166
+ </article>`;
160
167
  }
161
168
 
162
169
  // ── Flow card ─────────────────────────────────────────────────────────────────
@@ -164,19 +171,22 @@ function renderRoute(route) {
164
171
  function renderFlow(flow) {
165
172
  const status = flow.status ?? 'unknown';
166
173
  const findings = flow.findings ?? [];
167
- const statusColor = status === 'pass' ? '#16a34a' : '#dc2626';
174
+ const pass = status === 'pass';
168
175
  const findingRows = findings.length > 0
169
176
  ? findings.map(renderFinding).join('')
170
- : '<p style="color:#16a34a;margin:8px 0;font-size:13px">✓ All assertions passed.</p>';
177
+ : '<p class="all-clear">✓ All assertions passed.</p>';
171
178
 
172
179
  return `
173
- <div style="border:1px solid #e5e7eb;border-radius:8px;overflow:hidden;margin-bottom:16px">
174
- <div style="background:#f9fafb;border-bottom:1px solid #e5e7eb;padding:12px 20px;display:flex;align-items:center;justify-content:space-between">
175
- <span style="font-weight:600;font-size:15px">${esc(flow.flowName ?? flow.name ?? 'Flow')}</span>
176
- <span style="font-size:13px;font-weight:600;color:${statusColor}">${status.toUpperCase()} (${flow.stepsCompleted ?? '?'}/${flow.totalSteps ?? '?'} steps)</span>
177
- </div>
178
- <div style="padding:14px 20px">${findingRows}</div>
179
- </div>`;
180
+ <article class="route ${pass ? 'ok' : 'critical'}">
181
+ <header class="route-head">
182
+ <div class="route-id">
183
+ <span class="dot ${pass ? 'ok' : 'critical'}"></span>
184
+ <h3 class="route-name">${esc(flow.flowName ?? flow.name ?? 'Flow')}</h3>
185
+ </div>
186
+ <span class="pill ${pass ? 'ok' : 'critical'}">${esc(status.toUpperCase())} · ${flow.stepsCompleted ?? '?'}/${flow.totalSteps ?? '?'} steps</span>
187
+ </header>
188
+ <div class="route-body"><div class="route-block">${findingRows}</div></div>
189
+ </article>`;
180
190
  }
181
191
 
182
192
  // ── Full HTML document ────────────────────────────────────────────────────────
@@ -185,26 +195,36 @@ function buildHtml(report) {
185
195
  const { generatedAt, baseUrl, summary, routes = [], flows = [] } = report;
186
196
  const runDate = new Date(generatedAt).toLocaleString(undefined, { dateStyle: 'long', timeStyle: 'short' });
187
197
 
188
- const totalBg = summary.critical > 0 ? '#dc2626' : summary.warning > 0 ? '#d97706' : '#16a34a';
189
-
190
- const summaryCards = `
191
- <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:14px;margin-bottom:32px">
192
- ${[
193
- ['Total', summary.total ?? 0, '#374151', '#f3f4f6'],
194
- ['Critical', summary.critical ?? 0, '#991b1b', '#fef2f2'],
195
- ['Warning', summary.warning ?? 0, '#92400e', '#fffbeb'],
196
- ['Info', summary.info ?? 0, '#1e40af', '#eff6ff'],
197
- ].map(([label, count, color, bg]) => `
198
- <div style="background:${bg};border:1px solid #e5e7eb;border-radius:8px;padding:18px;text-align:center">
199
- <div style="font-size:32px;font-weight:700;color:${color}">${count}</div>
200
- <div style="font-size:12px;color:#6b7280;margin-top:4px;text-transform:uppercase;letter-spacing:.5px">${label}</div>
201
- </div>`).join('')}
202
- </div>`;
198
+ const crit = summary.critical ?? 0;
199
+ const warn = summary.warning ?? 0;
200
+ const info = summary.info ?? 0;
201
+ const total = summary.total ?? (crit + warn + info);
202
+
203
+ // Overall verdict
204
+ const verdict = crit > 0
205
+ ? { cls: 'critical', icon: '', title: `${crit} critical issue${crit === 1 ? '' : 's'} need${crit === 1 ? 's' : ''} attention`,
206
+ sub: `${warn} warning${warn === 1 ? '' : 's'} · ${info} info · ${routes.length} route${routes.length === 1 ? '' : 's'} audited` }
207
+ : warn > 0
208
+ ? { cls: 'warning', icon: '!', title: `${warn} warning${warn === 1 ? '' : 's'} to review`,
209
+ sub: `0 critical · ${info} info · ${routes.length} route${routes.length === 1 ? '' : 's'} audited` }
210
+ : { cls: 'ok', icon: '✓', title: 'All clear — no issues detected',
211
+ sub: `${routes.length} route${routes.length === 1 ? '' : 's'} audited · clean run` };
212
+
213
+ const cards = [
214
+ { k: 'total', n: total, label: 'Total' },
215
+ { k: 'critical', n: crit, label: 'Critical' },
216
+ { k: 'warning', n: warn, label: 'Warning' },
217
+ { k: 'info', n: info, label: 'Info' },
218
+ ].map(c => `
219
+ <div class="card ${c.k}">
220
+ <div class="num">${c.n}</div>
221
+ <div class="lbl">${c.label}</div>
222
+ </div>`).join('');
203
223
 
204
224
  const routeSections = routes.map(renderRoute).join('');
205
225
 
206
226
  const flowSection = flows.length > 0 ? `
207
- <h2 style="font-size:18px;color:#111827;border-bottom:2px solid #e5e7eb;padding-bottom:8px;margin:32px 0 16px">User Flows (${flows.length})</h2>
227
+ <h2 class="section">User Flows <span class="count">${flows.length}</span></h2>
208
228
  ${flows.map(renderFlow).join('')}` : '';
209
229
 
210
230
  return `<!DOCTYPE html>
@@ -213,28 +233,199 @@ function buildHtml(report) {
213
233
  <meta charset="UTF-8">
214
234
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
215
235
  <title>Argus Report — ${esc(runDate)}</title>
236
+ <link rel="preconnect" href="https://fonts.googleapis.com">
237
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
238
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
216
239
  <style>
240
+ :root {
241
+ --accent:#5E0ED7; --accent-soft:#9b6cf2; --ink:#0d0d0d;
242
+ --bg:#f6f4fc; --card:#ffffff; --border:#ece8f7; --border-strong:#ddd5f1;
243
+ --text:#1b1726; --muted:#6c6580;
244
+ --crit:#dc2626; --crit-bg:#fef2f3; --crit-bd:#f4cdcd;
245
+ --warn:#d97706; --warn-bg:#fffaeb; --warn-bd:#f3e0a8;
246
+ --info:#2563eb; --info-bg:#eef4ff; --info-bd:#cadcfb;
247
+ --ok:#15a34a; --ok-bg:#eefcf3; --ok-bd:#bce8cc;
248
+ }
217
249
  *, *::before, *::after { box-sizing: border-box; }
218
- body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f9fafb; color: #111827; }
219
- a { color: inherit; }
250
+ body {
251
+ margin: 0; color: var(--text);
252
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
253
+ background:
254
+ radial-gradient(1200px 480px at 100% -8%, rgba(94,14,215,.07), transparent 60%),
255
+ radial-gradient(900px 420px at -10% 0%, rgba(94,14,215,.05), transparent 55%),
256
+ var(--bg);
257
+ -webkit-font-smoothing: antialiased;
258
+ }
259
+ a { color: inherit; text-decoration: none; }
260
+ code, .mono { font-family: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace; }
261
+
262
+ /* top accent line + branded header */
263
+ .accent-line { height: 4px; background: linear-gradient(90deg, var(--accent), var(--accent-soft) 55%, #cbb4f7); }
264
+ .topbar {
265
+ position: relative; overflow: hidden; background: var(--ink); color: #fff;
266
+ padding: 22px 32px; display: flex; align-items: center; justify-content: space-between;
267
+ gap: 14px; flex-wrap: wrap;
268
+ }
269
+ .topbar::before { content:''; position:absolute; inset:0;
270
+ background: radial-gradient(130% 200% at 0% 0%, rgba(94,14,215,.55), transparent 52%); }
271
+ .topbar > * { position: relative; z-index: 1; }
272
+ .brand { display: flex; align-items: center; gap: 13px; }
273
+ .brand .logo-wrap { display:grid; place-items:center; width:46px; height:46px; border-radius:13px;
274
+ background: rgba(94,14,215,.16); border: 1px solid rgba(155,108,242,.35); }
275
+ .brand .name { font-size: 21px; font-weight: 800; letter-spacing: -.4px; line-height: 1; }
276
+ .brand .name .dim { color: rgba(255,255,255,.5); font-weight: 600; margin-left: 2px; }
277
+ .brand .tagline { font-size: 11.5px; color: rgba(255,255,255,.45); margin-top: 4px; letter-spacing: .2px; }
278
+ .meta { text-align: right; font-size: 12.5px; color: rgba(255,255,255,.72); line-height: 1.7; }
279
+ .meta .meta-url { color: var(--accent-soft); }
280
+
281
+ .wrap { max-width: 1080px; margin: 0 auto; padding: 30px 24px 56px; }
282
+
283
+ /* verdict banner */
284
+ .verdict { display: flex; align-items: center; gap: 16px; padding: 18px 22px;
285
+ border: 1px solid var(--border); border-radius: 18px; margin-bottom: 24px; background: var(--card);
286
+ box-shadow: 0 1px 2px rgba(13,13,13,.04), 0 20px 40px -28px rgba(94,14,215,.28); }
287
+ .verdict .v-icon { flex: 0 0 auto; width: 46px; height: 46px; border-radius: 14px; display: grid;
288
+ place-items: center; font-size: 22px; font-weight: 800; color: #fff; }
289
+ .verdict.ok { border-color: var(--ok-bd); }
290
+ .verdict.warning { border-color: var(--warn-bd); }
291
+ .verdict.critical { border-color: var(--crit-bd); }
292
+ .verdict.ok .v-icon { background: var(--ok); }
293
+ .verdict.warning .v-icon { background: var(--warn); }
294
+ .verdict.critical .v-icon { background: var(--crit); }
295
+ .verdict .v-title { font-weight: 700; font-size: 17px; letter-spacing: -.2px; }
296
+ .verdict .v-sub { color: var(--muted); font-size: 13px; margin-top: 3px; }
297
+
298
+ /* summary cards */
299
+ .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
300
+ gap: 16px; margin-bottom: 34px; }
301
+ .card { position: relative; overflow: hidden; background: var(--card); border: 1px solid var(--border);
302
+ border-radius: 18px; padding: 22px 20px 20px; box-shadow: 0 1px 2px rgba(13,13,13,.04); }
303
+ .card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 4px; }
304
+ .card .num { font-size: 42px; font-weight: 800; letter-spacing: -1.5px; line-height: 1; }
305
+ .card .lbl { margin-top: 8px; font-size: 11.5px; font-weight: 600; text-transform: uppercase;
306
+ letter-spacing: 1px; color: var(--muted); }
307
+ .card.total::before { background: linear-gradient(90deg, var(--accent), var(--accent-soft)); }
308
+ .card.total .num { color: var(--accent); }
309
+ .card.critical::before { background: var(--crit); }
310
+ .card.critical .num { color: var(--crit); }
311
+ .card.warning::before { background: var(--warn); }
312
+ .card.warning .num { color: var(--warn); }
313
+ .card.info::before { background: var(--info); }
314
+ .card.info .num { color: var(--info); }
315
+
316
+ /* section heading */
317
+ .section { display: flex; align-items: center; gap: 10px; font-size: 16px; font-weight: 700;
318
+ letter-spacing: -.2px; margin: 0 0 18px; padding-bottom: 12px; border-bottom: 1px solid var(--border); }
319
+ .section::before { content: ''; width: 4px; height: 18px; border-radius: 3px;
320
+ background: linear-gradient(180deg, var(--accent), var(--accent-soft)); }
321
+ .section .count { font-size: 12px; font-weight: 700; color: var(--accent);
322
+ background: rgba(94,14,215,.09); border: 1px solid rgba(94,14,215,.16);
323
+ padding: 2px 9px; border-radius: 999px; }
324
+
325
+ /* route card */
326
+ .route { background: var(--card); border: 1px solid var(--border); border-radius: 18px;
327
+ overflow: hidden; margin-bottom: 18px; box-shadow: 0 1px 2px rgba(13,13,13,.04), 0 16px 36px -30px rgba(94,14,215,.35); }
328
+ .route-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
329
+ flex-wrap: wrap; padding: 16px 22px; border-bottom: 1px solid var(--border); }
330
+ .route.ok .route-head { background: linear-gradient(180deg, var(--ok-bg), transparent); }
331
+ .route.warning .route-head { background: linear-gradient(180deg, var(--warn-bg), transparent); }
332
+ .route.critical .route-head { background: linear-gradient(180deg, var(--crit-bg), transparent); }
333
+ .route-id { display: flex; align-items: center; gap: 12px; min-width: 0; }
334
+ .dot { flex: 0 0 auto; width: 10px; height: 10px; border-radius: 50%; margin-top: 6px;
335
+ box-shadow: 0 0 0 4px rgba(0,0,0,.04); }
336
+ .dot.ok { background: var(--ok); } .dot.warning { background: var(--warn); } .dot.critical { background: var(--crit); }
337
+ .route-name { margin: 0; font-size: 16px; font-weight: 700; letter-spacing: -.2px;
338
+ font-family: 'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace; }
339
+ .route-url { display: inline-block; margin-top: 3px; font-size: 12px; color: var(--muted); word-break: break-all; }
340
+ .route-url:hover { color: var(--accent); }
341
+ .pills { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
342
+ .route-body { padding: 18px 22px; }
343
+ .route-block + .route-block { margin-top: 18px; }
344
+ .block-title { margin: 0 0 10px; font-size: 11.5px; font-weight: 700; text-transform: uppercase;
345
+ letter-spacing: 1px; color: var(--muted); }
346
+
347
+ /* pills + chips */
348
+ .pill { font-size: 12px; font-weight: 600; padding: 3px 11px; border-radius: 999px; white-space: nowrap; }
349
+ .pill.critical { background: var(--crit-bg); color: var(--crit); border: 1px solid var(--crit-bd); }
350
+ .pill.warning { background: var(--warn-bg); color: var(--warn); border: 1px solid var(--warn-bd); }
351
+ .pill.info { background: var(--info-bg); color: var(--info); border: 1px solid var(--info-bd); }
352
+ .pill.ok { background: var(--ok-bg); color: var(--ok); border: 1px solid var(--ok-bd); }
353
+
354
+ /* findings */
355
+ .finding { border: 1px solid var(--border); border-left-width: 4px; border-radius: 12px;
356
+ padding: 12px 15px; margin: 9px 0; font-size: 13.5px; line-height: 1.55; }
357
+ .finding.critical { background: var(--crit-bg); border-left-color: var(--crit); }
358
+ .finding.warning { background: var(--warn-bg); border-left-color: var(--warn); }
359
+ .finding.info { background: var(--info-bg); border-left-color: var(--info); }
360
+ .finding-head { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }
361
+ .chip { font-size: 10.5px; font-weight: 700; letter-spacing: .6px; color: #fff;
362
+ padding: 3px 8px; border-radius: 6px; white-space: nowrap; }
363
+ .chip.critical { background: var(--crit); } .chip.warning { background: var(--warn); } .chip.info { background: var(--info); }
364
+ .type { background: rgba(13,13,13,.06); border: 1px solid rgba(13,13,13,.05); border-radius: 6px;
365
+ padding: 2px 7px; font-size: 12px; color: #2a2535; white-space: nowrap; }
366
+ .tag { font-size: 11px; font-weight: 600; }
367
+ .tag-flaky { color: var(--muted); } .tag-new { color: var(--ok); }
368
+ .finding-msg { margin-top: 7px; color: var(--text); word-break: break-word; }
369
+ .finding-url { margin-top: 4px; font-size: 11.5px; color: var(--muted); word-break: break-all; }
370
+ .all-clear { margin: 6px 0; font-size: 13.5px; color: var(--ok); font-weight: 500; }
371
+
372
+ /* screenshots */
373
+ .shot { max-width: 100%; border-radius: 12px; border: 1px solid var(--border-strong);
374
+ box-shadow: 0 8px 24px -18px rgba(13,13,13,.4); display: block; }
375
+ .shot-missing { color: var(--muted); font-size: 13px; }
376
+ .vp-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; }
377
+ .vp { margin: 0; text-align: center; }
378
+ .vp figcaption { font-size: 11px; color: var(--muted); margin-bottom: 5px; font-weight: 600; }
379
+
380
+ /* footer */
381
+ .foot { display: flex; align-items: center; justify-content: center; gap: 9px; margin-top: 40px;
382
+ color: var(--muted); font-size: 12.5px; }
383
+ .foot a { color: var(--accent); font-weight: 600; }
384
+ .foot .logo { opacity: .85; }
385
+
386
+ @media (max-width: 560px) {
387
+ .topbar { padding: 18px 20px; } .wrap { padding: 22px 16px 44px; }
388
+ .meta { text-align: left; }
389
+ }
220
390
  </style>
221
391
  </head>
222
392
  <body>
223
- <!-- top bar -->
224
- <div style="background:${totalBg};padding:14px 32px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px">
225
- <span style="color:#fff;font-weight:700;font-size:20px;letter-spacing:-.3px">🛡 Argus Report</span>
226
- <span style="color:rgba(255,255,255,.85);font-size:13px">${esc(runDate)} · ${esc(baseUrl)}</span>
227
- </div>
393
+ <div class="accent-line"></div>
394
+ <header class="topbar">
395
+ <div class="brand">
396
+ <span class="logo-wrap">${logoSvg(26)}</span>
397
+ <div>
398
+ <div class="name">Argus<span class="dim">Report</span></div>
399
+ <div class="tagline">Automated QA · Chrome DevTools Protocol</div>
400
+ </div>
401
+ </div>
402
+ <div class="meta">
403
+ <div>${esc(runDate)}</div>
404
+ <div class="meta-url">${esc(baseUrl)}</div>
405
+ </div>
406
+ </header>
228
407
 
229
- <div style="max-width:1100px;margin:0 auto;padding:32px 24px">
230
- ${summaryCards}
408
+ <div class="wrap">
409
+ <div class="verdict ${verdict.cls}">
410
+ <div class="v-icon">${verdict.icon}</div>
411
+ <div>
412
+ <div class="v-title">${esc(verdict.title)}</div>
413
+ <div class="v-sub">${esc(verdict.sub)}</div>
414
+ </div>
415
+ </div>
231
416
 
232
- <h2 style="font-size:18px;color:#111827;border-bottom:2px solid #e5e7eb;padding-bottom:8px;margin:0 0 16px">Routes (${routes.length})</h2>
417
+ <div class="cards">${cards}
418
+ </div>
419
+
420
+ <h2 class="section">Routes <span class="count">${routes.length}</span></h2>
233
421
  ${routeSections}
234
422
 
235
423
  ${flowSection}
236
424
 
237
- <p style="text-align:center;font-size:12px;color:#9ca3af;margin-top:32px">Generated by <strong>Argus</strong> · ${esc(runDate)}</p>
425
+ <div class="foot">
426
+ ${logoSvg(18, ACCENT, ACCENT)}
427
+ <span>Generated by <strong>Argus</strong> · <a href="https://argus-qa.com">argus-qa.com</a> · ${esc(runDate)}</span>
428
+ </div>
238
429
  </div>
239
430
  </body>
240
431
  </html>`;