@booboo-brain/panel 0.5.3 → 0.5.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.
package/dist/index.js CHANGED
@@ -1,73 +1,171 @@
1
1
  // src/Panel.tsx
2
2
  import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
3
- import { orgBootSlice } from "@booboo-brain/spec";
3
+ import { orgBootSlice, relTime } from "@booboo-brain/spec";
4
4
 
5
5
  // src/panel-css.ts
6
6
  var PANEL_CSS = String.raw`
7
+ /* GENERATED from design/tokens.json by scripts/gen-tokens.mjs — DO NOT EDIT */
8
+ :root {
9
+ --bg: #06080e;
10
+ --bg-lift: #0a0d15;
11
+ --card: #0f131c;
12
+ --line: #1c2130;
13
+ --line-hi: #2b3243;
14
+ --text: #E8DCC4;
15
+ --dim: #8a8268;
16
+ --faint: #585240;
17
+ --gold: #c9a04a;
18
+ --gold-hi: #E8C877;
19
+ --teal: #4ECDC4;
20
+ --violet: #a78bd0;
21
+ --green: #5fae7e;
22
+ --amber: #d6a23e;
23
+ --red: #d05a5a;
24
+ --wine: #0b0509;
25
+ --wine2: #140a11;
26
+ --plate: #1c1119;
27
+ --plate-lift: #241624;
28
+ --brass: #d9a05b;
29
+ --brass-hi: #efc389;
30
+ --brass-deep: #a9753a;
31
+ --mint: #8be9b0;
32
+ --cream: #f4eee2;
33
+ --cream-dim: #b3a897;
34
+ --cream-faint: #7c7365;
35
+ --verb-reports-to: #c9a04a;
36
+ --verb-declares: #E8C877;
37
+ --verb-amends: #E8C877;
38
+ --verb-inherits: #8a8268;
39
+ --verb-owns: #4ECDC4;
40
+ --verb-reads: #3a7a74;
41
+ --verb-escalates-to: #d05a5a;
42
+ --verb-covers: #5fae7e;
43
+ --verb-supplies: #a8815a;
44
+ --verb-audits: #a78bd0;
45
+ --verb-spine: #29242f;
46
+ --display: Fraunces, ui-serif, Georgia, serif;
47
+ --ui: 'DM Sans', ui-sans-serif, system-ui, sans-serif;
48
+ --mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace;
49
+ --ease-swift: cubic-bezier(0.2, 0.7, 0.25, 1);
50
+ --ease-settle: cubic-bezier(0.16, 1, 0.3, 1);
51
+ --z-canvas: 0;
52
+ --z-hud: 10;
53
+ --z-dossier: 20;
54
+ --z-palette: 30;
55
+ --z-toast: 40;
56
+ }
57
+
7
58
  /* Booboo Organigram — alive, simple, for everyone. One accent (verdigris,
8
59
  the dachshund's collar), a slow aurora breathing behind everything, cards
9
60
  that lift and settle, numbers that count up. Dark by default. */
10
61
 
11
62
  :root {
12
- --bg: #0b0c10;
13
- --bg-1: #10131a;
14
- --bg-2: #171b24;
15
- --line: #242a36;
16
- --ink: #e9edf3;
17
- --ink-2: #9aa7b6;
18
- --ink-3: #5d6776;
19
- --accent: #4fb8a0;
20
- --accent-2: #7fd4c0;
21
- --accent-dim: rgba(79, 184, 160, 0.13);
22
- --warn: #e0b45f;
23
- --font: -apple-system, "Segoe UI", Inter, system-ui, sans-serif;
24
- --mono: ui-monospace, "SF Mono", Menlo, monospace;
63
+ /* THE STAFF BOARD — a master-key rack, not a dashboard.
64
+ Structure comes from two real artefacts: a master-key hierarchy (passe
65
+ général → passe partiel → cylinder, which is literally how a hotel is
66
+ keyed, and exactly how authority cascades here) and a ministry organigram
67
+ (rank is NAMED in column headers, never inferred from indentation).
68
+ Material comes from the approved FHQ hero designs — wine-black ground,
69
+ brass rails, real vendor marks on white chips.
70
+ Raw values are generated into app/tokens.css from design/tokens.json
71
+ (scripts/gen-tokens.mjs); these are the panel's semantic aliases over them.
72
+ RANK READS THROUGH FINISH, NEVER HUE. The old board coloured each card by
73
+ hash(agent.id) — nine departments, nine arbitrary neon hues — which is the
74
+ loudest "generated by a dev tool" signal there is, and it disagreed with
75
+ the cosmos's own gold/silver/bronze ladder. */
76
+ /* LIGHT IS THE DEFAULT (Jesse, 2026-07-19: "design for everyone — a white
77
+ background, very light bubbles, beautiful refined display"). A whole hotel
78
+ uses this, not a room of developers; a stranger should not land in a
79
+ terminal. Dark remains, one toggle away, below.
80
+ The ground is a WARM white, never flat #fff — a lit room, not a sheet of
81
+ paper. Depth comes from layered shadow, so ink stays the only dark thing
82
+ on screen and brass drops from material to ~5% accent. */
83
+ --bg: #fcfbf9;
84
+ --bg-1: #fffefb; /* tinted toward the brass hue, never flat #fff */
85
+ --bg-2: #f6f4f0;
86
+ --line: #e9e5df;
87
+ --line-soft: #f0ece6;
88
+ --line-2: #d9d3c9;
89
+ --surface-tray: rgba(255, 255, 255, .5);
90
+ /* the ground-plane dot. White-on-white rendered NOTHING on the light board
91
+ — the texture was there in the stylesheet and absent on screen. */
92
+ --dot: rgba(27, 24, 21, 0.055);
93
+ --surface-lift: linear-gradient(180deg, #fffdf8, #ffffff);
94
+ --surface-lift-edge: #e8d9bd;
95
+ --ink: #1b1815;
96
+ --ink-2: #6b6459;
97
+ --ink-3: #9a9287;
98
+ --accent: #a9753a;
99
+ --accent-2: #c9954f;
100
+ --accent-dim: #f7f0e4;
101
+ --warn: #b8862c;
102
+ --warn-wash: #fbf3e3;
103
+ --ok: #3f9c6d;
104
+ --ok-wash: #eaf5ef;
105
+ --fail: #c0553f;
106
+ --fail-wash: #fbeeea;
107
+ /* one shadow set, three depths — the only source of elevation */
108
+ --sh-1: 0 1px 2px rgba(27, 24, 21, .04), 0 1px 1px rgba(27, 24, 21, .03);
109
+ --sh-2: 0 2px 4px rgba(27, 24, 21, .04), 0 6px 16px -6px rgba(27, 24, 21, .10);
110
+ --sh-3: 0 4px 8px rgba(27, 24, 21, .05), 0 16px 34px -12px rgba(27, 24, 21, .16);
111
+ /* motion — slight, never showy. Everything eases on these two. */
112
+ --ease: cubic-bezier(.2, .7, .25, 1);
113
+ --settle: cubic-bezier(.16, 1, .3, 1);
114
+ --font: "DM Sans", -apple-system, "Segoe UI", Inter, system-ui, sans-serif;
115
+ --mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;
116
+ --display: "Fraunces", ui-serif, Georgia, serif;
117
+ --brass-line: linear-gradient(180deg, var(--brass-hi), var(--brass) 55%, var(--brass-deep));
25
118
  }
26
119
 
27
- /* LIGHT theme same verdigris accent, swapped surfaces. Toggle in the top bar. */
28
- :root[data-theme="light"] {
29
- --bg: #f5f7f9;
30
- --bg-1: #ffffff;
31
- --bg-2: #eef1f4;
32
- --line: #dbe1e8;
33
- --ink: #182029;
34
- --ink-2: #55636f;
35
- --ink-3: #8b97a3;
36
- --accent: #1e8f79;
37
- --accent-2: #12796a;
38
- --accent-dim: rgba(30, 143, 121, 0.12);
39
- --warn: #b57b1e;
40
- }
41
- [data-theme="light"] .bar,
42
- [data-theme="light"] .tabs { background: rgba(255, 255, 255, 0.82); }
43
- [data-theme="light"] .btn.primary { color: #ffffff; }
44
- [data-theme="light"] .pnl-aurora { opacity: 0.32; }
45
- [data-theme="light"] .doss-rep.tap:hover { background: rgba(0, 0, 0, 0.03); }
46
- [data-theme="light"] .bk-card:hover,
47
- [data-theme="light"] .mem-row:hover { box-shadow: 0 8px 22px rgba(20, 40, 60, 0.08); }
48
- /* surfaces that hardcode a dark tint — retint for the light theme */
49
- [data-theme="light"] .oc-row.wrap { background: rgba(20, 40, 60, 0.04); }
50
- [data-theme="light"] .doss { background: rgba(255, 255, 255, 0.9); }
51
- [data-theme="light"] .ag {
52
- background: linear-gradient(180deg, hsl(var(--h) 42% 93% / 0.9), var(--bg-1) 60%);
53
- border-color: hsl(var(--h) 30% 78%);
54
- }
55
- [data-theme="light"] .tl-ava { background: hsl(var(--h) 45% 90% / 0.9); border-color: hsl(var(--h) 40% 75% / 0.5); }
56
- [data-theme="light"] .oc-tray { background: linear-gradient(180deg, #e7ebef, #eef1f4); box-shadow: inset 0 3px 7px rgba(20, 40, 60, 0.08); }
57
- [data-theme="light"] .oc-mac { background: rgba(20, 40, 60, 0.05); border-color: rgba(20, 40, 60, 0.1); }
120
+ /* DARKthe wine-and-brass board, now one toggle away instead of the default.
121
+ Same structure, same shadows-as-depth logic, inverted ground. */
122
+ :root[data-theme="dark"] {
123
+ --bg: var(--wine);
124
+ --bg-1: var(--plate);
125
+ --bg-2: var(--wine2);
126
+ --line: rgba(244, 238, 226, 0.13);
127
+ --line-soft: rgba(244, 238, 226, 0.08);
128
+ --line-2: rgba(244, 238, 226, 0.22);
129
+ --surface-tray: rgba(11, 5, 9, .4);
130
+ --dot: rgba(255, 255, 255, 0.035);
131
+ --surface-lift: linear-gradient(168deg, #3a2a16, #1d1410);
132
+ --surface-lift-edge: rgba(239, 195, 137, .4);
133
+ --ink: var(--cream);
134
+ --ink-2: var(--cream-dim);
135
+ --ink-3: var(--cream-faint);
136
+ --accent: var(--brass);
137
+ --accent-2: var(--brass-hi);
138
+ --accent-dim: rgba(217, 160, 91, 0.12);
139
+ --warn: var(--brass);
140
+ --warn-wash: rgba(217, 160, 91, 0.10);
141
+ --ok: var(--mint);
142
+ --ok-wash: rgba(139, 233, 176, 0.10);
143
+ --fail: #e07b6a;
144
+ --fail-wash: rgba(224, 123, 106, 0.10);
145
+ --sh-1: 0 1px 2px rgba(0, 0, 0, .5);
146
+ --sh-2: 0 2px 4px rgba(0, 0, 0, .5), 0 16px 34px -18px rgba(0, 0, 0, .85);
147
+ --sh-3: 0 4px 8px rgba(0, 0, 0, .5), 0 22px 48px -18px rgba(0, 0, 0, .9);
148
+ }
149
+ [data-theme="dark"] body { background: var(--wine); }
150
+ [data-theme="dark"] .ag { background: linear-gradient(168deg, var(--plate-lift), #180e16); }
151
+ [data-theme="dark"] .bar, [data-theme="dark"] .tabs { background: rgba(11, 5, 9, 0.72); }
152
+ [data-theme="dark"] .rack .wt { background: #fff; border-color: transparent; }
153
+ [data-theme="dark"] .pnl-stars { opacity: .45; }
58
154
 
59
155
  * { box-sizing: border-box; }
60
156
  body { font-family: var(--font); color: var(--ink); background: var(--bg); overflow-x: hidden; }
61
157
 
62
158
  .pnl { display: flex; flex-direction: column; position: relative; overflow: hidden; }
63
159
 
64
- /* the breatha slow aurora drifting behind everything */
160
+ /* the groundwarm wine light pooling from the top-left, exactly as the hero
161
+ grades it. NO GRAIN/NOISE LAYER: the mockup carried a feTurbulence overlay
162
+ and it read as a blurry filter smeared over everything (Jesse, 2026-07-19).
163
+ The atmosphere is gradient + constellation only. */
65
164
  .pnl-aurora {
66
- position: fixed; inset: -30%; pointer-events: none; z-index: 0; opacity: 0.5;
165
+ position: fixed; inset: -30%; pointer-events: none; z-index: 0; opacity: 1;
67
166
  background:
68
- radial-gradient(38% 30% at 24% 30%, rgba(79, 184, 160, 0.10), transparent 70%),
69
- radial-gradient(30% 26% at 78% 64%, rgba(90, 130, 200, 0.08), transparent 70%),
70
- radial-gradient(24% 22% at 55% 15%, rgba(79, 184, 160, 0.06), transparent 70%);
167
+ radial-gradient(55% 45% at 12% 0%, rgba(201, 149, 79, .055), transparent 62%),
168
+ radial-gradient(45% 40% at 95% 100%, rgba(63, 156, 109, .035), transparent 62%);
71
169
  animation: aurora 48s ease-in-out infinite alternate;
72
170
  }
73
171
  @keyframes aurora {
@@ -87,12 +185,12 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
87
185
  /* top bar */
88
186
  .bar {
89
187
  position: relative; z-index: 2; display: flex; align-items: center; gap: 22px;
90
- padding: 14px 26px; border-bottom: 1px solid var(--line);
91
- background: rgba(16, 19, 26, 0.82); backdrop-filter: blur(10px);
188
+ padding: 14px 26px; border-bottom: 1px solid var(--line-soft);
189
+ background: rgba(252, 251, 249, 0.82); backdrop-filter: blur(10px);
92
190
  flex-wrap: wrap;
93
191
  }
94
- .bar-brand { font-size: 15px; }
95
- .bar-brand b { font-weight: 650; }
192
+ .bar-brand { font-size: 19px; font-family: var(--display); font-weight: 600; letter-spacing: -0.01em; }
193
+ .bar-brand b { font-weight: 600; color: var(--accent-2); }
96
194
  .bar-stats { display: flex; gap: 18px; color: var(--ink-2); font-size: 12.5px; }
97
195
  .bar-stats b { color: var(--ink); font-variant-numeric: tabular-nums; font-size: 14px; }
98
196
  .bar-stats .tap { cursor: pointer; transition: color 0.15s; }
@@ -106,22 +204,23 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
106
204
  .btn {
107
205
  border: 1px solid var(--line); background: var(--bg-2); color: var(--ink);
108
206
  border-radius: 9px; padding: 8px 15px; font-size: 12.5px; cursor: pointer;
109
- transition: border-color 0.15s, transform 0.1s;
207
+ transition: border-color .18s var(--ease), transform .12s var(--ease), box-shadow .2s var(--ease);
110
208
  }
111
209
  .btn:hover { border-color: var(--accent); }
112
210
  .btn:active { transform: scale(0.97); }
113
- .btn.primary { background: var(--accent); border-color: var(--accent); color: #08110e; font-weight: 650; }
211
+ .btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 650; }
114
212
  .btn.primary:disabled { opacity: 0.5; cursor: wait; }
115
213
  .btn.ghost { background: transparent; color: var(--ink-2); }
116
214
 
117
215
  /* tabs */
118
216
  .tabs {
119
217
  position: relative; z-index: 2; display: flex; gap: 4px; padding: 0 22px;
120
- border-bottom: 1px solid var(--line); background: rgba(16, 19, 26, 0.7); backdrop-filter: blur(8px);
218
+ border-bottom: 1px solid var(--line); background: rgba(252, 251, 249, 0.7); backdrop-filter: blur(8px);
121
219
  overflow-x: auto;
122
220
  }
123
221
  .tab {
124
222
  background: none; border: none; border-bottom: 2px solid transparent; color: var(--ink-2);
223
+ transition: color .18s var(--ease), border-color .22s var(--ease);
125
224
  font-size: 12.5px; padding: 12px 15px 11px; cursor: pointer; white-space: nowrap;
126
225
  transition: color 0.15s, border-color 0.2s;
127
226
  }
@@ -197,7 +296,7 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
197
296
 
198
297
  /* rules */
199
298
  .rule-list { display: flex; flex-direction: column; gap: 9px; }
200
- .rule-card { background: var(--bg-1); border: 1px solid var(--line); border-left: 2px solid var(--accent); border-radius: 11px; padding: 12px 15px 10px; animation: settle 0.35s ease both; transition: border-color 0.15s; }
299
+ .rule-card { background: var(--bg-1); border: 1px solid var(--line); border-radius: 11px; padding: 12px 15px 10px; animation: settle 0.35s ease both; transition: border-color 0.15s; }
201
300
  .rule-card:hover { border-color: var(--accent); }
202
301
  .rule-top { display: flex; align-items: baseline; gap: 10px; margin-bottom: 5px; }
203
302
  .rule-ref { font-family: var(--mono); font-size: 12.5px; }
@@ -229,19 +328,17 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
229
328
  /* the machine rack — a recessed slot flush under the owner's card */
230
329
  .oc-tray {
231
330
  display: flex; flex-wrap: wrap; gap: 4px; justify-content: center;
232
- max-width: 210px; margin-top: -6px; padding: 10px 8px 7px;
233
- background: linear-gradient(180deg, #0a0c11, #0d1017);
234
- border: 1px solid var(--line); border-top: 0; border-radius: 0 0 14px 14px;
235
- box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.55);
331
+ max-width: 210px; margin-top: -4px; padding: 10px 8px 7px;
332
+ background: transparent; border: 0; padding: 8px 0 0;
236
333
  }
237
- .oc-tray.fail { border-color: rgba(224, 123, 106, 0.55); box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.55), 0 6px 18px -8px rgba(224, 123, 106, 0.3); }
238
- .oc-tray.warn { border-color: rgba(224, 180, 95, 0.45); }
334
+ .oc-tray.fail { border-color: rgba(224, 123, 106, 0.5); box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.5), 0 6px 18px -8px rgba(224, 123, 106, 0.28); }
335
+ .oc-tray.warn { border-color: rgba(217, 160, 91, 0.42); }
239
336
  .oc-mac {
240
- display: inline-flex; align-items: center; gap: 5px;
241
- background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.05);
242
- border-radius: 6px; padding: 4px 8px; cursor: pointer;
243
- font-family: var(--mono); font-size: 10px; color: var(--ink-2);
244
- transition: border-color 0.15s, transform 0.1s;
337
+ display: inline-flex; align-items: center; gap: 6px;
338
+ background: var(--bg-2); border: 1px solid var(--line);
339
+ border-radius: 8px; padding: 5px 9px; cursor: pointer;
340
+ font-family: var(--mono); font-size: 9.5px; color: var(--ink-2);
341
+ transition: border-color .18s var(--ease), transform .12s var(--ease), box-shadow .2s var(--ease);
245
342
  }
246
343
  .oc-mac:hover { border-color: var(--accent); color: var(--ink); transform: translateY(-1px); }
247
344
  .oc-mac.sel { border-color: var(--accent); background: var(--accent-dim); color: var(--ink); }
@@ -249,6 +346,21 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
249
346
  .oc-mac.dragging { opacity: 0.45; cursor: grabbing; }
250
347
  .oc-mac.more:hover { color: var(--accent); }
251
348
  .mac-emoji { font-size: 11px; }
349
+ /* real vendor marks, tinted to the house so twelve logos read as one system.
350
+ Full brand colour on hover — recognition when you want it, calm when you
351
+ don't. */
352
+ /* the hero's treatment: a real vendor mark on a WHITE chip. Twelve tinted
353
+ monochrome logos read as a sticker album; twelve white chips read as a rack
354
+ of real systems — and it is the one thing on the old board that already
355
+ looked genuine, so everything else was raised to it rather than it lowered. */
356
+ .mac-brand {
357
+ width: 24px; height: 24px; flex: 0 0 24px; display: block;
358
+ background: var(--bg-2); border: 1px solid var(--line); border-radius: 7px;
359
+ padding: 4px; box-sizing: border-box;
360
+ transition: transform .2s ease, box-shadow .2s ease;
361
+ }
362
+ .oc-mac:hover .mac-brand,
363
+ .oc-mac.sel .mac-brand { transform: scale(1.06); box-shadow: 0 5px 14px -5px rgba(0, 0, 0, 0.85); }
252
364
  .mac-name { white-space: nowrap; max-width: 76px; overflow: hidden; text-overflow: ellipsis; }
253
365
  .mac-dot { width: 7px; height: 7px; border-radius: 50%; flex: 0 0 7px; display: inline-block; }
254
366
  .mac-dot.ok { background: var(--accent); box-shadow: 0 0 6px rgba(79, 184, 160, 0.7); }
@@ -285,10 +397,35 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
285
397
  .bk-agents.unassigned { color: var(--ink-3); font-style: italic; }
286
398
 
287
399
  .pending {
288
- position: relative; z-index: 2; display: flex; gap: 8px; flex-wrap: wrap;
289
- padding: 10px 26px; border-bottom: 1px solid var(--line); background: rgba(224, 180, 95, 0.06);
400
+ position: relative; z-index: 2; display: flex; gap: 8px; flex-wrap: wrap; align-items: center;
401
+ padding: 10px 26px; border-bottom: 1px solid var(--line); background: var(--warn-wash);
402
+ }
403
+ .pending-lead {
404
+ font-family: var(--mono); font-size: 9px; letter-spacing: .18em; text-transform: uppercase;
405
+ color: var(--warn); margin-right: 2px;
406
+ }
407
+ /* every pending change is its own undo button. A global discard makes one
408
+ mis-drop cost every other edit, so people stop dragging entirely. */
409
+ .pending-item {
410
+ display: inline-flex; align-items: center; gap: 8px; cursor: pointer;
411
+ font-size: 11.5px; font-family: var(--mono); color: var(--ink-2);
412
+ background: var(--bg-1); border: 1px solid var(--line); border-radius: 100px;
413
+ padding: 4px 6px 4px 12px;
414
+ transition: border-color .18s var(--ease), color .18s ease, transform .16s var(--ease);
415
+ }
416
+ .pending-item i {
417
+ font-style: normal; font-size: 8.5px; letter-spacing: .12em; text-transform: uppercase;
418
+ color: var(--warn); border: 1px solid rgba(184,134,44,.3); border-radius: 100px; padding: 2px 7px;
419
+ transition: background .18s ease, color .18s ease;
420
+ }
421
+ .pending-item:hover { border-color: var(--warn); color: var(--ink); transform: translateY(-1px); }
422
+ .pending-item:hover i { background: var(--warn); color: var(--bg-1); }
423
+
424
+ /* the lane lights up as a drop target, so a near-miss is impossible */
425
+ .oc-row.lanes > .oc-child.lane-over {
426
+ border-color: var(--accent); background: var(--accent-dim);
427
+ box-shadow: 0 0 0 3px var(--accent-dim), var(--sh-2);
290
428
  }
291
- .pending-item { font-size: 11.5px; color: var(--warn); font-family: var(--mono); }
292
429
  .pnl-err { position: relative; z-index: 2; padding: 10px 26px; color: #e07b6a; font-size: 12.5px; }
293
430
 
294
431
  /* layout — 100% so a HOST container decides the height (embedded panel);
@@ -301,17 +438,59 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
301
438
  + a verdigris pool anchoring the eye at the root. Static — capture-safe. */
302
439
  background:
303
440
  radial-gradient(900px 420px at 50% 120px, rgba(79, 184, 160, 0.05), transparent 70%),
304
- radial-gradient(circle at 1px 1px, rgba(255, 255, 255, 0.035) 1px, transparent 1.5px);
441
+ radial-gradient(circle at 1px 1px, var(--dot) 1px, transparent 1.5px);
305
442
  background-size: auto, 28px 28px;
306
443
  }
307
- .tree-hint { color: var(--ink-3); font-size: 11.5px; margin: 0 0 20px; text-align: center; }
444
+ .tree-hint { color: var(--ink-3); font-family: var(--mono); font-size: 10px; letter-spacing: .05em; margin: 0 0 14px; text-align: center; }
445
+
446
+ /* ── RANK HEADERS (from the ministry organigram) ───────────────────────────
447
+ Rank is NAMED, never inferred from indentation. Roman numerals + a rule
448
+ under each, spanning the columns the cascade actually occupies. */
449
+ /* four headers over the four real cascade columns: law · gm · departments ·
450
+ staff. Widths mirror .cascade so a header sits over the column it names —
451
+ headers labelling columns that do not exist is worse than no headers. */
452
+ .ranks {
453
+ display: grid;
454
+ grid-template-columns: 232px var(--rail-w, 56px) 268px var(--rail-w, 56px) minmax(232px, 300px) var(--rail-w, 56px) 1fr;
455
+ margin: 0 0 16px; padding: 0 16px; flex: 0 0 auto;
456
+ }
457
+ .rank:nth-child(1) { grid-column: 1; }
458
+ .rank:nth-child(2) { grid-column: 3; }
459
+ .rank:nth-child(3) { grid-column: 5; }
460
+ .rank:nth-child(4) { grid-column: 7; }
461
+ .rank {
462
+ font-family: var(--mono); font-size: 9px; letter-spacing: .2em; text-transform: uppercase;
463
+ color: var(--ink-3); padding-bottom: 8px; border-bottom: 1px solid rgba(217, 160, 91, 0.16);
464
+ position: relative; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
465
+ }
466
+ .rank b { color: var(--brass); font-weight: 500; margin-right: 7px; }
467
+ .rank::after { content: ""; position: absolute; left: 0; bottom: -1px; width: 24px; height: 1px; background: var(--brass); }
468
+
469
+ /* the constellation — the hero's own atmosphere, drawn once behind the rack.
470
+ Gradient + points only; NO grain/noise overlay (it read as a blurry filter
471
+ smeared across the board). */
472
+ .pnl-stars { position: fixed; inset: 0; z-index: 0; pointer-events: none; opacity: 0; width: 100%; height: 100%; }
308
473
 
309
474
  /* THE COMPANY CHART — root on top, branches fanning beneath. Connectors are
310
475
  crafted: curved elbows at the row's ends, branch-hued drops, a glowing
311
476
  junction node where the parent's line meets the bar. Pure CSS. */
312
477
  /* the measuring viewport — flex:1 + min-height:0 lets it consume the height
313
478
  .tree gives it; align/justify center the scaled block for narrow orgs. */
314
- .chart-fit { position: relative; flex: 1; min-height: 0; display: flex; align-items: flex-start; justify-content: center; overflow: hidden; }
479
+ /* Always scrollable. It used to be overflow:hidden unless the user manually
480
+ zoomed — safe only while auto-fit guaranteed the whole board fit one screen.
481
+ A lane board grows downward, so that assumption sealed every lane below the
482
+ fold behind an invisible wall. */
483
+ .chart-fit { position: relative; flex: 1; min-height: 0; display: flex; align-items: flex-start; justify-content: center; overflow: auto; }
484
+ /* transform:scale() does not change layout size, so the scaled block gets a
485
+ spacer sized to its VISUAL footprint — otherwise the scroll extent is the
486
+ unscaled height and you scroll into empty space. */
487
+ /* overflow:hidden clips the UNSCALED layout box of the absolutely-positioned
488
+ chart, which otherwise adds its full natural height to the scroll extent —
489
+ ~900px of empty scroll past the last lane. The visual (scaled) content is
490
+ exactly the spacer's size, so nothing real is clipped. */
491
+ .chart-scaled { position: relative; flex: 0 0 auto; overflow: hidden; }
492
+ .chart-scaled > .chart { position: absolute; top: 0; left: 50%; transform-origin: top center; }
493
+ .chart-fit.zoomed .chart-scaled > .chart { left: 0; transform-origin: top left; }
315
494
  /* user-zoomed: native scroll is the pan; top-left origin keeps the scaled
316
495
  overflow fully reachable (top/center origin would clip the left half). */
317
496
  .chart-fit.zoomed { overflow: auto; justify-content: flex-start; }
@@ -329,35 +508,252 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
329
508
  .zoomer button:hover { background: var(--accent-dim); color: var(--ink); }
330
509
  .zoomer button.on { color: var(--accent); }
331
510
  .zoomer-pct { min-width: 40px; text-align: center; color: var(--ink-3); }
511
+ .zoomer-sep { width: 1px; height: 16px; background: var(--line); margin: 0 2px; }
512
+
513
+ /* ── the ledger shelf: every bucket as a pigeonhole, hover a role to see its
514
+ reach. Docked under the chart, always visible — the memory half of the
515
+ two pillars (structure above, memory below), never a separate tab. */
516
+ .ledger-shelf {
517
+ position: relative; z-index: 2; flex: 0 0 auto; margin-top: 14px;
518
+ padding: 10px 16px 12px; border-radius: 10px;
519
+ border: 1px solid var(--line); background: var(--bg-1); box-shadow: var(--sh-2);
520
+ width: min(1180px, 92vw);
521
+ }
522
+ .shelf-label {
523
+ display: block; font-family: var(--mono); font-size: 9px; letter-spacing: 0.14em;
524
+ text-transform: uppercase; color: var(--brass); margin-bottom: 8px;
525
+ }
526
+ .shelf-slots { display: flex; flex-wrap: wrap; gap: 6px; }
527
+ .shelf-slot {
528
+ font-family: var(--mono); font-size: 10px; color: var(--ink-3);
529
+ background: var(--bg-2); border: 1px solid var(--line); border-radius: 5px;
530
+ padding: 4px 9px; transition: border-color .18s ease, color .18s ease, box-shadow .25s ease, transform .18s ease;
531
+ }
532
+ .shelf-slot.lit {
533
+ color: var(--brass-hi); border-color: var(--brass); transform: translateY(-1px);
534
+ box-shadow: 0 0 0 1px rgba(201, 160, 74, 0.3), 0 6px 16px -6px rgba(201, 160, 74, 0.5);
535
+ background: rgba(201, 160, 74, 0.1);
536
+ }
537
+ .shelf-slot.sealed { color: var(--ink-3); font-style: italic; border-style: dashed; opacity: 0.75; }
538
+ .shelf-slot.sealed.lit { color: var(--ink-3); border-color: var(--line); background: var(--bg-2); box-shadow: none; transform: none; }
539
+ @media (prefers-reduced-motion: reduce) { .shelf-slot { transition: none; } }
540
+
541
+ /* the law toggle in the top bar */
542
+ .law-toggle { font-family: var(--mono); letter-spacing: 0.02em; }
543
+ .law-toggle.on { color: var(--brass-hi); border-color: var(--brass); background: rgba(201, 160, 74, 0.12); }
332
544
  /* min-width:max-content keeps connectors geometrically correct at natural size
333
545
  BEFORE scaling; the measured --fit shrinks the whole block toward its head. */
546
+ /* transform is set inline by Panel.tsx (see the note there) — only origin,
547
+ sizing and the easing live here. */
334
548
  .chart {
335
549
  min-width: max-content; padding: 8px 14px 20px;
336
- transform: scale(var(--fit, 1)); transform-origin: top center;
550
+ transform-origin: top center; position: relative; /* the rail plane's origin */
337
551
  transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1); will-change: transform;
338
552
  }
553
+ /* plates sit above the rail plane */
554
+ .chart .cascade { position: relative; z-index: 1; }
555
+
556
+ /* rank I — the law. Brass-edged like the GM but unfilled: it is a document,
557
+ not an operator. */
558
+ .law-plate { width: 232px; cursor: pointer; }
559
+ .law-plate .ag-ava { font-family: var(--display); color: var(--accent); }
560
+ .law-plate .ag-name { color: var(--accent); font-size: 13px; }
561
+ .law-plate .ag-role { word-break: break-all; }
562
+ /* ── THE CASCADE (master-key structure) ───────────────────────────────────
563
+ Rank runs LEFT TO RIGHT, one column each, exactly as a master-key chart
564
+ reads: THE STANDARD → GM → departments → staff. Two nested grids give the
565
+ four columns; the brass elbows between them are drawn by CascadeRails as
566
+ one measured SVG (orthogonal geometry between arbitrary boxes is not a
567
+ thing CSS pseudo-elements can do honestly). */
568
+ .cascade {
569
+ display: grid;
570
+ grid-template-columns: max-content var(--rail-w, 56px) 1fr;
571
+ /* START, not centre. Centring is right for a 3-branch key chart; with nine
572
+ departments the stack is ~2,200px tall, so it buried the master key and the
573
+ GM below the fold — the two plates that must be seen first. Top-aligned
574
+ also matches the ministry chart's reading order. */
575
+ align-items: start;
576
+ }
577
+ .cascade > .law-plate { grid-column: 1; }
578
+ .cascade > .ocn { grid-column: 3; }
579
+
580
+ /* rank II–IV live inside the root .ocn, which is itself a cascade */
581
+ .cascade > .ocn {
582
+ display: grid;
583
+ /* the lanes column is BOUNDED so staff wrap inside their lane. Left at 1fr
584
+ under .chart's max-content sizing it grew to fit every staff plate on one
585
+ row — 2,400px natural width, which fit-to-width then scaled to 52% and made
586
+ every plate unreadable. Rank IV wraps; the board stays legible. */
587
+ grid-template-columns: max-content var(--rail-w, 56px) minmax(420px, 940px);
588
+ grid-template-areas: "gm rail lanes";
589
+ align-items: start;
590
+ }
591
+ .cascade > .ocn > .casc-head { grid-area: gm; display: flex; flex-direction: column; align-items: stretch; }
592
+ .cascade > .ocn > .oc-down { grid-area: rail; }
593
+ .cascade > .ocn > .oc-row.lanes { grid-area: lanes; }
594
+
595
+ /* the SVG rail plane — sits under the plates, never intercepts a click */
596
+ .rails { position: absolute; inset: 0; z-index: 0; pointer-events: none; overflow: visible; width: 100%; height: 100%; }
597
+
339
598
  .ocn { display: flex; flex-direction: column; align-items: center; }
340
- .oc-down { width: 1.5px; height: 18px; position: relative;
341
- background: linear-gradient(180deg, hsl(165 40% 45% / 0.5), var(--line)); }
599
+ /* the connective tissue reads brass everywhere — a company chart drawn with
600
+ the house's own material, not a generic diagram line. */
601
+ .oc-down { width: 2px; height: 18px; position: relative; background: var(--brass-line); opacity: 0.75; }
342
602
  .oc-down::after { content: ""; position: absolute; bottom: -2px; left: 50%;
343
603
  width: 5px; height: 5px; transform: translateX(-50%); border-radius: 50%;
344
- background: var(--accent); box-shadow: 0 0 8px rgba(79, 184, 160, 0.5); }
604
+ background: var(--brass-hi); box-shadow: 0 0 8px rgba(201, 160, 74, 0.55); }
345
605
  .oc-row { display: flex; align-items: flex-start; }
346
- .oc-child { position: relative; padding: 18px 5px 0; display: flex; flex-direction: column; align-items: center; }
347
- .oc-child::before { content: ""; position: absolute; top: 0; left: 50%; width: 1.5px; height: 18px;
348
- background: linear-gradient(180deg, var(--line), hsl(var(--h, 165) 32% 34%)); }
349
- .oc-child::after { content: ""; position: absolute; top: 0; left: 0; right: 0; height: 16px;
350
- border-top: 1.5px solid var(--line); }
351
- .oc-child:first-child::after { left: 50%; border-left: 1.5px solid var(--line); border-top-left-radius: 14px; }
352
- .oc-child:last-child::after { right: 50%; border-right: 1.5px solid var(--line); border-top-right-radius: 14px; }
353
- .oc-child:first-child::before, .oc-child:last-child::before { display: none; }
354
- .oc-child:only-child::after { display: none; }
355
- .oc-child:only-child::before { display: block; }
356
606
 
357
- /* the branch wakes when you hover its parent */
358
- .ocn:has(> .ag:hover) > .oc-down,
359
- .ocn:has(> .ag:hover) > .oc-row > .oc-child::before { background: hsl(165 45% 45% / 0.6); }
360
- .ocn:has(> .ag:hover) > .oc-row > .oc-child::after { border-color: hsl(165 45% 45% / 0.6); }
607
+ /* ── THE STAFF BOARD: departments as lanes, stacking DOWNWARD ──────────────
608
+ Nine departments fanned sideways forced a canvas nobody could read at 17%
609
+ zoom. Each lane is a row of the board: a brass rail down the left, the
610
+ head's card, then its people flowing right and wrapping inside the lane.
611
+ The board scrolls like a document; every card stays at legible size. */
612
+ /* the lanes are rank III+IV: one shelf per department, stacked. Width now comes
613
+ from the cascade's 1fr column, not a hardcoded viewport clamp. */
614
+ .oc-row.lanes {
615
+ flex-direction: column;
616
+ align-items: stretch;
617
+ gap: 14px;
618
+ padding-top: 0;
619
+ width: 100%;
620
+ }
621
+ /* a lane is a SHELF on the rack — a hairline tray, not a coloured panel. The
622
+ old lane had a hash-hue left border and tint per department; the rack is one
623
+ material throughout and the department is named, not coloured. */
624
+ .oc-row.lanes > .oc-child {
625
+ flex: 1 1 auto;
626
+ position: relative;
627
+ border: 1px solid var(--line);
628
+ border-radius: 18px;
629
+ background: var(--surface-tray);
630
+ padding: 16px 18px 18px;
631
+ transition: border-color .25s ease, box-shadow .35s var(--ease);
632
+ }
633
+ .oc-row.lanes > .oc-child:hover { border-color: var(--line-2, #d9d3c9); box-shadow: var(--sh-2); }
634
+ @media (prefers-reduced-motion: reduce) {
635
+ .oc-row.lanes > .oc-child:hover { transform: none; }
636
+ }
637
+ /* lanes own their connectors — kill the fan's drop-lines inside them */
638
+ .oc-row.lanes > .oc-child::before,
639
+ .oc-row.lanes > .oc-child::after { display: none; }
640
+ .oc-row.lanes > .oc-child > .ocn > .oc-down { display: none; }
641
+
642
+ /* inside a lane: head card top-left, its service tray beneath the head, a
643
+ brass rail column, its people filling the right. Explicit areas — an
644
+ implicit 2-column grid put the tray in the staff slot the moment every
645
+ department gained services, which shunted the people into a one-card
646
+ column. The rail column is the elbow HANDOFF.md flagged as hidden: the
647
+ department head commanding its people, drawn once, orthogonally. */
648
+ .oc-row.lanes > .oc-child > .ocn {
649
+ display: grid;
650
+ grid-template-columns: minmax(278px, 300px) var(--rail-w, 56px) 1fr;
651
+ grid-template-areas:
652
+ "head rail staff"
653
+ "macs rail staff";
654
+ grid-template-rows: auto 1fr;
655
+ align-items: start;
656
+ gap: 10px 0;
657
+ }
658
+ .oc-row.lanes > .oc-child > .ocn > .ag { grid-area: head; }
659
+ .oc-row.lanes > .oc-child > .ocn > .oc-tray { grid-area: macs; }
660
+ .oc-row.lanes > .oc-child > .ocn > .oc-row { grid-area: staff; margin-left: 4px; }
661
+
662
+ /* the rail: a vertical brass spine for the department, capped where it meets
663
+ the head, with one orthogonal branch reaching into the staff block. This
664
+ is the ".oc-down" element — hidden below (line ~490 in the old cascade),
665
+ here it's re-purposed as the rail instead of the plain tree drop-line. */
666
+ .oc-row.lanes > .oc-child > .ocn > .oc-down {
667
+ display: block; grid-area: rail; position: relative; width: 100%; height: 100%;
668
+ min-height: 44px;
669
+ /* the base .oc-down is a 2px-wide FILLED brass drop-line. Widening it to the
670
+ rail column without clearing its background painted a solid brass slab down
671
+ every lane — the rail is drawn by ::before/::after, not by the box itself. */
672
+ background: none;
673
+ }
674
+ /* No ::before/::after brass here any more — the SVG rail plane draws every
675
+ connector from measured box positions. Leaving these would double each rail. */
676
+
677
+ /* semantic zoom lever: the rail itself is the fold/unfold control at the
678
+ department level — house → department → role, one click at a time. */
679
+ .oc-down.foldable { cursor: pointer; }
680
+ .oc-down.foldable:focus-visible { outline: 2px solid var(--brass); outline-offset: 2px; border-radius: 4px; }
681
+ .oc-fold-glyph {
682
+ position: absolute; left: 50%; top: 22px; transform: translate(-50%, -50%);
683
+ font-style: normal; font-size: 10px; line-height: 1; width: 15px; height: 15px;
684
+ display: grid; place-items: center; border-radius: 50%;
685
+ background: var(--bg-1); border: 1px solid var(--line-2); color: var(--accent);
686
+ transition: transform .15s ease, background .15s ease;
687
+ }
688
+ .oc-down.foldable:hover .oc-fold-glyph { background: rgba(201, 160, 74, 0.18); transform: translate(-50%, -50%) scale(1.15); }
689
+ .oc-down.folded .oc-fold-glyph { color: var(--ink-2); border-color: var(--line); }
690
+
691
+ /* the folded department: head visible, staff summarised to one line — the
692
+ "department" rung of house → department → role. */
693
+ .oc-folded-summary {
694
+ grid-area: staff; align-self: center; justify-self: start; text-align: left;
695
+ background: none; border: 1px dashed var(--line); border-radius: 8px;
696
+ padding: 9px 13px; margin-left: 4px; cursor: pointer;
697
+ font-family: var(--mono); font-size: 11px; color: var(--ink-3);
698
+ transition: border-color .15s ease, color .15s ease;
699
+ }
700
+ .oc-folded-summary:hover { border-color: var(--brass); color: var(--brass-hi); }
701
+ /* "show the law": the SAME rails, re-read as authority. The SVG plane swaps to
702
+ a travelling dashed brass so the law is seen flowing down the cascade. */
703
+ .rail-line { stroke: var(--line-2, #d9d3c9); stroke-width: 1.5; stroke-linecap: round; fill: none; }
704
+ .rail-dash { stroke: var(--accent-2); stroke-width: 1; fill: none; stroke-dasharray: 2 9; opacity: 0; }
705
+ .law-on .rail-line { stroke: var(--brass-hi); stroke-width: 1.9; opacity: .9; }
706
+ .law-on .rail-dash { opacity: .95; animation: rail-flow 700ms linear infinite; }
707
+ @keyframes rail-flow { to { stroke-dashoffset: -11; } }
708
+ @media (prefers-reduced-motion: reduce) { .law-on .rail-dash { animation: none; } }
709
+
710
+ /* the tray is the department's stack of real services — one per line, so the
711
+ vendor marks read as a list of systems rather than a scatter of chips */
712
+ .oc-row.lanes > .oc-child > .ocn > .oc-tray {
713
+ display: flex; flex-direction: column; align-items: stretch;
714
+ gap: 4px; margin: 0; padding: 0; max-width: 232px;
715
+ }
716
+ .oc-row.lanes > .oc-child > .ocn > .oc-tray > .oc-mac { justify-content: flex-start; width: 100%; }
717
+ .oc-row.lanes > .oc-child > .ocn > .oc-tray > .oc-mac .mac-name { max-width: none; }
718
+
719
+ @media (max-width: 820px) {
720
+ .oc-row.lanes > .oc-child > .ocn {
721
+ grid-template-columns: 1fr;
722
+ grid-template-areas: "head" "macs" "staff";
723
+ }
724
+ /* no rail column on a single-column stack — the rail has nothing to span */
725
+ .oc-row.lanes > .oc-child > .ocn > .oc-down { display: none; }
726
+ .oc-row.lanes > .oc-child > .ocn > .oc-tray { max-width: none; flex-direction: row; flex-wrap: wrap; }
727
+ }
728
+ /* inside a lane the people always flow right and wrap — the ".wrap" grid
729
+ (which grows downward) would otherwise stack a big department into a
730
+ one-card-wide column. Specificity here deliberately beats .oc-row.wrap. */
731
+ .oc-row.lanes > .oc-child > .ocn > .oc-row,
732
+ .oc-row.lanes > .oc-child > .ocn > .oc-row.wrap {
733
+ display: flex;
734
+ flex-direction: row;
735
+ flex-wrap: wrap;
736
+ justify-content: flex-start;
737
+ gap: 8px;
738
+ margin: 0;
739
+ padding: 0;
740
+ max-width: none;
741
+ border: 0;
742
+ border-radius: 0;
743
+ background: none;
744
+ align-content: flex-start;
745
+ }
746
+ .oc-row.lanes > .oc-child > .ocn > .oc-row > .oc-child { padding: 0; flex: 0 0 auto; }
747
+ .oc-row.lanes > .oc-child > .ocn > .oc-row > .oc-child::before,
748
+ .oc-row.lanes > .oc-child > .ocn > .oc-row > .oc-child::after { display: none; }
749
+
750
+ @media (max-width: 820px) {
751
+ .oc-row.lanes > .oc-child > .ocn { grid-template-columns: 1fr; gap: 12px; }
752
+ }
753
+ /* Connectors are drawn by the SVG rail plane now, not by pseudo-elements. The
754
+ old fan drop-lines/elbows here would double every rail. */
755
+ .oc-child { position: relative; padding: 0; display: flex; flex-direction: column; align-items: center; }
756
+ .oc-child::before, .oc-child::after { display: none; }
361
757
 
362
758
  /* wide sibling sets — grouped subtrees that wrap to natural width (never crush a
363
759
  branch into a fixed cell — that overflowed and collided with the next branch) */
@@ -370,78 +766,171 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
370
766
  .oc-row.wrap > .oc-child { padding: 8px 4px 0; flex: 0 0 auto; }
371
767
  .oc-row.wrap > .oc-child::before, .oc-row.wrap > .oc-child::after { display: none; }
372
768
 
373
- /* agent cards company badges with a rank system (size + saturation recede
374
- with depth) and a light-from-above material model. */
769
+ /* ── THE PLATE ────────────────────────────────────────────────────────────
770
+ An engraved plate on a key rack. Left edge is milled brass; rank reads
771
+ through FINISH and SIZE (polished → satin → dulled), never hue. Radius 4px
772
+ per CRAFT §0 (radii 2/3/6) — the old 15px rounded rect is what made this
773
+ read as a SaaS card. Content is left-aligned like a name plate, not
774
+ centred like a business card. */
775
+ /* ══ THE SOURCE CARD (variant B — "the spine") ══════════════════════════
776
+ This board is not a picture of the org: it IS the org file booboo_boot
777
+ reads. Rules inherit down it, bucket reach derives from it, every agent
778
+ boots against it. So the card states its CONSEQUENCE, not just its
779
+ attributes — what it declares, who it binds, what it boots on.
780
+ Health is the card's own left edge, so scanning nine stacked departments
781
+ you find the amber one peripherally, before reading a single word. */
375
782
  .ag {
376
- position: relative; display: flex; flex-direction: column; align-items: center; gap: 6px;
377
- width: calc(160px - min(var(--d, 0), 2) * 12px); /* 160 / 148 / 136 by rank */
378
- padding: 13px 11px 10px; text-align: center;
379
- background: linear-gradient(180deg, hsl(var(--h) 30% 14% / 0.55), var(--bg-1) 55%);
380
- border: 1px solid rgba(255, 255, 255, 0.06); border-radius: 15px; cursor: grab; overflow: hidden;
381
- filter: saturate(calc(1 - min(var(--d, 0), 2) * 0.15));
382
- box-shadow:
383
- inset 0 1px 0 rgba(255, 255, 255, 0.05),
384
- inset 0 0 0 1px hsl(var(--h) 40% 40% / 0.07),
385
- 0 1px 2px rgba(0, 0, 0, 0.5),
386
- 0 16px 32px -18px hsl(var(--h) 55% 40% / 0.28);
387
- transition: border-color 0.15s, transform 0.15s, box-shadow 0.2s, opacity 0.15s, filter 0.2s;
388
- animation: settle 0.4s ease both;
783
+ position: relative; display: flex; flex-direction: column; align-items: stretch;
784
+ width: calc(300px - min(var(--d, 0), 2) * 22px); /* 300 / 278 / 256 by rank */
785
+ padding: 15px 18px 13px 21px; text-align: left;
786
+ background: var(--bg-1);
787
+ border: 1px solid var(--line); border-radius: 14px; cursor: grab; overflow: hidden;
788
+ box-shadow: var(--sh-2);
789
+ transition: box-shadow .22s var(--ease), transform .22s var(--ease), border-color .18s ease, opacity .18s ease;
790
+ animation: settle .45s var(--settle) both;
389
791
  }
390
- .ag::before { content: ""; position: absolute; top: 0; left: 0; right: 0;
391
- height: calc(4px - min(var(--d, 0), 2) * 1px); /* lanyard thins with rank */
392
- background: linear-gradient(90deg, hsl(var(--h) 55% 55% / 0.9), hsl(var(--h) 55% 45% / 0.25)); }
393
- @keyframes settle { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
394
- .ag:hover { border-color: hsl(var(--h) 45% 50% / 0.6); transform: translateY(-2px); filter: saturate(1);
395
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08), 0 2px 4px rgba(0, 0, 0, 0.5), 0 24px 44px -18px hsl(var(--h) 55% 45% / 0.4); }
396
- .ag.sel { border-color: hsl(var(--h) 55% 55%); filter: saturate(1);
397
- box-shadow: 0 0 0 2px hsl(var(--h) 55% 55% / 0.18), 0 10px 28px hsl(var(--h) 50% 45% / 0.2); }
792
+ /* the spine health as the card's own edge */
793
+ .ag::before {
794
+ content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
795
+ background: var(--ink-3); transition: background .2s ease;
796
+ }
797
+ .ag.h-ok::before { background: var(--ok); }
798
+ .ag.h-warn::before { background: var(--warn); }
799
+ .ag.h-fail::before { background: var(--fail); }
800
+ /* an unhealthy card also carries a wash, so the state survives a greyscale
801
+ print and never rests on hue alone */
802
+ .ag.h-warn { background: linear-gradient(180deg, var(--warn-wash), var(--bg-1) 52%); border-color: #ecd9ae; }
803
+ .ag.h-fail { background: linear-gradient(180deg, var(--fail-wash), var(--bg-1) 52%); border-color: #edc9bf; }
804
+ @keyframes settle { from { opacity: 0; transform: translateY(8px) scale(.985); } to { opacity: 1; transform: none; } }
805
+ .ag:hover { transform: translateY(-2px); box-shadow: var(--sh-3); border-color: var(--line-2); }
806
+ .ag:active { transform: translateY(0) scale(.995); transition-duration: .08s; }
807
+ .ag.sel { border-color: var(--accent); box-shadow: var(--sh-3), 0 0 0 3px var(--accent-dim); }
398
808
  .ag:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
399
- .ag.over { border-color: var(--accent-2); box-shadow: 0 0 0 3px var(--accent-dim), 0 12px 30px rgba(79, 184, 160, 0.2); transform: scale(1.03); }
400
- .ag.dragging { opacity: 0.45; cursor: grabbing; }
809
+ .ag.over { border-color: var(--accent-2); box-shadow: var(--sh-3), 0 0 0 4px var(--accent-dim); transform: scale(1.02); }
810
+ .ag.dragging { opacity: 0.4; cursor: grabbing; }
811
+
812
+ /* WHERE IT BELONGS — reads first, so a card lifted out of its lane still
813
+ knows its own place. */
814
+ .ag-eyebrow {
815
+ font-family: var(--mono); font-size: 9px; letter-spacing: .16em; text-transform: uppercase;
816
+ color: var(--accent); display: block; margin-bottom: 7px;
817
+ }
818
+ /* WHAT FLOWS OUT — the consequence line. Plain English, never glyphs. */
819
+ .ag-flows {
820
+ display: block; font-size: 11.5px; color: var(--ink-3); margin-top: 10px;
821
+ font-variant-numeric: tabular-nums;
822
+ }
401
823
 
402
- .ag-light { position: absolute; top: 10px; right: 10px; width: 7px; height: 7px; border-radius: 50%; }
403
- .ag-light.ok { background: var(--accent); box-shadow: 0 0 6px rgba(79, 184, 160, 0.7); }
404
- .ag-light.warn { background: var(--warn); box-shadow: 0 0 6px rgba(224, 180, 95, 0.7); }
405
- .ag-light.fail { background: #e07b6a; box-shadow: 0 0 8px rgba(224, 123, 106, 0.9); animation: pulse 1.4s ease-in-out infinite; }
824
+ /* ── THE HEALTH LIGHT ──────────────────────────────────────────────────────
825
+ A real lamp: a lit core inside a soft halo, with a slow breath so a healthy
826
+ board is visibly ALIVE rather than merely static. The halo is a separate
827
+ ::after ring that expands and fades one pulse every 2.6s, offset per
828
+ bubble so nine departments never blink in unison (a synchronised board
829
+ reads as a screensaver; a staggered one reads as a building). */
830
+ .ag-light {
831
+ position: absolute; top: 13px; right: 14px; width: 8px; height: 8px; border-radius: 50%;
832
+ background: var(--ink-3);
833
+ }
834
+ .ag-light::after {
835
+ content: ""; position: absolute; inset: -3px; border-radius: 50%;
836
+ border: 1px solid currentColor; opacity: 0;
837
+ animation: lamp 2.6s var(--ease) infinite;
838
+ animation-delay: var(--lamp-delay, 0s);
839
+ }
840
+ .ag-light.ok { background: var(--ok); color: var(--ok); box-shadow: 0 0 0 3px var(--ok-wash); }
841
+ .ag-light.warn { background: var(--warn); color: var(--warn); box-shadow: 0 0 0 3px var(--warn-wash); }
842
+ .ag-light.fail { background: var(--fail); color: var(--fail); box-shadow: 0 0 0 3px var(--fail-wash); }
843
+ /* a failing lamp beats faster — urgency in the rhythm, not just the hue */
844
+ .ag-light.fail::after { animation-duration: 1.2s; }
845
+ @keyframes lamp {
846
+ 0% { transform: scale(.7); opacity: .85; }
847
+ 70%, 100% { transform: scale(1.9); opacity: 0; }
848
+ }
849
+ @media (prefers-reduced-motion: reduce) { .ag-light::after { animation: none; } }
406
850
 
407
- .ag.root { cursor: default; width: 200px; padding: 16px 13px 11px;
408
- border-color: rgba(79, 184, 160, 0.35); filter: none;
409
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.07), 0 0 0 1px rgba(79, 184, 160, 0.25), 0 24px 60px -20px rgba(79, 184, 160, 0.35); }
410
- .ag.root .ag-ava { width: 46px; height: 46px; font-size: 23px; }
411
- .ag.root .ag-name { font-size: 15px; font-weight: 700; letter-spacing: -0.01em; }
412
- .ag.root::after {
413
- content: ""; position: absolute; inset: 0; border-radius: 15px; pointer-events: none;
414
- box-shadow: inset 0 0 26px rgba(79, 184, 160, 0.08); animation: ring 3.2s ease-in-out infinite;
415
- }
416
- @keyframes ring { 0%, 100% { opacity: 0.35; } 50% { opacity: 1; } }
851
+ /* rank I·II — the GM. Polished brass: the only plate that is lit rather than lit-upon. */
852
+ .ag.root { cursor: default; width: 268px; padding: 15px 17px;
853
+ background: var(--surface-lift);
854
+ border-color: var(--surface-lift-edge); box-shadow: var(--sh-3); }
855
+ .ag.root .ag-name { font-size: 14.5px; }
417
856
 
857
+ /* plate header: the mark sits BESIDE the name, not above it — a name plate,
858
+ not a business card. Staff plates carry no mark at all (see Panel.tsx): a
859
+ brass plaque does not have a cartoon on it, and 51 identical 🤖 was the
860
+ cheapest token on the board. */
861
+ .ag-head { display: flex; align-items: center; gap: 9px; min-width: 0; }
418
862
  .ag-ava {
419
- width: 38px; height: 38px; display: grid; place-items: center; font-size: 19px; margin-top: 2px;
420
- border-radius: 13px; background: linear-gradient(140deg, hsl(var(--h) 42% 26% / 0.85), hsl(var(--h) 45% 14% / 0.9));
421
- border: 1px solid hsl(var(--h) 45% 45% / 0.35);
422
- box-shadow: inset 0 1px 0 hsl(var(--h) 50% 60% / 0.25), 0 4px 10px -4px hsl(var(--h) 50% 30% / 0.6);
423
- }
424
- .ag-name { font-size: 13px; font-weight: 650; letter-spacing: -0.01em; white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
425
- .ag-role { font-size: 11px; color: var(--ink-2); line-height: 1.35;
426
- display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
427
- .ag-meta { display: flex; align-items: center; justify-content: center; gap: 8px; margin-top: 2px; }
428
- .ag-meta em { font-style: normal; font-size: 10.5px; color: var(--ink-3); font-variant-numeric: tabular-nums; white-space: nowrap; }
863
+ width: 26px; height: 26px; flex: 0 0 26px; display: grid; place-items: center; font-size: 14px;
864
+ border-radius: 9px; background: var(--accent-dim); border: 1px solid #eadcc3;
865
+ }
866
+ .ag.root .ag-ava { width: 30px; height: 30px; flex-basis: 30px; font-size: 16px; border-color: rgba(239, 195, 137, 0.45); }
867
+
868
+ /* the name engraved: a dark groove under, a hairline of light above */
869
+ .ag-name {
870
+ font-family: var(--display); font-size: 18px; font-weight: 600; letter-spacing: -0.015em;
871
+ line-height: 1.18; color: var(--ink);
872
+ white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
873
+ }
874
+ /* staff plates: quieter, lighter weight — rank in the type as well as the metal */
875
+ .ag.staff .ag-name { font-family: var(--font); font-size: 15px; font-weight: 600; letter-spacing: -0.01em; color: var(--ink); }
876
+
877
+ .ag-role {
878
+ font-size: 12.5px; line-height: 1.5; color: var(--ink-2); margin-top: 6px;
879
+ display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
880
+ }
881
+ .ag-meta { display: flex; align-items: center; gap: 9px; margin-top: 7px; }
882
+ .ag-meta em { font-style: normal; font-family: var(--mono); font-size: 9px; color: var(--ink-3); font-variant-numeric: tabular-nums; white-space: nowrap; }
429
883
  .ag-kids {
430
- font-size: 10px; background: var(--accent-dim); color: var(--accent); border-radius: 99px;
431
- padding: 2px 8px; font-variant-numeric: tabular-nums; font-weight: 650; text-transform: uppercase; letter-spacing: 0.06em;
884
+ font-family: var(--mono); font-size: 8.5px; color: var(--brass); border: 1px solid rgba(217, 160, 91, 0.28);
885
+ border-radius: 3px; padding: 2px 7px; font-variant-numeric: tabular-nums; text-transform: uppercase; letter-spacing: 0.1em;
886
+ }
887
+
888
+ /* ── the plate face: health · bucket reach · rule count · last report ──────
889
+ Replaces a role line that used to just repeat the name ("Lift Engineer" /
890
+ "Lift Engineer — Engineering"). Four facts, left-aligned, mono, no chrome. */
891
+ .ag-facts {
892
+ display: flex; align-items: center; gap: 10px;
893
+ margin-top: 12px; padding-top: 11px; border-top: 1px solid var(--line);
894
+ }
895
+ .ag-fact { font-style: normal; font-size: 11.5px; font-weight: 600; color: var(--ink-3); }
896
+ .ag-fact-health.ok { color: var(--ok); }
897
+ .ag-fact-health.warn { color: var(--warn); }
898
+ .ag-fact-health.fail { color: var(--fail); }
899
+ .ag-fact-health.none { color: var(--ink-3); font-weight: 500; }
900
+ .fact-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--ink-3); flex: 0 0 auto; }
901
+ .ag-fact-health.ok .fact-dot { background: var(--ok); box-shadow: 0 0 6px rgba(139, 233, 176, 0.75); }
902
+ .ag-fact-health.warn .fact-dot { background: var(--brass); box-shadow: 0 0 6px rgba(217, 160, 91, 0.85); }
903
+ .ag-fact-health.fail .fact-dot { background: var(--fail); box-shadow: 0 0 7px rgba(224, 123, 106, 0.85); }
904
+ .ag-fact-health.ok, .ag-fact-health.warn, .ag-fact-health.fail { color: var(--ink-2); }
905
+ .ag-fact-report { margin-left: auto; color: var(--ink-3); font-weight: 400; font-family: var(--mono); font-size: 10px; }
906
+ /* the expected beat, quieter than the fact it qualifies — it is the ruler, not
907
+ the measurement, so it must never out-shout "reported 4d ago". */
908
+ .ag-cadence { font-weight: 400; color: var(--ink-3); opacity: .62; }
909
+
910
+ /* "show the law": the boot-order chain, printed on the plate only while the
911
+ toggle is on — the product's core idea, made literal per plate. */
912
+ .ag-law { display: none; }
913
+ .law-on .ag-law {
914
+ display: block; margin-top: 7px; padding-top: 7px; border-top: 1px dashed rgba(217, 160, 91, 0.32);
915
+ font-family: var(--mono); font-size: 8.5px; letter-spacing: 0.03em; color: var(--brass-hi);
916
+ line-height: 1.6; word-break: break-word;
432
917
  }
918
+ .ag-law-arrow { font-style: normal; color: var(--brass); margin: 0 3px; opacity: 0.85; }
919
+ .law-on.ag { box-shadow: 0 0 0 1px rgba(217, 160, 91, 0.3), 0 16px 34px -18px rgba(0, 0, 0, 0.85); }
433
920
 
434
921
  /* the dossier */
435
922
  .doss {
436
923
  width: 340px; flex: 0 0 340px; border-left: 1px solid var(--line);
437
- background: rgba(16, 19, 26, 0.75); backdrop-filter: blur(10px);
924
+ background: var(--bg-1); backdrop-filter: blur(10px);
438
925
  padding: 26px 24px 40px; overflow-y: auto; animation: slidein 0.25s ease both;
439
926
  }
440
927
  @keyframes slidein { from { opacity: 0; transform: translateX(14px); } to { opacity: 1; transform: none; } }
441
- .doss-head { display: flex; gap: 12px; align-items: center; margin-bottom: 14px; }
442
- .doss-emoji { font-size: 30px; }
443
- .doss h2 { margin: 0; font-size: 18px; }
444
- .doss-role { margin: 3px 0 0; color: var(--ink-2); font-size: 12.5px; }
928
+ .doss-head { display: flex; gap: 12px; align-items: center; margin-bottom: 8px; }
929
+ .doss-emoji { font-size: 30px; flex: none; }
930
+ .doss h2 { margin: 0; font-size: 18px; min-width: 0; }
931
+ /* full panel width: a body of four seats is a list, not a subtitle squeezed
932
+ beside three buttons. Middots become the wrap points. */
933
+ .doss-role { margin: 0 0 14px; color: var(--ink-2); font-size: 12.5px; line-height: 1.55; }
445
934
 
446
935
  .doss-chain { font-size: 11px; color: var(--ink-3); margin-bottom: 18px; line-height: 1.7; }
447
936
  .doss-chain i { font-style: normal; margin: 0 5px; }
@@ -462,8 +951,12 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
462
951
  .doss-rules em { color: var(--ink-3); font-style: normal; font-size: 9.5px; margin-left: 5px; }
463
952
  .doss-chips { display: flex; gap: 6px; flex-wrap: wrap; }
464
953
  .doss-reps { display: flex; flex-direction: column; gap: 7px; }
954
+ /* No side-stripe. It was a 2px brass border-left that said nothing the
955
+ status dot doesn't already say properly, and a coloured side-stripe on a
956
+ card is a banned pattern in the house design laws. Full 1px border; the
957
+ whole edge answers on hover. */
465
958
  .doss-rep {
466
- background: var(--bg-1); border: 1px solid var(--line); border-left: 2px solid var(--accent);
959
+ background: var(--bg-1); border: 1px solid var(--line);
467
960
  border-radius: 9px; padding: 9px 11px; animation: settle 0.35s ease both;
468
961
  }
469
962
  .doss-rep-top { display: flex; align-items: baseline; gap: 9px; }
@@ -471,7 +964,7 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
471
964
  .doss-rep-label { font-size: 12px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
472
965
  .doss-rep-sum { margin: 5px 0 0; font-size: 11.5px; color: var(--ink-2); line-height: 1.55; }
473
966
  .doss-rep.tap { cursor: pointer; transition: border-color .12s ease, background .12s ease; }
474
- .doss-rep.tap:hover { border-left-color: var(--warn); background: rgba(255, 255, 255, 0.03); }
967
+ .doss-rep.tap:hover { border-color: var(--accent); background: var(--accent-dim); }
475
968
  .doss-rep.tap:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
476
969
  .doss-rep-caret { margin-left: auto; font-size: 10px; color: var(--ink-2); align-self: center; }
477
970
  .rep-dot { width: 7px; height: 7px; border-radius: 50%; flex: 0 0 auto; align-self: center; }
@@ -479,7 +972,7 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
479
972
  .rep-ok { color: var(--accent); } .rep-warn { color: var(--warn); } .rep-fail { color: #e07b6a; }
480
973
  .doss-rep-detail { margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--line); font-size: 11px; line-height: 1.7; color: var(--ink-2); font-family: var(--mono); word-break: break-word; }
481
974
  .doss-rep-detail .doss-l { display: inline-block; min-width: 74px; opacity: .6; text-transform: uppercase; letter-spacing: .04em; font-size: 9.5px; }
482
- .chip { font-size: 11px; background: var(--accent-dim); color: var(--accent-2); border-radius: 7px; padding: 4px 10px; }
975
+ .chip { transition: transform .16s var(--ease), background .18s ease; font-size: 11px; background: var(--accent-dim); color: var(--accent-2); border-radius: 7px; padding: 4px 10px; }
483
976
  .chip.alt { background: var(--bg-2); color: var(--ink-2); }
484
977
  .doss-boot { font-size: 12px; color: var(--ink-2); line-height: 1.6; margin: 0; white-space: pre-wrap; }
485
978
  /* contract — one click to open, edit in place, save flows to the org file */
@@ -554,17 +1047,6 @@ var TABS = [
554
1047
  { id: "rules", glyph: "\xA7", label: "rules" },
555
1048
  { id: "graph", glyph: "\u25C9", label: "graph" }
556
1049
  ];
557
- function relTime(iso) {
558
- if (typeof iso !== "string") return "";
559
- const t = new Date(iso).getTime();
560
- if (!t) return "";
561
- const s = (Date.now() - t) / 1e3;
562
- if (s < 60) return "just now";
563
- if (s < 3600) return `${Math.floor(s / 60)}m ago`;
564
- if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
565
- if (s < 172800) return "yesterday";
566
- return `${Math.floor(s / 86400)}d ago`;
567
- }
568
1050
  function nodeAt(n) {
569
1051
  const d = n.data ?? {};
570
1052
  for (const k of ["at", "ts", "time", "created_at", "date", "bst", "ts_bst", "finished_at"]) {
@@ -581,13 +1063,14 @@ function timeMs(at) {
581
1063
  function buildHealthMap(nodes) {
582
1064
  const m = /* @__PURE__ */ new Map();
583
1065
  for (const r of nodes) {
584
- if (!r.cluster) continue;
1066
+ const who = reportAgentId(r);
1067
+ if (!who) continue;
585
1068
  const d = r.data ?? {};
586
1069
  if (typeof d.status !== "string") continue;
587
1070
  const status = d.status;
588
1071
  const at = nodeAt(r);
589
1072
  const atMs = timeMs(at);
590
- const cur = m.get(r.cluster) ?? { lastAt: "", lastMs: -Infinity, lastStatus: "", n: 0, fails: 0 };
1073
+ const cur = m.get(who) ?? { lastAt: "", lastMs: -Infinity, lastStatus: "", n: 0, fails: 0 };
591
1074
  cur.n++;
592
1075
  if (status === "fail") cur.fails++;
593
1076
  if (Number.isFinite(atMs) && atMs >= cur.lastMs) {
@@ -595,7 +1078,7 @@ function buildHealthMap(nodes) {
595
1078
  cur.lastAt = at;
596
1079
  cur.lastStatus = status;
597
1080
  }
598
- m.set(r.cluster, cur);
1081
+ m.set(who, cur);
599
1082
  }
600
1083
  return m;
601
1084
  }
@@ -609,6 +1092,15 @@ function pulseFor(a, health) {
609
1092
  }
610
1093
  return best;
611
1094
  }
1095
+ function everyN(h) {
1096
+ if (h <= 1) return "hourly";
1097
+ if (h < 24) return `every ${h}h`;
1098
+ if (h === 24) return "daily";
1099
+ if (h === 168) return "weekly";
1100
+ if (h >= 672 && h <= 744) return "monthly";
1101
+ if (h % 24 === 0) return `every ${h / 24} days`;
1102
+ return `every ${h}h`;
1103
+ }
612
1104
  function lightFor(a, health) {
613
1105
  const p = pulseFor(a, health);
614
1106
  if (!p || !p.lastAt) return "none";
@@ -631,6 +1123,10 @@ function nodeSummary(n) {
631
1123
  }
632
1124
  return "";
633
1125
  }
1126
+ function reportAgentId(n) {
1127
+ const d = n.data ?? {};
1128
+ return n.cluster || (typeof d.agent === "string" ? d.agent : "") || "";
1129
+ }
634
1130
  async function fetchReports(api, cluster, limit = 500) {
635
1131
  const q = async (t) => {
636
1132
  try {
@@ -656,23 +1152,30 @@ function slugify(s) {
656
1152
  return s.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
657
1153
  }
658
1154
  var REDUCED = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
1155
+ var THEME_KEY = "booboo-theme-v2";
659
1156
  function readTheme() {
660
1157
  try {
661
- return localStorage.getItem("booboo-theme") === "light" ? "light" : "dark";
1158
+ if (new URLSearchParams(window.location.search).get("embed")) return "light";
1159
+ return localStorage.getItem(THEME_KEY) === "dark" ? "dark" : "light";
662
1160
  } catch {
663
- return "dark";
1161
+ return "light";
664
1162
  }
665
1163
  }
666
1164
  function useTheme() {
667
1165
  const [theme, setTheme] = useState(readTheme);
1166
+ const chosen = useRef(false);
668
1167
  useEffect(() => {
669
1168
  document.documentElement.setAttribute("data-theme", theme);
1169
+ if (!chosen.current) return;
670
1170
  try {
671
- localStorage.setItem("booboo-theme", theme);
1171
+ localStorage.setItem(THEME_KEY, theme);
672
1172
  } catch {
673
1173
  }
674
1174
  }, [theme]);
675
- return [theme, () => setTheme((t) => t === "dark" ? "light" : "dark")];
1175
+ return [theme, () => {
1176
+ chosen.current = true;
1177
+ setTheme((t) => t === "dark" ? "light" : "dark");
1178
+ }];
676
1179
  }
677
1180
  function useCountUp(target, ms = 900) {
678
1181
  const [v, setV] = useState(REDUCED ? target : 0);
@@ -720,8 +1223,216 @@ function bucketHue(name) {
720
1223
  for (const c of name) h = (h * 31 + c.charCodeAt(0)) % 360;
721
1224
  return h;
722
1225
  }
1226
+ var SEALED_BUCKET = "guest-registry";
1227
+ function LedgerShelf({ org, lit }) {
1228
+ const buckets = useMemo(() => {
1229
+ const s = /* @__PURE__ */ new Set();
1230
+ for (const a of org.agents) for (const b of a.buckets ?? []) s.add(b);
1231
+ return [...s].sort();
1232
+ }, [org]);
1233
+ return /* @__PURE__ */ jsxs("div", { className: "ledger-shelf", role: "list", "aria-label": "the ledger \u2014 hover a role to see what it reaches", children: [
1234
+ /* @__PURE__ */ jsx("span", { className: "shelf-label", children: "the ledger" }),
1235
+ /* @__PURE__ */ jsxs("div", { className: "shelf-slots", children: [
1236
+ buckets.map((b) => /* @__PURE__ */ jsx("span", { className: `shelf-slot${lit.has(b) ? " lit" : ""}`, role: "listitem", children: b }, b)),
1237
+ /* @__PURE__ */ jsxs("span", { className: "shelf-slot sealed", role: "listitem", title: "ledger:guest-registry \u2014 sealed by wall. Visible, never emitted.", children: [
1238
+ "\u{1F512} ",
1239
+ SEALED_BUCKET
1240
+ ] })
1241
+ ] })
1242
+ ] });
1243
+ }
1244
+ function Constellation() {
1245
+ const svg = useMemo(() => {
1246
+ const W = 1600, H = 1e3, N = 58;
1247
+ let s = 20260719;
1248
+ const rnd = () => (s = s * 1103515245 + 12345 & 2147483647) / 2147483647;
1249
+ const pts = [];
1250
+ for (let i = 0; i < N; i++) pts.push([rnd() * W, rnd() * H]);
1251
+ let d = "";
1252
+ for (let i = 0; i < N; i++) {
1253
+ for (let j = i + 1; j < N; j++) {
1254
+ const dist = Math.hypot(pts[i][0] - pts[j][0], pts[i][1] - pts[j][1]);
1255
+ if (dist < 200) d += `<line x1="${pts[i][0].toFixed(1)}" y1="${pts[i][1].toFixed(1)}" x2="${pts[j][0].toFixed(1)}" y2="${pts[j][1].toFixed(1)}" stroke="rgba(217,160,91,${((1 - dist / 200) * 0.14).toFixed(3)})" stroke-width="1"/>`;
1256
+ }
1257
+ }
1258
+ for (const [x, y] of pts) d += `<circle cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" r="${(rnd() * 1.4 + 0.6).toFixed(2)}" fill="rgba(239,195,137,.45)"/>`;
1259
+ return d;
1260
+ }, []);
1261
+ return /* @__PURE__ */ jsx("svg", { className: "pnl-stars", viewBox: "0 0 1600 1000", preserveAspectRatio: "xMidYMid slice", "aria-hidden": true, dangerouslySetInnerHTML: { __html: svg } });
1262
+ }
1263
+ function LawPlate({ org, selected, onSelect }) {
1264
+ const root = org.agents.find((a) => a.id === org.root);
1265
+ const rules = root?.rules ?? [];
1266
+ if (!rules.length) return null;
1267
+ return /* @__PURE__ */ jsxs(
1268
+ "div",
1269
+ {
1270
+ className: `ag law-plate${selected ? " sel" : ""}`,
1271
+ "data-rail": "law",
1272
+ tabIndex: 0,
1273
+ title: "the law every agent in the house boots against",
1274
+ onClick: (e) => {
1275
+ e.stopPropagation();
1276
+ if (root) onSelect(root.id);
1277
+ },
1278
+ onKeyDown: (e) => {
1279
+ if ((e.key === "Enter" || e.key === " ") && root) {
1280
+ e.preventDefault();
1281
+ onSelect(root.id);
1282
+ }
1283
+ },
1284
+ children: [
1285
+ /* @__PURE__ */ jsxs("div", { className: "ag-head", children: [
1286
+ /* @__PURE__ */ jsx("span", { className: "ag-ava", children: "\xA7" }),
1287
+ /* @__PURE__ */ jsx("span", { className: "ag-name", children: "The House Standard" })
1288
+ ] }),
1289
+ /* @__PURE__ */ jsx("span", { className: "ag-role", children: rules[0] }),
1290
+ /* @__PURE__ */ jsxs("span", { className: "ag-facts", children: [
1291
+ /* @__PURE__ */ jsxs("em", { className: "ag-fact", children: [
1292
+ "binds ",
1293
+ org.agents.length
1294
+ ] }),
1295
+ /* @__PURE__ */ jsxs("em", { className: "ag-fact", children: [
1296
+ rules.length,
1297
+ " clause",
1298
+ rules.length === 1 ? "" : "s"
1299
+ ] })
1300
+ ] })
1301
+ ]
1302
+ }
1303
+ );
1304
+ }
1305
+ function boxIn(el, root) {
1306
+ let x = 0, y = 0;
1307
+ let cur = el;
1308
+ while (cur && cur !== root) {
1309
+ x += cur.offsetLeft;
1310
+ y += cur.offsetTop;
1311
+ cur = cur.offsetParent;
1312
+ }
1313
+ return { x, y, w: el.offsetWidth, h: el.offsetHeight, cy: y + el.offsetHeight / 2 };
1314
+ }
1315
+ function elbow(x1, y1, x2, y2) {
1316
+ if (Math.abs(y2 - y1) < 1.5) return `M${x1},${y1.toFixed(1)} H${x2}`;
1317
+ const mx = x1 + (x2 - x1) * 0.45;
1318
+ const r = Math.min(9, Math.abs(y2 - y1) / 2);
1319
+ const dir = y2 > y1 ? 1 : -1;
1320
+ return `M${x1},${y1.toFixed(1)} H${(mx - r).toFixed(1)} Q${mx},${y1.toFixed(1)} ${mx},${(y1 + r * dir).toFixed(1)} V${(y2 - r * dir).toFixed(1)} Q${mx},${y2.toFixed(1)} ${(mx + r).toFixed(1)},${y2.toFixed(1)} H${x2}`;
1321
+ }
1322
+ function CascadeRails({ chartRef, version }) {
1323
+ const [paths, setPaths] = useState([]);
1324
+ useLayoutEffect(() => {
1325
+ const root = chartRef.current;
1326
+ if (!root) return;
1327
+ const measure = () => {
1328
+ const pick = (sel) => Array.from(root.querySelectorAll(sel));
1329
+ const law = root.querySelector('[data-rail="law"]');
1330
+ const gm = root.querySelector('[data-rail="gm"]');
1331
+ const depts = pick('[data-rail="dept"]');
1332
+ const staff = pick('[data-rail="staff"]');
1333
+ const out = [];
1334
+ const gmB = gm ? boxIn(gm, root) : null;
1335
+ if (law && gmB) {
1336
+ const lawB = boxIn(law, root);
1337
+ out.push(elbow(lawB.x + lawB.w, lawB.cy, gmB.x, gmB.cy));
1338
+ }
1339
+ const deptBox = /* @__PURE__ */ new Map();
1340
+ for (const d of depts) {
1341
+ const b = boxIn(d, root);
1342
+ deptBox.set(d.dataset.id ?? "", b);
1343
+ if (gmB) out.push(elbow(gmB.x + gmB.w, gmB.cy, b.x, b.cy));
1344
+ }
1345
+ for (const s of staff) {
1346
+ const parent = deptBox.get(s.dataset.parent ?? "");
1347
+ if (!parent) continue;
1348
+ const b = boxIn(s, root);
1349
+ out.push(elbow(parent.x + parent.w, parent.cy, b.x, b.cy));
1350
+ }
1351
+ setPaths((prev) => prev.length === out.length && prev.every((p, i) => p === out[i]) ? prev : out);
1352
+ };
1353
+ measure();
1354
+ const ro = new ResizeObserver(measure);
1355
+ ro.observe(root);
1356
+ return () => ro.disconnect();
1357
+ }, [chartRef, version]);
1358
+ return /* @__PURE__ */ jsxs("svg", { className: "rails", "aria-hidden": true, children: [
1359
+ /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("linearGradient", { id: "railGrad", x1: "0", y1: "0", x2: "1", y2: "0", children: [
1360
+ /* @__PURE__ */ jsx("stop", { offset: "0", stopColor: "var(--brass)", stopOpacity: "0.3" }),
1361
+ /* @__PURE__ */ jsx("stop", { offset: "1", stopColor: "var(--brass-hi)", stopOpacity: "0.85" })
1362
+ ] }) }),
1363
+ paths.map((d, i) => /* @__PURE__ */ jsxs("g", { children: [
1364
+ /* @__PURE__ */ jsx("path", { className: "rail-line", d }),
1365
+ /* @__PURE__ */ jsx("path", { className: "rail-dash", d })
1366
+ ] }, i))
1367
+ ] });
1368
+ }
1369
+ function Lane({ laneId, isLane, dragId, onDropOn, children }) {
1370
+ const [over, setOver] = useState(false);
1371
+ if (!isLane) return /* @__PURE__ */ jsx("div", { className: "oc-child", children });
1372
+ return /* @__PURE__ */ jsx(
1373
+ "div",
1374
+ {
1375
+ className: `oc-child${over && dragId && dragId !== laneId ? " lane-over" : ""}`,
1376
+ onDragOver: (e) => {
1377
+ if (dragId && dragId !== laneId) {
1378
+ e.preventDefault();
1379
+ setOver(true);
1380
+ }
1381
+ },
1382
+ onDragLeave: (e) => {
1383
+ if (!e.currentTarget.contains(e.relatedTarget)) setOver(false);
1384
+ },
1385
+ onDrop: (e) => {
1386
+ e.preventDefault();
1387
+ setOver(false);
1388
+ if (dragId && dragId !== laneId) onDropOn(laneId);
1389
+ },
1390
+ children
1391
+ }
1392
+ );
1393
+ }
1394
+ var HEALTH_WORD = { ok: "Healthy", warn: "Needs a look", fail: "Failing", none: "No reports yet" };
1395
+ function consequenceOf(a, org, slice) {
1396
+ if (!slice) return "";
1397
+ const descendants = (id) => {
1398
+ const kids = org.agents.filter((x) => x.parent === id);
1399
+ return kids.reduce((n, k) => n + 1 + descendants(k.id), 0);
1400
+ };
1401
+ const declares = a.rules?.length ?? 0;
1402
+ const below = descendants(a.id);
1403
+ if (below === 0) return "";
1404
+ const parts = [];
1405
+ if (declares) parts.push(`declares ${declares} rule${declares === 1 ? "" : "s"}`);
1406
+ parts.push(`binds ${below} below`);
1407
+ return parts.join(" \xB7 ");
1408
+ }
1409
+ function AgentFacts({ a, org, health, showLaw }) {
1410
+ const slice = useMemo(() => orgBootSlice(org, a.id), [org, a.id]);
1411
+ const consequence = useMemo(() => consequenceOf(a, org, slice), [a, org, slice]);
1412
+ const pulse = pulseFor(a, health);
1413
+ const light = lightFor(a, health);
1414
+ if (!slice) return null;
1415
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1416
+ consequence && /* @__PURE__ */ jsx("span", { className: "ag-flows", title: "what flows out of this node \u2014 the org file is the source booboo_boot reads", children: consequence }),
1417
+ /* @__PURE__ */ jsxs("span", { className: "ag-facts", children: [
1418
+ /* @__PURE__ */ jsx("em", { className: `ag-fact ag-fact-health ${light}`, children: HEALTH_WORD[light] }),
1419
+ /* @__PURE__ */ jsxs("em", { className: "ag-fact ag-fact-report", title: typeof a.cadence === "number" ? `${pulse?.lastAt ? `last report filed ${relTime(pulse.lastAt)}` : "no report filed yet"} \xB7 expected ${everyN(a.cadence)}; amber past about twice that` : pulse?.lastAt ? `last report filed ${relTime(pulse.lastAt)}` : "no report filed yet", children: [
1420
+ pulse?.lastAt ? `reported ${relTime(pulse.lastAt)}` : "",
1421
+ typeof a.cadence === "number" && a.kind !== "automation" && /* @__PURE__ */ jsxs("b", { className: "ag-cadence", children: [
1422
+ " \xB7 ",
1423
+ everyN(a.cadence)
1424
+ ] })
1425
+ ] })
1426
+ ] }),
1427
+ showLaw && /* @__PURE__ */ jsx("span", { className: "ag-law", title: "inheritance in boot order", children: slice.chain.map((c, i) => /* @__PURE__ */ jsxs("span", { children: [
1428
+ i > 0 && /* @__PURE__ */ jsx("i", { className: "ag-law-arrow", children: "\u2193" }),
1429
+ c.name
1430
+ ] }, c.id)) })
1431
+ ] });
1432
+ }
723
1433
  function AgentCard({
724
1434
  a,
1435
+ org,
725
1436
  isRoot,
726
1437
  depth,
727
1438
  order,
@@ -731,16 +1442,19 @@ function AgentCard({
731
1442
  onDragStart,
732
1443
  onDropOn,
733
1444
  childCount,
734
- light = "none"
1445
+ light = "none",
1446
+ health = null,
1447
+ onHover,
1448
+ showLaw = false
735
1449
  }) {
736
1450
  const [over, setOver] = useState(false);
737
- const nBuckets = a.buckets?.length ?? 0;
738
1451
  const nSkills = a.skills?.length ?? 0;
1452
+ const parentName = a.parent ? org.agents.find((x) => x.id === a.parent)?.name : null;
739
1453
  return /* @__PURE__ */ jsxs(
740
1454
  "div",
741
1455
  {
742
- className: `ag${isRoot ? " root" : ""}${selected ? " sel" : ""}${over ? " over" : ""}${dragId === a.id ? " dragging" : ""}`,
743
- style: { ["--h"]: bucketHue(a.id), ["--d"]: depth, animationDelay: `${Math.min(depth * 70 + order * 45, 600)}ms` },
1456
+ className: `ag${isRoot ? " root" : ""}${depth >= 2 ? " staff" : ""}${selected ? " sel" : ""}${over ? " over" : ""}${dragId === a.id ? " dragging" : ""}${showLaw ? " law-on" : ""}${light !== "none" ? " h-" + light : ""}`,
1457
+ style: { ["--d"]: depth, animationDelay: `${Math.min(depth * 70 + order * 45, 600)}ms` },
744
1458
  draggable: !isRoot,
745
1459
  tabIndex: 0,
746
1460
  onKeyDown: (e) => {
@@ -769,16 +1483,28 @@ function AgentCard({
769
1483
  setOver(false);
770
1484
  onDropOn(a.id);
771
1485
  },
1486
+ onMouseEnter: () => onHover?.(a.id),
1487
+ onMouseLeave: () => onHover?.(null),
1488
+ "data-rail": isRoot ? "gm" : depth === 1 ? "dept" : "staff",
1489
+ "data-id": a.id,
1490
+ "data-parent": a.parent ?? "",
772
1491
  children: [
773
- light !== "none" && /* @__PURE__ */ jsx("i", { className: `ag-light ${light}`, title: `fleet health: ${light}` }),
774
- /* @__PURE__ */ jsx("span", { className: "ag-ava", children: a.emoji || "\u{1F916}" }),
775
- /* @__PURE__ */ jsx("span", { className: "ag-name", children: a.name }),
776
- /* @__PURE__ */ jsx("span", { className: "ag-role", children: a.role || " " }),
777
- /* @__PURE__ */ jsxs("span", { className: "ag-meta", children: [
778
- nBuckets > 0 && /* @__PURE__ */ jsxs("em", { title: "memory buckets", children: [
779
- "\u25A4 ",
780
- nBuckets
781
- ] }),
1492
+ light !== "none" && /* @__PURE__ */ jsx(
1493
+ "i",
1494
+ {
1495
+ className: `ag-light ${light}`,
1496
+ style: { ["--lamp-delay"]: `${bucketHue(a.id) % 26 / 10}s` },
1497
+ title: `health: ${light === "ok" ? "healthy" : light === "warn" ? "needs a look" : "failing"}`
1498
+ }
1499
+ ),
1500
+ /* @__PURE__ */ jsx("span", { className: "ag-eyebrow", children: depth === 0 ? "The house \xB7 rank II" : depth === 1 ? "Department \xB7 rank III" : parentName ?? "Staff" }),
1501
+ /* @__PURE__ */ jsxs("div", { className: "ag-head", children: [
1502
+ depth <= 1 && a.emoji && /* @__PURE__ */ jsx("span", { className: "ag-ava", children: a.emoji }),
1503
+ /* @__PURE__ */ jsx("span", { className: "ag-name", children: a.name })
1504
+ ] }),
1505
+ a.role && /* @__PURE__ */ jsx("span", { className: "ag-role", children: a.role }),
1506
+ /* @__PURE__ */ jsx(AgentFacts, { a, org, health, showLaw }),
1507
+ (nSkills > 0 || childCount > 0) && /* @__PURE__ */ jsxs("span", { className: "ag-meta", children: [
782
1508
  nSkills > 0 && /* @__PURE__ */ jsxs("em", { title: "skills", children: [
783
1509
  "\u2726 ",
784
1510
  nSkills
@@ -799,7 +1525,8 @@ function ChartNode({
799
1525
  order,
800
1526
  ...cardProps
801
1527
  }) {
802
- const kids = org.agents.filter((c) => c.parent === a.id && c.kind !== "automation");
1528
+ const kidsRaw = org.agents.filter((c) => c.parent === a.id && c.kind !== "automation");
1529
+ const kids = depth === 0 ? [...kidsRaw].sort((x, y) => x.id.localeCompare(y.id)) : kidsRaw;
803
1530
  const machines = (() => {
804
1531
  const out = [];
805
1532
  const walk = (pid) => {
@@ -814,17 +1541,20 @@ function ChartNode({
814
1541
  })();
815
1542
  const lights = machines.map((m) => lightFor(m, cardProps.health));
816
1543
  const trayLight = worst(lights);
1544
+ const isDept = depth === 1 && kids.length > 0;
1545
+ const folded = isDept && cardProps.collapsed?.has(a.id);
817
1546
  const TRAY_MAX = 8;
818
1547
  const hidden = machines.length - TRAY_MAX;
819
1548
  const hiddenBad = machines.slice(TRAY_MAX).reduce(
820
1549
  (n, m) => n + (lightFor(m, cardProps.health) !== "ok" && lightFor(m, cardProps.health) !== "none" ? 1 : 0),
821
1550
  0
822
1551
  );
823
- return /* @__PURE__ */ jsxs("div", { className: "ocn", children: [
1552
+ const cardAndRack = /* @__PURE__ */ jsxs(Fragment, { children: [
824
1553
  /* @__PURE__ */ jsx(
825
1554
  AgentCard,
826
1555
  {
827
1556
  a,
1557
+ org,
828
1558
  isRoot: a.id === org.root,
829
1559
  depth,
830
1560
  order,
@@ -834,7 +1564,10 @@ function ChartNode({
834
1564
  onDragStart: cardProps.onDragStart,
835
1565
  onDropOn: cardProps.onDropOn,
836
1566
  childCount: kids.length,
837
- light: trayLight
1567
+ light: trayLight,
1568
+ health: cardProps.health,
1569
+ onHover: cardProps.onHover,
1570
+ showLaw: cardProps.showLaw
838
1571
  }
839
1572
  ),
840
1573
  machines.length > 0 && /* @__PURE__ */ jsxs("div", { className: `oc-tray ${trayLight}`, children: [
@@ -855,7 +1588,7 @@ function ChartNode({
855
1588
  },
856
1589
  children: [
857
1590
  /* @__PURE__ */ jsx("i", { className: `mac-dot ${lightFor(m, cardProps.health)}${unstableFor(m, cardProps.health) ? " unstable" : ""}` }),
858
- /* @__PURE__ */ jsx("span", { className: "mac-emoji", children: m.emoji || "\u2699\uFE0F" }),
1591
+ /* @__PURE__ */ jsx(BrandMark, { agent: m }),
859
1592
  /* @__PURE__ */ jsx("span", { className: "mac-name", children: m.name })
860
1593
  ]
861
1594
  },
@@ -879,13 +1612,78 @@ function ChartNode({
879
1612
  ]
880
1613
  }
881
1614
  )
882
- ] }),
1615
+ ] })
1616
+ ] });
1617
+ return /* @__PURE__ */ jsxs("div", { className: "ocn", children: [
1618
+ depth === 0 ? /* @__PURE__ */ jsx("div", { className: "casc-head", children: cardAndRack }) : cardAndRack,
883
1619
  kids.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
884
- /* @__PURE__ */ jsx("div", { className: "oc-down" }),
885
- /* @__PURE__ */ jsx("div", { className: `oc-row${kids.length > 3 ? " wrap" : ""}`, children: kids.map((k, i) => /* @__PURE__ */ jsx("div", { className: "oc-child", style: { ["--h"]: bucketHue(k.id) }, children: /* @__PURE__ */ jsx(ChartNode, { org, a: k, depth: depth + 1, order: i, ...cardProps }) }, k.id)) })
1620
+ /* @__PURE__ */ jsx(
1621
+ "div",
1622
+ {
1623
+ className: `oc-down${isDept ? " foldable" : ""}${folded ? " folded" : ""}`,
1624
+ role: isDept ? "button" : void 0,
1625
+ tabIndex: isDept ? 0 : void 0,
1626
+ title: isDept ? folded ? `${kids.length} staff \u2014 click to expand (semantic zoom: department \u2192 role)` : "click to fold this department to head-only (semantic zoom: role \u2192 department)" : void 0,
1627
+ onClick: isDept ? (e) => {
1628
+ e.stopPropagation();
1629
+ cardProps.onToggleCollapse?.(a.id);
1630
+ } : void 0,
1631
+ onKeyDown: isDept ? (e) => {
1632
+ if (e.key === "Enter" || e.key === " ") {
1633
+ e.preventDefault();
1634
+ cardProps.onToggleCollapse?.(a.id);
1635
+ }
1636
+ } : void 0,
1637
+ children: isDept && /* @__PURE__ */ jsx("i", { className: "oc-fold-glyph", children: folded ? "\u25B8" : "\u25BE" })
1638
+ }
1639
+ ),
1640
+ folded ? /* @__PURE__ */ jsxs(
1641
+ "button",
1642
+ {
1643
+ type: "button",
1644
+ className: "oc-folded-summary",
1645
+ onClick: (e) => {
1646
+ e.stopPropagation();
1647
+ cardProps.onToggleCollapse?.(a.id);
1648
+ },
1649
+ title: "click to expand \u2014 semantic zoom: department \u2192 role",
1650
+ children: [
1651
+ kids.length,
1652
+ " staff \xB7 folded to department level \u2014 click to expand"
1653
+ ]
1654
+ }
1655
+ ) : /* @__PURE__ */ jsx("div", { className: `oc-row${depth > 0 && kids.length > 3 ? " wrap" : ""}${depth === 0 ? " lanes" : ""}`, children: kids.map((k, i) => /* @__PURE__ */ jsx(
1656
+ Lane,
1657
+ {
1658
+ laneId: k.id,
1659
+ isLane: depth === 0,
1660
+ dragId: cardProps.dragId,
1661
+ onDropOn: cardProps.onDropOn,
1662
+ children: /* @__PURE__ */ jsx(ChartNode, { org, a: k, depth: depth + 1, order: i, ...cardProps })
1663
+ },
1664
+ k.id
1665
+ )) })
886
1666
  ] })
887
1667
  ] });
888
1668
  }
1669
+ function BrandMark({ agent }) {
1670
+ const brand = (agent.data ?? {}).brand;
1671
+ const [failed, setFailed] = useState(false);
1672
+ if (typeof brand !== "string" || !brand || failed) {
1673
+ return /* @__PURE__ */ jsx("span", { className: "mac-emoji", children: agent.emoji || "\u2699\uFE0F" });
1674
+ }
1675
+ return /* @__PURE__ */ jsx(
1676
+ "img",
1677
+ {
1678
+ className: "mac-brand",
1679
+ src: `https://cdn.simpleicons.org/${brand}/000`,
1680
+ alt: "",
1681
+ loading: "lazy",
1682
+ draggable: false,
1683
+ onError: () => setFailed(true)
1684
+ }
1685
+ );
1686
+ }
889
1687
  function Chip({ children, tone, onClick }) {
890
1688
  return /* @__PURE__ */ jsx("span", { className: `chip${tone ? ` ${tone}` : ""}${onClick ? " tap" : ""}`, onClick, children });
891
1689
  }
@@ -938,8 +1736,19 @@ function Dossier({
938
1736
  )
939
1737
  ).then((counts) => setMemCount(counts.reduce((s, n) => s + n, 0)));
940
1738
  fetchReports(api, id, 100).then(({ total, nodes }) => {
941
- setRepCount(total);
942
- setReports(nodes.slice(0, 4));
1739
+ if (total > 0) {
1740
+ setRepCount(total);
1741
+ setReports(nodes.slice(0, 4));
1742
+ return;
1743
+ }
1744
+ return fetchReports(api, null, 2e3).then(({ nodes: all }) => {
1745
+ const mine = all.filter((n) => reportAgentId(n) === id);
1746
+ setRepCount(mine.length);
1747
+ setReports(mine.slice(0, 4));
1748
+ });
1749
+ }).catch(() => {
1750
+ setRepCount(0);
1751
+ setReports([]);
943
1752
  });
944
1753
  }, [id, hasSnapshot, slice, api]);
945
1754
  const mem = useCountUp(memCount ?? 0);
@@ -978,13 +1787,10 @@ function Dossier({
978
1787
  return /* @__PURE__ */ jsxs("aside", { className: "doss", onClick: (e) => e.stopPropagation(), children: [
979
1788
  /* @__PURE__ */ jsxs("div", { className: "doss-head", children: [
980
1789
  /* @__PURE__ */ jsx("span", { className: "doss-emoji", children: a.emoji || "\u{1F916}" }),
981
- /* @__PURE__ */ jsxs("div", { children: [
982
- /* @__PURE__ */ jsxs("h2", { children: [
983
- a.name,
984
- " ",
985
- a.kind === "automation" && /* @__PURE__ */ jsx("span", { className: "auto-badge", children: "automation" })
986
- ] }),
987
- a.role && /* @__PURE__ */ jsx("p", { className: "doss-role", children: a.role })
1790
+ /* @__PURE__ */ jsxs("h2", { children: [
1791
+ a.name,
1792
+ " ",
1793
+ a.kind === "automation" && /* @__PURE__ */ jsx("span", { className: "auto-badge", children: "automation" })
988
1794
  ] }),
989
1795
  /* @__PURE__ */ jsxs("div", { className: "doss-head-actions", children: [
990
1796
  !edit && /* @__PURE__ */ jsx("button", { className: "doss-3d", title: "edit this agent", onClick: startEdit, children: "\u270E edit" }),
@@ -992,6 +1798,7 @@ function Dossier({
992
1798
  /* @__PURE__ */ jsx("button", { className: "doss-close", title: "close the dossier", "aria-label": "close the dossier", onClick: onClose, children: "\u2715" })
993
1799
  ] })
994
1800
  ] }),
1801
+ a.role && /* @__PURE__ */ jsx("p", { className: "doss-role", children: a.role }),
995
1802
  edit && /* @__PURE__ */ jsxs("div", { className: "doss-edit", children: [
996
1803
  /* @__PURE__ */ jsxs("div", { className: "doss-edit-row2", children: [
997
1804
  /* @__PURE__ */ jsxs("label", { children: [
@@ -1044,9 +1851,8 @@ function Dossier({
1044
1851
  p.n
1045
1852
  ] }),
1046
1853
  typeof a.cadence === "number" && /* @__PURE__ */ jsxs("span", { children: [
1047
- "expected every ",
1048
- a.cadence,
1049
- "h"
1854
+ "reports ",
1855
+ everyN(a.cadence)
1050
1856
  ] })
1051
1857
  ] });
1052
1858
  })(),
@@ -1217,7 +2023,8 @@ function OrgScreen({
1217
2023
  hasSnapshot,
1218
2024
  onUpdate,
1219
2025
  onAdd,
1220
- onRemove
2026
+ onRemove,
2027
+ showLaw
1221
2028
  }) {
1222
2029
  const api = useApi();
1223
2030
  const [health, setHealth] = useState(null);
@@ -1226,10 +2033,27 @@ function OrgScreen({
1226
2033
  fetchReports(api, null, 1e4).then(({ nodes }) => setHealth(buildHealthMap(nodes))).catch(() => {
1227
2034
  });
1228
2035
  }, [hasSnapshot, api]);
2036
+ const [hoverId, setHoverId] = useState(null);
2037
+ const litBuckets = useMemo(
2038
+ () => new Set(hoverId ? orgBootSlice(draft, hoverId)?.buckets ?? [] : []),
2039
+ [hoverId, draft]
2040
+ );
2041
+ const [collapsed, setCollapsed] = useState(() => /* @__PURE__ */ new Set());
2042
+ const depts = useMemo(() => draft.agents.filter((a) => a.parent === draft.root), [draft]);
2043
+ const toggleCollapse = useCallback((id) => {
2044
+ setCollapsed((s) => {
2045
+ const n = new Set(s);
2046
+ n.has(id) ? n.delete(id) : n.add(id);
2047
+ return n;
2048
+ });
2049
+ }, []);
2050
+ const allFolded = depts.length > 0 && depts.every((d) => collapsed.has(d.id));
2051
+ const railVersion = `${draft.agents.length}:${[...collapsed].sort().join(",")}:${showLaw}:${health ? 1 : 0}:${selected ?? ""}`;
1229
2052
  const fitRef = useRef(null);
1230
2053
  const chartRef = useRef(null);
1231
2054
  const [fit, setFit] = useState(1);
1232
2055
  const [zoom, setZoom] = useState(null);
2056
+ const [nat, setNat] = useState({ w: 0, h: 0 });
1233
2057
  const eff = zoom ?? fit;
1234
2058
  useLayoutEffect(() => {
1235
2059
  const vp = fitRef.current, chart = chartRef.current;
@@ -1239,15 +2063,18 @@ function OrgScreen({
1239
2063
  const natW = chart.scrollWidth, natH = chart.scrollHeight;
1240
2064
  const availW = vp.clientWidth - PAD * 2, availH = vp.clientHeight - PAD * 2;
1241
2065
  if (natW <= 0 || natH <= 0) return;
1242
- const k = Math.min(1, availW / natW, availH / natH);
1243
- setFit(k > 0 ? k : 1);
2066
+ setNat((p) => p.w === natW && p.h === natH ? p : { w: natW, h: natH });
2067
+ const kW = availW / natW;
2068
+ const kH = availH / natH;
2069
+ const k = natH > availH * 1.35 ? kW : Math.min(kW, kH);
2070
+ setFit(Math.min(1, k > 0 ? k : 1));
1244
2071
  };
1245
2072
  measure();
1246
2073
  const ro = new ResizeObserver(measure);
1247
2074
  ro.observe(vp);
1248
2075
  ro.observe(chart);
1249
2076
  return () => ro.disconnect();
1250
- }, [draft, health, selected]);
2077
+ }, [draft, health, selected, collapsed]);
1251
2078
  const effRef = useRef(eff);
1252
2079
  effRef.current = eff;
1253
2080
  useEffect(() => {
@@ -1264,24 +2091,74 @@ function OrgScreen({
1264
2091
  }, []);
1265
2092
  const root = draft.agents.find((a) => a.id === draft.root);
1266
2093
  return /* @__PURE__ */ jsxs("div", { className: "body", onClick: () => setSelected(null), children: [
1267
- /* @__PURE__ */ jsxs("main", { className: "tree", onClick: (e) => e.stopPropagation(), children: [
1268
- /* @__PURE__ */ jsx("p", { className: "tree-hint", children: "drag an agent \u2014 or a tray machine \u2014 onto its new parent \xB7 click for its dossier \xB7 machine trays show live health" }),
1269
- /* @__PURE__ */ jsx("div", { className: `chart-fit${zoom !== null ? " zoomed" : ""}`, ref: fitRef, children: /* @__PURE__ */ jsx("div", { className: "chart", ref: chartRef, style: { ["--fit"]: eff }, children: root && /* @__PURE__ */ jsx(
1270
- ChartNode,
2094
+ /* @__PURE__ */ jsxs("main", { className: `tree${showLaw ? " law-on" : ""}`, onClick: (e) => e.stopPropagation(), children: [
2095
+ /* @__PURE__ */ jsx("p", { className: "tree-hint", children: showLaw ? "the rails now trace authority \u2014 brass flows from the House Standard through every SOP to every role" : "drag a plate \u2014 or a machine \u2014 onto its new parent \xB7 click for its dossier \xB7 machine racks show live health" }),
2096
+ /* @__PURE__ */ jsxs("div", { className: "ranks", children: [
2097
+ /* @__PURE__ */ jsxs("div", { className: "rank", children: [
2098
+ /* @__PURE__ */ jsx("b", { children: "I" }),
2099
+ "The standard"
2100
+ ] }),
2101
+ /* @__PURE__ */ jsxs("div", { className: "rank", children: [
2102
+ /* @__PURE__ */ jsx("b", { children: "II" }),
2103
+ "The executive"
2104
+ ] }),
2105
+ /* @__PURE__ */ jsxs("div", { className: "rank", children: [
2106
+ /* @__PURE__ */ jsx("b", { children: "III" }),
2107
+ "Departments"
2108
+ ] }),
2109
+ /* @__PURE__ */ jsxs("div", { className: "rank", children: [
2110
+ /* @__PURE__ */ jsx("b", { children: "IV" }),
2111
+ "Staff & machines"
2112
+ ] })
2113
+ ] }),
2114
+ /* @__PURE__ */ jsx("div", { className: `chart-fit${zoom !== null ? " zoomed" : ""}`, ref: fitRef, children: /* @__PURE__ */ jsx("div", { className: "chart-scaled", style: { width: nat.w * eff, height: nat.h * eff }, children: /* @__PURE__ */ jsxs(
2115
+ "div",
1271
2116
  {
1272
- org: draft,
1273
- a: root,
1274
- depth: 0,
1275
- order: 0,
1276
- selected,
1277
- dragId,
1278
- health,
1279
- onSelect: setSelected,
1280
- onDragStart: setDragId,
1281
- onDropOn: dropOn
2117
+ className: "chart",
2118
+ ref: chartRef,
2119
+ style: {
2120
+ ["--fit"]: eff,
2121
+ transform: zoom !== null ? `scale(${eff})` : `translateX(-50%) scale(${eff})`
2122
+ },
2123
+ children: [
2124
+ /* @__PURE__ */ jsx(CascadeRails, { chartRef, version: railVersion }),
2125
+ /* @__PURE__ */ jsxs("div", { className: "cascade", children: [
2126
+ /* @__PURE__ */ jsx(LawPlate, { org: draft, selected: selected === draft.root, onSelect: setSelected }),
2127
+ root && /* @__PURE__ */ jsx(
2128
+ ChartNode,
2129
+ {
2130
+ org: draft,
2131
+ a: root,
2132
+ depth: 0,
2133
+ order: 0,
2134
+ selected,
2135
+ dragId,
2136
+ health,
2137
+ onSelect: setSelected,
2138
+ onDragStart: setDragId,
2139
+ onDropOn: dropOn,
2140
+ onHover: setHoverId,
2141
+ showLaw,
2142
+ collapsed,
2143
+ onToggleCollapse: toggleCollapse
2144
+ }
2145
+ )
2146
+ ] })
2147
+ ]
1282
2148
  }
1283
2149
  ) }) }),
1284
2150
  /* @__PURE__ */ jsxs("div", { className: "zoomer", children: [
2151
+ /* @__PURE__ */ jsx(
2152
+ "button",
2153
+ {
2154
+ type: "button",
2155
+ className: allFolded ? "on" : "",
2156
+ title: allFolded ? "unfold every department \u2014 see roles again" : "fold every department to head-only \u2014 house \u2192 department",
2157
+ onClick: () => setCollapsed(allFolded ? /* @__PURE__ */ new Set() : new Set(depts.map((d) => d.id))),
2158
+ children: allFolded ? "\u2302 house" : "\u25BE roles"
2159
+ }
2160
+ ),
2161
+ /* @__PURE__ */ jsx("span", { className: "zoomer-sep" }),
1285
2162
  /* @__PURE__ */ jsx("button", { type: "button", title: "zoom out (ctrl+wheel)", onClick: () => setZoom(Math.max(0.2, eff / 1.25)), children: "\u2212" }),
1286
2163
  /* @__PURE__ */ jsxs("span", { className: "zoomer-pct", children: [
1287
2164
  Math.round(eff * 100),
@@ -1289,7 +2166,8 @@ function OrgScreen({
1289
2166
  ] }),
1290
2167
  /* @__PURE__ */ jsx("button", { type: "button", title: "zoom in (ctrl+wheel)", onClick: () => setZoom(Math.min(2.5, eff * 1.25)), children: "\uFF0B" }),
1291
2168
  /* @__PURE__ */ jsx("button", { type: "button", className: zoom === null ? "on" : "", title: "fit the whole org in view", onClick: () => setZoom(null), children: "fit" })
1292
- ] })
2169
+ ] }),
2170
+ /* @__PURE__ */ jsx(LedgerShelf, { org: draft, lit: litBuckets })
1293
2171
  ] }),
1294
2172
  selected && /* @__PURE__ */ jsx(
1295
2173
  Dossier,
@@ -1418,8 +2296,8 @@ function ReportsScreen({ org, hasSnapshot }) {
1418
2296
  if (!hasSnapshot) return;
1419
2297
  fetchReports(api, null, 1e3).then(({ nodes }) => setRows(nodes)).catch(() => setRows([]));
1420
2298
  }, [hasSnapshot, api]);
1421
- const agents = useMemo(() => [...new Set((rows ?? []).map((r) => r.cluster ?? ""))].filter(Boolean), [rows]);
1422
- const shown = (rows ?? []).filter((r) => !who || r.cluster === who);
2299
+ const agents = useMemo(() => [...new Set((rows ?? []).map(reportAgentId))].filter(Boolean), [rows]);
2300
+ const shown = (rows ?? []).filter((r) => !who || reportAgentId(r) === who);
1423
2301
  const total = useCountUp(shown.length);
1424
2302
  if (!hasSnapshot)
1425
2303
  return /* @__PURE__ */ jsxs("div", { className: "screen", children: [
@@ -1450,15 +2328,16 @@ function ReportsScreen({ org, hasSnapshot }) {
1450
2328
  ")."
1451
2329
  ] }) : /* @__PURE__ */ jsxs("div", { className: "timeline", children: [
1452
2330
  shown.slice(0, 100).map((r) => {
1453
- const a = r.cluster ? nameOf.get(r.cluster) : void 0;
2331
+ const filer = reportAgentId(r);
2332
+ const a = filer ? nameOf.get(filer) : void 0;
1454
2333
  const when = relTime(nodeAt(r));
1455
2334
  const sum = nodeSummary(r);
1456
- return /* @__PURE__ */ jsxs("div", { className: "tl-row", style: { ["--h"]: bucketHue(r.cluster ?? "x") }, children: [
2335
+ return /* @__PURE__ */ jsxs("div", { className: "tl-row", style: { ["--h"]: bucketHue(filer || "x") }, children: [
1457
2336
  /* @__PURE__ */ jsx("div", { className: "tl-dot" }),
1458
2337
  /* @__PURE__ */ jsxs("div", { className: "tl-body", children: [
1459
2338
  /* @__PURE__ */ jsxs("div", { className: "tl-top", children: [
1460
2339
  /* @__PURE__ */ jsx("span", { className: "tl-ava", children: a?.emoji ?? "\u{1F916}" }),
1461
- /* @__PURE__ */ jsx("span", { className: "tl-agent", children: a?.name ?? r.cluster ?? "unknown" }),
2340
+ /* @__PURE__ */ jsx("span", { className: "tl-agent", children: a?.name ?? filer ?? "unknown" }),
1462
2341
  when && /* @__PURE__ */ jsx("span", { className: "tl-when", children: when })
1463
2342
  ] }),
1464
2343
  /* @__PURE__ */ jsx("p", { className: "tl-sum", children: sum || r.label })
@@ -1558,7 +2437,8 @@ function App() {
1558
2437
  api("/org").then((o) => {
1559
2438
  setSaved(o);
1560
2439
  setDraft(o);
1561
- setSelected(o.root);
2440
+ const embed = new URLSearchParams(window.location.search).get("embed");
2441
+ if (!embed) setSelected(o.root);
1562
2442
  }).catch(() => setErr("Can't load the organigram \u2014 is `booboo panel --org` running?"));
1563
2443
  api("/stats").then(setStats).catch(() => setStats(null));
1564
2444
  Promise.all([
@@ -1576,15 +2456,32 @@ function App() {
1576
2456
  for (const a of draft.agents) {
1577
2457
  const b = before.get(a.id);
1578
2458
  if (!b) {
1579
- out.push(`\uFF0B ${a.name} under ${a.parent ? name(a.parent) : "root"}`);
2459
+ out.push({ key: `add:${a.id}`, id: a.id, kind: "added", label: `\uFF0B ${a.name} under ${a.parent ? name(a.parent) : "root"}` });
1580
2460
  continue;
1581
2461
  }
1582
- if ((b.parent ?? null) !== (a.parent ?? null)) out.push(`${a.name} \u2192 now under ${a.parent ? name(a.parent) : "root"}`);
1583
- if (JSON.stringify(noParent(b)) !== JSON.stringify(noParent(a))) out.push(`\u270E ${a.name} edited`);
2462
+ if ((b.parent ?? null) !== (a.parent ?? null)) out.push({ key: `move:${a.id}`, id: a.id, kind: "moved", label: `${a.name} \u2192 now under ${a.parent ? name(a.parent) : "root"}` });
2463
+ if (JSON.stringify(noParent(b)) !== JSON.stringify(noParent(a))) out.push({ key: `edit:${a.id}`, id: a.id, kind: "edited", label: `\u270E ${a.name} edited` });
1584
2464
  }
1585
- for (const b of saved.agents) if (!after.has(b.id)) out.push(`\u2212 ${b.name} removed`);
2465
+ for (const b of saved.agents) if (!after.has(b.id)) out.push({ key: `rm:${b.id}`, id: b.id, kind: "removed", label: `\u2212 ${b.name} removed` });
1586
2466
  return out;
1587
2467
  }, [saved, draft]);
2468
+ const revertOne = useCallback((c) => {
2469
+ if (!saved || !draft) return;
2470
+ const was = saved.agents.find((a) => a.id === c.id);
2471
+ setDraft((d) => {
2472
+ if (!d) return d;
2473
+ if (c.kind === "added") return { ...d, agents: d.agents.filter((a) => a.id !== c.id) };
2474
+ if (c.kind === "removed" && was) return { ...d, agents: [...d.agents, was] };
2475
+ if (!was) return d;
2476
+ return {
2477
+ ...d,
2478
+ agents: d.agents.map((a) => {
2479
+ if (a.id !== c.id) return a;
2480
+ return c.kind === "moved" ? { ...a, parent: was.parent } : { ...was, parent: a.parent };
2481
+ })
2482
+ };
2483
+ });
2484
+ }, [saved, draft]);
1588
2485
  const updateAgent = useCallback((id, patch) => {
1589
2486
  setDraft((d) => d ? { ...d, agents: d.agents.map((a) => a.id === id ? { ...a, ...patch } : a) } : d);
1590
2487
  }, []);
@@ -1657,10 +2554,12 @@ function App() {
1657
2554
  const memTotal = useCountUp(totals?.mem ?? 0, 1100);
1658
2555
  const repTotal = useCountUp(totals?.rep ?? 0, 1300);
1659
2556
  const [theme, toggleTheme] = useTheme();
2557
+ const [showLaw, setShowLaw] = useState(false);
1660
2558
  if (err && !draft) return /* @__PURE__ */ jsx("div", { className: "pnl-fatal", children: err });
1661
2559
  if (!draft) return /* @__PURE__ */ jsx("div", { className: "pnl-fatal calm", children: "waking the organigram\u2026" });
1662
2560
  return /* @__PURE__ */ jsxs("div", { className: "pnl", children: [
1663
2561
  /* @__PURE__ */ jsx("div", { className: "pnl-aurora", "aria-hidden": true }),
2562
+ /* @__PURE__ */ jsx(Constellation, {}),
1664
2563
  /* @__PURE__ */ jsxs("header", { className: "bar", children: [
1665
2564
  /* @__PURE__ */ jsxs("div", { className: "bar-brand", children: [
1666
2565
  "\u{1F43E} ",
@@ -1685,6 +2584,16 @@ function App() {
1685
2584
  ] })
1686
2585
  ] }),
1687
2586
  /* @__PURE__ */ jsxs("div", { className: "bar-actions", children: [
2587
+ tab === "org" && /* @__PURE__ */ jsx(
2588
+ "button",
2589
+ {
2590
+ className: `btn ghost law-toggle${showLaw ? " on" : ""}`,
2591
+ title: "trace rule inheritance \u2014 House Standard \u2192 SOP \u2192 role",
2592
+ "aria-pressed": showLaw,
2593
+ onClick: () => setShowLaw((v) => !v),
2594
+ children: "\u2696 show the law"
2595
+ }
2596
+ ),
1688
2597
  /* @__PURE__ */ jsx("button", { className: "btn ghost theme-toggle", title: "light / dark", "aria-label": "toggle light or dark theme", onClick: toggleTheme, children: theme === "dark" ? "\u2600" : "\u263E" }),
1689
2598
  changes.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
1690
2599
  /* @__PURE__ */ jsxs("span", { className: "bar-draft", children: [
@@ -1702,7 +2611,23 @@ function App() {
1702
2611
  " ",
1703
2612
  t.label
1704
2613
  ] }, t.id)) }),
1705
- changes.length > 0 && tab === "org" && /* @__PURE__ */ jsx("div", { className: "pending", children: changes.map((c) => /* @__PURE__ */ jsx("span", { className: "pending-item", children: c }, c)) }),
2614
+ changes.length > 0 && tab === "org" && /* @__PURE__ */ jsxs("div", { className: "pending", children: [
2615
+ /* @__PURE__ */ jsx("span", { className: "pending-lead", children: "unapplied" }),
2616
+ changes.map((c) => /* @__PURE__ */ jsxs(
2617
+ "button",
2618
+ {
2619
+ type: "button",
2620
+ className: "pending-item",
2621
+ title: "undo just this change",
2622
+ onClick: () => revertOne(c),
2623
+ children: [
2624
+ c.label,
2625
+ /* @__PURE__ */ jsx("i", { "aria-hidden": "true", children: "undo" })
2626
+ ]
2627
+ },
2628
+ c.key
2629
+ ))
2630
+ ] }),
1706
2631
  err && /* @__PURE__ */ jsx("div", { className: "pnl-err", children: err }),
1707
2632
  /* @__PURE__ */ jsxs("div", { className: "content", children: [
1708
2633
  tab === "org" && /* @__PURE__ */ jsx(
@@ -1717,7 +2642,8 @@ function App() {
1717
2642
  hasSnapshot: !!stats,
1718
2643
  onUpdate: updateAgent,
1719
2644
  onAdd: addAgent,
1720
- onRemove: removeAgent
2645
+ onRemove: removeAgent,
2646
+ showLaw
1721
2647
  }
1722
2648
  ),
1723
2649
  tab === "buckets" && /* @__PURE__ */ jsx(BucketsScreen, { org: draft, param, hasSnapshot: !!stats }),
@@ -1737,7 +2663,7 @@ function Panel({ api = defaultApi } = {}) {
1737
2663
  // package.json
1738
2664
  var package_default = {
1739
2665
  name: "@booboo-brain/panel",
1740
- version: "0.5.3",
2666
+ version: "0.5.4",
1741
2667
  description: "Booboo panel \u2014 the organigram. Run your agents like a company: a drag-drop hierarchy your agents boot from, with buckets, reports, rules and the 3D graph, over one org file + snapshot.",
1742
2668
  license: "MIT",
1743
2669
  repository: {