penthera 1.5.0 → 1.6.1

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 (104) hide show
  1. package/CHANGELOG.md +179 -0
  2. package/README.md +128 -663
  3. package/bin/scan.js +19 -0
  4. package/docs/commands/README.md +23 -0
  5. package/docs/commands/action.md +21 -0
  6. package/docs/commands/agents.md +38 -0
  7. package/docs/commands/deps.md +115 -0
  8. package/docs/commands/fix.md +53 -0
  9. package/docs/commands/hook.md +69 -0
  10. package/docs/commands/install.md +53 -0
  11. package/docs/commands/scan.md +100 -0
  12. package/docs/commands/skill.md +58 -0
  13. package/docs/compare/README.md +25 -0
  14. package/docs/compare/github.md +60 -0
  15. package/docs/compare/semgrep.md +54 -0
  16. package/docs/compare/snyk.md +62 -0
  17. package/docs/compare/socket.md +78 -0
  18. package/docs/compare/zap.md +59 -0
  19. package/docs/dashboard.md +244 -0
  20. package/docs/evidence.md +8 -4
  21. package/docs/examples/github-actions-deps.yml +4 -4
  22. package/docs/examples/github-actions-scan.yml +4 -4
  23. package/docs/index.md +9 -0
  24. package/docs/internal-audit.md +83 -0
  25. package/docs/marketing.md +151 -0
  26. package/docs/plan.md +4 -3
  27. package/docs/privacy.md +38 -0
  28. package/docs/references.bib +10 -0
  29. package/docs/references.md +1 -0
  30. package/docs/research/socket-positioning-notes.md +183 -0
  31. package/docs/use-cases/README.md +14 -0
  32. package/docs/use-cases/agencies.md +71 -0
  33. package/docs/use-cases/ai-coding-teams.md +81 -0
  34. package/docs/use-cases/eu-sme-compliance.md +68 -0
  35. package/docs/use-cases/open-source-maintainers.md +75 -0
  36. package/docs/use-cases/solo-founder.md +65 -0
  37. package/lib/blackbox/email-dns.js +5 -0
  38. package/lib/compliance/questionnaire.js +4 -2
  39. package/lib/deps/capabilities.js +49 -12
  40. package/lib/deps/index.js +290 -30
  41. package/lib/deps/lockfile.js +25 -2
  42. package/lib/deps/popular-pypi.json +1 -0
  43. package/lib/deps/python/capabilities.js +447 -0
  44. package/lib/deps/python/index.js +234 -0
  45. package/lib/deps/python/lockfiles.js +359 -0
  46. package/lib/deps/python/source.js +396 -0
  47. package/lib/deps/python/toml.js +219 -0
  48. package/lib/deps/report.js +37 -6
  49. package/lib/deps/source.js +149 -24
  50. package/lib/deps/typosquat.js +24 -13
  51. package/lib/fix/apply.js +83 -19
  52. package/lib/hook/index.js +41 -8
  53. package/lib/install/index.js +59 -18
  54. package/lib/install/policy.js +17 -5
  55. package/lib/memory/repo-suppress.js +11 -3
  56. package/lib/references.json +77 -0
  57. package/lib/remediation/index.js +9 -0
  58. package/lib/retirejs.js +69 -17
  59. package/lib/rules/regex-guard.js +368 -0
  60. package/lib/templates.js +13 -6
  61. package/lib/tls.js +7 -0
  62. package/lib/whitebox/frameworks.js +8 -5
  63. package/lib/whitebox/secrets.js +20 -7
  64. package/package.json +7 -3
  65. package/skills/penthera/SKILL.md +23 -5
  66. package/src/cli/deps.js +60 -5
  67. package/src/cli/evidence.js +14 -4
  68. package/src/cli/install.js +14 -5
  69. package/src/cli/report-outputs.js +25 -2
  70. package/src/cli/ui.js +89 -0
  71. package/src/cli.js +19 -7
  72. package/src/report/audience.js +10 -10
  73. package/src/report/evidence.js +33 -14
  74. package/src/report/footer.js +11 -0
  75. package/src/report/markdown.js +6 -3
  76. package/src/report/md-escape.js +16 -0
  77. package/src/reporter.js +1 -0
  78. package/src/scan-url.js +5 -1
  79. package/src/ui/api/data.js +309 -0
  80. package/src/ui/api/demo.js +464 -0
  81. package/src/ui/api/routes.js +134 -0
  82. package/src/ui/api/runs.js +243 -0
  83. package/src/ui/api/summarize.js +451 -0
  84. package/src/ui/security.js +153 -0
  85. package/src/ui/server.js +186 -0
  86. package/src/ui/static/app.css +723 -0
  87. package/src/ui/static/favicon.svg +1 -0
  88. package/src/ui/static/index.html +57 -0
  89. package/src/ui/static/js/api.js +29 -0
  90. package/src/ui/static/js/app.js +138 -0
  91. package/src/ui/static/js/charts.js +207 -0
  92. package/src/ui/static/js/copy.js +79 -0
  93. package/src/ui/static/js/dom.js +120 -0
  94. package/src/ui/static/js/format.js +42 -0
  95. package/src/ui/static/js/theme.js +10 -0
  96. package/src/ui/static/js/typer.js +99 -0
  97. package/src/ui/static/js/views/ai.js +68 -0
  98. package/src/ui/static/js/views/deps.js +96 -0
  99. package/src/ui/static/js/views/evidence.js +115 -0
  100. package/src/ui/static/js/views/findings.js +165 -0
  101. package/src/ui/static/js/views/history.js +92 -0
  102. package/src/ui/static/js/views/overview.js +184 -0
  103. package/src/ui/static/js/views/run.js +349 -0
  104. package/src/ui/static/js/views/settings.js +67 -0
@@ -0,0 +1,723 @@
1
+ /*
2
+ * penthera ui :: a 1-bit desktop with a terminal inside.
3
+ *
4
+ * Register: System 7 era Mac (menu bar, pinstriped title bars, dithered
5
+ * desktop, rounded push buttons, alert dialogs), worn the way a calm modern
6
+ * product UI wears it: pinstripes only on real windows and dialogs, hairline
7
+ * group boxes, one stat strip, figure captions that say where a number came
8
+ * from. The CLI's voice and its #0000ed wordmark run through all of it.
9
+ * Black, white and patterns do the work; #0000ed is the one accent; green is
10
+ * kept for the cursor, the prompt and "OK".
11
+ *
12
+ * One theming system: [data-theme] on <html>, set before paint by
13
+ * /js/theme.js (localStorage, else prefers-color-scheme). There is no
14
+ * prefers-color-scheme block here, so the two can never drift.
15
+ *
16
+ * Contrast, measured (WCAG 2.x, on the ground):
17
+ * dark #000: text-1 21.0 · text-2 (ink .72) 10.3 · text-3 (ink .52) 5.6 · accent-ink #7a7aff 6.06
18
+ * green #55ff00 15.7 · white on #0000ed 9.45 · focus #7a7aff 6.06 (>=3 non-text)
19
+ * light #fdfdfa: text-1 19.4 · text-2 (ink .72) 8.0 · text-3 (ink .6) 5.24 · #0000ed 9.27
20
+ * green #137300 5.92 · white on #0000ed 9.27
21
+ * #0000ed itself is 2.22:1 on black, so in the dark theme it is only ever a
22
+ * fill (with white on it), a frame or a glow: never text, never a 1px line.
23
+ * The canonical ink ladder's 0.5 step measures 3.6:1 on the light ground, so
24
+ * light text-3 is 0.6, not 0.5. Same ladder, measured per polarity.
25
+ */
26
+
27
+ :root,
28
+ :root[data-theme="dark"] {
29
+ color-scheme: dark;
30
+ --ground: #000000;
31
+ --ink-rgb: 255, 255, 255;
32
+ --text-1: rgb(var(--ink-rgb));
33
+ --text-2: rgba(var(--ink-rgb), 0.72);
34
+ --text-3: rgba(var(--ink-rgb), 0.52);
35
+ --text-4: rgba(var(--ink-rgb), 0.3); /* ornament only, never text you must read */
36
+ --rule: rgba(var(--ink-rgb), 0.3);
37
+ --hair: rgba(var(--ink-rgb), 0.14);
38
+ --frame: rgb(var(--ink-rgb));
39
+ --wash: rgba(var(--ink-rgb), 0.06);
40
+ --accent: #0000ed;
41
+ --accent-hover: #2a2aff;
42
+ --accent-ink: #7a7aff; /* same hue, 6.06:1 on black */
43
+ --on-accent: #ffffff;
44
+ --focus: #7a7aff;
45
+ --series: #7a7aff;
46
+ --ok: #55ff00;
47
+ --dot: rgba(var(--ink-rgb), 0.26); /* desktop pattern */
48
+ --glow: 0 0 6px rgba(85, 255, 0, 0.6);
49
+ --logo-glow: 0 0 10px rgba(0, 0, 237, 0.95), 0 0 26px rgba(0, 0, 237, 0.6);
50
+ --scan: rgba(255, 255, 255, 0.035);
51
+ }
52
+
53
+ :root[data-theme="light"] {
54
+ color-scheme: light;
55
+ --ground: #fdfdfa;
56
+ --ink-rgb: 10, 10, 10;
57
+ --text-1: rgb(var(--ink-rgb));
58
+ --text-2: rgba(var(--ink-rgb), 0.72);
59
+ --text-3: rgba(var(--ink-rgb), 0.6);
60
+ --text-4: rgba(var(--ink-rgb), 0.3);
61
+ --rule: rgba(var(--ink-rgb), 0.36);
62
+ --hair: rgba(var(--ink-rgb), 0.14);
63
+ --frame: rgb(var(--ink-rgb));
64
+ --wash: rgba(var(--ink-rgb), 0.045);
65
+ --accent: #0000ed;
66
+ --accent-hover: #2a2aff;
67
+ --accent-ink: #0000ed;
68
+ --on-accent: #ffffff;
69
+ --focus: #0000ed;
70
+ --series: #0000ed;
71
+ --ok: #137300;
72
+ --dot: rgba(var(--ink-rgb), 0.42);
73
+ --glow: none; /* no glow on paper */
74
+ --logo-glow: none;
75
+ --scan: transparent; /* no scanlines on paper */
76
+ }
77
+
78
+ :root {
79
+ --mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, "Liberation Mono", monospace;
80
+ --fs: 13px;
81
+ --gap: 16px;
82
+ --radius-btn: 6px; /* the one radius: classic push buttons. Windows stay square */
83
+ --target: 28px; /* WCAG 2.5.8 floor is 24; coarse pointers get 44 below */
84
+ }
85
+
86
+ * { box-sizing: border-box; }
87
+ [hidden] { display: none !important; }
88
+ html { background: var(--ground); }
89
+ body {
90
+ margin: 0;
91
+ min-height: 100dvh;
92
+ color: var(--text-2);
93
+ font: var(--fs) / 1.5 var(--mono);
94
+ font-variant-numeric: tabular-nums lining-nums;
95
+ -webkit-font-smoothing: antialiased;
96
+ /* The desktop: fine 45-degree lines, the pattern behind every window. */
97
+ background:
98
+ repeating-linear-gradient(45deg, var(--dot) 0 1px, transparent 1px 4px),
99
+ var(--ground);
100
+ }
101
+
102
+ ::selection { background: var(--accent); color: var(--on-accent); }
103
+ a { color: var(--accent-ink); text-decoration: underline; text-underline-offset: 3px; }
104
+ a:hover { background: var(--accent); color: var(--on-accent); text-decoration: none; }
105
+ :focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
106
+ .sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
107
+ .skip { position: absolute; left: 8px; top: -40px; z-index: 100; background: var(--accent); color: var(--on-accent); padding: 4px 8px; }
108
+ .skip:focus { top: 8px; }
109
+
110
+ /* ── Menu bar ──────────────────────────────────────────────────────────── */
111
+
112
+ .menubar {
113
+ position: sticky;
114
+ top: 0;
115
+ z-index: 20;
116
+ display: flex;
117
+ align-items: stretch;
118
+ gap: 4px;
119
+ min-height: 30px;
120
+ padding: 0 10px;
121
+ background: var(--ground);
122
+ border-bottom: 1px solid var(--frame);
123
+ }
124
+ .mb-mark { display: flex; align-items: center; gap: 8px; padding: 0 10px 0 2px; color: var(--text-1); font-weight: 700; white-space: nowrap; }
125
+ .mb-mark svg { display: block; }
126
+ .mb-mark .px { fill: var(--accent); }
127
+ :root[data-theme="dark"] .mb-mark .px { fill: var(--accent-ink); }
128
+ .menubar nav { display: flex; min-width: 0; }
129
+ .menubar ol { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; }
130
+ .menubar nav a {
131
+ display: flex;
132
+ align-items: center;
133
+ height: 100%;
134
+ min-height: var(--target);
135
+ padding: 0 10px;
136
+ color: var(--text-1);
137
+ text-decoration: none;
138
+ white-space: nowrap;
139
+ }
140
+ .menubar nav a .key { color: var(--text-3); margin-right: 6px; font-size: 11px; }
141
+ .menubar nav a:hover { background: var(--frame); color: var(--ground); }
142
+ .menubar nav a:hover .key { color: inherit; }
143
+ .menubar nav a[aria-current="page"] { background: var(--accent); color: var(--on-accent); }
144
+ .menubar nav a[aria-current="page"] .key { color: var(--on-accent); }
145
+ .mb-right { margin-left: auto; display: flex; align-items: center; gap: 8px; padding-left: 8px; white-space: nowrap; }
146
+ #mb-chips { display: flex; gap: 6px; }
147
+ @media (max-width: 1320px) { .menubar nav a .key { display: none; } .menubar nav a { padding: 0 8px; } }
148
+ .num .row-btn { text-align: right; }
149
+ .mb-clock { color: var(--text-1); min-width: 5ch; text-align: right; }
150
+
151
+ /* ── The window ────────────────────────────────────────────────────────── */
152
+
153
+ .desktop { padding: 20px 22px 30px; }
154
+ .window {
155
+ position: relative;
156
+ max-width: 1440px;
157
+ margin: 0 auto;
158
+ background: var(--ground);
159
+ border: 1px solid var(--frame);
160
+ box-shadow: 2px 2px 0 var(--frame); /* the hard 1-bit drop shadow */
161
+ }
162
+ /* Registration crosshairs at the window's corners. */
163
+ .window::before, .window::after {
164
+ content: "+";
165
+ position: absolute;
166
+ color: var(--text-3);
167
+ font-size: 13px;
168
+ line-height: 1;
169
+ pointer-events: none;
170
+ }
171
+ .window::before { top: -15px; left: -14px; }
172
+ .window::after { bottom: -16px; right: -15px; }
173
+
174
+ .titlebar {
175
+ position: relative;
176
+ display: flex;
177
+ align-items: center;
178
+ justify-content: center;
179
+ height: 22px;
180
+ margin: 0;
181
+ padding: 0 8px;
182
+ border-bottom: 1px solid var(--frame);
183
+ /* Pinstripes: 1px lines on a 2px rhythm, inset from every edge. */
184
+ background:
185
+ repeating-linear-gradient(to bottom, var(--frame) 0 1px, transparent 1px 2px) 4px 4px / calc(100% - 8px) 13px no-repeat,
186
+ var(--ground);
187
+ }
188
+ .titlebar .box {
189
+ position: absolute;
190
+ top: 4px;
191
+ width: 13px;
192
+ height: 13px;
193
+ border: 1px solid var(--frame);
194
+ background: var(--ground);
195
+ box-shadow: 0 0 0 2px var(--ground);
196
+ }
197
+ .titlebar .box.close { left: 10px; }
198
+ .titlebar .box.zoom { right: 10px; }
199
+ .titlebar .box.zoom::after { content: ""; position: absolute; left: 0; top: 0; width: 6px; height: 6px; border-right: 1px solid var(--frame); border-bottom: 1px solid var(--frame); }
200
+ .titlebar .title {
201
+ padding: 0 10px;
202
+ background: var(--ground);
203
+ color: var(--text-1);
204
+ font-weight: 700;
205
+ font-size: 12px;
206
+ white-space: nowrap;
207
+ overflow: hidden;
208
+ text-overflow: ellipsis;
209
+ max-width: calc(100% - 64px);
210
+ }
211
+
212
+ .prompt {
213
+ padding: 8px 20px;
214
+ border-bottom: 1px solid var(--hair);
215
+ color: var(--text-2);
216
+ white-space: nowrap;
217
+ overflow: hidden;
218
+ text-overflow: ellipsis;
219
+ }
220
+ .prompt .user { color: var(--ok); }
221
+ .prompt .path { color: var(--accent-ink); }
222
+
223
+ .main { padding: 24px 20px 28px; min-width: 0; outline: none; }
224
+
225
+ .status {
226
+ display: flex;
227
+ flex-wrap: wrap;
228
+ gap: 6px 18px;
229
+ padding: 6px 20px;
230
+ border-top: 1px solid var(--frame);
231
+ color: var(--text-3);
232
+ font-size: 11px;
233
+ }
234
+ .status kbd { color: var(--text-1); }
235
+
236
+ .cursor {
237
+ display: inline-block;
238
+ width: 1ch;
239
+ height: 1.15em;
240
+ vertical-align: text-bottom;
241
+ background: var(--ok);
242
+ box-shadow: var(--glow);
243
+ margin-left: 1px;
244
+ animation: blink 1s steps(1, end) infinite;
245
+ }
246
+ @keyframes blink { 50% { opacity: 0; } }
247
+
248
+ /* ── Type ──────────────────────────────────────────────────────────────── */
249
+
250
+ h1, h2, h3 { color: var(--text-1); font-weight: 700; letter-spacing: 0; }
251
+ h1 { font-size: 17px; margin: 0 0 4px; }
252
+ h2 { font-size: 13px; margin: 0 0 8px; }
253
+ h3 { font-size: 13px; margin: 0 0 6px; }
254
+ .lede { color: var(--text-3); margin: 0 0 22px; max-width: 78ch; }
255
+ .label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.12em; color: var(--text-3); margin: 14px 0 6px; }
256
+ .t1 { color: var(--text-1); }
257
+ .muted, .faint { color: var(--text-3); }
258
+ .ok { color: var(--ok); }
259
+ .bad { color: var(--text-1); font-weight: 700; }
260
+ .warn { color: var(--text-1); }
261
+ .blue { color: var(--accent-ink); }
262
+ .num { text-align: right; font-variant-numeric: tabular-nums; }
263
+ .strong { color: var(--text-1); font-weight: 700; }
264
+ kbd { font: inherit; border: 1px solid var(--rule); border-radius: 3px; padding: 0 4px; }
265
+ code { color: var(--text-1); }
266
+
267
+ /* ── Group boxes ───────────────────────────────────────────────────────── */
268
+
269
+ .grid { display: grid; gap: 24px var(--gap); grid-template-columns: repeat(12, minmax(0, 1fr)); }
270
+ .span-12 { grid-column: span 12; }
271
+ .span-8 { grid-column: span 8; }
272
+ .span-7 { grid-column: span 7; }
273
+ .span-6 { grid-column: span 6; }
274
+ .span-5 { grid-column: span 5; }
275
+ .span-4 { grid-column: span 4; }
276
+
277
+ /* Every section is a small window: 1px frame, hard shadow, pinstriped bar. */
278
+ .panel {
279
+ position: relative;
280
+ min-width: 0;
281
+ border: 1px solid var(--frame);
282
+ box-shadow: 2px 2px 0 var(--frame);
283
+ background: var(--ground);
284
+ }
285
+ .panel-bar {
286
+ position: relative;
287
+ display: flex;
288
+ align-items: center;
289
+ gap: 10px;
290
+ height: 20px;
291
+ padding: 0 6px;
292
+ border-bottom: 1px solid var(--frame);
293
+ background:
294
+ repeating-linear-gradient(to bottom, var(--frame) 0 1px, transparent 1px 2px) 0 4px / 100% 11px no-repeat,
295
+ var(--ground);
296
+ }
297
+ .panel-bar .box { width: 11px; height: 11px; flex: none; border: 1px solid var(--frame); background: var(--ground); box-shadow: 0 0 0 2px var(--ground); }
298
+ .panel-h {
299
+ margin: 0 auto;
300
+ padding: 0 8px;
301
+ background: var(--ground);
302
+ color: var(--text-1);
303
+ font-size: 12px;
304
+ font-weight: 700;
305
+ white-space: nowrap;
306
+ min-width: 0;
307
+ overflow: hidden;
308
+ text-overflow: ellipsis;
309
+ }
310
+ .panel-meta { padding: 0 6px; background: var(--ground); color: var(--text-2); font-size: 11px; white-space: nowrap; }
311
+ .panel-bar .grip { width: 9px; height: 11px; flex: none; background: repeating-linear-gradient(to bottom, var(--frame) 0 1px, var(--ground) 1px 3px); box-shadow: 0 0 0 2px var(--ground); }
312
+ .panel-body { padding: 12px 14px 14px; min-width: 0; }
313
+
314
+ /* Columns of short lists, NOBSUI-style: bold heading, a rule, then rows. */
315
+ .cols { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); }
316
+ .cols > div { padding: 10px 14px 12px; min-width: 0; border-left: 1px solid var(--frame); }
317
+ .cols > div:first-child { border-left: 0; }
318
+ .cols h3 { margin: 0 0 8px; padding-bottom: 6px; border-bottom: 1px solid var(--frame); font-size: 12px; }
319
+ .rows { list-style: none; margin: 0; padding: 0; display: grid; gap: 5px; }
320
+ .rows li { display: grid; grid-template-columns: 3ch minmax(0, 1fr); gap: 8px; align-items: baseline; }
321
+ .rows .code { color: var(--text-3); font-size: 11px; }
322
+ .rows .t { color: var(--text-1); overflow-wrap: anywhere; }
323
+ .rows .t.b { font-weight: 700; }
324
+ .rows .t .where { color: var(--text-3); font-weight: 400; }
325
+
326
+ /* Gantt: finding lifetimes between scans. */
327
+ .gantt .lane-label { fill: var(--text-1); font: 11px var(--mono); }
328
+ .gantt .lane-label.b { font-weight: 700; }
329
+ .gantt .lane-code { fill: var(--text-3); font: 10px var(--mono); }
330
+ .gantt .bar { fill: var(--ground); stroke: var(--frame); stroke-width: 1; }
331
+ .gantt .bar.open { fill: url(#pt-hatch-ink); }
332
+ .gantt .bar.crit { fill: var(--frame); }
333
+ .gantt .scan-line { stroke: var(--frame); stroke-width: 1; stroke-dasharray: 3 3; }
334
+ .gantt .now-line { stroke: var(--frame); stroke-width: 1.5; }
335
+ .gantt .chip { fill: var(--frame); }
336
+ .gantt .chip-t { fill: var(--ground); font: 700 10px var(--mono); }
337
+ .gantt .axis-t { fill: var(--text-2); font: 10px var(--mono); }
338
+ .hatch-ink { stroke: var(--frame); stroke-width: 1; }
339
+
340
+ /* Figure caption: where a number came from, set like a drawing's title block. */
341
+ .fig {
342
+ display: flex;
343
+ justify-content: space-between;
344
+ gap: 12px;
345
+ margin: 12px 0 0;
346
+ padding-top: 6px;
347
+ border-top: 1px dotted var(--rule);
348
+ font-size: 10.5px;
349
+ letter-spacing: 0.1em;
350
+ text-transform: uppercase;
351
+ color: var(--text-3);
352
+ }
353
+ .panel-note { color: var(--text-3); font-size: 11px; margin: 10px 0 0; }
354
+
355
+ /* A window inside the window: pinstriped title, hard shadow. Dialogs and the terminal only. */
356
+ .subwin { border: 1px solid var(--frame); box-shadow: 2px 2px 0 var(--frame); background: var(--ground); min-width: 0; }
357
+ .subwin > .titlebar { height: 20px; background:
358
+ repeating-linear-gradient(to bottom, var(--frame) 0 1px, transparent 1px 2px) 4px 4px / calc(100% - 8px) 11px no-repeat, var(--ground); }
359
+ .subwin > .titlebar .box { top: 3px; width: 11px; height: 11px; }
360
+ .subwin-body { padding: 14px; }
361
+
362
+ /* ── About window: the wordmark (the one bold move) ────────────────────── */
363
+
364
+ .about { margin-bottom: 28px; }
365
+ .about .subwin-body { display: grid; gap: 4px; padding: 22px 24px 18px; }
366
+ .wordmark {
367
+ margin: 0 0 8px;
368
+ font-size: clamp(5px, 1.3vw, 12.5px);
369
+ line-height: 1.08;
370
+ white-space: pre;
371
+ user-select: none;
372
+ overflow: hidden;
373
+ }
374
+ .wordmark .fill { color: var(--accent); text-shadow: var(--logo-glow); }
375
+ .wordmark .edge { color: var(--accent-ink); }
376
+ :root[data-theme="light"] .wordmark .edge { color: var(--text-1); }
377
+ .about .tagline { color: var(--text-1); }
378
+ .about .facts { display: flex; flex-wrap: wrap; gap: 4px 22px; margin-top: 10px; color: var(--text-3); font-size: 12px; }
379
+ .about .facts b { color: var(--text-1); font-weight: 700; }
380
+
381
+ /* ── Stat strip ────────────────────────────────────────────────────────── */
382
+
383
+ .strip { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); border: 1px solid var(--frame); margin-bottom: 28px; }
384
+ .strip .cell { padding: 10px 14px 12px; min-width: 0; border-left: 1px solid var(--rule); }
385
+ .strip .cell:first-child { border-left: 0; }
386
+ .strip .label { margin: 0 0 4px; display: block; }
387
+ .strip .big { font-size: 26px; line-height: 1.15; color: var(--text-1); font-weight: 700; }
388
+ .strip .big .unit { font-size: 13px; font-weight: 400; color: var(--text-3); margin-left: 2px; }
389
+ .strip .big.ok { color: var(--ok); }
390
+ .strip .sub { color: var(--text-3); font-size: 11px; margin-top: 2px; }
391
+ .big { font-size: 22px; line-height: 1.2; color: var(--text-1); font-weight: 700; }
392
+
393
+ /* ── Severity: 1-bit. Word first, then a pattern swatch, never colour alone ── */
394
+
395
+ .sev { white-space: nowrap; font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; display: inline-flex; align-items: center; gap: 6px; color: var(--text-1); }
396
+ .sev::before { content: ""; display: inline-block; width: 10px; height: 10px; border: 1px solid var(--frame); flex: none; }
397
+ .sev-critical::before { background: var(--frame); }
398
+ .sev-high::before { background: repeating-linear-gradient(45deg, var(--frame) 0 1.5px, transparent 1.5px 3px); }
399
+ .sev-medium::before { background: conic-gradient(from 90deg at 1px 1px, transparent 50%, var(--frame) 0 75%, transparent 0) 0 0 / 2px 2px, conic-gradient(var(--frame) 25%, transparent 0) 0 0 / 2px 2px; }
400
+ .sev-low::before { background: conic-gradient(from 90deg at 1px 1px, transparent 75%, var(--frame) 0) 0 0 / 3px 3px; }
401
+ .sev-info::before { background: transparent; }
402
+ .sev-critical { font-weight: 700; }
403
+ .sev-low, .sev-info { color: var(--text-2); }
404
+
405
+ /* Shade characters for text bars: █ ▓ ▒ ░ read as 1-bit patterns. */
406
+ .shade { color: var(--text-1); }
407
+ .shade.track { color: var(--text-4); }
408
+ .bars { display: grid; grid-template-columns: max-content minmax(0, 1fr) max-content; gap: 3px 12px; align-items: baseline; }
409
+ .bars .bar { white-space: pre; overflow: hidden; letter-spacing: -0.04em; }
410
+ .stackbar { white-space: pre; overflow: hidden; letter-spacing: -0.04em; color: var(--text-1); line-height: 1.2; margin: 2px 0 10px; }
411
+ td .stackbar { margin: 0; max-width: 14ch; }
412
+
413
+ /* ── Grade ─────────────────────────────────────────────────────────────── */
414
+
415
+ .grade { display: inline-grid; place-items: center; min-width: 26px; height: 22px; border: 1px solid var(--frame); color: var(--text-1); font-weight: 700; }
416
+ .grade-F, .grade-D { background: var(--frame); color: var(--ground); }
417
+ .grade-big { margin: 0 16px 0 0; font-size: 22px; line-height: 1; white-space: pre; color: var(--text-1); }
418
+ .grade-big.grade-F, .grade-big.grade-D { background: none; color: var(--text-1); }
419
+
420
+ /* ── Tables ────────────────────────────────────────────────────────────── */
421
+
422
+ .table-wrap { overflow-x: auto; }
423
+ table { width: 100%; border-collapse: collapse; }
424
+ th {
425
+ text-align: left;
426
+ font-weight: 400;
427
+ font-size: 11px;
428
+ text-transform: uppercase;
429
+ letter-spacing: 0.08em;
430
+ color: var(--text-3);
431
+ border-bottom: 1px solid var(--frame);
432
+ padding: 4px 10px 6px 0;
433
+ background: var(--ground);
434
+ white-space: nowrap;
435
+ }
436
+ td { padding: 7px 10px 7px 0; border-bottom: 1px dotted var(--rule); vertical-align: top; }
437
+ tr.group td { padding: 4px 8px; background: var(--wash); color: var(--text-1); font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; border-bottom: 1px solid var(--rule); }
438
+ tr.sel td { background: var(--accent); color: var(--on-accent); }
439
+ tr.sel td * { color: var(--on-accent) !important; }
440
+ tr.sel .sev::before { border-color: var(--on-accent); }
441
+ .row-btn { all: unset; cursor: pointer; color: var(--text-1); display: block; min-height: 20px; }
442
+ .row-btn:hover { text-decoration: underline; }
443
+ .row-btn:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
444
+ .trunc { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 40ch; }
445
+
446
+ /* ── Push buttons ──────────────────────────────────────────────────────── */
447
+
448
+ .btn {
449
+ font: inherit;
450
+ display: inline-flex;
451
+ align-items: center;
452
+ gap: 6px;
453
+ min-height: var(--target);
454
+ padding: 3px 14px;
455
+ color: var(--text-1);
456
+ background: var(--ground);
457
+ border: 1px solid var(--frame);
458
+ border-radius: var(--radius-btn);
459
+ cursor: pointer;
460
+ white-space: nowrap;
461
+ text-decoration: none;
462
+ }
463
+ .btn:hover { background: var(--wash); color: var(--text-1); }
464
+ .btn:active { background: var(--frame); color: var(--ground); } /* classic press: invert */
465
+ .btn[disabled] { color: var(--text-4); border-color: var(--text-4); cursor: not-allowed; background: var(--ground); }
466
+ /* The default button: blue face, the thick outer ring. */
467
+ .btn.primary { background: var(--accent); color: var(--on-accent); box-shadow: 0 0 0 2px var(--ground), 0 0 0 4px var(--frame); margin: 4px; }
468
+ .btn.primary:hover { background: var(--accent-hover); color: var(--on-accent); }
469
+ .btn.primary:active { background: var(--frame); color: var(--ground); }
470
+ .btn.primary[disabled] { background: var(--ground); color: var(--text-4); box-shadow: 0 0 0 2px var(--ground), 0 0 0 4px var(--text-4); }
471
+ .btn .k { opacity: 0.8; font-size: 11px; }
472
+
473
+ .chip { display: inline-block; border: 1px solid var(--rule); padding: 0 6px; font-size: 11px; color: var(--text-2); white-space: nowrap; }
474
+ .chip.on { color: var(--ok); border-color: currentColor; }
475
+ .chip.inv { background: var(--frame); color: var(--ground); border-color: var(--frame); }
476
+ .chip.blue { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }
477
+
478
+ .toggle { font: inherit; font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; min-height: 24px; padding: 2px 8px; border: 1px solid var(--frame); border-radius: var(--radius-btn); background: var(--ground); color: var(--text-2); cursor: pointer; }
479
+ .toggle[aria-pressed="true"] { background: var(--frame); color: var(--ground); }
480
+
481
+ .themebtn { font: inherit; font-size: 12px; min-height: 22px; padding: 0 8px; background: var(--ground); color: var(--text-1); border: 1px solid var(--frame); border-radius: var(--radius-btn); cursor: pointer; white-space: nowrap; }
482
+ .themebtn:active { background: var(--frame); color: var(--ground); }
483
+
484
+ /* ── Fields ────────────────────────────────────────────────────────────── */
485
+
486
+ .field { display: grid; gap: 4px; min-width: 0; }
487
+ .field > span { font-size: 12px; color: var(--text-1); font-weight: 700; }
488
+ .field .hint { font-size: 11px; color: var(--text-3); font-weight: 400; }
489
+ input[type="text"], input[type="search"], input[type="number"], select {
490
+ font: inherit;
491
+ color: var(--text-1);
492
+ background: var(--ground);
493
+ border: 1px solid var(--frame);
494
+ box-shadow: inset 1px 1px 0 var(--rule);
495
+ padding: 5px 8px;
496
+ min-height: var(--target);
497
+ min-width: 0;
498
+ width: 100%;
499
+ border-radius: 0;
500
+ }
501
+ input::placeholder { color: var(--text-3); }
502
+ input:focus-visible, select:focus-visible { outline: 2px solid var(--focus); outline-offset: 0; }
503
+ input[aria-invalid="true"] { border-width: 2px; }
504
+ input[type="checkbox"], input[type="radio"] { accent-color: var(--accent); width: 14px; height: 14px; margin: 0 6px 0 0; vertical-align: -2px; }
505
+ .check { display: inline-flex; align-items: center; cursor: pointer; color: var(--text-1); min-height: 24px; }
506
+ .filters { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 14px; }
507
+ .filters .filters { margin: 0; }
508
+ .filters .search { flex: 1 1 220px; max-width: 360px; width: auto; }
509
+ .filters select { width: auto; max-width: 280px; }
510
+
511
+ .form-grid { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: 12px; align-items: end; }
512
+ .c12 { grid-column: span 12; }
513
+ .c6 { grid-column: span 6; }
514
+ .c4 { grid-column: span 4; }
515
+ .c2 { grid-column: span 2; }
516
+
517
+ /* Radio group as a strip of push buttons. */
518
+ .seg { display: inline-flex; flex-wrap: wrap; border: 1px solid var(--frame); border-radius: var(--radius-btn); overflow: hidden; }
519
+ .seg label { position: relative; cursor: pointer; }
520
+ .seg label + label { border-left: 1px solid var(--frame); }
521
+ .seg input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; }
522
+ .seg span { display: flex; align-items: center; min-height: var(--target); padding: 0 12px; color: var(--text-1); }
523
+ .seg input:checked + span { background: var(--frame); color: var(--ground); }
524
+ .seg input:focus-visible + span { outline: 2px solid var(--focus); outline-offset: -4px; }
525
+
526
+ .switch { font: inherit; display: inline-flex; align-items: center; gap: 10px; min-height: var(--target); padding: 0 12px; background: var(--ground); color: var(--text-1); border: 1px solid var(--frame); border-radius: var(--radius-btn); cursor: pointer; }
527
+ .switch .state { font-weight: 700; }
528
+ .switch[aria-checked="true"] .state { color: var(--ok); }
529
+
530
+ .error { color: var(--text-1); margin: 10px 0 0; padding: 6px 10px; border: 2px solid var(--frame); }
531
+ .error::before { content: "[!] "; font-weight: 700; }
532
+ .empty { color: var(--text-3); padding: 6px 0; margin: 0; }
533
+ .notice { border: 1px dashed var(--rule); padding: 12px 14px; color: var(--text-2); }
534
+
535
+ .kv { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 3px 16px; margin: 0; }
536
+ .kv dt { color: var(--text-3); }
537
+ .kv dd { margin: 0; min-width: 0; overflow-wrap: anywhere; color: var(--text-1); }
538
+
539
+ .list { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
540
+ .list li { display: flex; gap: 10px; align-items: baseline; min-width: 0; }
541
+ .list li .grow { flex: 1; min-width: 0; overflow-wrap: anywhere; }
542
+ .list a { display: inline-block; min-height: 24px; line-height: 24px; }
543
+ .bullets { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }
544
+ .bullets li::before { content: "▸ "; color: var(--text-3); }
545
+
546
+ /* "What next?" :: the CLI's numbered choices, as buttons. */
547
+ .next { margin-top: 28px; padding-top: 14px; border-top: 1px solid var(--rule); }
548
+ .next ol { list-style: none; margin: 6px 0 0; padding: 0; display: grid; gap: 4px; counter-reset: n; }
549
+ .next li { counter-increment: n; display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
550
+ .next li::before { content: counter(n) "."; color: var(--text-3); min-width: 2ch; text-align: right; }
551
+ .next button { all: unset; cursor: pointer; color: var(--text-1); min-height: 22px; padding: 0 2px; }
552
+ .next button:hover { background: var(--frame); color: var(--ground); }
553
+ .next button:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
554
+ .next .why { color: var(--text-3); }
555
+
556
+ /* Start here (first run) */
557
+ .steps { display: grid; gap: 20px; }
558
+ .step h3 { margin: 0 0 4px; }
559
+ .step h3 .n { color: var(--text-3); font-weight: 400; margin-right: 8px; }
560
+ .step p { margin: 0 0 8px; max-width: 72ch; }
561
+
562
+ /* ── Charts ────────────────────────────────────────────────────────────── */
563
+
564
+ svg.chart { display: block; width: 100%; height: auto; overflow: visible; }
565
+ svg.spark { display: block; width: 100%; max-width: 140px; height: 24px; }
566
+ .chart .axis { stroke: var(--frame); stroke-width: 1; }
567
+ .chart .grid-line { stroke: var(--hair); stroke-width: 1; }
568
+ .chart .tick { fill: var(--text-3); font: 10px var(--mono); }
569
+ .chart .line-total { fill: none; stroke: var(--series); stroke-width: 2; }
570
+ .chart .area-total { fill: url(#pt-hatch); }
571
+ .hatch-line { stroke: var(--series); stroke-width: 1; opacity: 0.55; }
572
+ .chart .line-severe { fill: none; stroke: var(--text-1); stroke-width: 1.2; stroke-dasharray: 3 3; }
573
+ .chart .dot { fill: var(--ground); stroke: var(--series); stroke-width: 1.5; }
574
+ .chart .col { fill: url(#pt-dither); stroke: var(--frame); stroke-width: 1; }
575
+ .dither-dot { fill: var(--frame); }
576
+ .chart .col-label { fill: var(--text-3); font: 10px var(--mono); }
577
+ .chart .col-value { fill: var(--text-1); font: 11px var(--mono); }
578
+ .spark .s-line { fill: none; stroke: var(--series); stroke-width: 1.5; }
579
+ .spark .s-dot { fill: var(--series); }
580
+ .legend { display: flex; gap: 14px; flex-wrap: wrap; font-size: 11px; color: var(--text-3); margin-top: 8px; }
581
+ .legend .sw { display: inline-block; width: 14px; height: 0; vertical-align: middle; margin-right: 6px; border-top: 2px solid var(--series); }
582
+ .legend .sw-severe { border-top: 2px dashed var(--text-1); }
583
+
584
+ /* ── Findings split view ───────────────────────────────────────────────── */
585
+
586
+ .split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 0.95fr); gap: var(--gap); align-items: start; }
587
+ .detail { position: sticky; top: 44px; max-height: calc(100dvh - 64px); overflow-y: auto; padding-top: 10px; }
588
+ .detail h2:not(.panel-h) { font-size: 15px; margin: 6px 0 10px; }
589
+ .detail .panel-body > section { margin-top: 16px; }
590
+ .plain { border: 1px solid var(--frame); padding: 10px 12px; margin-top: 12px; display: grid; gap: 6px; }
591
+ .plain p { margin: 0; }
592
+ pre.block {
593
+ margin: 6px 0 0;
594
+ padding: 10px 12px;
595
+ border: 1px solid var(--rule);
596
+ background: var(--wash);
597
+ white-space: pre-wrap;
598
+ overflow-wrap: anywhere;
599
+ color: var(--text-1);
600
+ max-height: 340px;
601
+ overflow: auto;
602
+ }
603
+ pre.diff { white-space: pre; overflow-x: auto; }
604
+ .diff .add { color: var(--text-1); font-weight: 700; }
605
+ .diff .del { color: var(--text-3); text-decoration: line-through; text-decoration-color: var(--text-4); }
606
+ .diff .hunk { color: var(--accent-ink); }
607
+ .diff .meta { color: var(--text-3); }
608
+
609
+ /* ── Terminal (Run) ────────────────────────────────────────────────────── */
610
+
611
+ .term {
612
+ min-height: 340px;
613
+ max-height: 60dvh;
614
+ overflow-y: auto;
615
+ padding: 12px 14px;
616
+ font-size: 12.5px;
617
+ line-height: 1.55;
618
+ color: var(--text-2);
619
+ background:
620
+ repeating-linear-gradient(to bottom, var(--scan) 0 1px, transparent 1px 3px),
621
+ var(--ground);
622
+ }
623
+ .term .ln { white-space: pre-wrap; overflow-wrap: anywhere; min-height: 1.55em; }
624
+ .term .ln.cmd { color: var(--text-1); font-weight: 700; }
625
+ .term .ln.cmd::before { content: "$ "; color: var(--ok); }
626
+ .term .ln.dim::before { content: "▸ "; color: var(--text-3); }
627
+ .term .ln.ok { color: var(--ok); }
628
+ .term .ln.bad { color: var(--text-1); font-weight: 700; }
629
+ .term .ln.bad::before { content: "[!] "; }
630
+ .term .ln.hl { color: var(--text-1); font-weight: 700; margin-top: 6px; }
631
+ .term-head { display: flex; justify-content: space-between; gap: 12px; align-items: center; padding: 8px 14px; border-bottom: 1px solid var(--hair); flex-wrap: wrap; }
632
+
633
+ /* The approval: a classic alert dialog. Double frame, icon, default button right. */
634
+ .alert {
635
+ margin: 14px;
636
+ padding: 16px 18px;
637
+ border: 1px solid var(--frame);
638
+ box-shadow: inset 0 0 0 3px var(--ground), inset 0 0 0 4px var(--frame), 2px 2px 0 var(--frame);
639
+ background: var(--ground);
640
+ display: grid;
641
+ grid-template-columns: auto minmax(0, 1fr);
642
+ gap: 4px 14px;
643
+ }
644
+ .alert .icon { grid-row: span 3; width: 34px; height: 34px; display: grid; place-items: center; border: 2px solid var(--frame); font-weight: 700; color: var(--text-1); }
645
+ .alert h3 { margin: 2px 0 2px; }
646
+ .alert p { margin: 0; }
647
+ .alert .body { grid-column: 1 / -1; min-width: 0; }
648
+ .alert .actions { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 10px; justify-content: flex-end; align-items: center; margin-top: 10px; }
649
+
650
+ .timeline { list-style: none; margin: 0; padding: 0; }
651
+ .timeline > li { padding-left: 18px; position: relative; padding-bottom: 12px; }
652
+ .timeline > li::before { content: ""; position: absolute; left: 0; top: 5px; width: 8px; height: 8px; background: var(--frame); }
653
+ .timeline > li::after { content: ""; position: absolute; left: 4px; top: 16px; bottom: 0; border-left: 1px dotted var(--rule); }
654
+ .timeline > li:last-child::after { display: none; }
655
+ .timeline .fixes { list-style: none; margin: 6px 0 0; padding: 0; display: grid; gap: 3px; }
656
+ .timeline .fixes li { display: flex; gap: 8px; min-width: 0; }
657
+ .timeline .fixes .t { flex: 1; min-width: 0; overflow-wrap: anywhere; }
658
+
659
+ /* History: three columns, new / fixed / still open. */
660
+ .cmp { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 24px var(--gap); }
661
+
662
+ /* Scrollbars, 1-bit. */
663
+ * { scrollbar-color: var(--frame) var(--ground); scrollbar-width: thin; }
664
+
665
+ /* ── Responsive ────────────────────────────────────────────────────────── */
666
+
667
+ @media (max-width: 1180px) {
668
+ .span-8, .span-7, .span-6, .span-5, .span-4 { grid-column: span 12; }
669
+ .split { grid-template-columns: minmax(0, 1fr); }
670
+ .detail { position: static; max-height: none; }
671
+ .cmp { grid-template-columns: minmax(0, 1fr); }
672
+ .cols { grid-template-columns: minmax(0, 1fr); }
673
+ .cols > div { border-left: 0; border-top: 1px solid var(--frame); }
674
+ .cols > div:first-child { border-top: 0; }
675
+ }
676
+
677
+ @media (max-width: 820px) {
678
+ .menubar { flex-wrap: wrap; padding: 0 6px; position: static; }
679
+ .menubar nav { order: 3; width: 100%; border-top: 1px solid var(--hair); }
680
+ .menubar nav a { padding: 0 8px; }
681
+ .menubar nav a .key { display: none; }
682
+ #mb-chips { display: flex; gap: 6px; }
683
+ @media (max-width: 1320px) { .menubar nav a .key { display: none; } .menubar nav a { padding: 0 8px; } }
684
+ .num .row-btn { text-align: right; }
685
+ .mb-clock { display: none; }
686
+ .desktop { padding: 12px 8px 20px; }
687
+ .window::before, .window::after { display: none; }
688
+ .main { padding: 18px 12px 20px; }
689
+ .prompt { padding: 8px 12px; }
690
+ .status { display: none; }
691
+ .c6, .c4, .c2 { grid-column: span 12; }
692
+ .strip { grid-template-columns: repeat(2, minmax(0, 1fr)); }
693
+ .strip .cell:nth-child(3) { border-left: 0; }
694
+ .strip .cell:nth-child(n + 3) { border-top: 1px solid var(--rule); }
695
+ .strip .big { font-size: 20px; }
696
+ .cols { grid-template-columns: minmax(0, 1fr); }
697
+ .cols > div { border-left: 0; border-top: 1px solid var(--frame); }
698
+ .cols > div:first-child { border-top: 0; }
699
+ .hide-narrow { display: none; }
700
+ .trunc { max-width: 22ch; }
701
+ td svg.spark { min-width: 64px; }
702
+ }
703
+
704
+ @media (pointer: coarse) {
705
+ :root { --target: 44px; }
706
+ .menubar nav a { min-height: 44px; }
707
+ .row-btn, .next button { min-height: 32px; }
708
+ }
709
+
710
+ /* ── Reduced motion: no typing, no blink. Content is never hidden. ──────── */
711
+
712
+ @media (prefers-reduced-motion: reduce) {
713
+ *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }
714
+ .cursor { opacity: 1; }
715
+ }
716
+ .week { margin-bottom: 26px; }
717
+ .week .panel-body { padding: 0; }
718
+ .cols .row-btn { display: inline; min-height: 0; }
719
+ .panel-body > .cols { margin: -12px -14px -14px; }
720
+ .stack { margin-top: 24px; }
721
+ .week .panel-body > .cols { margin: 0; }
722
+ svg.chart.gantt { min-width: 760px; }
723
+ @media (max-width: 520px) { .panel-meta { display: none; } }