clearotron 0.3.0-beta.2 → 0.3.0-beta.4

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 (40) hide show
  1. package/INSTALL.md +32 -6
  2. package/bin/connect.mjs +68 -25
  3. package/bin/disconnect.mjs +16 -11
  4. package/bin/onboard.mjs +65 -6
  5. package/bin/start.mjs +7 -1
  6. package/build-info.json +2 -2
  7. package/driver/CHANGELOG.md +38 -0
  8. package/driver/driver.config.mjs +5 -0
  9. package/driver/engine/anthropic-agent.mjs +34 -17
  10. package/driver/gateway.mjs +76 -15
  11. package/driver/package.json +1 -1
  12. package/driver/portal-service.mjs +1 -1
  13. package/driver/profile-service.mjs +31 -3
  14. package/driver/publish/index.mjs +12 -9
  15. package/driver/publish/knockout.mjs +4 -2
  16. package/driver/publish/office-record-links.mjs +56 -21
  17. package/driver/recipe-service.mjs +14 -5
  18. package/driver/record-origins.mjs +14 -0
  19. package/driver/suite-census.json +34 -28
  20. package/mcp-server/CHANGELOG.md +8 -0
  21. package/mcp-server/lib/driver.mjs +2 -0
  22. package/mcp-server/lib/knockout.mjs +2 -2
  23. package/mcp-server/lib/options.mjs +9 -2
  24. package/mcp-server/package.json +1 -1
  25. package/package.json +1 -1
  26. package/portal-ui/dist/assets/{index-CsCuPshD.css → index-Cv-E_agg.css} +199 -86
  27. package/portal-ui/dist/assets/{index-CcFjgM78.js → index-DWYCsOCJ.js} +514 -380
  28. package/portal-ui/dist/index.html +2 -2
  29. package/portal-ui/package.json +1 -1
  30. package/providers/clarivate/src/core.js +5 -0
  31. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  32. package/providers/oauth-mcp-bridge/package.json +1 -1
  33. package/scripts/e2e-unread-terminals.mjs +1 -1
  34. package/scripts/e2e.mjs +114 -9
  35. package/scripts/revisit-render-check.mjs +22 -4
  36. package/scripts/travelling-predicates.mjs +1 -1
  37. package/shared/connect-clients.mjs +282 -321
  38. package/shared/names-in-force.mjs +1 -0
  39. package/shared/stdio-connect.mjs +63 -0
  40. package/shared/store-in-repo.mjs +50 -2
@@ -148,8 +148,8 @@
148
148
  "todos": 0
149
149
  },
150
150
  "a-company-created-in-the-browser-is-a-whole-company.test.mjs": {
151
- "tests": 7,
152
- "asserts": 36,
151
+ "tests": 11,
152
+ "asserts": 61,
153
153
  "skips": 0,
154
154
  "todos": 0
155
155
  },
@@ -370,8 +370,8 @@
370
370
  "todos": 0
371
371
  },
372
372
  "a-knockout-filing-links-to-its-office.test.mjs": {
373
- "tests": 12,
374
- "asserts": 56,
373
+ "tests": 15,
374
+ "asserts": 76,
375
375
  "skips": 0,
376
376
  "todos": 0
377
377
  },
@@ -1366,8 +1366,8 @@
1366
1366
  "todos": 0
1367
1367
  },
1368
1368
  "connect-clients-are-data.test.mjs": {
1369
- "tests": 12,
1370
- "asserts": 46,
1369
+ "tests": 15,
1370
+ "asserts": 75,
1371
1371
  "skips": 0,
1372
1372
  "todos": 0
1373
1373
  },
@@ -1397,7 +1397,7 @@
1397
1397
  },
1398
1398
  "connector-annotations-and-pack.test.mjs": {
1399
1399
  "tests": 7,
1400
- "asserts": 11,
1400
+ "asserts": 12,
1401
1401
  "skips": 0,
1402
1402
  "todos": 0
1403
1403
  },
@@ -1804,8 +1804,8 @@
1804
1804
  "todos": 0
1805
1805
  },
1806
1806
  "doctor-reads-the-units-not-the-shell.test.mjs": {
1807
- "tests": 24,
1808
- "asserts": 67,
1807
+ "tests": 30,
1808
+ "asserts": 85,
1809
1809
  "skips": 0,
1810
1810
  "todos": 0
1811
1811
  },
@@ -2026,8 +2026,8 @@
2026
2026
  "todos": 0
2027
2027
  },
2028
2028
  "engine.anthropic.test.mjs": {
2029
- "tests": 60,
2030
- "asserts": 215,
2029
+ "tests": 61,
2030
+ "asserts": 222,
2031
2031
  "skips": 0,
2032
2032
  "todos": 0
2033
2033
  },
@@ -2188,8 +2188,8 @@
2188
2188
  "todos": 0
2189
2189
  },
2190
2190
  "exit1-artifact-rescue.test.mjs": {
2191
- "tests": 20,
2192
- "asserts": 79,
2191
+ "tests": 25,
2192
+ "asserts": 101,
2193
2193
  "skips": 0,
2194
2194
  "todos": 0
2195
2195
  },
@@ -3058,8 +3058,8 @@
3058
3058
  "todos": 0
3059
3059
  },
3060
3060
  "portal-service.test.mjs": {
3061
- "tests": 122,
3062
- "asserts": 656,
3061
+ "tests": 123,
3062
+ "asserts": 662,
3063
3063
  "skips": 0,
3064
3064
  "todos": 0
3065
3065
  },
@@ -3322,8 +3322,8 @@
3322
3322
  "todos": 0
3323
3323
  },
3324
3324
  "recipe-service.test.mjs": {
3325
- "tests": 12,
3326
- "asserts": 67,
3325
+ "tests": 16,
3326
+ "asserts": 77,
3327
3327
  "skips": 0,
3328
3328
  "todos": 0
3329
3329
  },
@@ -3478,8 +3478,8 @@
3478
3478
  "todos": 0
3479
3479
  },
3480
3480
  "register-findings-link-to-the-office-record.test.mjs": {
3481
- "tests": 16,
3482
- "asserts": 44,
3481
+ "tests": 19,
3482
+ "asserts": 61,
3483
3483
  "skips": 0,
3484
3484
  "todos": 0
3485
3485
  },
@@ -3665,7 +3665,7 @@
3665
3665
  },
3666
3666
  "report-record-link-host.test.mjs": {
3667
3667
  "tests": 3,
3668
- "asserts": 26,
3668
+ "asserts": 27,
3669
3669
  "skips": 0,
3670
3670
  "todos": 0
3671
3671
  },
@@ -4479,6 +4479,12 @@
4479
4479
  "skips": 0,
4480
4480
  "todos": 0
4481
4481
  },
4482
+ "the-doors-comparison-knows-an-admit-case.test.mjs": {
4483
+ "tests": 7,
4484
+ "asserts": 20,
4485
+ "skips": 0,
4486
+ "todos": 0
4487
+ },
4482
4488
  "the-doubt-carries-its-placement-tier.test.mjs": {
4483
4489
  "tests": 17,
4484
4490
  "asserts": 56,
@@ -4858,8 +4864,8 @@
4858
4864
  "todos": 0
4859
4865
  },
4860
4866
  "the-settings-surface-reads-the-customer-store.test.mjs": {
4861
- "tests": 9,
4862
- "asserts": 37,
4867
+ "tests": 10,
4868
+ "asserts": 43,
4863
4869
  "skips": 0,
4864
4870
  "todos": 0
4865
4871
  },
@@ -5397,8 +5403,8 @@
5397
5403
  "todos": 0
5398
5404
  },
5399
5405
  "describe-options.test.mjs": {
5400
- "tests": 22,
5401
- "asserts": 94,
5406
+ "tests": 23,
5407
+ "asserts": 98,
5402
5408
  "skips": 0,
5403
5409
  "todos": 0
5404
5410
  },
@@ -5757,7 +5763,7 @@
5757
5763
  },
5758
5764
  "nav.test.ts": {
5759
5765
  "tests": 12,
5760
- "asserts": 55,
5766
+ "asserts": 57,
5761
5767
  "skips": 0,
5762
5768
  "todos": 0
5763
5769
  },
@@ -5768,8 +5774,8 @@
5768
5774
  "todos": 0
5769
5775
  },
5770
5776
  "newCompanyReachableFromProfile.test.ts": {
5771
- "tests": 3,
5772
- "asserts": 7,
5777
+ "tests": 5,
5778
+ "asserts": 13,
5773
5779
  "skips": 0,
5774
5780
  "todos": 0
5775
5781
  },
@@ -5907,7 +5913,7 @@
5907
5913
  },
5908
5914
  "useYourAI.test.ts": {
5909
5915
  "tests": 13,
5910
- "asserts": 46,
5916
+ "asserts": 41,
5911
5917
  "skips": 0,
5912
5918
  "todos": 0
5913
5919
  },
@@ -1,5 +1,13 @@
1
1
  # trademark-artifacts-mcp
2
2
 
3
+ ## 0.3.0-beta.4
4
+
5
+ No changes in this release.
6
+
7
+ ## 0.3.0-beta.3
8
+
9
+ No changes in this release.
10
+
3
11
  ## 0.3.0-beta.2
4
12
 
5
13
  ### Patch Changes
@@ -26,6 +26,8 @@ export { koPaths } from "../../driver/stages-knockout.mjs";
26
26
  // this same function rather than from a second reading of the numbers. office-record-links.mjs imports
27
27
  // nothing, so the read-only server stays light.
28
28
  export { addressListedFilings } from "../../driver/publish/office-record-links.mjs";
29
+ // Which registers publish no page per record, as publish decides it: the sidecar's register, looked up.
30
+ export { declaredRecordOrigins } from "../../driver/record-origins.mjs";
29
31
 
30
32
  // stripInternal/stripEngineInternals/stripTelemetry are the driver's OWN client-safety transforms — the
31
33
  // same ones publish/render.mjs applies to the client HTML export. lib/scrub.mjs composes them so the MCP
@@ -27,7 +27,7 @@
27
27
  import { existsSync, readFileSync, readdirSync } from "node:fs";
28
28
  import { basename, join } from "node:path";
29
29
 
30
- import { koPaths, addressListedFilings } from "./driver.mjs";
30
+ import { koPaths, addressListedFilings, declaredRecordOrigins } from "./driver.mjs";
31
31
  import { driverDir } from "../../shared/driver-dir.mjs";
32
32
 
33
33
  const readJson = (p) => { try { return JSON.parse(readFileSync(p, "utf8")); } catch { return null; } };
@@ -213,7 +213,7 @@ export function knockoutEvidence(run) {
213
213
  // office's own page, or the office and the number and why that is not a link. This view states the same,
214
214
  // from the same function over the same sidecar, so the connector and the report cannot give one filing
215
215
  // two links. On any other register the call sets nothing, and every row is exactly as it was.
216
- if (Array.isArray(recordsDoc?.marks)) addressListedFilings(recordsDoc);
216
+ if (Array.isArray(recordsDoc?.marks)) addressListedFilings(recordsDoc, declaredRecordOrigins(recordsDoc?.provider));
217
217
  const out = [];
218
218
 
219
219
  for (const entry of (Array.isArray(recordsDoc?.marks) ? recordsDoc.marks : [])) {
@@ -238,11 +238,18 @@ function accountFor(key, { scope, now }) {
238
238
  * wherever it resolves — so there is no shut state left to probe, and the invitation cannot disagree with
239
239
  * the enforcement because neither consults an environment.
240
240
  *
241
- * An account that simply has no saved searches gets an empty list and no note.
241
+ * An account that simply has no saved searches gets an empty list and no note. A store that cannot be
242
+ * READ gets the empty list AND a note, because those are different facts, and an assistant told "none"
243
+ * repeats it to the client as the answer. The note names no variable and no path: its reader may be a
244
+ * client, and the reason belongs to whoever runs the installation, whose `clearotron doctor` names it.
242
245
  */
243
246
  function savedSearchesFor(key) {
244
247
  let recipes;
245
- try { recipes = loadRecipes({ force: true, proseGuard: recipeProseGuard }); } catch { return { savedSearches: [], savedSearchesNote: null }; }
248
+ try { recipes = loadRecipes({ force: true, proseGuard: recipeProseGuard }); }
249
+ catch {
250
+ return { savedSearches: [], savedSearchesNote: "Saved searches could not be read on this installation, so none are "
251
+ + "listed. That is not the same as having none, and the people who run the installation can see why." };
252
+ }
246
253
  return {
247
254
  savedSearches: [...recipes.entries()]
248
255
  .filter(([k, r]) => k.startsWith(`${key}/`) && !r.archived)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trademark-artifacts-mcp",
3
- "version": "0.3.0-beta.2",
3
+ "version": "0.3.0-beta.4",
4
4
  "license": "AGPL-3.0-only",
5
5
  "private": true,
6
6
  "description": "MCP server to interrogate clearotron trademark-clearance runs — list/read artifacts, trace the full decision flow, telemetry/cost, coverage, single-run search, and a gated single-step what-if. Imports the clearotron-driver read-only; touches no driver/template/deploy files.",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clearotron",
3
3
  "type": "module",
4
- "version": "0.3.0-beta.2",
4
+ "version": "0.3.0-beta.4",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
7
7
  "type": "git",
@@ -2205,127 +2205,249 @@ input.filter {
2205
2205
  }
2206
2206
 
2207
2207
 
2208
- .ai-connect-head { margin-top: 28px; }
2208
+ /* ── WHAT YOU CAN DO ─────────────────────────────────────────────────────────────────────────────── */
2209
+ .ai-can { margin-top: 20px; max-width: 860px; }
2209
2210
 
2210
- /* ── ONE QUESTION, WHEN THE DEPLOYMENT LEAVES IT OPEN ─────────────────────────── */
2211
- .ai-where { margin: 0 0 14px; max-width: var(--screen-measure); }
2211
+ .ai-screen .ai-can h2 { font-size: 14.5px; margin: 0 0 3px; }
2212
2212
 
2213
- .ai-where-q { margin: 0 0 8px; font-size: 15px; color: var(--text-strong); }
2213
+ .ai-can-list {
2214
+ list-style: none;
2215
+ margin: 8px 0 0;
2216
+ padding: 0;
2217
+ display: grid;
2218
+ grid-template-columns: 1fr 1fr;
2219
+ gap: 8px 28px;
2220
+ }
2214
2221
 
2215
- .ai-seg { display: inline-flex; border: 1px solid var(--border-hairline); border-radius: 7px; overflow: hidden; }
2222
+ @media (max-width: 700px) { .ai-can-list { grid-template-columns: 1fr; } }
2216
2223
 
2217
- .ai-seg-btn {
2218
- appearance: none;
2219
- border: 0;
2220
- background: transparent;
2221
- padding: 7px 14px;
2222
- font: inherit;
2224
+ .ai-can-list li { display: flex; gap: 9px; font-size: 14px; line-height: 1.5; color: var(--text-body); }
2225
+
2226
+ .ai-can-list li svg { margin-top: 3px; color: var(--accent-quiet); }
2227
+
2228
+ .ai-can-foot {
2229
+ margin: 13px 0 0;
2230
+ padding-top: 11px;
2231
+ border-top: 1px solid var(--border-hairline);
2232
+ font-size: 13.5px;
2223
2233
  color: var(--text-muted);
2224
- cursor: pointer;
2225
2234
  }
2226
2235
 
2227
- .ai-seg-btn + .ai-seg-btn { border-left: 1px solid var(--border-hairline); }
2228
-
2229
- .ai-seg-btn[data-on] { background: var(--surface-sunken); color: var(--text-strong); }
2236
+ .ai-can-foot strong { color: var(--text-strong); }
2230
2237
 
2231
- .ai-where-sub { margin: 10px 0 14px; color: var(--text-muted); max-width: var(--screen-measure); }
2238
+ .ai-connect { margin-top: 34px; }
2232
2239
 
2233
- .ai-none { margin: 0; max-width: var(--screen-measure); color: var(--text-muted); line-height: 1.65; }
2240
+ .ai-screen .ai-connect-head { margin: 0 0 12px; }
2234
2241
 
2235
- .ai-none strong { color: var(--text-strong); }
2242
+ /* ── THE WHERE-CHOICE ─────────────────────────────────────────────────────────────────────────────
2243
+ ONE control, the same size before and after a pick. Selection is shown the way every other choice in
2244
+ the portal shows it — accent wash, accent border, filled radio — and never a second colour. */
2245
+ .where-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
2236
2246
 
2237
- /* ── THE DESTINATION LIST ─────────────────────────────────────────────────────────────────────────── */
2238
- .ai-dests {
2239
- display: flex;
2240
- flex-direction: column;
2241
- gap: 8px;
2242
- max-width: var(--screen-measure);
2243
- }
2247
+ @media (max-width: 640px) { .where-grid { grid-template-columns: 1fr; } }
2244
2248
 
2245
- /* ONE HEIGHT AND ONE WIDTH, ALWAYS. The owner reported the old row set as "stacked, inconsistent
2246
- heights"; more importantly a row that resizes when it is pressed moves its neighbours, which is the
2247
- reflow this rebuild exists to end. */
2248
- .ai-dest {
2249
+ .where-card {
2249
2250
  display: flex;
2250
2251
  align-items: center;
2251
- justify-content: space-between;
2252
- gap: 16px;
2253
- width: 100%;
2254
- min-height: 46px;
2255
- padding: 8px 14px;
2256
- border: 1px solid var(--border-hairline);
2257
- border-radius: 8px;
2252
+ gap: 12px;
2253
+ padding: 18px;
2254
+ border: 1px solid var(--border-strong);
2255
+ border-radius: 12px;
2258
2256
  background: var(--surface-raised);
2259
2257
  font: inherit;
2258
+ font-size: 16px;
2259
+ font-weight: 700;
2260
+ line-height: 1.35;
2261
+ color: var(--text-strong);
2260
2262
  text-align: left;
2261
2263
  cursor: pointer;
2262
2264
  }
2263
2265
 
2264
- .ai-dest[data-selected] { border-color: var(--accent); }
2266
+ .where-card:hover { border-color: var(--accent-ink); }
2265
2267
 
2266
- /* THE PRESSED CONTROL IS THE CONFIRMATION. Solid accent, not a muted caption underneath — the previous
2267
- feedback was grey text below the button and the owner could not see it happen. */
2268
- .ai-dest[data-copied] {
2269
- background: var(--accent);
2270
- border-color: var(--accent);
2271
- color: var(--text-on-accent);
2268
+ .where-card[aria-pressed='true'],
2269
+ .where-card[aria-pressed='true']:hover { border-color: var(--accent); background: var(--accent-wash); }
2270
+
2271
+ .ai-none { margin: 22px 0 0; max-width: 860px; border-left-color: var(--tone-medium); }
2272
+
2273
+ .ai-none p { margin: 6px 0 0; color: var(--text-muted); font-size: 13.5px; line-height: 1.55; }
2274
+
2275
+ /* ── THE APP LIST AND THE PANEL BESIDE IT ─────────────────────────────────────────────────────── */
2276
+ .ai-connect-grid {
2277
+ display: grid;
2278
+ grid-template-columns: minmax(260px, 360px) minmax(0, 1fr);
2279
+ gap: 20px;
2280
+ margin-top: 22px;
2281
+ align-items: start;
2272
2282
  }
2273
2283
 
2274
- .ai-dest[data-copied] .ai-dest-sub { color: inherit; opacity: 0.85; }
2284
+ @media (max-width: 860px) { .ai-connect-grid { grid-template-columns: 1fr; } }
2285
+
2286
+ .ai-apps { display: flex; flex-direction: column; gap: 8px; margin-top: 6px; }
2287
+
2288
+ /* The New Clearance picker's radio-row, centred because these rows carry a name and at most one line. */
2289
+ .ai-app { width: 100%; align-items: center; font: inherit; }
2275
2290
 
2276
- .ai-dest-name { display: flex; flex-direction: column; gap: 2px; color: var(--text-strong); }
2291
+ .ai-app-label { min-width: 0; }
2277
2292
 
2278
- .ai-dest[data-copied] .ai-dest-name { color: inherit; }
2293
+ .ai-app-name { display: block; font-size: 14px; font-weight: 700; color: var(--text-strong); }
2279
2294
 
2280
- .ai-dest-sub { font-size: 12.5px; color: var(--text-faint); }
2295
+ .ai-app-sub { display: block; margin-top: 1px; font-size: 12px; line-height: 1.5; color: var(--text-muted); }
2296
+
2297
+ .ai-app-go { margin-left: auto; display: inline-flex; color: var(--text-accent); opacity: 0; }
2298
+
2299
+ .pick-row-on .ai-app-go { opacity: 1; }
2300
+
2301
+ /* ── THE RESERVED SLOT ──────────────────────────────────────────────────────────────────────────────
2302
+ A MINIMUM HEIGHT WHETHER OR NOT ANYTHING IS IN IT, and the page raises it to the tallest panel this
2303
+ card can show — so neither picking an app nor pressing Copy moves anything around it. */
2304
+ .ai-slot { position: relative; min-height: 180px; }
2305
+
2306
+ /* The hidden copy of every panel the slot's height is taken from. Laid out at the slot's own width, never
2307
+ seen, never reached by a pointer or a keyboard. */
2308
+ .ai-probe {
2309
+ position: absolute;
2310
+ top: 0;
2311
+ left: 0;
2312
+ right: 0;
2313
+ display: grid;
2314
+ align-items: start;
2315
+ visibility: hidden;
2316
+ pointer-events: none;
2317
+ }
2281
2318
 
2282
- /* THE SLOT BOTH LABELS SHARE. Laid out with a grid so the cell is as wide as the WIDER of the two, in
2283
- every row, whether or not it has been pressed — swapping the text then costs zero layout. */
2284
- .ai-dest-say { display: grid; font-size: 13px; }
2319
+ /* Stacked in one cell, so the copy is as tall as its tallest panel and never taller than the slot. */
2320
+ .ai-probe > * { grid-area: 1 / 1; }
2285
2321
 
2286
- .ai-dest-idle, .ai-dest-done { grid-area: 1 / 1; white-space: nowrap; }
2322
+ .steps-panel {
2323
+ border: 1px solid var(--border-strong);
2324
+ border-left: 3px solid var(--accent);
2325
+ border-radius: 12px;
2326
+ background: var(--surface-raised);
2327
+ padding: 18px 20px 20px;
2328
+ }
2287
2329
 
2288
- .ai-dest-idle { color: var(--text-faint); }
2330
+ .ai-slot > .steps-panel { min-height: inherit; }
2289
2331
 
2290
- .ai-dest-done { color: inherit; font-weight: 600; visibility: hidden; }
2332
+ .steps-panel-empty {
2333
+ border-left-color: var(--border-strong);
2334
+ display: flex;
2335
+ align-items: center;
2336
+ justify-content: center;
2337
+ }
2291
2338
 
2292
- .ai-dest[data-copied] .ai-dest-idle { visibility: hidden; }
2339
+ .steps-empty { display: flex; align-items: center; gap: 9px; color: var(--text-faint); font-size: 14px; }
2293
2340
 
2294
- .ai-dest[data-copied] .ai-dest-done { visibility: visible; }
2341
+ .ai-screen .steps-name { margin: 3px 0 0; font-size: 18px; font-weight: 700; color: var(--text-strong); }
2295
2342
 
2296
- /* ── THE RESERVED SLOT ────────────────────────────────────────────────────────────────────────────── */
2297
- /* A MINIMUM HEIGHT WHETHER OR NOT ANYTHING IS IN IT. This is what makes "nothing above the panel moves"
2298
- a property of the layout rather than a hope about content sizes. */
2299
- .ai-slot {
2343
+ /* ── THE NUMBERED STEPS ─────────────────────────────────────────────────────────────────────────── */
2344
+ ol.steps {
2345
+ list-style: none;
2346
+ counter-reset: step;
2300
2347
  margin: 16px 0 0;
2301
- min-height: 240px;
2302
- max-width: var(--screen-measure);
2348
+ padding: 0;
2349
+ display: flex;
2350
+ flex-direction: column;
2351
+ gap: 16px;
2303
2352
  }
2304
2353
 
2305
- .ai-slot-empty { margin: 0; color: var(--text-faint); }
2354
+ ol.steps > li {
2355
+ counter-increment: step;
2356
+ display: grid;
2357
+ grid-template-columns: 24px minmax(0, 1fr);
2358
+ gap: 11px;
2359
+ }
2306
2360
 
2307
- .ai-panel {
2308
- padding: 14px 16px;
2309
- border: 1px solid var(--border-hairline);
2310
- border-radius: 8px;
2361
+ ol.steps > li::before {
2362
+ content: counter(step);
2363
+ width: 23px;
2364
+ height: 23px;
2365
+ margin-top: -1px;
2366
+ display: flex;
2367
+ align-items: center;
2368
+ justify-content: center;
2369
+ border-radius: 50%;
2311
2370
  background: var(--surface-sunken);
2371
+ border: 1px solid var(--border-hairline);
2372
+ font-family: var(--font-mono);
2373
+ font-size: 11px;
2374
+ font-weight: 600;
2375
+ color: var(--text-muted);
2312
2376
  }
2313
2377
 
2314
- .ai-panel-head { margin: 0 0 8px; font-size: 15px; color: var(--text-strong); }
2378
+ .step-text { font-size: 14px; line-height: 1.55; color: var(--text-body); }
2379
+
2380
+ .step-text b { color: var(--text-strong); }
2381
+
2382
+ .step-hint { margin-top: 4px; font-size: 12.5px; line-height: 1.5; color: var(--text-muted); }
2315
2383
 
2316
- .ai-panel-landed { margin: 0 0 10px; font-size: 13px; color: var(--text-muted); }
2384
+ .ai-code {
2385
+ padding: 1px 5px;
2386
+ border: 1px solid var(--border-hairline);
2387
+ border-radius: 4px;
2388
+ background: var(--surface-sunken);
2389
+ font-family: var(--font-mono);
2390
+ font-size: 12px;
2391
+ color: var(--text-strong);
2392
+ overflow-wrap: anywhere;
2393
+ }
2317
2394
 
2318
- .ai-panel-steps {
2395
+ .codeblock {
2396
+ margin-top: 8px;
2397
+ display: flex;
2398
+ align-items: flex-start;
2399
+ gap: 10px;
2400
+ padding: 10px 10px 10px 13px;
2401
+ border: 1px solid var(--border-hairline);
2402
+ border-radius: 10px;
2403
+ background: var(--surface-sunken);
2404
+ }
2405
+
2406
+ .codeblock pre {
2407
+ flex: 1;
2408
+ min-width: 0;
2319
2409
  margin: 0;
2320
- padding-left: 20px;
2321
- line-height: 1.7;
2322
- color: var(--text-body);
2410
+ padding-top: 3px;
2411
+ overflow-x: auto;
2412
+ font-family: var(--font-mono);
2413
+ font-size: 12px;
2414
+ line-height: 1.6;
2415
+ color: var(--text-strong);
2323
2416
  }
2324
2417
 
2325
- .ai-panel-stamp { margin: 10px 0 0; font-size: 12.5px; color: var(--text-faint); }
2418
+ .codeblock .btn-ghost { padding: 6px 12px; white-space: nowrap; }
2419
+
2420
+ .secret-btn { margin-top: 8px; }
2421
+
2422
+ /* THE PRESSED CONTROL IS THE CONFIRMATION: solid accent on the button pressed, never a muted caption. */
2423
+ .is-copied,
2424
+ .is-copied:hover,
2425
+ .btn-primary.is-copied:hover:not(:disabled) { background: var(--accent); border-color: var(--accent); color: var(--text-on-accent); }
2426
+
2427
+ .landed {
2428
+ margin-top: 10px;
2429
+ padding: 10px 13px;
2430
+ border: 1px solid var(--border-hairline);
2431
+ border-radius: 10px;
2432
+ background: var(--surface-sunken);
2433
+ }
2434
+
2435
+ .landed-label {
2436
+ font-size: 10px;
2437
+ font-weight: 700;
2438
+ letter-spacing: 0.13em;
2439
+ text-transform: uppercase;
2440
+ color: var(--text-faint);
2441
+ }
2442
+
2443
+ .landed .mono { font-size: 12px; color: var(--text-strong); overflow-wrap: anywhere; }
2444
+
2445
+ .landed .mono + .mono { margin-top: 2px; }
2326
2446
 
2327
2447
  .ai-said { margin: 0 0 6px; color: var(--text-muted); }
2328
2448
 
2449
+ .ai-failed { margin: 10px 0 0; }
2450
+
2329
2451
  .ai-pre {
2330
2452
  margin: 0;
2331
2453
  padding: 10px;
@@ -2335,21 +2457,12 @@ input.filter {
2335
2457
  font-size: 13px;
2336
2458
  }
2337
2459
 
2338
- /* The help a person needs, on the page. The link to the document keeps an honest label beside it. */
2339
- .ai-help { margin: 30px 0 0; max-width: var(--screen-measure); }
2340
-
2341
- .ai-help > summary {
2342
- cursor: pointer;
2343
- color: var(--text-muted);
2344
- font-size: 13px;
2345
- font-style: italic;
2346
- }
2347
-
2348
- .ai-help-body { margin: 10px 0 0; color: var(--text-muted); line-height: 1.65; }
2460
+ /* The help a person needs, as a quiet notice. The link opens the document where a reader's own AI can
2461
+ read it too. */
2462
+ .ai-help { margin-top: 30px; max-width: 860px; }
2349
2463
 
2350
- .ai-help-body p { margin: 0 0 10px; }
2464
+ .ai-help p { margin: 6px 0 0; color: var(--text-muted); font-size: 13.5px; line-height: 1.55; }
2351
2465
 
2352
- .ai-help-doc { font-size: 13px; }
2353
2466
 
2354
2467
  /* ── Give someone access: the points on the tree a new person can be given ─────────────────────────────
2355
2468
  One row per point. A BUTTON, because choosing a point is an action and a keyboard has to reach it; the