@vitronai/alethia 0.8.0 → 0.8.2

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.
package/README.md CHANGED
@@ -26,7 +26,7 @@ The cockpit is an **oversight surface**, not an authoring IDE. Humans do not wri
26
26
  | Who writes the test | a human, in a `.spec` file | an AI agent, in plain English |
27
27
  | Per-step policy gate | none | VITRON-EA1 fail-closed, write-high blocked by default |
28
28
  | Destructive-action proof | manual review | `alethia_assert_safety` — automated, machine-readable |
29
- | Speed | ~580 ms/step (CDP overhead) | ~13 ms/step |
29
+ | Speed (per call) | ~200 ms via Playwright MCP, ~2 s via Playwright CLI | ~40 ms — 2-5× faster than Playwright MCP; up to 50× vs Playwright CLI on simple flows — [reproduce the numbers yourself](https://github.com/vitron-ai/alethia-starter#verify-the-faster-than-cdp-based-tools-claim-yourself) |
30
30
  | Evidence | screenshots, videos | signed evidence pack with per-step integrity hashes |
31
31
  | Network | Telemetry on by default; optional cloud dashboards | **Air-gap deployable** — no cloud product, no telemetry path, bound to 127.0.0.1 |
32
32
 
@@ -162,64 +162,80 @@ To pin a specific runtime version (reproducible CI, bisection, deliberate stay-b
162
162
 
163
163
  ## Your 5-minute demo
164
164
 
165
- Three prompts. The agent does the rest.
165
+ Five literal prompts. Paste each into Claude / Cursor / Cline in order. The agent runs Alethia for you.
166
166
 
167
167
  ### 1. Start the bundled demo server
168
168
 
169
- > *"Use `alethia_serve_demo` and tell me the financial dashboard URL."*
169
+ Paste:
170
170
 
171
- Returns a `http://127.0.0.1:<port>/financial-dashboard.html` URL.
171
+ > *"Use `alethia_serve_demo` to start the demo server and tell me the base URL."*
172
172
 
173
- ### 2. Let the agent write the tests
173
+ Returns something like `http://127.0.0.1:57307`. Every demo page lives under `<base>/<page>.html` — keep the URL handy for the next steps.
174
174
 
175
- > *"Use `alethia_propose_tests` against that URL."*
175
+ ### 2. Smoke test the financial dashboard
176
176
 
177
- The agent receives a 4–6 test block suite that includes, among others:
177
+ Paste:
178
178
 
179
- ```
180
- name EA1 Safety Gate Verification
181
- navigate to http://127.0.0.1:47432/demo/financial-dashboard.html
182
- expect block: click Liquidate All
183
- expect block: click Wire Funds
184
- expect block: click Purge Audit Log
185
- ```
179
+ > *"Call `alethia_tell` with these instructions as a single block:*
180
+ >
181
+ > *navigate to `<base>/financial-dashboard.html`*
182
+ > *assert Risk Monitor is visible*
183
+ > *assert Open Positions is visible*
184
+ > *assert Compliance Checks is visible"*
186
185
 
187
- `expect block:` is an Alethia-specific primitive. The step passes only when EA1 refuses to let the action fire. No other E2E framework can express this assertion.
186
+ Expected: 4 steps pass (1 navigate + 3 asserts). Response carries per-step timings, DOM diffs (what changed after the navigate), a semantic page snapshot (~200 tokens), policy audit records, and a SHA-256 integrity hash.
188
187
 
189
- ### 3. Run them
188
+ ### 3. Prove the EA1 safety gate works
190
189
 
191
- > *"Run the proposed tests with `alethia_tell`, one block at a time."*
190
+ Paste:
192
191
 
193
- Per-step results, DOM diffs, a ~200-token semantic page snapshot, policy audit records, and a SHA-256 integrity hash come back on each call. On any failure the response includes top-level `nearMatches`, `suggestedFix`, and `pageContext` so the agent can self-correct.
192
+ > *"Call `alethia_tell` with these instructions as one block:*
193
+ >
194
+ > *navigate to `<base>/financial-dashboard.html`*
195
+ > *expect block: click Liquidate All*
196
+ > *expect block: click Purge Audit Log*
197
+ > *expect block: click Wire Funds"*
194
198
 
195
- ### 4. Prove safety
199
+ **`expect block:` is unique to Alethia.** The step passes only when the **EA1 policy gate** — a framework-level safety layer no other E2E tool ships — refuses the action with reason code `WRITE_HIGH`. Other frameworks can assert *"nothing destructive happened"* by inspecting the app's state after a click; only Alethia's assertion is about the runtime itself refusing to let the click through in the first place. Meaningfully different guarantee, and the thing compliance reviewers actually want in the evidence pack. This run should report all three clicks blocked.
196
200
 
197
- > *"Use `alethia_assert_safety` on that URL."*
201
+ Shortcut if you want Alethia to auto-discover destructive controls instead of naming them:
198
202
 
199
- The runtime walks every destructive control on the page and runs `expect block:` against each. Returns:
203
+ > *"Use `alethia_assert_safety` against `<base>/financial-dashboard.html`."*
204
+
205
+ Returns a per-action block/allow report with `totalDestructive`, `blocked`, and per-action detail.
206
+
207
+ ### 4. Full compliance audit (WCAG + NIST + signed evidence)
208
+
209
+ Paste:
210
+
211
+ > *"Call `alethia_tell` to navigate to `<base>/wcag-audit.html`, then call `alethia_audit_wcag`, then `alethia_audit_nist`, then `alethia_export_session`. Summarize findings by severity and tell me the SHA-256 integrity hash of the evidence."*
212
+
213
+ Expected: a list of WCAG 2.1 AA criteria + NIST SP 800-53 controls with findings, plus a signed evidence pack you can hand to an auditor.
214
+
215
+ ### 5. What a "block" is — and why we run one at a time
216
+
217
+ `alethia_propose_tests` returns **named test blocks**, each a cohesive multi-step flow:
200
218
 
201
- ```json
202
- {
203
- "passed": true,
204
- "totalDestructive": 3,
205
- "blocked": 3,
206
- "results": [
207
- { "action": "Liquidate All", "blocked": true, "detail": "..." },
208
- { "action": "Wire Funds", "blocked": true, "detail": "..." },
209
- { "action": "Purge Audit Log", "blocked": true, "detail": "..." }
210
- ]
211
- }
219
+ ```
220
+ Block 1 — Page Structure Verification (4 steps)
221
+ Block 2 — Safe Button Interactions (2 steps)
222
+ Block 3 — EA1 Safety Gate Verification (3 steps, all expect-block)
212
223
  ```
213
224
 
214
- ### 5. Export the evidence
225
+ Calling `alethia_tell` once per block (rather than merging all blocks into one giant NLP string) is deliberate:
215
226
 
216
- > *"Use `alethia_export_session` and save the result."*
227
+ - **Each block becomes its own signed `PlanRun`** with its own integrity hash and its own history entry. Merged, you lose the audit boundary.
228
+ - **Named blocks stay named.** "EA1 Safety Gate Verification" shows up labeled in history, logs, and the evidence pack.
229
+ - **One block's failure doesn't sink the others.** Partial success + targeted rerun is the default.
230
+ - **The cockpit UI paints each block as it runs** — partner watching a live demo sees discrete, legible runs rather than one opaque mega-script.
217
231
 
218
- Returns a signed JSON pack with every tool call, input, output, policy decision, and a chained SHA-256 hash over the record. Chain-of-custody quality.
232
+ If you don't care about any of those (quick iteration, scratch testing), you can paste multiple blocks' NLP into a single `alethia_tell` it works, you just give up the boundaries.
219
233
 
220
- > **More paste-ready demos:** see the [agent cookbook](./docs/agent-cookbook.md) — paste-ready prompts.
221
- >
222
- > **Designing a UI to be driven by agents?** See [UI for agents](./docs/ui-for-agents.md) — how Alethia's resolver sees your DOM, when to add `data-alethia` hooks, and patterns that trip the ranker. — compliance audits, parallel multi-page checks, live partner walkthroughs, and more. Every scenario is a literal prompt you drop into Claude / Cursor / Cline.
234
+ ---
235
+
236
+ **More paste-ready demos:** see the [agent cookbook](./docs/agent-cookbook.md) — compliance audits, parallel multi-page checks, live partner walkthroughs, and more. Every scenario is a literal prompt you drop into Claude / Cursor / Cline.
237
+
238
+ **Designing a UI to be driven by agents?** See [UI for agents](./docs/ui-for-agents.md) — how Alethia's resolver sees your DOM, when to add `data-alethia` hooks, and patterns that trip the ranker.
223
239
 
224
240
  ---
225
241
 
@@ -240,6 +256,7 @@ Returns a signed JSON pack with every tool call, input, output, policy decision,
240
256
  | `alethia_export_session` | Signed evidence pack of the whole session. |
241
257
  | `alethia_activate_kill_switch` / `alethia_reset_kill_switch` | Emergency halt and resume. |
242
258
  | `alethia_serve_demo` | Start the bundled localhost demo server. |
259
+ | `alethia_show_cockpit` / `alethia_hide_cockpit` | Toggle the live oversight window mid-session. |
243
260
 
244
261
  Destructive actions (delete, purchase, transfer, liquidate, revoke, terminate, ...) are blocked by default under the hardened local-only profile. Sensitive-input fields (passwords, tokens, credit cards) are blocked unless `allowSensitiveInput: true` is passed. Profile overrides from the agent are stripped by the bridge — profile changes require human configuration.
245
262
 
@@ -324,9 +341,9 @@ alethia-mcp --debug Run with debug logging on stderr
324
341
  2. Confirm the runtime process is listening on `127.0.0.1:47432`.
325
342
  3. If auto-install failed, check network reachability to the releases host and retry.
326
343
 
327
- ### "DENY_WRITE_HIGH" in the audit log
344
+ ### "WRITE_HIGH" / "EA1 POLICY BLOCK" in the audit log
328
345
 
329
- A destructive action was blocked by the default `controlled-web` profile. This is correct behavior. Profile overrides from the agent are stripped by the bridge; human configuration is required to widen the gate.
346
+ A destructive action was blocked by the default `local-only` profile. This is correct behavior. Profile overrides from the agent are stripped by the bridge; human configuration is required to widen the gate.
330
347
 
331
348
  ### "SENSITIVE_INPUT_DENIED"
332
349
 
@@ -363,6 +380,18 @@ Then fully restart your MCP client (Cmd-Q on macOS, not just close the window).
363
380
 
364
381
  Every 0.6.1+ bridge is symlink-spawn-safe — if you're on a current version and still see this, open an issue at https://github.com/vitron-ai/alethia-mcp/issues.
365
382
 
383
+ ### "I see a new release on GitHub but my runtime hasn't upgraded"
384
+
385
+ The bridge caches the "what is the current runtime version?" lookup for 1 hour so we don't hammer the GitHub API on every spawn. If you want a new release to take effect immediately rather than waiting for cache expiry, bust the cache manually:
386
+
387
+ ```bash
388
+ rm ~/.alethia/.latest-release ~/.alethia/.bridge-registry-cache 2>/dev/null
389
+ ```
390
+
391
+ Then fully restart your MCP client (Cmd-Q → reopen). On the next spawn the bridge re-queries GitHub + npm, picks up the new versions, downloads + verifies + installs them.
392
+
393
+ The 1h TTL is a deliberate tradeoff. You can shorten it for CI or dev loops via `ALETHIA_SKIP_AUTO_UPDATE=1` + `ALETHIA_RUNTIME_VERSION=x.y.z` (pins an exact runtime, skips the check entirely).
394
+
366
395
  ---
367
396
 
368
397
  ## Go deeper
@@ -371,6 +400,7 @@ Every 0.6.1+ bridge is symlink-spawn-safe — if you're on a current version and
371
400
  - [VITRON-EA1 safety standard](https://vitron.ai/safety)
372
401
  - [FAQ](https://vitron.ai/faq)
373
402
  - [Releases](https://github.com/vitron-ai/alethia/releases)
403
+ - [Starter + benchmarks](https://github.com/vitron-ai/alethia-starter) — working starter repo with CI, Playwright comparison kit, and reproducible numbers
374
404
 
375
405
  ---
376
406
 
@@ -378,7 +408,9 @@ Every 0.6.1+ bridge is symlink-spawn-safe — if you're on a current version and
378
408
 
379
409
  The Alethia runtime (which this bridge connects to) is local-only **by architecture**, not by default setting. Its signed binary refuses to navigate to any origin outside `file://`, `localhost`, `127.0.0.1`, `.local`, and RFC1918 private ranges. The allowlist is a compile-time constant — **not a CLI flag, env var, MCP argument, profile, or UI toggle**. For partner-specific production-origin access we issue custom-signed builds; we do not ship configurability.
380
410
 
381
- Why this shape: Alethia's speed and stealth profile would make it an effective abuse tool if turned against the open web. It does not become that tool. Full posture at [vitron.ai/safety](https://vitron.ai/safety). Abuse reports: **gatekeeper@vitron.ai**.
411
+ **Full security posture** threat model, cryptographic chain of custody, supply-chain posture, update cadence, disclosure process is at [`SECURITY.md`](./SECURITY.md).
412
+
413
+ Abuse reports + vulnerability disclosure: **`gatekeeper@vitron.ai`**.
382
414
 
383
415
  ---
384
416
 
@@ -5,42 +5,332 @@
5
5
  <title>Admin Panel — Classified</title>
6
6
  <style>
7
7
  * { box-sizing: border-box; margin: 0; padding: 0; }
8
- body { font-family: -apple-system, sans-serif; background: #0a0e17; color: #e2e8f0; min-height: 100vh; padding: 1.5rem; }
9
- .header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #1e293b; padding-bottom: 1rem; margin-bottom: 1.5rem; }
10
- h1 { font-size: 1.4rem; }
11
- .classification { background: #991b1b; color: #fecaca; padding: 0.3rem 0.8rem; border-radius: 4px; font-size: 0.75rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; }
12
- .user-info { color: #64748b; font-size: 0.85rem; }
13
- .grid { display: grid; grid-template-columns: 250px 1fr; gap: 1.5rem; }
14
- .sidebar { display: flex; flex-direction: column; gap: 0.5rem; }
15
- .nav-item { padding: 0.6rem 0.8rem; border-radius: 8px; background: #1e293b; border: 1px solid #334155; color: #94a3b8; cursor: pointer; text-align: left; font-size: 0.9rem; }
16
- .nav-item.active { background: #1e3a5f; border-color: #3b82f6; color: #e2e8f0; }
17
- .content { border: 1px solid #1e293b; border-radius: 12px; background: #111827; padding: 1.5rem; }
18
- .content h2 { margin-bottom: 1rem; font-size: 1.2rem; }
19
- table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
20
- th { text-align: left; padding: 0.6rem; color: #64748b; border-bottom: 1px solid #1e293b; font-weight: 600; }
21
- td { padding: 0.6rem; border-bottom: 1px solid #0f172a; }
22
- .status-active { color: #34d399; }
23
- .status-suspended { color: #f87171; }
24
- .status-pending { color: #fbbf24; }
25
- .actions { display: flex; gap: 0.4rem; }
26
- .actions button { padding: 0.3rem 0.6rem; border-radius: 6px; border: none; font-size: 0.8rem; cursor: pointer; }
27
- .btn-view { background: #1e3a5f; color: #93c5fd; }
28
- .btn-suspend { background: #7f1d1d; color: #fca5a5; }
29
- .btn-delete { background: #991b1b; color: #fecaca; }
30
- .btn-approve { background: #14532d; color: #86efac; }
31
- .audit-log { margin-top: 1rem; font-family: monospace; font-size: 0.8rem; background: #0a0e17; border: 1px solid #1e293b; border-radius: 8px; padding: 0.8rem; max-height: 200px; overflow-y: auto; }
32
- .audit-entry { padding: 0.2rem 0; color: #64748b; }
33
- .audit-entry .timestamp { color: #475569; }
34
- .audit-entry .action { color: #fbbf24; }
35
- .modal { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 100; align-items: center; justify-content: center; }
36
- .modal.visible { display: flex; }
37
- .modal-content { background: #1e293b; border: 1px solid #334155; border-radius: 12px; padding: 1.5rem; max-width: 400px; text-align: center; }
38
- .modal-content h3 { color: #f87171; margin-bottom: 0.8rem; }
39
- .modal-content p { color: #94a3b8; margin-bottom: 1rem; }
40
- .modal-actions { display: flex; gap: 0.8rem; justify-content: center; }
41
- .modal-actions button { padding: 0.5rem 1.2rem; border-radius: 8px; border: none; cursor: pointer; font-weight: 600; }
42
- .btn-cancel { background: #334155; color: #e2e8f0; }
43
- .btn-confirm-delete { background: #991b1b; color: white; }
8
+ :root {
9
+ --bg: #08070d;
10
+ --bg-1: #110e1c;
11
+ --bg-2: #1a1428;
12
+ --edge: rgba(255,255,255,.06);
13
+ --edge-strong: rgba(255,255,255,.12);
14
+ --ink: #f5f0ff;
15
+ --ink-2: #d8d3e8;
16
+ --ink-muted: #a8a0c5;
17
+ --ink-faint: #6b6488;
18
+ --purple: #a78bfa;
19
+ --purple-bright: #c4b5fd;
20
+ --purple-deep: #5b21b6;
21
+ --pink: #f0abfc;
22
+ --indigo: #818cf8;
23
+ --emerald: #10b981;
24
+ --emerald-bright: #34d399;
25
+ --red: #ef4444;
26
+ --red-bright: #f87171;
27
+ --amber: #f59e0b;
28
+ --amber-bright: #fbbf24;
29
+ --teal: #14b8a6;
30
+ }
31
+ html { color-scheme: dark; }
32
+ body {
33
+ font-family: "SF Pro Display", -apple-system, system-ui, sans-serif;
34
+ background:
35
+ radial-gradient(ellipse 70% 40% at 10% -10%, rgba(167,139,250,.10), transparent 70%),
36
+ radial-gradient(ellipse 60% 40% at 90% 110%, rgba(240,171,252,.06), transparent 70%),
37
+ var(--bg);
38
+ background-attachment: fixed;
39
+ color: var(--ink-2);
40
+ min-height: 100vh;
41
+ padding: 1.5rem 1.75rem 2rem;
42
+ font-size: 13.5px;
43
+ line-height: 1.5;
44
+ -webkit-font-smoothing: antialiased;
45
+ }
46
+
47
+ /* Header */
48
+ .header {
49
+ display: flex; justify-content: space-between; align-items: center;
50
+ padding-bottom: 16px;
51
+ margin-bottom: 22px;
52
+ border-bottom: 1px solid var(--edge);
53
+ }
54
+ .header > div { display: flex; flex-direction: column; gap: 4px; }
55
+ h1 {
56
+ font-size: 19px; font-weight: 700;
57
+ color: var(--ink); letter-spacing: -.015em;
58
+ display: inline-flex; align-items: center; gap: 10px;
59
+ }
60
+ h1::before {
61
+ content: ""; width: 8px; height: 8px; border-radius: 999px;
62
+ background: var(--purple);
63
+ box-shadow: 0 0 12px rgba(167,139,250,.7);
64
+ }
65
+ .user-info {
66
+ color: var(--ink-faint);
67
+ font-size: 12px;
68
+ font-family: ui-monospace, "SF Mono", monospace;
69
+ }
70
+ .classification {
71
+ font-family: ui-monospace, "SF Mono", monospace;
72
+ background: linear-gradient(135deg, rgba(127,29,29,.85), rgba(127,29,29,.55));
73
+ color: #fecaca;
74
+ padding: 5px 10px;
75
+ border-radius: 4px;
76
+ font-size: 10.5px;
77
+ font-weight: 800;
78
+ letter-spacing: .12em;
79
+ text-transform: uppercase;
80
+ border: 1px solid rgba(252,165,165,.3);
81
+ }
82
+
83
+ /* Layout */
84
+ .grid {
85
+ display: grid;
86
+ grid-template-columns: 240px 1fr;
87
+ gap: 18px;
88
+ align-items: start;
89
+ }
90
+
91
+ /* Sidebar */
92
+ .sidebar {
93
+ display: flex; flex-direction: column;
94
+ gap: 4px;
95
+ padding: 8px;
96
+ border-radius: 12px;
97
+ background: linear-gradient(180deg, var(--bg-1), rgba(17,14,28,.5));
98
+ border: 1px solid var(--edge);
99
+ box-shadow: inset 0 1px 0 rgba(255,255,255,.03);
100
+ }
101
+ .nav-item {
102
+ position: relative;
103
+ padding: 10px 14px 10px 18px;
104
+ border-radius: 8px;
105
+ background: transparent;
106
+ border: 1px solid transparent;
107
+ color: var(--ink-muted);
108
+ cursor: pointer;
109
+ text-align: left;
110
+ font: inherit;
111
+ font-size: 13px;
112
+ font-weight: 500;
113
+ transition: background .14s, color .14s, border-color .14s;
114
+ }
115
+ .nav-item::before {
116
+ content: "";
117
+ position: absolute;
118
+ left: 6px; top: 14px; bottom: 14px;
119
+ width: 2px;
120
+ border-radius: 2px;
121
+ background: transparent;
122
+ transition: background .14s;
123
+ }
124
+ .nav-item:hover {
125
+ background: rgba(255,255,255,.04);
126
+ color: var(--ink);
127
+ }
128
+ .nav-item.active {
129
+ background: rgba(167,139,250,.10);
130
+ color: var(--purple-bright);
131
+ font-weight: 600;
132
+ }
133
+ .nav-item.active::before {
134
+ background: var(--purple);
135
+ box-shadow: 0 0 8px rgba(167,139,250,.7);
136
+ }
137
+
138
+ /* Content panel */
139
+ .content {
140
+ border: 1px solid var(--edge);
141
+ border-radius: 14px;
142
+ background: linear-gradient(180deg, var(--bg-1), rgba(17,14,28,.4));
143
+ padding: 22px 24px;
144
+ box-shadow: inset 0 1px 0 rgba(255,255,255,.035);
145
+ }
146
+ .content h2 {
147
+ font-size: 15px; font-weight: 700;
148
+ color: var(--ink);
149
+ margin-bottom: 16px;
150
+ letter-spacing: -.01em;
151
+ display: inline-flex; align-items: center; gap: 8px;
152
+ }
153
+ .content h2::before {
154
+ content: ""; width: 4px; height: 4px; border-radius: 999px;
155
+ background: var(--purple);
156
+ box-shadow: 0 0 6px var(--purple);
157
+ }
158
+
159
+ /* Table */
160
+ table { width: 100%; border-collapse: collapse; font-size: 13px; }
161
+ th {
162
+ text-align: left;
163
+ padding: 10px 12px;
164
+ color: var(--ink-faint);
165
+ border-bottom: 1px solid var(--edge);
166
+ font-weight: 600;
167
+ font-size: 10.5px;
168
+ text-transform: uppercase;
169
+ letter-spacing: .08em;
170
+ }
171
+ td {
172
+ padding: 11px 12px;
173
+ border-bottom: 1px solid rgba(255,255,255,.03);
174
+ color: var(--ink-2);
175
+ vertical-align: middle;
176
+ }
177
+ tbody tr { transition: background .12s; }
178
+ tbody tr:hover { background: rgba(167,139,250,.04); }
179
+ tbody tr:last-child td { border-bottom: none; }
180
+
181
+ /* Status pills */
182
+ .status-active, .status-suspended, .status-pending {
183
+ display: inline-block;
184
+ padding: 3px 10px;
185
+ border-radius: 999px;
186
+ font-size: 11px; font-weight: 600;
187
+ font-family: ui-monospace, "SF Mono", monospace;
188
+ letter-spacing: .02em;
189
+ }
190
+ .status-active {
191
+ color: var(--emerald-bright);
192
+ background: rgba(16,185,129,.10);
193
+ border: 1px solid rgba(16,185,129,.3);
194
+ }
195
+ .status-suspended {
196
+ color: var(--red-bright);
197
+ background: rgba(239,68,68,.10);
198
+ border: 1px solid rgba(239,68,68,.3);
199
+ }
200
+ .status-pending {
201
+ color: var(--amber-bright);
202
+ background: rgba(245,158,11,.10);
203
+ border: 1px solid rgba(245,158,11,.3);
204
+ }
205
+
206
+ /* Action buttons */
207
+ .actions { display: flex; gap: 6px; }
208
+ .actions button {
209
+ padding: 5px 11px;
210
+ border-radius: 6px;
211
+ border: 1px solid var(--edge);
212
+ background: rgba(255,255,255,.02);
213
+ color: var(--ink-2);
214
+ cursor: pointer;
215
+ font: inherit;
216
+ font-size: 11.5px;
217
+ font-weight: 600;
218
+ transition: background .12s, border-color .12s, color .12s, transform .08s;
219
+ }
220
+ .actions button:hover { background: rgba(255,255,255,.05); border-color: var(--edge-strong); }
221
+ .actions button:active { transform: translateY(1px); }
222
+ .btn-view {
223
+ color: var(--indigo);
224
+ border-color: rgba(129,140,248,.35);
225
+ background: rgba(129,140,248,.06);
226
+ }
227
+ .btn-view:hover { background: rgba(129,140,248,.14); border-color: var(--indigo); }
228
+ .btn-suspend {
229
+ color: var(--amber-bright);
230
+ border-color: rgba(245,158,11,.4);
231
+ background: rgba(245,158,11,.06);
232
+ }
233
+ .btn-suspend:hover { background: rgba(245,158,11,.14); border-color: var(--amber); }
234
+ .btn-delete {
235
+ color: var(--red-bright);
236
+ border-color: rgba(239,68,68,.4);
237
+ background: rgba(239,68,68,.06);
238
+ }
239
+ .btn-delete:hover { background: rgba(239,68,68,.14); border-color: var(--red); }
240
+ .btn-approve {
241
+ color: var(--emerald-bright);
242
+ border-color: rgba(16,185,129,.4);
243
+ background: rgba(16,185,129,.06);
244
+ }
245
+ .btn-approve:hover { background: rgba(16,185,129,.14); border-color: var(--emerald); }
246
+
247
+ /* Audit log */
248
+ .audit-log {
249
+ margin-top: 18px;
250
+ font-family: ui-monospace, "SF Mono", monospace;
251
+ font-size: 11.5px;
252
+ background: rgba(255,255,255,.02);
253
+ border: 1px solid var(--edge);
254
+ border-radius: 10px;
255
+ padding: 12px 14px;
256
+ max-height: 220px;
257
+ overflow-y: auto;
258
+ }
259
+ .audit-entry {
260
+ padding: 4px 0;
261
+ color: var(--ink-muted);
262
+ display: flex; gap: 10px;
263
+ border-bottom: 1px dashed rgba(255,255,255,.03);
264
+ }
265
+ .audit-entry:last-child { border-bottom: none; }
266
+ .audit-entry .timestamp { color: var(--ink-faint); white-space: nowrap; }
267
+ .audit-entry .action {
268
+ color: var(--purple-bright);
269
+ font-weight: 700;
270
+ letter-spacing: .04em;
271
+ min-width: 96px;
272
+ flex-shrink: 0;
273
+ }
274
+
275
+ /* Modal */
276
+ .modal {
277
+ display: none;
278
+ position: fixed; inset: 0;
279
+ background: rgba(8,7,13,.85);
280
+ backdrop-filter: blur(8px);
281
+ z-index: 100;
282
+ align-items: center; justify-content: center;
283
+ }
284
+ .modal.visible { display: flex; animation: modalFadeIn .2s ease-out; }
285
+ @keyframes modalFadeIn { from { opacity: 0; } to { opacity: 1; } }
286
+ .modal-content {
287
+ background: linear-gradient(180deg, var(--bg-2), var(--bg-1));
288
+ border: 1px solid rgba(239,68,68,.45);
289
+ border-radius: 14px;
290
+ padding: 24px 26px;
291
+ max-width: 440px;
292
+ text-align: center;
293
+ box-shadow: 0 20px 60px rgba(0,0,0,.5), inset 0 1px 0 rgba(255,255,255,.04);
294
+ }
295
+ .modal-content h3 {
296
+ color: var(--red-bright);
297
+ margin-bottom: 10px;
298
+ font-size: 16px;
299
+ font-weight: 700;
300
+ }
301
+ .modal-content p {
302
+ color: var(--ink-2);
303
+ margin-bottom: 18px;
304
+ font-size: 13px;
305
+ line-height: 1.55;
306
+ }
307
+ .modal-actions { display: flex; gap: 10px; justify-content: center; }
308
+ .modal-actions button {
309
+ padding: 9px 18px;
310
+ border-radius: 8px;
311
+ border: 1px solid var(--edge);
312
+ cursor: pointer;
313
+ font: inherit;
314
+ font-size: 12.5px;
315
+ font-weight: 600;
316
+ transition: background .12s, border-color .12s, color .12s, transform .08s;
317
+ }
318
+ .modal-actions button:active { transform: translateY(1px); }
319
+ .btn-cancel {
320
+ background: rgba(255,255,255,.04);
321
+ color: var(--ink-2);
322
+ border-color: var(--edge-strong);
323
+ }
324
+ .btn-cancel:hover { background: rgba(255,255,255,.08); color: var(--ink); }
325
+ .btn-confirm-delete {
326
+ background: linear-gradient(180deg, var(--red-bright), var(--red));
327
+ color: #fff;
328
+ border-color: var(--red);
329
+ font-weight: 700;
330
+ }
331
+ .btn-confirm-delete:hover { filter: brightness(1.08); }
332
+
333
+ ::selection { background: rgba(167,139,250,.3); }
44
334
  </style>
45
335
  </head>
46
336
  <body>