qa-engineer 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +167 -59
  2. package/package.json +1 -1
  3. package/packages/engine/bin/qa-engine.mjs +232 -8
  4. package/packages/engine/lib/analysis/har.mjs +34 -0
  5. package/packages/engine/lib/analysis/network.mjs +237 -0
  6. package/packages/engine/lib/analysis/report-html.mjs +47 -734
  7. package/packages/engine/lib/artifacts/manager.mjs +453 -0
  8. package/packages/engine/lib/artifacts/mime.mjs +109 -0
  9. package/packages/engine/lib/artifacts/zip-write.mjs +143 -0
  10. package/packages/engine/lib/report/components/charts.mjs +424 -0
  11. package/packages/engine/lib/report/components/evidence.mjs +207 -0
  12. package/packages/engine/lib/report/components/findings.mjs +258 -0
  13. package/packages/engine/lib/report/components/nav.mjs +99 -0
  14. package/packages/engine/lib/report/components/primitives.mjs +246 -0
  15. package/packages/engine/lib/report/components/runtime.mjs +246 -0
  16. package/packages/engine/lib/report/components/timeline.mjs +65 -0
  17. package/packages/engine/lib/report/core/model.mjs +270 -0
  18. package/packages/engine/lib/report/core/normalize.mjs +226 -0
  19. package/packages/engine/lib/report/core/sections.mjs +978 -0
  20. package/packages/engine/lib/report/export/bundle.mjs +293 -0
  21. package/packages/engine/lib/report/export/html.mjs +183 -0
  22. package/packages/engine/lib/report/export/machine.mjs +290 -0
  23. package/packages/engine/lib/report/export/markdown.mjs +323 -0
  24. package/packages/engine/lib/report/schemas/qa-report.schema.json +555 -0
  25. package/packages/engine/lib/report/theme/css.mjs +529 -0
  26. package/packages/engine/lib/report/theme/tokens.mjs +137 -0
  27. package/packages/engine/lib/report/version.mjs +78 -0
  28. package/packages/engine/package.json +2 -2
  29. package/packages/installer/lib/agents/targets.mjs +90 -0
  30. package/packages/installer/lib/agents/user-level.mjs +80 -0
  31. package/packages/installer/lib/cli/flags.mjs +20 -2
  32. package/packages/installer/lib/commands/doctor.mjs +6 -4
  33. package/packages/installer/lib/commands/install.mjs +134 -93
  34. package/packages/installer/lib/commands/repair.mjs +10 -6
  35. package/packages/installer/lib/commands/self-test.mjs +4 -3
  36. package/packages/installer/lib/commands/uninstall.mjs +14 -8
  37. package/packages/installer/lib/commands/update.mjs +9 -4
  38. package/packages/installer/lib/commands/verify.mjs +13 -12
  39. package/packages/installer/lib/constants.mjs +13 -0
  40. package/packages/installer/lib/core/conflict.mjs +5 -4
  41. package/packages/installer/lib/core/fs-safe.mjs +146 -6
  42. package/packages/installer/lib/core/integrity.mjs +59 -0
  43. package/packages/installer/lib/core/lockfile.mjs +19 -3
  44. package/packages/installer/lib/core/plan.mjs +213 -0
  45. package/packages/installer/lib/core/qa-home.mjs +145 -0
  46. package/packages/installer/lib/core/scope.mjs +274 -0
  47. package/packages/installer/lib/core/validate-install.mjs +37 -12
  48. package/packages/installer/package.json +1 -1
  49. package/packages/installer/schemas/qa-lock.schema.json +119 -21
  50. package/shared/tooling/qa-tool.mjs +41 -5
  51. package/skills/qa-api/scripts/qa-tool.mjs +41 -5
  52. package/skills/qa-audit/scripts/qa-tool.mjs +41 -5
  53. package/skills/qa-debug/SKILL.md +3 -0
  54. package/skills/qa-debug/references/failure-handoff.md +44 -0
  55. package/skills/qa-debug/scripts/qa-tool.mjs +41 -5
  56. package/skills/qa-explore/SKILL.md +26 -10
  57. package/skills/qa-explore/contracts/explore-result.schema.json +517 -11
  58. package/skills/qa-explore/references/api-replay.md +40 -1
  59. package/skills/qa-explore/references/report-pipeline.md +265 -95
  60. package/skills/qa-explore/scripts/qa-tool.mjs +41 -5
  61. package/skills/qa-fix/scripts/qa-tool.mjs +41 -5
  62. package/skills/qa-flaky/scripts/qa-tool.mjs +41 -5
  63. package/skills/qa-init/scripts/qa-tool.mjs +41 -5
  64. package/skills/qa-report/scripts/qa-tool.mjs +41 -5
  65. package/skills/qa-run/README.md +7 -0
  66. package/skills/qa-run/SKILL.md +12 -7
  67. package/skills/qa-run/contracts/execution-result.schema.json +77 -0
  68. package/skills/qa-run/examples/execute-playwright.md +42 -10
  69. package/skills/qa-run/examples/plan-a-run.md +5 -3
  70. package/skills/qa-run/references/artifact-collector.md +2 -1
  71. package/skills/qa-run/references/command-builder.md +18 -1
  72. package/skills/qa-run/references/execution-strategy.md +2 -0
  73. package/skills/qa-run/references/failure-handoff.md +44 -0
  74. package/skills/qa-run/references/playwright-artifacts.md +3 -1
  75. package/skills/qa-run/references/playwright-execution.md +14 -1
  76. package/skills/qa-run/scripts/qa-tool.mjs +41 -5
@@ -0,0 +1,529 @@
1
+ // The report stylesheet, emitted as one inline <style>.
2
+ //
3
+ // Inline and self-contained is a hard requirement, not a preference: a QA report is
4
+ // forwarded as an attachment and opened on a plane, from a `file://` path, inside a
5
+ // corporate mail client that blocks remote content. A CDN stylesheet or a web font
6
+ // turns a premium report into unstyled text at exactly the moment it is being read
7
+ // by the person who matters most.
8
+ //
9
+ // So: system font stack, no @import, no external asset, and every colour derived
10
+ // from tokens.mjs so the badges, the charts, and the score gauges cannot disagree.
11
+ //
12
+ // ## Three renderings, one document
13
+ //
14
+ // screen light — the default, driven by prefers-color-scheme
15
+ // screen dark — the same tokens at a different luminance
16
+ // print — glass and shadow removed, cards forced open, links given their href
17
+ //
18
+ // The theme toggle writes `data-theme` on <html>, and those rules are last so a
19
+ // reader's explicit choice beats their OS setting in both directions.
20
+
21
+ import { SEVERITY, STATUS, TONE, SEVERITY_ORDER } from './tokens.mjs';
22
+
23
+ /** Per-severity badge and accent rules, generated so a new severity cannot be missed. */
24
+ function severityRules() {
25
+ const out = [];
26
+ for (const key of SEVERITY_ORDER) {
27
+ const { light, dark } = SEVERITY[key];
28
+ out.push(
29
+ `.sev-${key}{--sev-fg:${light.fg};--sev-bg:${light.bg};--sev-border:${light.border};--sev-solid:${light.solid}}`,
30
+ );
31
+ out.push(
32
+ `[data-theme="dark"] .sev-${key}{--sev-fg:${dark.fg};--sev-bg:${dark.bg};--sev-border:${dark.border};--sev-solid:${dark.solid}}`,
33
+ );
34
+ }
35
+ out.push('@media (prefers-color-scheme:dark){');
36
+ for (const key of SEVERITY_ORDER) {
37
+ const { dark } = SEVERITY[key];
38
+ out.push(
39
+ `:root:not([data-theme="light"]) .sev-${key}{--sev-fg:${dark.fg};--sev-bg:${dark.bg};--sev-border:${dark.border};--sev-solid:${dark.solid}}`,
40
+ );
41
+ }
42
+ out.push('}');
43
+ return out.join('');
44
+ }
45
+
46
+ /** The same treatment for pass/fail/blocked/skipped chips. */
47
+ function statusRules() {
48
+ const out = [];
49
+ for (const [key, value] of Object.entries(STATUS)) {
50
+ out.push(`.st-${key}{--st-fg:${value.light.fg};--st-bg:${value.light.bg};--st-solid:${value.light.solid}}`);
51
+ out.push(`[data-theme="dark"] .st-${key}{--st-fg:${value.dark.fg};--st-bg:${value.dark.bg};--st-solid:${value.dark.solid}}`);
52
+ }
53
+ out.push('@media (prefers-color-scheme:dark){');
54
+ for (const [key, value] of Object.entries(STATUS)) {
55
+ out.push(`:root:not([data-theme="light"]) .st-${key}{--st-fg:${value.dark.fg};--st-bg:${value.dark.bg};--st-solid:${value.dark.solid}}`);
56
+ }
57
+ out.push('}');
58
+ return out.join('');
59
+ }
60
+
61
+ /** Tone rules for scores, gauges, and the verdict surface. */
62
+ function toneRules() {
63
+ const out = [];
64
+ for (const [key, value] of Object.entries(TONE)) {
65
+ out.push(`.tone-${key}{--tone:${value.light}}`);
66
+ out.push(`[data-theme="dark"] .tone-${key}{--tone:${value.dark}}`);
67
+ }
68
+ out.push('@media (prefers-color-scheme:dark){');
69
+ for (const [key, value] of Object.entries(TONE)) {
70
+ out.push(`:root:not([data-theme="light"]) .tone-${key}{--tone:${value.dark}}`);
71
+ }
72
+ out.push('}');
73
+ return out.join('');
74
+ }
75
+
76
+ const LIGHT_TOKENS = `
77
+ --bg:#f4f5f8;
78
+ --bg-mesh-a:rgba(83,68,232,.10);
79
+ --bg-mesh-b:rgba(11,165,236,.09);
80
+ --bg-mesh-c:rgba(238,70,188,.06);
81
+ --surface:rgba(255,255,255,.76);
82
+ --surface-solid:#ffffff;
83
+ --surface-raised:rgba(255,255,255,.92);
84
+ --surface-sunken:rgba(16,24,40,.03);
85
+ --border:rgba(16,24,40,.09);
86
+ --border-strong:rgba(16,24,40,.16);
87
+ --text:#0d1117;
88
+ --text-soft:#3c4658;
89
+ --text-muted:#667085;
90
+ --accent:#5344e8;
91
+ --accent-soft:rgba(83,68,232,.10);
92
+ --accent-contrast:#ffffff;
93
+ --shadow-sm:0 1px 2px rgba(16,24,40,.05);
94
+ --shadow-md:0 4px 16px -4px rgba(16,24,40,.10),0 2px 4px -2px rgba(16,24,40,.05);
95
+ --shadow-lg:0 20px 44px -12px rgba(16,24,40,.16);
96
+ --code-bg:rgba(16,24,40,.05);
97
+ --pre-bg:#10141c;
98
+ --pre-fg:#dfe4ec;
99
+ --glass-blur:saturate(180%) blur(18px);
100
+ `;
101
+
102
+ const DARK_TOKENS = `
103
+ --bg:#080b12;
104
+ --bg-mesh-a:rgba(83,68,232,.22);
105
+ --bg-mesh-b:rgba(11,165,236,.16);
106
+ --bg-mesh-c:rgba(238,70,188,.10);
107
+ --surface:rgba(20,26,38,.68);
108
+ --surface-solid:#111725;
109
+ --surface-raised:rgba(28,35,50,.82);
110
+ --surface-sunken:rgba(255,255,255,.035);
111
+ --border:rgba(255,255,255,.10);
112
+ --border-strong:rgba(255,255,255,.20);
113
+ --text:#eef1f7;
114
+ --text-soft:#b9c2d3;
115
+ --text-muted:#8792a8;
116
+ --accent:#9b8afb;
117
+ --accent-soft:rgba(155,138,251,.16);
118
+ --accent-contrast:#0b0e16;
119
+ --shadow-sm:0 1px 2px rgba(0,0,0,.4);
120
+ --shadow-md:0 8px 24px -8px rgba(0,0,0,.6);
121
+ --shadow-lg:0 24px 56px -16px rgba(0,0,0,.7);
122
+ --code-bg:rgba(255,255,255,.07);
123
+ --pre-bg:#05070c;
124
+ --pre-fg:#d5dcea;
125
+ --glass-blur:saturate(150%) blur(18px);
126
+ `;
127
+
128
+ /** The complete stylesheet. */
129
+ export function stylesheet() {
130
+ return `
131
+ *,*::before,*::after{box-sizing:border-box}
132
+ html{-webkit-text-size-adjust:100%;scroll-behavior:smooth;scroll-padding-top:1.5rem}
133
+ :root{
134
+ ${LIGHT_TOKENS}
135
+ --radius-sm:8px;--radius:14px;--radius-lg:20px;
136
+ --sp-1:.25rem;--sp-2:.5rem;--sp-3:.75rem;--sp-4:1rem;--sp-5:1.5rem;--sp-6:2rem;--sp-7:3rem;
137
+ --font:-apple-system,BlinkMacSystemFont,"Segoe UI",Inter,Roboto,"Helvetica Neue",Arial,sans-serif;
138
+ --mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace;
139
+ --ease:cubic-bezier(.22,.61,.36,1);
140
+ --sidebar-w:16rem;
141
+ color-scheme:light dark;
142
+ }
143
+ [data-theme="dark"]{${DARK_TOKENS}}
144
+ @media (prefers-color-scheme:dark){:root:not([data-theme="light"]){${DARK_TOKENS}}}
145
+ ${severityRules()}
146
+ ${statusRules()}
147
+ ${toneRules()}
148
+
149
+ body{
150
+ margin:0;background:var(--bg);color:var(--text);
151
+ font:400 15px/1.65 var(--font);letter-spacing:-.005em;
152
+ -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
153
+ text-rendering:optimizeLegibility;
154
+ }
155
+ /* A single painted mesh rather than three stacked elements: it is decorative, it
156
+ must not intercept a click, and it must vanish for print. */
157
+ body::before{
158
+ content:"";position:fixed;inset:0;z-index:-1;pointer-events:none;
159
+ background:
160
+ radial-gradient(60rem 40rem at 12% -8%,var(--bg-mesh-a),transparent 60%),
161
+ radial-gradient(50rem 36rem at 96% 4%,var(--bg-mesh-b),transparent 58%),
162
+ radial-gradient(44rem 34rem at 50% 108%,var(--bg-mesh-c),transparent 62%);
163
+ }
164
+
165
+ /* ── Layout ─────────────────────────────────────────────────────────────── */
166
+ .shell{display:grid;grid-template-columns:var(--sidebar-w) minmax(0,1fr);gap:var(--sp-6);
167
+ max-width:96rem;margin:0 auto;padding:var(--sp-5) var(--sp-5) var(--sp-7)}
168
+ .main{min-width:0}
169
+ .section{scroll-margin-top:1.5rem;margin-bottom:var(--sp-7)}
170
+ .section:last-child{margin-bottom:var(--sp-5)}
171
+
172
+ /* ── Sidebar ────────────────────────────────────────────────────────────── */
173
+ .sidebar{position:sticky;top:var(--sp-5);align-self:start;max-height:calc(100vh - 3rem);
174
+ display:flex;flex-direction:column;gap:var(--sp-4);overflow:auto;padding-bottom:var(--sp-4)}
175
+ .sidebar-brand{display:flex;align-items:center;gap:.625rem;padding:0 .375rem}
176
+ .sidebar-mark{width:1.75rem;height:1.75rem;border-radius:9px;flex:none;
177
+ background:linear-gradient(135deg,var(--accent),#0ba5ec);
178
+ display:grid;place-items:center;color:#fff;font-weight:800;font-size:.75rem;letter-spacing:0}
179
+ .sidebar-name{font-weight:650;font-size:.875rem;letter-spacing:-.01em}
180
+ .sidebar-sub{font-size:.6875rem;color:var(--text-muted)}
181
+ nav.toc{display:flex;flex-direction:column;gap:1px}
182
+ nav.toc a{display:flex;align-items:center;justify-content:space-between;gap:.5rem;
183
+ padding:.4375rem .625rem;border-radius:var(--radius-sm);color:var(--text-soft);
184
+ text-decoration:none;font-size:.8125rem;font-weight:500;
185
+ transition:background .16s var(--ease),color .16s var(--ease)}
186
+ nav.toc a:hover{background:var(--surface-sunken);color:var(--text)}
187
+ nav.toc a.active{background:var(--accent-soft);color:var(--accent);font-weight:600}
188
+ nav.toc .n{font-variant-numeric:tabular-nums;font-size:.6875rem;color:var(--text-muted);
189
+ background:var(--surface-sunken);border-radius:20px;padding:.0625rem .375rem;min-width:1.375rem;text-align:center}
190
+ nav.toc a.active .n{background:transparent;color:inherit}
191
+
192
+ /* ── Glass surfaces ─────────────────────────────────────────────────────── */
193
+ .card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
194
+ box-shadow:var(--shadow-sm);backdrop-filter:var(--glass-blur);-webkit-backdrop-filter:var(--glass-blur)}
195
+ .card+.card{margin-top:var(--sp-3)}
196
+ .card-body{padding:var(--sp-5)}
197
+ .card-tight{padding:var(--sp-4)}
198
+
199
+ /* ── Masthead ───────────────────────────────────────────────────────────── */
200
+ .masthead{position:relative;overflow:hidden;border-radius:var(--radius-lg);
201
+ border:1px solid var(--border);background:var(--surface-raised);
202
+ backdrop-filter:var(--glass-blur);-webkit-backdrop-filter:var(--glass-blur);
203
+ box-shadow:var(--shadow-md);padding:var(--sp-6);margin-bottom:var(--sp-5)}
204
+ .masthead::after{content:"";position:absolute;inset:0;pointer-events:none;
205
+ background:linear-gradient(120deg,var(--accent-soft),transparent 42%)}
206
+ .masthead>*{position:relative;z-index:1}
207
+ .eyebrow{display:inline-flex;align-items:center;gap:.5rem;font-size:.6875rem;font-weight:700;
208
+ letter-spacing:.1em;text-transform:uppercase;color:var(--accent)}
209
+ .eyebrow::before{content:"";width:.4375rem;height:.4375rem;border-radius:50%;background:currentColor;
210
+ box-shadow:0 0 0 3px var(--accent-soft)}
211
+ h1{margin:.75rem 0 .5rem;font-size:clamp(1.75rem,3.4vw,2.75rem);line-height:1.08;
212
+ letter-spacing:-.033em;font-weight:750}
213
+ .subject{color:var(--text-soft);font-size:1.0625rem;margin:0 0 var(--sp-4);max-width:56ch}
214
+ .subject a{color:var(--accent);text-decoration:none;border-bottom:1px solid var(--accent-soft)}
215
+ .subject a:hover{border-bottom-color:currentColor}
216
+ .factbar{display:flex;flex-wrap:wrap;gap:var(--sp-5) var(--sp-6);
217
+ padding-top:var(--sp-4);border-top:1px solid var(--border)}
218
+ .fact{min-width:7rem}
219
+ .fact dt{font-size:.625rem;font-weight:700;letter-spacing:.09em;text-transform:uppercase;
220
+ color:var(--text-muted);margin:0 0 .1875rem}
221
+ .fact dd{margin:0;font-size:.9375rem;font-weight:600;letter-spacing:-.01em}
222
+
223
+ /* ── Verdict ────────────────────────────────────────────────────────────── */
224
+ .verdict{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-4);
225
+ margin:var(--sp-5) 0 0;padding:var(--sp-4) var(--sp-5);border-radius:var(--radius);
226
+ border:1px solid color-mix(in srgb,var(--tone) 38%,transparent);
227
+ background:color-mix(in srgb,var(--tone) 10%,transparent)}
228
+ .verdict-label{display:flex;align-items:center;gap:.5rem;font-size:1.0625rem;font-weight:750;
229
+ letter-spacing:-.015em;color:var(--tone)}
230
+ .verdict-dot{width:.625rem;height:.625rem;border-radius:50%;background:var(--tone);flex:none;
231
+ box-shadow:0 0 0 4px color-mix(in srgb,var(--tone) 22%,transparent)}
232
+ .verdict-blurb{color:var(--text-soft);font-size:.9375rem;flex:1 1 16rem;min-width:0}
233
+ .verdict-confidence{font-size:.75rem;color:var(--text-muted);font-variant-numeric:tabular-nums}
234
+
235
+ /* ── KPI grid ───────────────────────────────────────────────────────────── */
236
+ .kpis{display:grid;gap:var(--sp-3);grid-template-columns:repeat(auto-fill,minmax(9.5rem,1fr))}
237
+ .kpi{position:relative;overflow:hidden;padding:var(--sp-4);border-radius:var(--radius);
238
+ border:1px solid var(--border);background:var(--surface);
239
+ backdrop-filter:var(--glass-blur);-webkit-backdrop-filter:var(--glass-blur);
240
+ transition:transform .18s var(--ease),box-shadow .18s var(--ease),border-color .18s var(--ease)}
241
+ .kpi:hover{transform:translateY(-2px);box-shadow:var(--shadow-md);border-color:var(--border-strong)}
242
+ .kpi::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;
243
+ background:var(--kpi-accent,var(--border-strong))}
244
+ .kpi.sev .kpi-n{color:var(--sev-solid)}
245
+ .kpi.sev{--kpi-accent:var(--sev-solid)}
246
+ .kpi.st .kpi-n{color:var(--st-solid)}
247
+ .kpi.st{--kpi-accent:var(--st-solid)}
248
+ .kpi-n{display:block;font-size:1.875rem;font-weight:750;line-height:1.05;letter-spacing:-.03em;
249
+ font-variant-numeric:tabular-nums}
250
+ .kpi-l{display:block;margin-top:.25rem;font-size:.6875rem;font-weight:650;letter-spacing:.07em;
251
+ text-transform:uppercase;color:var(--text-muted)}
252
+ .kpi-sub{display:block;margin-top:.1875rem;font-size:.75rem;color:var(--text-muted)}
253
+ .kpi-zero .kpi-n{color:var(--text-muted);opacity:.55}
254
+
255
+ /* ── Score gauges ───────────────────────────────────────────────────────── */
256
+ .scores{display:grid;gap:var(--sp-3);grid-template-columns:repeat(auto-fill,minmax(10rem,1fr))}
257
+ .score{display:flex;align-items:center;gap:var(--sp-3);padding:var(--sp-4);
258
+ border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);
259
+ backdrop-filter:var(--glass-blur);-webkit-backdrop-filter:var(--glass-blur)}
260
+ .score svg{flex:none}
261
+ .score-l{font-size:.8125rem;font-weight:600}
262
+ .score-b{font-size:.6875rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em}
263
+
264
+ /* ── Headings ───────────────────────────────────────────────────────────── */
265
+ h2{margin:0 0 var(--sp-4);font-size:1.25rem;font-weight:700;letter-spacing:-.022em;
266
+ display:flex;align-items:baseline;gap:.625rem;flex-wrap:wrap}
267
+ h2 .count{font-size:.75rem;font-weight:600;color:var(--text-muted);
268
+ background:var(--surface-sunken);border:1px solid var(--border);
269
+ border-radius:20px;padding:.125rem .5rem;letter-spacing:0}
270
+ h3{margin:0;font-size:1rem;font-weight:650;letter-spacing:-.014em;line-height:1.4}
271
+ h4{margin:0 0 var(--sp-2);font-size:.6875rem;font-weight:700;letter-spacing:.09em;
272
+ text-transform:uppercase;color:var(--text-muted)}
273
+ .section-note{margin:calc(var(--sp-4) * -1 + .25rem) 0 var(--sp-4);color:var(--text-muted);
274
+ font-size:.875rem;max-width:70ch}
275
+ p{margin:0 0 var(--sp-3);max-width:78ch}
276
+ p:last-child{margin-bottom:0}
277
+ a{color:var(--accent)}
278
+
279
+ /* ── Badges & chips ─────────────────────────────────────────────────────── */
280
+ .badge{display:inline-flex;align-items:center;gap:.3125rem;padding:.1875rem .5rem;
281
+ border-radius:6px;font-size:.6875rem;font-weight:750;letter-spacing:.045em;
282
+ text-transform:uppercase;white-space:nowrap;
283
+ color:var(--sev-fg);background:var(--sev-bg);border:1px solid var(--sev-border)}
284
+ .badge::before{content:"";width:.375rem;height:.375rem;border-radius:50%;background:var(--sev-solid)}
285
+ .pill{display:inline-flex;align-items:center;gap:.3125rem;padding:.1875rem .5rem;border-radius:20px;
286
+ font-size:.6875rem;font-weight:650;white-space:nowrap;
287
+ color:var(--st-fg);background:var(--st-bg);border:1px solid transparent}
288
+ .chip{display:inline-flex;align-items:center;gap:.3125rem;padding:.1875rem .5rem;
289
+ border-radius:6px;background:var(--surface-sunken);border:1px solid var(--border);
290
+ color:var(--text-muted);font-size:.6875rem;font-weight:600;white-space:nowrap}
291
+
292
+ /* ── Toolbar ────────────────────────────────────────────────────────────── */
293
+ .toolbar{position:sticky;top:0;z-index:20;display:flex;flex-wrap:wrap;gap:var(--sp-2);
294
+ align-items:center;margin-bottom:var(--sp-4);padding:var(--sp-3);
295
+ border:1px solid var(--border);border-radius:var(--radius);background:var(--surface-raised);
296
+ backdrop-filter:var(--glass-blur);-webkit-backdrop-filter:var(--glass-blur);box-shadow:var(--shadow-sm)}
297
+ .search{flex:1 1 14rem;min-width:0;display:flex;align-items:center;gap:.5rem;
298
+ padding:.4375rem .625rem;border-radius:var(--radius-sm);
299
+ border:1px solid var(--border);background:var(--surface-sunken)}
300
+ .search svg{flex:none;color:var(--text-muted)}
301
+ .search input{flex:1;min-width:0;border:0;background:transparent;color:inherit;
302
+ font:inherit;font-size:.875rem;outline:none}
303
+ .search input::placeholder{color:var(--text-muted)}
304
+ .filters{display:flex;flex-wrap:wrap;gap:.25rem}
305
+ .filter{display:inline-flex;align-items:center;gap:.3125rem;padding:.3125rem .5625rem;
306
+ border-radius:20px;border:1px solid var(--border);background:transparent;color:var(--text-soft);
307
+ font:inherit;font-size:.75rem;font-weight:600;cursor:pointer;
308
+ transition:background .14s var(--ease),border-color .14s var(--ease),color .14s var(--ease)}
309
+ .filter:hover{border-color:var(--border-strong);background:var(--surface-sunken)}
310
+ .filter[aria-pressed="true"]{background:var(--accent);border-color:var(--accent);color:var(--accent-contrast)}
311
+ .filter .dot{width:.4375rem;height:.4375rem;border-radius:50%;background:var(--sev-solid,var(--text-muted))}
312
+ .filter[aria-pressed="true"] .dot{background:currentColor}
313
+ .iconbtn{display:inline-grid;place-items:center;width:2rem;height:2rem;flex:none;
314
+ border-radius:var(--radius-sm);border:1px solid var(--border);background:transparent;
315
+ color:var(--text-soft);cursor:pointer;transition:background .14s var(--ease),color .14s var(--ease)}
316
+ .iconbtn:hover{background:var(--surface-sunken);color:var(--text)}
317
+ .match-count{font-size:.75rem;color:var(--text-muted);font-variant-numeric:tabular-nums;
318
+ margin-left:auto;padding-left:.5rem}
319
+
320
+ /* ── Findings ───────────────────────────────────────────────────────────── */
321
+ .finding{border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);
322
+ backdrop-filter:var(--glass-blur);-webkit-backdrop-filter:var(--glass-blur);
323
+ margin-bottom:var(--sp-3);overflow:hidden;scroll-margin-top:5rem;
324
+ transition:border-color .18s var(--ease),box-shadow .18s var(--ease)}
325
+ .finding:hover{border-color:var(--border-strong)}
326
+ .finding[open]{box-shadow:var(--shadow-md);border-color:var(--border-strong)}
327
+ .finding[hidden]{display:none}
328
+ .finding>summary{display:flex;align-items:flex-start;gap:var(--sp-3);padding:var(--sp-4);
329
+ cursor:pointer;list-style:none;user-select:none}
330
+ .finding>summary::-webkit-details-marker{display:none}
331
+ .finding>summary:focus-visible{outline:2px solid var(--accent);outline-offset:-2px;border-radius:var(--radius)}
332
+ .finding-grow{flex:1;min-width:0}
333
+ .finding-title{display:block;font-size:1rem;font-weight:650;letter-spacing:-.014em;line-height:1.4}
334
+ .finding-meta{display:flex;flex-wrap:wrap;align-items:center;gap:.375rem;margin-top:.4375rem}
335
+ .finding-id{font-family:var(--mono);font-size:.6875rem;color:var(--text-muted);letter-spacing:0}
336
+ .caret{flex:none;margin-top:.1875rem;color:var(--text-muted);transition:transform .2s var(--ease)}
337
+ .finding[open] .caret{transform:rotate(90deg)}
338
+ .finding-body{padding:0 var(--sp-4) var(--sp-4);border-top:1px solid var(--border)}
339
+ .finding-body>*:first-child{margin-top:var(--sp-4)}
340
+
341
+ .behaviour{margin:0;display:grid;grid-template-columns:minmax(7.5rem,auto) minmax(0,1fr);
342
+ gap:var(--sp-3) var(--sp-5)}
343
+ .behaviour dt{font-size:.625rem;font-weight:750;letter-spacing:.09em;text-transform:uppercase;
344
+ color:var(--text-muted);padding-top:.1875rem}
345
+ .behaviour dd{margin:0;min-width:0}
346
+ .rail{border-left:2px solid var(--rail,var(--border-strong));padding-left:var(--sp-3)}
347
+ .rail-now{--rail:var(--tone-bad,#f04438)}
348
+ .rail-want{--rail:#17b26a}
349
+ .rail-fix{--rail:var(--accent)}
350
+ .rail-impact{--rail:#f79009}
351
+ .rail-cause{--rail:#0ba5ec}
352
+ [data-theme="dark"] .rail-now{--rail:#f97066}
353
+ [data-theme="dark"] .rail-want{--rail:#47cd89}
354
+ @media (prefers-color-scheme:dark){
355
+ :root:not([data-theme="light"]) .rail-now{--rail:#f97066}
356
+ :root:not([data-theme="light"]) .rail-want{--rail:#47cd89}
357
+ }
358
+ ol.steps,ol.chain{margin:0;padding-left:1.25rem}
359
+ ol.steps li,ol.chain li{margin:.1875rem 0}
360
+ ol.chain li::marker{color:var(--text-muted);font-variant-numeric:tabular-nums}
361
+ ul.clean{margin:0;padding-left:1.125rem}
362
+ ul.clean li{margin:.25rem 0}
363
+ .subgrid{display:grid;gap:var(--sp-3);grid-template-columns:repeat(auto-fit,minmax(13rem,1fr));
364
+ margin-top:var(--sp-4)}
365
+ .panel{border:1px solid var(--border);border-radius:var(--radius-sm);padding:var(--sp-3);
366
+ background:var(--surface-sunken)}
367
+ .panel p{font-size:.875rem}
368
+
369
+ /* ── Tables ─────────────────────────────────────────────────────────────── */
370
+ .table-wrap{overflow-x:auto;border:1px solid var(--border);border-radius:var(--radius);
371
+ background:var(--surface);backdrop-filter:var(--glass-blur);-webkit-backdrop-filter:var(--glass-blur)}
372
+ table{border-collapse:collapse;width:100%;font-size:.8125rem;min-width:26rem}
373
+ th,td{text-align:left;padding:.625rem .75rem;border-bottom:1px solid var(--border);vertical-align:top}
374
+ th{font-size:.625rem;text-transform:uppercase;letter-spacing:.08em;color:var(--text-muted);
375
+ font-weight:750;background:var(--surface-sunken);position:sticky;top:0;white-space:nowrap}
376
+ tbody tr:last-child td{border-bottom:0}
377
+ tbody tr{transition:background .12s var(--ease)}
378
+ tbody tr:hover{background:var(--surface-sunken)}
379
+ td.num{text-align:right;font-variant-numeric:tabular-nums}
380
+ tr[hidden]{display:none}
381
+ .bar-cell{min-width:8rem}
382
+ .bar{height:.375rem;border-radius:3px;background:var(--surface-sunken);overflow:hidden}
383
+ .bar>span{display:block;height:100%;border-radius:3px;background:var(--bar,var(--accent))}
384
+
385
+ code{font-family:var(--mono);font-size:.8125em;background:var(--code-bg);
386
+ padding:.0625rem .3125rem;border-radius:5px;word-break:break-word}
387
+ pre{margin:0;background:var(--pre-bg);color:var(--pre-fg);padding:var(--sp-4);
388
+ border-radius:var(--radius-sm);overflow-x:auto;font:.75rem/1.6 var(--mono);
389
+ border:1px solid var(--border)}
390
+ pre code{background:none;padding:0;font-size:inherit;color:inherit}
391
+
392
+ /* ── Charts ─────────────────────────────────────────────────────────────── */
393
+ .chart-grid{display:grid;gap:var(--sp-3);grid-template-columns:repeat(auto-fit,minmax(18rem,1fr))}
394
+ .chart{padding:var(--sp-4);border:1px solid var(--border);border-radius:var(--radius);
395
+ background:var(--surface);backdrop-filter:var(--glass-blur);-webkit-backdrop-filter:var(--glass-blur)}
396
+ .chart h4{margin-bottom:var(--sp-3)}
397
+ .chart svg{display:block;width:100%;height:auto;overflow:visible}
398
+ .legend{display:flex;flex-wrap:wrap;gap:.5rem .875rem;margin-top:var(--sp-3);font-size:.75rem}
399
+ .legend span{display:inline-flex;align-items:center;gap:.375rem;color:var(--text-soft)}
400
+ .legend i{width:.625rem;height:.625rem;border-radius:3px;flex:none}
401
+ .legend b{font-weight:650;font-variant-numeric:tabular-nums;color:var(--text)}
402
+ .axis{font-size:10px;fill:var(--text-muted)}
403
+ .gridline{stroke:var(--border);stroke-width:1}
404
+
405
+ /* ── Timeline ───────────────────────────────────────────────────────────── */
406
+ .timeline{position:relative;margin:0;padding:0 0 0 1.375rem;list-style:none}
407
+ .timeline::before{content:"";position:absolute;left:.3125rem;top:.375rem;bottom:.375rem;width:2px;
408
+ background:linear-gradient(180deg,var(--accent),transparent)}
409
+ .timeline li{position:relative;padding:0 0 var(--sp-4)}
410
+ .timeline li:last-child{padding-bottom:0}
411
+ .timeline li::before{content:"";position:absolute;left:-1.375rem;top:.4375rem;width:.75rem;height:.75rem;
412
+ border-radius:50%;background:var(--surface-solid);border:2px solid var(--tl,var(--accent))}
413
+ .tl-ok{--tl:#17b26a}.tl-warned{--tl:#f79009}.tl-failed{--tl:#f04438}.tl-skipped{--tl:#98a2b3}
414
+ .tl-head{display:flex;flex-wrap:wrap;align-items:baseline;gap:.5rem}
415
+ .tl-label{font-weight:600;font-size:.9375rem}
416
+ .tl-dur{font-size:.75rem;color:var(--text-muted);font-variant-numeric:tabular-nums}
417
+ .tl-detail{font-size:.8125rem;color:var(--text-muted);margin-top:.125rem}
418
+
419
+ /* ── Evidence ───────────────────────────────────────────────────────────── */
420
+ .evidence{display:grid;gap:var(--sp-3);grid-template-columns:repeat(auto-fill,minmax(15rem,1fr));
421
+ margin-top:var(--sp-4)}
422
+ figure.shot{margin:0;border:1px solid var(--border);border-radius:var(--radius-sm);
423
+ overflow:hidden;background:var(--surface-sunken)}
424
+ figure.shot img,figure.shot video{display:block;width:100%;height:auto;background:var(--surface-sunken)}
425
+ figure.shot img{cursor:zoom-in;transition:opacity .16s var(--ease)}
426
+ figure.shot img:hover{opacity:.92}
427
+ figcaption{padding:.5rem .625rem;font-size:.6875rem;color:var(--text-muted);
428
+ border-top:1px solid var(--border);display:flex;flex-wrap:wrap;gap:.375rem;align-items:center}
429
+ figcaption .name{font-weight:650;color:var(--text-soft)}
430
+ figcaption code{font-size:.6875rem}
431
+ .shot-actions{margin-left:auto;display:flex;gap:.375rem}
432
+ .shot-actions a{font-size:.6875rem;font-weight:650;text-decoration:none;color:var(--accent)}
433
+ .shot-actions a:hover{text-decoration:underline}
434
+ /* The whole reason this module exists: a file that is not there is stated, never
435
+ left to a browser's broken-image glyph. */
436
+ .artifact-missing{display:flex;align-items:flex-start;gap:.625rem;padding:var(--sp-4);
437
+ border:1px dashed var(--border-strong);border-radius:var(--radius-sm);
438
+ background:var(--surface-sunken);color:var(--text-muted);font-size:.8125rem}
439
+ .artifact-missing svg{flex:none;margin-top:.125rem;opacity:.7}
440
+ .artifact-missing .why{display:block;font-weight:650;color:var(--text-soft);margin-bottom:.125rem}
441
+ .artifact-missing code{font-size:.6875rem}
442
+ .compare{display:grid;gap:var(--sp-2);grid-template-columns:1fr 1fr;align-items:start}
443
+ .compare figcaption .name::before{content:attr(data-side) " · ";color:var(--accent)}
444
+
445
+ /* ── Lightbox ───────────────────────────────────────────────────────────── */
446
+ .lightbox{position:fixed;inset:0;z-index:100;display:none;place-items:center;
447
+ padding:var(--sp-5);background:rgba(6,9,14,.86);backdrop-filter:blur(8px)}
448
+ .lightbox[open],.lightbox.on{display:grid}
449
+ .lightbox img{max-width:100%;max-height:86vh;border-radius:var(--radius);
450
+ box-shadow:var(--shadow-lg);cursor:zoom-out;transition:transform .2s var(--ease);transform-origin:center}
451
+ .lightbox.zoomed img{cursor:move;transform:scale(2)}
452
+ .lightbox-bar{position:absolute;top:var(--sp-4);left:var(--sp-5);right:var(--sp-5);
453
+ display:flex;align-items:center;gap:var(--sp-3);color:#e8ecf4;font-size:.8125rem}
454
+ .lightbox-bar .grow{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
455
+ .lightbox-bar button,.lightbox-bar a{background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.2);
456
+ color:#fff;border-radius:var(--radius-sm);padding:.3125rem .625rem;font:inherit;font-size:.75rem;
457
+ font-weight:600;cursor:pointer;text-decoration:none}
458
+ .lightbox-bar button:hover,.lightbox-bar a:hover{background:rgba(255,255,255,.2)}
459
+
460
+ /* ── Misc ───────────────────────────────────────────────────────────────── */
461
+ .empty{color:var(--text-muted);font-style:italic}
462
+ .muted{color:var(--text-muted)}
463
+ .tabular{font-variant-numeric:tabular-nums}
464
+ .no-results{display:none;padding:var(--sp-6);text-align:center;color:var(--text-muted)}
465
+ .no-results.on{display:block}
466
+ .skip{position:absolute;left:-9999px;top:0;padding:.5rem .875rem;background:var(--accent);
467
+ color:var(--accent-contrast);border-radius:0 0 var(--radius-sm) 0;z-index:200;font-weight:650}
468
+ .skip:focus{left:0}
469
+ :focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:4px}
470
+ .qa-pack-attribution{margin-top:var(--sp-6)}
471
+
472
+ /* ── Responsive ─────────────────────────────────────────────────────────── */
473
+ @media (max-width:72rem){
474
+ .shell{grid-template-columns:1fr;gap:var(--sp-4);padding:var(--sp-4)}
475
+ .sidebar{position:static;max-height:none;flex-direction:row;flex-wrap:wrap;align-items:center;
476
+ gap:var(--sp-3);padding:var(--sp-3);border:1px solid var(--border);
477
+ border-radius:var(--radius);background:var(--surface)}
478
+ nav.toc{flex-direction:row;flex-wrap:wrap;gap:.25rem}
479
+ nav.toc .n{display:none}
480
+ .sidebar-sub{display:none}
481
+ }
482
+ @media (max-width:40rem){
483
+ .shell{padding:var(--sp-3)}
484
+ .masthead{padding:var(--sp-4);border-radius:var(--radius)}
485
+ .card-body{padding:var(--sp-4)}
486
+ .behaviour{grid-template-columns:1fr;gap:.375rem}
487
+ .behaviour dt{padding-top:.625rem}
488
+ .kpis{grid-template-columns:repeat(auto-fill,minmax(7.5rem,1fr))}
489
+ .evidence,.compare{grid-template-columns:1fr}
490
+ .factbar{gap:var(--sp-4)}
491
+ .toolbar{position:static}
492
+ }
493
+
494
+ /* ── Motion ─────────────────────────────────────────────────────────────── */
495
+ @media (prefers-reduced-motion:no-preference){
496
+ .reveal{opacity:0;transform:translateY(8px);animation:rise .5s var(--ease) forwards}
497
+ @keyframes rise{to{opacity:1;transform:none}}
498
+ }
499
+ @media (prefers-reduced-motion:reduce){
500
+ *,*::before,*::after{animation-duration:.001ms !important;animation-iteration-count:1 !important;
501
+ transition-duration:.001ms !important;scroll-behavior:auto !important}
502
+ }
503
+
504
+ /* ── Print ──────────────────────────────────────────────────────────────── */
505
+ @media print{
506
+ @page{margin:14mm}
507
+ html{scroll-behavior:auto}
508
+ body{background:#fff;color:#000;font-size:10.5pt}
509
+ body::before,.sidebar,.toolbar,.lightbox,.skip,.shot-actions,.caret{display:none !important}
510
+ .shell{display:block;max-width:none;padding:0}
511
+ .card,.finding,.chart,.kpi,.masthead,.score,.table-wrap{
512
+ background:#fff !important;box-shadow:none !important;backdrop-filter:none !important;
513
+ -webkit-backdrop-filter:none !important;border-color:#d0d5dd !important;break-inside:avoid}
514
+ .masthead::after{display:none}
515
+ /* Collapsed findings print as headings with no content, which is how a printed
516
+ report loses the half of itself that matters. */
517
+ .finding>.finding-body{display:block !important}
518
+ .finding{break-inside:avoid}
519
+ .section{break-before:auto;margin-bottom:1.25rem}
520
+ h1{font-size:22pt}h2{font-size:13pt;break-after:avoid}h3{font-size:11pt;break-after:avoid}
521
+ .reveal{opacity:1 !important;transform:none !important;animation:none !important}
522
+ figure.shot img{max-height:9cm;object-fit:contain}
523
+ /* A printed link that only says "here" is a dead end. */
524
+ .subject a[href]::after{content:" (" attr(href) ")";font-size:8pt;color:#475467;word-break:break-all}
525
+ thead{display:table-header-group}
526
+ tr,img,figure{break-inside:avoid}
527
+ }
528
+ `.trim();
529
+ }