@songsid/agend 2.3.1 → 2.3.2-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/dist/authority/authority-state.d.ts +4 -0
  2. package/dist/authority/authority-state.js +16 -10
  3. package/dist/authority/authority-state.js.map +1 -1
  4. package/dist/authority/session-authorizations.d.ts +7 -1
  5. package/dist/authority/session-authorizations.js +6 -4
  6. package/dist/authority/session-authorizations.js.map +1 -1
  7. package/dist/authority/web-authority.d.ts +19 -6
  8. package/dist/authority/web-authority.js +22 -8
  9. package/dist/authority/web-authority.js.map +1 -1
  10. package/dist/channel/adapters/discord.d.ts +2 -0
  11. package/dist/channel/adapters/discord.js +33 -3
  12. package/dist/channel/adapters/discord.js.map +1 -1
  13. package/dist/channel/mcp-tools.js +5 -5
  14. package/dist/channel/mcp-tools.js.map +1 -1
  15. package/dist/channel/types.d.ts +11 -0
  16. package/dist/config-validator.js +40 -3
  17. package/dist/config-validator.js.map +1 -1
  18. package/dist/daemon.d.ts +34 -3
  19. package/dist/daemon.js +227 -46
  20. package/dist/daemon.js.map +1 -1
  21. package/dist/delivery-outbox.d.ts +12 -1
  22. package/dist/delivery-outbox.js +67 -15
  23. package/dist/delivery-outbox.js.map +1 -1
  24. package/dist/doctor.d.ts +3 -0
  25. package/dist/doctor.js +9 -0
  26. package/dist/doctor.js.map +1 -1
  27. package/dist/event-log.d.ts +5 -0
  28. package/dist/event-log.js +13 -0
  29. package/dist/event-log.js.map +1 -1
  30. package/dist/fleet-manager.d.ts +140 -0
  31. package/dist/fleet-manager.js +414 -29
  32. package/dist/fleet-manager.js.map +1 -1
  33. package/dist/fleet-process-handlers.d.ts +1 -0
  34. package/dist/fleet-process-handlers.js +9 -1
  35. package/dist/fleet-process-handlers.js.map +1 -1
  36. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +4 -4
  37. package/dist/general-knowledge/skills/web-appearance/SKILL.md +7 -7
  38. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +113 -55
  39. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +114 -56
  40. package/dist/guest-seen-people.d.ts +38 -0
  41. package/dist/guest-seen-people.js +78 -0
  42. package/dist/guest-seen-people.js.map +1 -0
  43. package/dist/instance-config-impact.js +4 -1
  44. package/dist/instance-config-impact.js.map +1 -1
  45. package/dist/instance-lifecycle.d.ts +1 -0
  46. package/dist/instance-lifecycle.js +16 -1
  47. package/dist/instance-lifecycle.js.map +1 -1
  48. package/dist/locale.js +38 -10
  49. package/dist/locale.js.map +1 -1
  50. package/dist/mac-auto-restore.d.ts +117 -0
  51. package/dist/mac-auto-restore.js +294 -0
  52. package/dist/mac-auto-restore.js.map +1 -0
  53. package/dist/outbound-schemas.d.ts +2 -0
  54. package/dist/outbound-schemas.js +2 -0
  55. package/dist/outbound-schemas.js.map +1 -1
  56. package/dist/public-web-gateway.js +9 -1
  57. package/dist/public-web-gateway.js.map +1 -1
  58. package/dist/settings-api.d.ts +26 -0
  59. package/dist/settings-api.js +107 -1
  60. package/dist/settings-api.js.map +1 -1
  61. package/dist/settings-change.js +37 -3
  62. package/dist/settings-change.js.map +1 -1
  63. package/dist/settings-effect.js +5 -2
  64. package/dist/settings-effect.js.map +1 -1
  65. package/dist/start-attempt.d.ts +9 -5
  66. package/dist/start-attempt.js +27 -8
  67. package/dist/start-attempt.js.map +1 -1
  68. package/dist/start-failure-chat.d.ts +4 -0
  69. package/dist/start-failure-chat.js +23 -6
  70. package/dist/start-failure-chat.js.map +1 -1
  71. package/dist/start-failure-notice.d.ts +4 -0
  72. package/dist/start-failure-notice.js +8 -0
  73. package/dist/start-failure-notice.js.map +1 -1
  74. package/dist/start-failure.d.ts +25 -0
  75. package/dist/start-failure.js +75 -2
  76. package/dist/start-failure.js.map +1 -1
  77. package/dist/turn-reply-guard.d.ts +25 -0
  78. package/dist/turn-reply-guard.js +111 -12
  79. package/dist/turn-reply-guard.js.map +1 -1
  80. package/dist/types.d.ts +18 -1
  81. package/dist/ui/backend-signin.js +5 -2
  82. package/dist/ui/chat-ambient.js +1 -1
  83. package/dist/ui/chat-effects.js +4 -4
  84. package/dist/ui/chat-render.js +5 -2
  85. package/dist/ui/chat-strings.js +2 -2
  86. package/dist/ui/chat-thread.js +2 -2
  87. package/dist/ui/first-run.js +2 -2
  88. package/dist/ui/fleet-canvas-view.js +114 -0
  89. package/dist/ui/fleet-canvas.js +15 -77
  90. package/dist/ui/fleet-office-editor.js +80 -5
  91. package/dist/ui/fleet-office-layout.js +197 -0
  92. package/dist/ui/fleet-office-model.js +6 -3
  93. package/dist/ui/fleet-office-overview.js +167 -0
  94. package/dist/ui/fleet-office-pack.d.ts +39 -0
  95. package/dist/ui/fleet-office-pack.js +240 -29
  96. package/dist/ui/fleet-office-skin.js +140 -29
  97. package/dist/ui/fleet-office-store.js +84 -5
  98. package/dist/ui/fleet-office-view.js +154 -0
  99. package/dist/ui/fleet-office-zones.js +95 -33
  100. package/dist/ui/fleet-office.js +779 -140
  101. package/dist/ui/fleet-usage.js +7 -6
  102. package/dist/ui/panel-chat.js +1 -1
  103. package/dist/ui/panel-details.js +2 -2
  104. package/dist/ui/panel-fleet.js +11 -77
  105. package/dist/ui/panel-log.js +2 -2
  106. package/dist/ui/panel-settings.js +578 -163
  107. package/dist/ui/search-dialog.js +26 -7
  108. package/dist/ui/settings-appearance.js +82 -29
  109. package/dist/ui/settings-apply.js +42 -7
  110. package/dist/ui/settings-dialogs.js +68 -22
  111. package/dist/ui/settings-draft.js +51 -0
  112. package/dist/ui/settings-guests.js +244 -78
  113. package/dist/ui/settings-learn.js +2 -2
  114. package/dist/ui/settings-model.js +2 -0
  115. package/dist/ui/settings-registry.js +142 -0
  116. package/dist/ui/settings-strings.js +102 -44
  117. package/dist/ui/settings-web-authority.js +34 -23
  118. package/dist/ui/shared/app-i18n.js +14 -0
  119. package/dist/ui/shared/app-nav.js +10 -5
  120. package/dist/ui/shared/app-prefs.js +80 -2
  121. package/dist/ui/shared/app-route.js +39 -5
  122. package/dist/ui/shared/app-shell.js +59 -4
  123. package/dist/ui/shared/app-stream.js +2 -1
  124. package/dist/ui/shared/app.css +254 -17
  125. package/dist/ui/shared/app.js +3 -0
  126. package/dist/ui/shared/effect-stock-trust.js +1 -1
  127. package/dist/ui/shared/identity-color.js +4 -4
  128. package/dist/ui/shared/signin.js +2 -2
  129. package/dist/ui/shared/ui-dialog.js +6 -1
  130. package/dist/ui/shared/ui-icons.js +2 -0
  131. package/dist/ui/shared/web-authorize.js +13 -12
  132. package/dist/web-api.d.ts +8 -2
  133. package/dist/web-api.js +160 -5
  134. package/dist/web-api.js.map +1 -1
  135. package/dist/web-auth.js +7 -1
  136. package/dist/web-auth.js.map +1 -1
  137. package/dist/web-experimental.d.ts +1 -1
  138. package/dist/web-experimental.js +1 -1
  139. package/dist/web-experimental.js.map +1 -1
  140. package/dist/web-mod-agent.js +2 -2
  141. package/dist/web-mod-agent.js.map +1 -1
  142. package/dist/web-office-skins.d.ts +108 -0
  143. package/dist/web-office-skins.js +298 -0
  144. package/dist/web-office-skins.js.map +1 -0
  145. package/dist/web-office-store.d.ts +8 -2
  146. package/dist/web-office-store.js +37 -7
  147. package/dist/web-office-store.js.map +1 -1
  148. package/dist/web-settings-patch.d.ts +6 -0
  149. package/dist/web-settings-patch.js +61 -4
  150. package/dist/web-settings-patch.js.map +1 -1
  151. package/dist/web-shell-routes.d.ts +4 -2
  152. package/dist/web-shell-routes.js +17 -7
  153. package/dist/web-shell-routes.js.map +1 -1
  154. package/dist/working-recovery.d.ts +104 -0
  155. package/dist/working-recovery.js +263 -0
  156. package/dist/working-recovery.js.map +1 -0
  157. package/dist/working-state-journal.d.ts +3 -1
  158. package/dist/working-state-journal.js +15 -2
  159. package/dist/working-state-journal.js.map +1 -1
  160. package/package.json +1 -1
@@ -130,17 +130,22 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
130
130
  A short box that clips to its corners (.msg-quote) moves the bar in (--lbar-left) and shortens it (--lbar-inset) so
131
131
  the corner never cuts its ends round.
132
132
  tests/web-left-bar-2330.test.ts keeps a coloured border-left off rounded boxes. */
133
- .lbar, .reply-bar, .msg-quote, .p-log .log-item.k-error, .p-log .log-item.s-failed, .org-node, .us-basis, .us-partial, .cmd-card.bad, .tb-tile,
133
+ .lbar, .reply-bar, .msg-quote, .p-log .log-item.k-error, .p-log .log-item.s-failed, .org-node, .us-basis, .us-partial, .cmd-card.bad, .tb-tile, .s-dirty,
134
134
  .search-row.on, .search-recent-item:hover, .search-recent-item:focus-visible, .search-ctx-line.here, .tw-line:hover, .tw-line:focus-visible,
135
- .u-crow:focus-visible { position: relative; }
135
+ .u-crow:focus-visible, .p-settings .s-nav .seg-item.active, .s-find-item.on { position: relative; }
136
136
  .lbar::before, .reply-bar::before, .msg-quote::before, .p-log .log-item.k-error::before, .p-log .log-item.s-failed::before, .org-node::before,
137
- .us-basis::before, .us-partial::before, .cmd-card.bad::before, .tb-tile::before,
137
+ .us-basis::before, .us-partial::before, .cmd-card.bad::before, .tb-tile::before, .s-dirty::before,
138
138
  /* #2478 item 7 (the user's fixed template): a selected, hovered or keyboard-focused row is the straight bar and a flat
139
139
  background at most — never a rounded fill, a curved inset edge or a rounded ring. */
140
140
  .search-row.on::before, .search-recent-item:hover::before, .search-recent-item:focus-visible::before, .search-ctx-line.here::before,
141
- .tw-line:hover::before, .tw-line:focus-visible::before, .u-crow:focus-visible::before {
141
+ .tw-line:hover::before, .tw-line:focus-visible::before, .u-crow:focus-visible::before,
142
+ .p-settings .s-nav .seg-item.active::before, .s-find-item.on::before {
142
143
  content: ""; position: absolute; left: var(--lbar-left, 0); top: var(--lbar-inset, var(--s-2)); bottom: var(--lbar-inset, var(--s-2)); width: 3px;
143
144
  background: var(--lbar-color, var(--accent)); pointer-events: none; }
145
+ /* #2347: a Settings field edited and not saved yet — the shared straight bar (above), a light tint, and the 3px the bar
146
+ takes added to its padding (and taken back from its margin), so the field's text stays put. */
147
+ .s-dirty { --lbar-color: var(--accent); --lbar-inset: var(--s-1); background: color-mix(in srgb, var(--accent) 7%, transparent);
148
+ padding-left: calc(var(--s-2) + 3px); margin-left: calc(-1 * (var(--s-2) + 3px)); border-radius: 0; align-self: start; }
144
149
  .idc-swatches, .idc-places { border: 0; padding: 0; margin: 0 0 var(--s-3); display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
145
150
  .idc-swatches legend, .idc-places legend { font-weight: var(--fw-semibold); margin-bottom: 6px; width: 100%; }
146
151
  .idc-swatch { --sw: var(--sw-dark); width: 28px; height: 28px; border-radius: 50%; border: 2px solid transparent; background: var(--sw); cursor: pointer; padding: 0; }
@@ -229,6 +234,9 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
229
234
  /* #1620: Settings → Web — the lead, and the read-only list of what is set in fleet.yaml. */
230
235
  .s-web .sub { margin: 0; color: var(--text-3); font-size: var(--fs-sm); }
231
236
  .s-web-fields:disabled select { opacity: 0.6; } /* over the public link: dimmed like the inputs (.field input:disabled) */
237
+ /* #2347: forms that write what an unsaved fleet.yaml edit writes — off, with the way to that edit above them. */
238
+ .s-yaml-lock:disabled select { opacity: 0.6; }
239
+ .s-yaml-lock-note { margin: 0 0 var(--s-3); }
232
240
  .s-web-ro { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: var(--s-1) var(--s-3); margin: 0; font-size: var(--fs-sm); }
233
241
  .s-web-ro > div { display: contents; }
234
242
  .s-web-ro dt { color: var(--text-3); }
@@ -385,6 +393,7 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
385
393
  .dlg-wide { width: min(640px, calc(100vw - 32px)); }
386
394
  .dlg::backdrop { background: var(--scrim); }
387
395
  .dlg-box { display: flex; flex-direction: column; max-height: 85vh; }
396
+ .dlg-box:focus { outline: none; } /* the open dialog's first stop (ui-dialog.js): no ring; Tab moves on with one */
388
397
  .dlg-head { display: flex; align-items: center; gap: var(--s-2); padding: var(--s-4) var(--s-3) var(--s-2) var(--s-5); }
389
398
  .dlg-title { flex: 1; font-size: var(--fs-lg); font-weight: var(--fw-semibold); }
390
399
  .dlg-body { padding: var(--s-2) var(--s-5) var(--s-4); overflow-y: auto; display: flex; flex-direction: column; gap: var(--s-3); }
@@ -440,6 +449,99 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
440
449
  .seg-item.active { background: var(--surface-2); color: var(--text); font-weight: var(--fw-semibold); }
441
450
  /* #1629: Settings has eight sections (Appearance added): they wrap to a second row rather than hide behind a scroll. */
442
451
  .p-settings .seg { flex-wrap: wrap; row-gap: var(--s-1); overflow-x: visible; }
452
+ /* #2347: the sections in two groups, "You" and "Fleet", each with its name; a column on the left on a wide screen. */
453
+ .s-nav { align-items: center; column-gap: var(--s-3); }
454
+ /* #2347, the fixed template (#2330): Settings' sections are square; the one shown is the shared straight bar and a flat
455
+ background (no rounded pill). `.seg` elsewhere (Fleet's tabs, the chat header) is not changed here. */
456
+ .p-settings .s-nav .seg-item { border-radius: 0; }
457
+ .p-settings .s-nav .seg-item.active { background: var(--surface-2); }
458
+ .s-nav-group { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-1); }
459
+ .s-nav-head { font-size: var(--fs-xs); color: var(--text-3, var(--text-2)); text-transform: uppercase; letter-spacing: 0.04em; padding: 0 var(--s-2); }
460
+ .s-anchor { scroll-margin-top: var(--s-4); }
461
+ /* #2347 §6: settings search — on top of the section nav; results grouped by section, the config key small; a setting
462
+ opened from it flashes for 2 s (a tint, no coloured edge). */
463
+ /* One column (`.p-settings .seg` wraps, which in a column would start a second one beside it). */
464
+ .p-settings > .s-nav { flex-direction: column; align-items: stretch; flex-wrap: nowrap; row-gap: var(--s-2); }
465
+ .s-nav-groups { display: flex; flex-wrap: wrap; align-items: center; column-gap: var(--s-3); row-gap: var(--s-1); }
466
+ /* The search never makes the nav wider than its column or row (a flex item's min-content would: a long config key). */
467
+ .s-find, .s-find-results, .s-find-group { min-width: 0; max-width: 100%; }
468
+ .s-find .s-search { width: 100%; box-sizing: border-box; min-width: 0; }
469
+ .s-find .s-search input { text-overflow: ellipsis; }
470
+ .s-find-results { max-height: min(60vh, 420px); overflow-y: auto; margin-top: var(--s-1);
471
+ background: var(--bg); border: 1px solid var(--border); border-radius: var(--r-sm); padding: var(--s-1) 0; }
472
+ .s-find-head { padding: var(--s-2) var(--s-3) var(--s-1); font-size: var(--fs-xs); color: var(--text-2); }
473
+ .s-find-item { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; column-gap: var(--s-2); padding: var(--s-2) var(--s-3); cursor: pointer; min-width: 0; }
474
+ .s-find-label { min-width: 0; overflow-wrap: anywhere; }
475
+ .s-find-item.on { background: var(--surface-2); }
476
+ .s-find-item mark { background: color-mix(in srgb, var(--accent) 22%, transparent); color: inherit; border-radius: 2px; }
477
+ .s-find-key { font-size: var(--fs-xs); color: var(--text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
478
+ .s-find-none, .s-find-foot { padding: var(--s-2) var(--s-3); margin: 0; font-size: var(--fs-xs); color: var(--text-2); }
479
+ .s-find-foot { border-top: 1px solid var(--border); }
480
+ /* #2382 review: "Fix N field(s) in <section> to save" goes there. */
481
+ .s-pending button.s-fix { border: 0; background: none; padding: 0; cursor: pointer; text-decoration: underline; font: inherit; }
482
+ @keyframes s-flash { from { background: color-mix(in srgb, var(--accent) 22%, transparent); } to { background: transparent; } }
483
+ .s-flash { animation: s-flash 2s ease-out; }
484
+ @media (prefers-reduced-motion: reduce) { .s-flash { animation: none; background: color-mix(in srgb, var(--accent) 12%, transparent); } }
485
+ /* #2347: on a phone the sections keep their names (bare icons could not be told apart), on one row that scrolls
486
+ sideways, each group after its name. */
487
+ @media (max-width: 479px) {
488
+ .p-settings > .s-nav .s-nav-groups { flex-wrap: nowrap; overflow-x: auto; }
489
+ .p-settings > .s-nav .s-nav-group { flex-wrap: nowrap; flex-shrink: 0; }
490
+ }
491
+ /* #2347 §7: on a phone (the app's narrow layout) Settings itself (/settings) is the list of sections — search on top,
492
+ the two groups as full-width rows — and a section opens on its own with "‹ Settings" back to the list. The strip
493
+ that scrolled sideways (and cut its ends) is gone there. Wider screens keep the list beside the section. */
494
+ .p-settings .s-back { display: none; }
495
+ @media (max-width: 899px) {
496
+ .p-settings[data-view="index"] > .panel-body { display: none; }
497
+ .p-settings[data-view="section"] > .s-nav { display: none; }
498
+ .p-settings > .panel-body .s-back { display: flex; align-items: center; gap: var(--s-2); min-height: var(--tap); margin: 0 0 var(--s-2);
499
+ color: var(--accent); font-size: var(--fs-sm); text-decoration: none; }
500
+ .p-settings .s-back .s-back-here { margin-left: auto; color: var(--text); font-weight: var(--fw-semibold); }
501
+ .p-settings[data-view="index"] > .s-nav { flex-direction: column; align-items: stretch; padding: 0 var(--s-4) var(--s-4); }
502
+ .p-settings[data-view="index"] > .s-nav .s-nav-groups { flex-direction: column; align-items: stretch; flex-wrap: nowrap; overflow: visible; gap: var(--s-4); }
503
+ .p-settings[data-view="index"] > .s-nav .s-nav-group { flex-direction: column; align-items: stretch; flex-wrap: nowrap; gap: 0; }
504
+ .p-settings[data-view="index"] > .s-nav .s-nav-head { padding: var(--s-2) var(--s-3) var(--s-1); }
505
+ .p-settings[data-view="index"] > .s-nav .seg-item { min-height: var(--tap); padding: 0 var(--s-3); font-size: var(--fs-md); white-space: normal;
506
+ border-bottom: 1px solid var(--border); }
507
+ /* In the list no section is open: none is marked as the one shown — every row alike. */
508
+ .p-settings[data-view="index"] > .s-nav .seg-item, .p-settings[data-view="index"] > .s-nav .seg-item.active { color: var(--text); }
509
+ .p-settings[data-view="index"] > .s-nav .seg-item.active { background: none; font-weight: inherit; }
510
+ /* The list fills the screen, so unsaved changes' bar stays at the bottom (above the tabs), not right under a short list;
511
+ on a short screen (a small phone, one turned) it takes the room there is and scrolls (#2501 review). */
512
+ .p-settings[data-view="index"] > .s-nav { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
513
+ /* The bar on a phone: the app's 16 px gutter, and Discard / Save a full tap target. */
514
+ .p-settings > .s-pending { padding-left: var(--s-4); padding-right: var(--s-4); }
515
+ .p-settings > .s-pending .btn { min-height: var(--tap); }
516
+ /* Settings' dialogs are full-screen sheets (§7): an agent's or a connection's fields get the whole screen, the title on
517
+ top and the actions at the bottom, inside it. The app's own questions (ask()) stay bottom sheets. */
518
+ .p-settings .dlg { width: 100vw; max-width: 100vw; height: 100dvh; max-height: 100dvh; margin: 0; border-radius: 0; }
519
+ .p-settings .dlg-box { height: 100%; max-height: 100dvh; }
520
+ .p-settings .dlg-head { padding-top: max(var(--s-3), env(safe-area-inset-top)); border-bottom: 1px solid var(--border); }
521
+ .p-settings .dlg-body { flex: 1; padding-top: var(--s-4); }
522
+ .p-settings .dlg-actions { border-top: 1px solid var(--border); padding-top: var(--s-3); }
523
+ /* Effect tags wrap under the label (§7): whole, on their own line, where the label's text starts — beside a long label
524
+ on a narrow screen they were squeezed or broken mid-tag. */
525
+ .p-settings .field > label:not(.check) .impact, .p-settings .field > legend .impact { display: block; margin: 2px 0 0; white-space: nowrap; }
526
+ /* A checkbox row: the box, the text (and its "default" mark), then the tag on a line of its own under the text. */
527
+ .p-settings .check:has(> .impact) { flex-wrap: wrap; row-gap: 2px; }
528
+ .p-settings .check:has(> .impact) > input { flex: none; width: 16px; height: 16px; margin: 0; }
529
+ .p-settings .check > .impact { flex: 1 0 calc(100% - 16px - var(--s-2)); margin-left: calc(16px + var(--s-2)); white-space: nowrap; }
530
+ /* A tag inside a note (a dialog's status-emoji line) goes to the next line whole, never broken mid-tag. */
531
+ .p-settings .note > .impact { display: inline-block; white-space: nowrap; }
532
+ .p-settings[data-view="index"] > .s-nav .seg-item.active::before { display: none; }
533
+ }
534
+ @media (min-width: 1100px) {
535
+ .p-settings { display: grid; grid-template-columns: 220px minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr) auto; }
536
+ .p-settings > :not(.s-nav):not(.panel-body) { grid-column: 1 / -1; }
537
+ .p-settings > .s-nav { grid-column: 1; grid-row: 2; margin: 0; max-width: none; min-width: 0; padding: var(--s-2) var(--s-2) var(--s-4) var(--s-4); overflow-x: hidden; overflow-y: auto; }
538
+ .p-settings > .s-nav .s-nav-groups { flex-direction: column; align-items: stretch; flex-wrap: nowrap; gap: var(--s-3); }
539
+ .p-settings > .s-nav .s-nav-group { flex-direction: column; align-items: stretch; flex-wrap: nowrap; }
540
+ .p-settings > .s-nav .s-nav-head { padding: var(--s-1) var(--s-3); }
541
+ .p-settings > .s-nav .seg-item { border-radius: 0; }
542
+ .p-settings > .panel-body { grid-column: 2; grid-row: 2; }
543
+ .p-settings > .s-pending { grid-column: 1 / -1; grid-row: 3; }
544
+ }
443
545
  .list-head { display: flex; justify-content: space-between; align-items: center; gap: var(--s-3); margin: var(--s-2) 0 var(--s-3); color: var(--text-2); font-size: var(--fs-sm); }
444
546
  .rows { display: flex; flex-direction: column; gap: var(--s-1); }
445
547
  .row-item { display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap; padding: var(--s-3) var(--s-4); border-radius: var(--r-md); background: var(--surface); font-size: var(--fs-sm); }
@@ -821,15 +923,45 @@ img.amb-p { object-fit: contain; }
821
923
  padding: var(--s-1) var(--s-2); border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--surface); color: var(--text);
822
924
  box-shadow: var(--shadow-2); }
823
925
  .webauth-tip[hidden] { display: none; }
824
- /* #1920 phase 3: Settings → Web → Guests. */
926
+ /* #1920 phase 3 / #2348: Settings → Access & security → Guests. The fixed template (#2330): flat rows split by a rule,
927
+ no rounded box or fill; an edited row is .s-dirty (the shared straight bar + tint, square). A state is coloured text. */
825
928
  .s-guests .guest-names { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 2px var(--s-3); max-height: 220px; overflow: auto;
826
- padding: var(--s-1) var(--s-2); border: 1px solid var(--border); border-radius: var(--r-sm); }
929
+ padding: var(--s-1) 0; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); }
827
930
  .s-guests .guest-name span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
828
- .s-guests .guest-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--s-2); }
829
- .s-guests .guest-row { padding: var(--s-2) var(--s-3); border: 1px solid var(--border); border-radius: var(--r-sm); }
830
- .s-guests .guest-actions { display: flex; flex-wrap: wrap; gap: var(--s-1); margin-top: var(--s-1); }
931
+ .s-guests .guest-list, .s-guests .g-pick { list-style: none; margin: 0 0 var(--s-3); padding: 0; display: flex; flex-direction: column; border-top: 1px solid var(--border); }
932
+ .s-guests .guest-row, .s-guests .g-cand { padding: var(--s-3) 0; border-bottom: 1px solid var(--border); min-width: 0; }
933
+ .s-guests .guest-row.s-dirty { border-radius: 0; margin-left: 0; align-self: stretch; padding-left: calc(var(--s-2) + 3px); padding-right: var(--s-2); }
934
+ .s-guests .guest-row.g-removed .g-id strong { text-decoration: line-through; color: var(--text-3); }
935
+ .s-guests .g-head { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; min-width: 0; }
936
+ .s-guests .g-av { flex: none; width: 28px; height: 28px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
937
+ background: color-mix(in srgb, var(--accent) 22%, transparent); color: var(--text); font-weight: 600; font-size: var(--fs-sm); }
938
+ .s-guests .g-id { flex: 1 1 220px; min-width: 0; overflow-wrap: anywhere; }
939
+ .s-guests .g-id .note { white-space: normal; }
940
+ .s-guests .g-state { flex: 0 1 auto; max-width: 100%; margin-left: auto; font-size: var(--fs-sm); color: var(--text-3); text-align: right; }
941
+ .s-guests .g-state.on { color: var(--ok); }
942
+ .s-guests .g-state.sent { color: var(--accent); }
943
+ .s-guests .g-state.warn { color: var(--danger); text-align: left; flex-basis: 100%; margin-left: calc(28px + var(--s-2)); }
944
+ .s-guests .g-chips { display: flex; flex-wrap: wrap; gap: var(--s-1); margin: var(--s-2) 0 0 calc(28px + var(--s-2)); }
945
+ .s-guests .g-chip { padding: 1px var(--s-2); border: 1px solid color-mix(in srgb, var(--text-3) 55%, transparent); border-radius: 0; font-size: var(--fs-sm); color: var(--text-2); white-space: nowrap; }
946
+ .s-guests .g-edit { margin: var(--s-2) 0 0 calc(28px + var(--s-2)); }
947
+ .s-guests .guest-actions { display: flex; flex-wrap: wrap; gap: var(--s-1); margin: var(--s-2) 0 0 calc(28px + var(--s-2)); }
948
+ .s-guests .g-error { color: var(--danger); margin-left: calc(28px + var(--s-2)); }
949
+ .s-guests .g-search { width: 100%; max-width: 360px; margin: var(--s-1) 0 var(--s-2); }
950
+ .s-guests .g-cand { display: grid; grid-template-columns: 28px minmax(0, 1fr) auto; align-items: center; gap: var(--s-2); }
951
+ .s-guests .g-legacy { margin: var(--s-2) 0; }
952
+ .s-guests .g-audit { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--border); font-size: var(--fs-sm); }
953
+ .s-guests .g-audit-row { padding: var(--s-1) 0; border-bottom: 1px solid var(--border); overflow-wrap: anywhere; }
954
+ .s-guests .g-audit-row .g-when { color: var(--text-3); }
955
+ .s-guests .g-add { color: var(--ok); }
956
+ .s-guests .g-del { color: var(--danger); }
957
+ .s-guests .g-when, .s-guests .g-wrote { white-space: nowrap; }
831
958
  .s-guests .guest-narrow-edit { margin-top: var(--s-2); }
832
959
  .s-guests .guest-away { color: var(--text-3); }
960
+ @media (max-width: 560px) {
961
+ .s-guests .g-state { flex-basis: 100%; margin-left: calc(28px + var(--s-2)); text-align: left; }
962
+ .s-guests .g-cand { grid-template-columns: 28px minmax(0, 1fr); }
963
+ .s-guests .g-cand > .btn { grid-column: 2; justify-self: start; }
964
+ }
833
965
  /* #1939 S2b: this browser's authorization, under the composer and in the session menu (one compact line). */
834
966
  .webauth-chip { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-1) var(--s-2); margin: var(--s-1) 0 0; font-size: var(--fs-xs); }
835
967
  .webauth-chip .webauth-ok { color: var(--ok); }
@@ -1224,7 +1356,7 @@ body.side-resizing, body.side-resizing * { cursor: col-resize !important; user-s
1224
1356
  .s-toolbar { display: flex; gap: var(--s-2); margin: var(--s-2) 0; }
1225
1357
  .s-search { flex: 1; display: flex; align-items: center; gap: var(--s-2); padding: 0 var(--s-3); border-radius: var(--r-sm); background: var(--surface); color: var(--text-3); }
1226
1358
  .s-search:focus-within { box-shadow: 0 0 0 2px var(--focus); }
1227
- .s-search input { flex: 1; min-width: 0; height: 36px; border: 0; background: transparent; color: var(--text); outline: none; font-size: var(--fs-sm); }
1359
+ .s-search input { flex: 1; min-width: 0; height: 36px; border: 0; background: transparent; color: var(--text); outline: none; font-size: var(--fs-sm); text-overflow: ellipsis; }
1228
1360
  .s-list { display: flex; flex-direction: column; gap: 2px; margin-bottom: var(--s-4); }
1229
1361
  .s-group { margin-bottom: var(--s-2); }
1230
1362
  .s-group-head { display: flex; align-items: center; gap: var(--s-2); width: 100%; padding: var(--s-2) var(--s-2); border: 0; background: transparent; color: var(--text-3);
@@ -1259,6 +1391,10 @@ body.side-resizing, body.side-resizing * { cursor: col-resize !important; user-s
1259
1391
  .grid2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s-3) var(--s-4); }
1260
1392
  .override { display: flex; flex-direction: column; gap: var(--s-1); }
1261
1393
  .inherit-row { font-size: var(--fs-xs); color: var(--text-3); }
1394
+ /* #2347 §10: the value in effect (an empty field's, an inherited one's) in plain text; "default" marks what nobody set. */
1395
+ .s-effect, .s-inherited { margin: 0; font-size: var(--fs-xs); color: var(--text-2); }
1396
+ .s-inherited a { color: var(--accent); }
1397
+ .tag.s-default { font-weight: var(--fw-regular); }
1262
1398
  .impact { font-size: 11px; font-weight: var(--fw-regular); color: var(--text-3); margin-left: var(--s-1); }
1263
1399
  .check .impact { margin-left: auto; }
1264
1400
  .feedback { font-size: var(--fs-xs); color: var(--text-2); }
@@ -1283,6 +1419,12 @@ body.side-resizing, body.side-resizing * { cursor: col-resize !important; user-s
1283
1419
  .btn.on { background: var(--surface-2); color: var(--text); font-weight: var(--fw-semibold); }
1284
1420
  .card h4 { font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: var(--text-2); }
1285
1421
  .pill.ver { text-transform: none; letter-spacing: 0; }
1422
+ /* #2347 §7: Settings' title stays whole on a narrow phone (at 320 px it was cut to "S…"): the version beside it takes the
1423
+ room that is left — shortened, or not shown when too little is. */
1424
+ .p-settings .panel-title h1 { flex: none; }
1425
+ .p-settings .panel-sub { flex: 1 1 0; min-width: 0; container: settings-ver / inline-size; }
1426
+ .p-settings .panel-sub .pill.ver { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; vertical-align: middle; }
1427
+ @container settings-ver (max-width: 56px) { .p-settings .panel-sub .pill.ver { display: none; } }
1286
1428
  .s-key { display: flex; flex-direction: column; gap: var(--s-1); padding: var(--s-2) 0; border-top: 1px solid var(--border); }
1287
1429
  .s-key:first-of-type { border-top: 0; }
1288
1430
  .s-key-row { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; }
@@ -1316,6 +1458,7 @@ textarea.s-yaml { white-space: pre; overflow-x: auto; }
1316
1458
  .s-pending { position: sticky; bottom: 0; display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap; padding: var(--s-3) var(--s-5);
1317
1459
  background: var(--bg); border-top: 1px solid var(--border); box-shadow: var(--shadow-2); }
1318
1460
  .s-pending .grow { flex: 1; }
1461
+ .s-pending-actions { display: flex; align-items: center; gap: var(--s-3); margin-left: auto; }
1319
1462
  .s-op { border: 1px solid var(--border); }
1320
1463
  .s-op.failed { border-color: var(--danger-line); }
1321
1464
  .s-op-head { display: flex; align-items: center; gap: var(--s-2); }
@@ -1357,6 +1500,10 @@ textarea.s-yaml { white-space: pre; overflow-x: auto; }
1357
1500
  .op-line { display: flex; align-items: center; justify-content: center; gap: var(--s-2); }
1358
1501
  .op-line.ok { color: var(--ok); }
1359
1502
  .op-line.bad { color: var(--danger); }
1503
+ /* #2347: saved changes waiting for an AgEnD restart — one line, the shared straight bar on its left. */
1504
+ .restart-line { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: var(--s-2); color: var(--text);
1505
+ --lbar-color: var(--warn); }
1506
+ .restart-line .restart-keys { font-family: var(--font-mono); }
1360
1507
  /* #2233: the backend sign-in dialog and Settings' list */
1361
1508
  .signin-field { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; margin: var(--s-2) 0; }
1362
1509
  .signin-label { color: var(--text-2); font-size: var(--fs-xs); min-width: 6em; }
@@ -1379,6 +1526,7 @@ textarea.s-yaml { white-space: pre; overflow-x: auto; }
1379
1526
  .shell:has(.s-pending) .pending-stack { bottom: calc(64px + 64px + var(--s-3)); }
1380
1527
  .shell:has(.s-pending) .toasts { bottom: auto; }
1381
1528
  .s-pending { padding: var(--s-2) var(--s-3); }
1529
+ .s-pending-actions { flex-basis: 100%; justify-content: flex-end; }
1382
1530
  }
1383
1531
 
1384
1532
  /* ── Needs you (#1408 step 4 = #1386 part b) ── */
@@ -1569,8 +1717,18 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
1569
1717
  .oc-mini { display: none; }
1570
1718
  }
1571
1719
  /* ── Fleet → Org chart → Office (fleet-office.js): the fleet as a pixel office on one <canvas>. Its size is set through
1572
- the CSSOM (never a style attribute, #1300); the stage scrolls, the page around it too. ── */
1573
- .oo { display: flex; flex-direction: column; gap: var(--s-2); }
1720
+ the CSSOM (never a style attribute, #1300). #2540: it fills the Fleet panel — no frame or margins around the canvas,
1721
+ the bars above it keep the column's padding; the stage pans and zooms (touch gestures are its own, touch-action:
1722
+ none on it only) and the page around it no longer scrolls sideways. The column's `margin: 0 auto` must go too: in
1723
+ the panel's flex column an auto margin shrinks the column to its content (the bars: the canvas is absolutely placed
1724
+ and adds no width) and centres it — the Office was as wide as its toolbar, not the panel. ── */
1725
+ .p-fleet .panel-body:has(> .col > .oc-office) { display: flex; flex-direction: column; }
1726
+ .p-fleet .panel-body > .col:has(> .oc-office) { flex: 1 0 auto; display: flex; flex-direction: column; max-width: none; padding: 0; margin: 0; width: 100%; align-self: stretch; }
1727
+ .oc-office { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
1728
+ .oc-office > .oc-bar { margin: 0; padding: var(--s-3) var(--s-5) var(--s-2); }
1729
+ .oc-office > .oc-legend { margin: 0 var(--s-5) var(--s-2); }
1730
+ .oo { flex: 1 0 auto; display: flex; flex-direction: column; gap: var(--s-2); min-width: 0; width: 100%; }
1731
+ .oo > :not(.oo-stage) { margin-left: var(--s-5); margin-right: var(--s-5); }
1574
1732
  .oo-bar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); }
1575
1733
  .oo-pick { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-xs); color: var(--text-2); }
1576
1734
  .oo-pick .tb-select { max-width: 200px; }
@@ -1586,6 +1744,13 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
1586
1744
  .oe-label { font-size: var(--fs-xs); color: var(--text-2); }
1587
1745
  .oe-name { min-width: 0; width: 16em; max-width: 100%; }
1588
1746
  .oe-remove { margin-left: auto; }
1747
+ /* #2513: the skin packs the host keeps — a row each with its delete, then the upload buttons (a hidden file input each). */
1748
+ .oe-packs { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); margin-top: var(--s-3); }
1749
+ .oe-packs > legend, .oe-packs > .note { flex-basis: 100%; }
1750
+ .oe-pack-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; flex-basis: 100%; }
1751
+ .oe-pack { display: flex; align-items: center; gap: var(--s-2); overflow-wrap: anywhere; }
1752
+ .oe-pack-remove { margin-left: auto; }
1753
+ .oe-upload { position: relative; cursor: pointer; }
1589
1754
  .oe-members { display: flex; flex-wrap: wrap; gap: var(--s-3); }
1590
1755
  .oe-group { border: 0; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; min-width: 12em; max-height: 240px; overflow-y: auto; }
1591
1756
  .oe-group legend { font-size: var(--fs-xs); font-weight: var(--fw-semibold); color: var(--text-2); padding: 0; margin-bottom: 4px; }
@@ -1596,24 +1761,96 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
1596
1761
  .oo-people-list { list-style: none; margin: 0; padding: 0; }
1597
1762
  .oo-people-list label { display: flex; align-items: center; gap: 8px; padding: 3px 2px; font-size: var(--fs-xs); color: var(--text); cursor: pointer; overflow-wrap: anywhere; }
1598
1763
  .oo-note { font-size: var(--fs-xs); }
1599
- .oo-stage { position: relative; height: clamp(420px, calc(100dvh - 280px), 960px); overflow: auto; border: 1px solid var(--border); border-radius: var(--r-lg); background: var(--bg); }
1600
- .oo-canvas { display: block; margin: 0 auto; image-rendering: pixelated; }
1764
+ .oo-stage { position: relative; flex: 1 1 auto; min-height: 340px; overflow: hidden; background: var(--bg); touch-action: none; user-select: none; -webkit-user-select: none; }
1765
+ /* #2554: clip, not a scroll container — nothing past the edge (a tile's label) can scroll the stage and its controls. */
1766
+ .oo-stage { overflow: clip; }
1767
+ .oo-canvas { position: absolute; left: 0; top: 0; display: block; image-rendering: pixelated; cursor: grab; touch-action: none; }
1768
+ .oo-canvas:active { cursor: grabbing; }
1769
+ .oo-canvas:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
1601
1770
  .oo-canvas.hot { cursor: pointer; }
1771
+ /* The canvas controls, as the org chart canvas's (.oc-zoom, .oc-mini): over the stage's corners. */
1772
+ .oo-zoom { z-index: 6; }
1773
+ .oo-zoom .icon-btn { color: var(--text); }
1774
+ /* The overview's box (left, top, width, height) is set by the page, inside the stage and scaled to it (miniBox). */
1775
+ .oo-mini { z-index: 6; right: auto; bottom: auto; overflow: hidden; }
1776
+ .oo-zoom-pct { display: block; padding: 3px 0; font-size: 11px; line-height: 14px; text-align: center; color: var(--text-2); font-variant-numeric: tabular-nums;
1777
+ border-bottom: 1px solid var(--border); user-select: none; }
1778
+ .oo-zoom .oo-fill { font-size: 11px; font-weight: var(--fw-semibold); }
1779
+ .oo-zoom .oo-fill.on { background: var(--accent-dim); color: var(--text); }
1780
+ .oo-stage.editing .oo-mini { display: none; }
1781
+ .oo-mini-scene { fill: var(--bg); }
1782
+ .oo-mini-room { fill: var(--surface); stroke: var(--border-strong); stroke-width: 6; }
1783
+ .oo-mini-dot { fill: var(--text-3); }
1784
+ .oo-mini-dot.st-working { fill: var(--ok); }
1785
+ .oo-mini-dot.st-needs, .oo-mini-dot.st-stuck { fill: var(--warn); }
1786
+ .oo-mini-dot.st-crashed { fill: var(--danger); }
1787
+ .oo-mini-dot.match { fill: var(--focus); }
1602
1788
  .oo-tip { position: absolute; left: 0; top: 0; z-index: 5; max-width: 240px; pointer-events: none; padding: 6px 10px; font-size: var(--fs-xs); line-height: 1.4;
1603
1789
  color: var(--text); background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md); box-shadow: var(--shadow-2); overflow-wrap: anywhere; }
1790
+ /* #2554: All offices — each tile's label (office name and head count) just above the tile, placed by the page (left, top,
1791
+ max-width through the CSSOM); a button that opens the office, the lobby's a plain chip. */
1792
+ .oo-tile-label { position: absolute; left: 0; top: 0; z-index: 3; transform: translateY(calc(-100% - 4px)); max-width: 240px; overflow: hidden; text-overflow: ellipsis;
1793
+ white-space: nowrap; padding: 2px 8px; font: inherit; font-size: var(--fs-xs); font-weight: var(--fw-semibold); line-height: 18px; color: var(--text);
1794
+ background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-sm); }
1795
+ button.oo-tile-label { cursor: pointer; }
1796
+ button.oo-tile-label:hover { border-color: var(--accent); }
1797
+ button.oo-tile-label:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
1798
+ .oo-tile-label.lobby { color: var(--text-2); }
1799
+ .oo-lobby-tile { font-size: var(--fs-xs); color: var(--text-2); }
1604
1800
  /* The desks as a list: for a screen reader, and shown (over the stage's corner) while the keyboard is in it. */
1605
1801
  .oo-desks { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; list-style: none; }
1606
- .oo-desks:focus-within { position: sticky; left: var(--s-2); bottom: var(--s-2); width: auto; height: auto; max-height: 40%; margin: 0; padding: var(--s-1); clip: auto; overflow-y: auto;
1802
+ .oo-desks:focus-within { position: absolute; z-index: 7; left: var(--s-2); bottom: var(--s-2); width: auto; height: auto; max-height: 40%; margin: 0; padding: var(--s-1); clip: auto; overflow-y: auto;
1607
1803
  display: inline-flex; flex-wrap: wrap; gap: 4px; max-width: calc(100% - 2 * var(--s-2)); background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md); white-space: normal; }
1608
1804
  .oo-desk { font-size: var(--fs-xs); padding: 2px 6px; border-radius: var(--r-sm); background: var(--bg); color: var(--text); border: 1px solid var(--border); }
1609
1805
  .oo-desk:focus-visible { outline: none; border-color: var(--focus); box-shadow: inset 0 -2px 0 var(--focus); }
1806
+ /* #2513: Edit layout (fleet-office-layout.js) — an SVG over the canvas (placed and sized through the CSSOM), its 1280×720
1807
+ grid, the areas, corridors and a handle for each seat, item, the door and waypoint; its bar sticks to the stage's foot. */
1808
+ .oo-hide-asleep { font-size: var(--fs-xs); color: var(--text-2); }
1809
+ /* #2540: fullscreen. The container is the page's fullscreen element (the Fullscreen API: the top layer), or, where the page
1810
+ cannot take one (an iPhone) or the browser refused, "immersive": it covers the window. Only the stage shows (the bars,
1811
+ notes and Edit offices are left out), the safe areas kept clear; floating over it the zoom controls, the overview
1812
+ (not on a phone) and a compact office switcher. Immersive lifts the Fleet panel's band (stock content, level 1) over
1813
+ the header (2), the phone's nav (35) and the drawer and Needs-you stack (40); menus (60) and toasts (100) stay above. */
1814
+ .oo.fs { gap: 0; margin: 0; background: var(--bg); box-sizing: border-box;
1815
+ padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left); }
1816
+ .oo.fs > :not(.oo-stage) { display: none; }
1817
+ .oo.fs > .oo-stage { flex: 1 1 auto; min-height: 0; }
1818
+ .oo.fs.immersive { position: fixed; inset: 0; z-index: 50; width: 100vw; height: 100vh; width: 100dvw; height: 100dvh; }
1819
+ html.oo-immersive, html.oo-immersive body { overflow: hidden; }
1820
+ html.oo-immersive .p-fleet > .panel-body { z-index: 50; }
1821
+ .oo-fs-bar { position: absolute; left: var(--s-3); top: var(--s-3); z-index: 6; display: flex; align-items: center; gap: var(--s-2); }
1822
+ .oo-fs-bar .tb-select { max-width: 180px; }
1823
+ .ol-overlay { position: absolute; left: 0; top: 0; z-index: 4; }
1824
+ .ol-svg { display: block; overflow: visible; touch-action: none; }
1825
+ .ol-grid { stroke: var(--text-3); stroke-opacity: 0.25; stroke-width: 1; }
1826
+ .ol-label, .ol-coord { fill: var(--text); font-size: 12px; font-family: var(--font-mono, monospace); paint-order: stroke; stroke: var(--bg); stroke-width: 3px; }
1827
+ .ol-area { fill: none; stroke: var(--text-2); stroke-dasharray: 6 4; stroke-width: 1.5; }
1828
+ .ol-edge { stroke: var(--focus); stroke-width: 3; stroke-opacity: 0.7; }
1829
+ .ol-handle { cursor: grab; outline: none; }
1830
+ .ol-handle > circle, .ol-handle > rect { fill: var(--surface); stroke: var(--text); stroke-width: 2; }
1831
+ .ol-seat > circle { fill: var(--ok); }
1832
+ .ol-furniture > rect { fill: var(--warn); }
1833
+ .ol-door > rect { fill: var(--danger); }
1834
+ .ol-waypoint > circle { fill: var(--focus); }
1835
+ .ol-handle.on > circle, .ol-handle.on > rect, .ol-handle:focus-visible > circle, .ol-handle:focus-visible > rect { stroke: var(--focus); stroke-width: 4; }
1836
+ .ol-bar { position: absolute; left: 0; right: 0; bottom: 0; z-index: 8; display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); padding: var(--s-2);
1837
+ background: var(--surface); border-top: 1px solid var(--border); }
1838
+ .ol-actions { display: inline-flex; gap: var(--s-2); margin-left: auto; }
1839
+ .ol-corridors { display: inline-flex; flex-wrap: wrap; gap: 4px; }
1840
+ .ol-msg { flex-basis: 100%; margin: 0; }
1610
1841
  @media (max-width: 640px) {
1611
- .oo-stage { height: calc(100dvh - 300px); min-height: 340px; }
1842
+ /* A phone: the bars at the column's narrower padding; the stage at least most of a screen; no overview (as the canvas). */
1843
+ .oc-office > .oc-bar { padding: var(--s-2) var(--s-3); }
1844
+ .oc-office > .oc-legend { margin: 0 var(--s-3) var(--s-2); }
1845
+ .oo > :not(.oo-stage) { margin-left: var(--s-3); margin-right: var(--s-3); }
1846
+ .oo-stage { min-height: max(300px, calc(100dvh - 300px)); }
1612
1847
  .oo-pick .tb-select { max-width: 150px; }
1848
+ .oo-mini { display: none; }
1849
+ .oo-zoom .icon-btn { width: 40px; height: 40px; }
1613
1850
  }
1614
1851
  /* A phone: Fleet's five tabs fit when only the open one shows its label (the others keep theirs for screen readers). */
1615
1852
  @media (max-width: 479px) {
1616
- .p-fleet .seg-item:not(.active) > span, .p-settings .seg-item:not(.active) > span { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
1853
+ .p-fleet .seg-item:not(.active) > span { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
1617
1854
  }
1618
1855
 
1619
1856
  /* ── Cache analysis (#1468): Fleet → Cache. Bars are SVG attributes; nothing here is an inline style. ── */
@@ -87,6 +87,9 @@ if (mode === "full") {
87
87
  if (!guest) {
88
88
  panels.set("fleet", { load: () => loadFleet().then((m) => m.FleetPanel) });
89
89
  panels.set("settings", { load: () => loadSettings().then((m) => m.SettingsPanel) });
90
+ // #2347: the restart banner's Restart now, in any panel — the Settings runner (behind the session) does it.
91
+ const loadApply = retryable((a) => import(retryUrl("/ui/js/settings-apply.js", a)));
92
+ appStore.set({ restartRunner: (jobId) => loadApply().then((m) => m.restartPendingChanges(jobId)) });
90
93
  panels.set("needs", { load: () => loadNeeds().then((m) => m.NeedsPanel) });
91
94
  // #1523 N2: one instance's Details (/ui/fleet/agent/<name>).
92
95
  panels.set("details", { load: () => loadDetails().then((m) => m.DetailsPanel) });
@@ -212,6 +212,6 @@ export function stockEffectTrust(host, { frameRoot = null, ownerCss = "unknown"
212
212
  if (doc.fullscreenElement || doc.pictureInPictureElement) barrier = true;
213
213
  // Focus alone does not change protected paint bounds. Composer/controls stay clipped; editable focus
214
214
  // separately suppresses pointer delivery in the registry, and keyboard events never enter a Worker.
215
- return { ok: true, rects, front: !barrier, reason: barrier ? "stock_overlay" : "stock", agentFront: !barrier && ownerCss === "off", agentReason: barrier ? "stock_overlay" : ownerCss === "off" ? "stock" : "owner_css", settingsHref: "/settings/appearance" };
215
+ return { ok: true, rects, front: !barrier, reason: barrier ? "stock_overlay" : "stock", agentFront: !barrier && ownerCss === "off", agentReason: barrier ? "stock_overlay" : ownerCss === "off" ? "stock" : "owner_css", settingsHref: "/settings/look" };
216
216
  } catch { return fail("stock_trust_unknown"); }
217
217
  }
@@ -4,7 +4,7 @@
4
4
  // paints the sidebar row through the CSSOM (no style attribute, #1300), and holds the Color dialog.
5
5
  // colour: a person's pick > the agent's (themes shown) > its own bot's role colour > automatic (a few soft colours)
6
6
  // places: a person's choice > the agent's (themes shown) > this host's default (color.show) > all three (#2056)
7
- // Settings → Web appearance can turn colours off for the host (color.off): nothing is painted, every pick is kept.
7
+ // Settings → Look can turn colours off for the host (color.off): nothing is painted, every pick is kept.
8
8
  import { html, useEffect, useState } from "./app-html.js";
9
9
  import { createStore, useStore } from "./app-store.js";
10
10
  import { onPrefsChanged, prefInForce, savePref } from "./app-prefs.js";
@@ -108,7 +108,7 @@ export const colorPrefsStore = createStore({ n: 0 });
108
108
  const bump = () => colorPrefsStore.set((s) => ({ n: s.n + 1 }));
109
109
  onPrefsChanged((changed) => { if (changed.has("color.show") || changed.has("color.off")) bump(); });
110
110
  const readLocal = (k) => { try { return localStorage.getItem(k); } catch { return null; } };
111
- /** Where colours show by default on this host (Settings → Web appearance). */
111
+ /** Where colours show by default on this host (Settings → Look). */
112
112
  export function defaultPlaces() {
113
113
  const f = prefInForce("color.show");
114
114
  let v = f ? f.value : undefined;
@@ -251,7 +251,7 @@ register("color", {
251
251
  src_person: "Shown now: picked by a person.", src_agent: "Shown now: the agent's own colour.", src_role: "Shown now: the colour of its bot's Discord role.",
252
252
  src_auto: "Shown now: an automatic colour.", save: "Save", cancel: "Cancel", reset: "Reset",
253
253
  saved: "Colour saved for {0}", failed: "Couldn't save the colour: {0}",
254
- offNote: "Colours are turned off on this host (Settings → Web appearance). What you pick here is kept for when they're on.",
254
+ offNote: "Colours are turned off on this host (Settings → Look). What you pick here is kept for when they're on.",
255
255
  p_red: "Red", p_orange: "Orange", p_amber: "Amber", p_lime: "Lime", p_green: "Green", p_teal: "Teal", p_cyan: "Cyan", p_blue: "Blue",
256
256
  p_indigo: "Indigo", p_violet: "Violet", p_pink: "Pink", p_slate: "Slate",
257
257
  },
@@ -261,7 +261,7 @@ register("color", {
261
261
  src_person: "目前顯示:有人挑選的顏色。", src_agent: "目前顯示:agent 自己的顏色。", src_role: "目前顯示:它的 bot 在 Discord 身分組的顏色。",
262
262
  src_auto: "目前顯示:自動配色。", save: "儲存", cancel: "取消", reset: "重設",
263
263
  saved: "已儲存 {0} 的顏色", failed: "無法儲存顏色:{0}",
264
- offNote: "這台主機已關閉顏色(設定 → 網頁外觀)。在這裡挑的顏色會保留,重新開啟後就會出現。",
264
+ offNote: "這台主機已關閉顏色(設定 → 外觀)。在這裡挑的顏色會保留,重新開啟後就會出現。",
265
265
  p_red: "紅", p_orange: "橙", p_amber: "琥珀", p_lime: "萊姆", p_green: "綠", p_teal: "青綠", p_cyan: "青", p_blue: "藍",
266
266
  p_indigo: "靛", p_violet: "紫", p_pink: "粉紅", p_slate: "石板灰",
267
267
  },
@@ -15,7 +15,7 @@
15
15
  label: "One-time code", submit: "Sign in", working: "Signing in…",
16
16
  hint: "No code? Send /web in General, then press a sign-in button to get one.",
17
17
  nobot: "No chat bot?", nobot_body: "Ask the operator to run agend web --code on the AgEnD computer.",
18
- safe: "Page looks wrong after signing in?", safe_body: "Your custom CSS may be hiding something. Add ?safe=1 to the end of the address to load the page without it, then fix it or turn it off in Settings → Web appearance.",
18
+ safe: "Page looks wrong after signing in?", safe_body: "Your custom CSS may be hiding something. Add ?safe=1 to the end of the address to load the page without it, then fix it or turn it off in Settings → Look.",
19
19
  refused: "The code is invalid or no longer works. Send /web in General, then press a sign-in button for a new code.",
20
20
  unconfirmed: "Sign-in could not be confirmed. Get a new code and try again; if it keeps failing, contact the operator.",
21
21
  paused: "Too many wrong codes — sign-in is paused for a few minutes.",
@@ -29,7 +29,7 @@
29
29
  label: "一次性登入碼", submit: "登入", working: "登入中…",
30
30
  hint: "沒收到?在 General 傳 /web,再按登入按鈕取碼。",
31
31
  nobot: "沒有聊天 bot?", nobot_body: "請管理員在 AgEnD 電腦執行 agend web --code。",
32
- safe: "登入後畫面不對勁?", safe_body: "可能是你的自訂 CSS 把東西藏起來了。在網址最後加上 ?safe=1,就會不套用它地載入;再到「設定 → 網頁外觀」修正或關閉。",
32
+ safe: "登入後畫面不對勁?", safe_body: "可能是你的自訂 CSS 把東西藏起來了。在網址最後加上 ?safe=1,就會不套用它地載入;再到「設定 → 外觀」修正或關閉。",
33
33
  refused: "登入碼無效或已失效。請在 General 傳 /web,再按登入按鈕取新碼。",
34
34
  unconfirmed: "登入暫時無法確認。請重新取碼再試;若仍失敗,請聯絡管理員。",
35
35
  paused: "錯誤次數過多,登入暫停幾分鐘。",
@@ -19,6 +19,11 @@ export function Dialog({ title, onClose, actions, children, busy = false, wide =
19
19
  const d = ref.current;
20
20
  opener.current = typeof document !== "undefined" ? document.activeElement : null;
21
21
  if (d && typeof d.showModal === "function" && !d.open) d.showModal();
22
+ // The browser's own first stop is the × (WebKit paints its focus ring at once): the dialog's box takes that focus
23
+ // instead, so opening shows no ring; Tab then reaches the × and every field with the keyboard's ring. A field a
24
+ // dialog focuses itself (autofocus) keeps it.
25
+ const x = d && d.querySelector ? d.querySelector(".dlg-x") : null, box = d && d.querySelector ? d.querySelector(".dlg-box") : null;
26
+ if (box && x && typeof document !== "undefined" && document.activeElement === x && typeof box.focus === "function") box.focus({ preventScroll: true });
22
27
  return () => {
23
28
  if (d && d.open && typeof d.close === "function") d.close();
24
29
  const back = opener.current;
@@ -37,7 +42,7 @@ export function Dialog({ title, onClose, actions, children, busy = false, wide =
37
42
  return () => { d.removeEventListener("cancel", onCancel); d.removeEventListener("click", onClick); };
38
43
  }, [onClose]);
39
44
  return html`<dialog data-trust="dialog" ref=${ref} class=${`dlg${wide ? " dlg-wide" : ""}${drawer ? " dlg-drawer" : ""}`} aria-label=${label || title}>
40
- <div class="dlg-box">
45
+ <div class="dlg-box" tabindex="-1">
41
46
  <div class="dlg-head"><h2 class="dlg-title">${title}</h2>${tools ? html`<span class="dlg-tools">${tools}</span>` : null}
42
47
  <button type="button" class="icon-btn dlg-x" aria-label=${t("app.close")} title=${t("app.close")} onClick=${() => { if (!busyRef.current) onClose(); }}><${Icon} name="close" /></button></div>
43
48
  <div class="dlg-body">${children}</div>
@@ -9,6 +9,8 @@ const P = {
9
9
  plus: () => html`<path d="M12 5v14M5 12h14"/>`,
10
10
  minus: () => html`<path d="M5 12h14"/>`, // #2333: the lightbox's zoom out
11
11
  fitwidth: () => html`<path d="M3 4v16M21 4v16M7 12h10M10 9l-3 3 3 3M14 9l3 3-3 3"/>`, // #2333: fit width
12
+ fullscreen: () => html`<path d="M4 9V4h5M15 4h5v5M20 15v5h-5M9 20H4v-5"/>`, // #2540: the Office's fullscreen
13
+ fullscreenexit: () => html`<path d="M9 4v5H4M20 9h-5V4M15 20v-5h5M4 15h5v5"/>`, // …and its way out
12
14
  edit: () => html`<path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z"/>`,
13
15
  more: () => html`<circle cx="5" cy="12" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="19" cy="12" r="1.5"/>`,
14
16
  chat: () => html`<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>`,
@@ -12,7 +12,6 @@ register("webauth", {
12
12
  authorize: "Authorize this browser",
13
13
  waiting: "Waiting for your Confirm in Discord or Telegram ({0})",
14
14
  sent: "Sent — confirm it in your DM within 5 minutes.",
15
- publicLink: "Not on the temporary public link: open the dashboard directly to authorize.",
16
15
  reason_disabled: "Web authority is turned off.",
17
16
  reason_loopback_only: "Only this computer can authorize, and this browser is not on it.",
18
17
  reason_no_account: "This browser was not signed in as an account (agend web): sign in with /web to authorize.",
@@ -20,7 +19,8 @@ register("webauth", {
20
19
  reason_not_admin_session: "Guests can't authorize.",
21
20
  reason_token_rotated: "The dashboard's token changed: sign in again.",
22
21
  reason_public_link: "Not on the temporary public link.",
23
- reason_named_link_off: "Over your named link: turn on \"Also over my named link\" in Settings → Web on this computer.",
22
+ reason_named_link_off: "Over your named link: turn on \"Also over my named link\" in Settings → Access & security on this computer.",
23
+ reason_quick_link_off: "Over the temporary public link: turn on \"Also over the temporary public link\" in Settings → Access & security on this computer.",
24
24
  reason_not_session: "Sign in to authorize.",
25
25
  reason_other: "This browser can't be authorized here.",
26
26
  refusal_pending: "A request is already waiting in your DM.",
@@ -39,7 +39,6 @@ register("webauth", {
39
39
  authorize: "授權這個瀏覽器",
40
40
  waiting: "等你在 Discord 或 Telegram 按下確認({0})",
41
41
  sent: "已送出——請在 5 分鐘內到私訊確認。",
42
- publicLink: "臨時公開連結上無法授權:請直接開啟儀表板再授權。",
43
42
  reason_disabled: "網頁授權已關閉。",
44
43
  reason_loopback_only: "只有這台電腦可以授權,而這個瀏覽器不在這台電腦上。",
45
44
  reason_no_account: "這個瀏覽器不是用帳號登入的(agend web):請用 /web 登入後再授權。",
@@ -47,7 +46,8 @@ register("webauth", {
47
46
  reason_not_admin_session: "訪客無法授權。",
48
47
  reason_token_rotated: "儀表板的 token 已變更:請重新登入。",
49
48
  reason_public_link: "臨時公開連結上無法授權。",
50
- reason_named_link_off: "透過固定網域:請在這台電腦的 設定 → 網頁 開啟「也允許透過我的固定網域」。",
49
+ reason_named_link_off: "透過固定網域:請在這台電腦的 設定 → 存取與安全 開啟「也允許透過我的固定網域」。",
50
+ reason_quick_link_off: "透過臨時公開連結:請在這台電腦的 設定 → 存取與安全 開啟「也允許透過臨時公開連結」。",
51
51
  reason_not_session: "登入後才能授權。",
52
52
  reason_other: "這個瀏覽器在這裡無法授權。",
53
53
  refusal_pending: "已經有一個請求在你的私訊中等待。",
@@ -62,7 +62,9 @@ register("webauth", {
62
62
  },
63
63
  });
64
64
  const tn = (k, ...v) => t(`webauth.${k}`, ...v);
65
- const REASONS = ["disabled", "loopback_only", "no_account", "not_admin", "not_admin_session", "token_rotated", "public_link", "named_link_off", "not_session"];
65
+ // The switch and "Only this computer" are changed in Settings on this computer — never over the public link (#2525 review).
66
+ const fixable = (reason) => (reason === "disabled" || reason === "loopback_only") && globalThis.document?.body?.dataset?.publicLink !== "1";
67
+ const REASONS = ["disabled", "loopback_only", "no_account", "not_admin", "not_admin_session", "token_rotated", "public_link", "named_link_off", "quick_link_off", "not_session"];
66
68
  // Until when: the time, with the day when it isn't today (an authorization lasts up to 48 h).
67
69
  const clock = (ms) => {
68
70
  const d = new Date(ms), today = new Date().toDateString() === d.toDateString();
@@ -70,7 +72,6 @@ const clock = (ms) => {
70
72
  : d.toLocaleString([], { weekday: "short", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
71
73
  };
72
74
  const mmss = (ms) => { const s = Math.max(0, Math.ceil(ms / 1000)); return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`; };
73
- const publicLink = () => globalThis.document?.body?.dataset?.publicLink === "1";
74
75
 
75
76
  // One read per page load, shared by every line that shows it (the composer's, the session menu's): coming back to a chat
76
77
  // reads nothing (#1374). It is read again only after an ask, and while a card is pending.
@@ -98,7 +99,9 @@ export function useWebAuthorization() {
98
99
  const [deadline, setDeadline] = useState(0), [now, setNow] = useState(() => Date.now());
99
100
  const alive = useRef(true);
100
101
  const load = async () => {
101
- if (publicLink() || guestPage()) return;
102
+ // §0.2 / §0.3: over the public link too — the server says whether this browser may be authorized there (its link's
103
+ // option on) or why not; a guest never asks.
104
+ if (guestPage()) return;
102
105
  const body = await readStatus();
103
106
  if (!alive.current) return;
104
107
  if (signedOut) { setSt(null); setNotice(null); return; }
@@ -144,7 +147,6 @@ export function resetWebAuthorization() { shared = null; answered = false; infli
144
147
  export function AuthorizeChip() {
145
148
  const a = useWebAuthorization();
146
149
  if (guestPage()) return null;
147
- if (publicLink()) return html`<p class="webauth-chip note" data-state="public">${tn("publicLink")}</p>`;
148
150
  if (!a.st) return null;
149
151
  return html`<div class="webauth-chip" data-state=${a.st.state}>${body(a)}${a.notice ? html` <span class="note" role="status">${a.notice}</span>` : null}</div>`;
150
152
  }
@@ -163,7 +165,6 @@ export function ComposerAuth() {
163
165
  const tip = useRef("wa-tip-" + Math.random().toString(36).slice(2, 8));
164
166
  useEffect(() => { setOpen(false); }, [a.st && a.st.state]); // a new state closes the bubble
165
167
  if (guestPage()) return null;
166
- if (publicLink()) return html`<span class="webauth-row note" data-state="public" title=${tn("publicLink")}>${tn("publicLink")}</span>`;
167
168
  if (!a.st) return null;
168
169
  const st = a.st, id = tip.current;
169
170
  const said = a.notice ? html`<span class="webauth-notice" role="status">${a.notice}</span>` : null;
@@ -184,8 +185,8 @@ export function ComposerAuth() {
184
185
  title=${tn("authorizeWhy")} aria-label=${tn("authorizeWhy")} onClick=${a.ask}>${tn("authorizeShort")}</button>${said}</span>`;
185
186
  }
186
187
  const why = REASONS.includes(st.reason) ? tn(`reason_${st.reason}`) : tn("reason_other");
187
- return html`<span class="webauth-row" data-state="none"><span class="note" title=${why}>${tn("notVerified")}<span class="sr-only">: ${why}</span></span>${st.reason === "disabled" || st.reason === "loopback_only"
188
- ? html` · <a href="/settings/web" title=${why}>${tn("settings")}</a>` : null}</span>`;
188
+ return html`<span class="webauth-row" data-state="none"><span class="note" title=${why}>${tn("notVerified")}<span class="sr-only">: ${why}</span></span>${fixable(st.reason)
189
+ ? html` · <a href="/settings/access" title=${why}>${tn("settings")}</a>` : null}</span>`;
189
190
  }
190
191
 
191
192
  function body(a) {
@@ -196,5 +197,5 @@ function body(a) {
196
197
  return html`<span class="note">${tn("notVerified")} ·</span> <button type="button" class="btn btn-sm" onClick=${a.ask}>${tn("authorize")}</button>`;
197
198
  const why = REASONS.includes(st.reason) ? tn(`reason_${st.reason}`) : tn("reason_other");
198
199
  // The fix, where there is one the person can take here: the switch lives in Settings → Web.
199
- return html`<span class="note">${tn("notVerified")}: ${why}</span>${st.reason === "disabled" || st.reason === "loopback_only" ? html` <a href="/settings/web">${tn("settings")}</a>` : null}`;
200
+ return html`<span class="note">${tn("notVerified")}: ${why}</span>${fixable(st.reason) ? html` <a href="/settings/access">${tn("settings")}</a>` : null}`;
200
201
  }