@songsid/agend 2.3.0 → 2.3.2-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/README.md +1 -1
  2. package/README.zh-TW.md +1 -1
  3. package/dist/authority/authority-state.d.ts +4 -0
  4. package/dist/authority/authority-state.js +16 -10
  5. package/dist/authority/authority-state.js.map +1 -1
  6. package/dist/authority/session-authorizations.d.ts +7 -1
  7. package/dist/authority/session-authorizations.js +6 -4
  8. package/dist/authority/session-authorizations.js.map +1 -1
  9. package/dist/authority/web-authority.d.ts +19 -6
  10. package/dist/authority/web-authority.js +22 -8
  11. package/dist/authority/web-authority.js.map +1 -1
  12. package/dist/backend/muse.d.ts +20 -2
  13. package/dist/backend/muse.js +154 -16
  14. package/dist/backend/muse.js.map +1 -1
  15. package/dist/channel/adapters/discord.d.ts +2 -0
  16. package/dist/channel/adapters/discord.js +33 -3
  17. package/dist/channel/adapters/discord.js.map +1 -1
  18. package/dist/channel/adapters/telegram.d.ts +1 -1
  19. package/dist/channel/adapters/telegram.js +3 -35
  20. package/dist/channel/adapters/telegram.js.map +1 -1
  21. package/dist/channel/mcp-tools.js +5 -5
  22. package/dist/channel/mcp-tools.js.map +1 -1
  23. package/dist/channel/types.d.ts +11 -0
  24. package/dist/cli.js +6 -3
  25. package/dist/cli.js.map +1 -1
  26. package/dist/config-validator.js +42 -5
  27. package/dist/config-validator.js.map +1 -1
  28. package/dist/daemon.d.ts +47 -3
  29. package/dist/daemon.js +380 -60
  30. package/dist/daemon.js.map +1 -1
  31. package/dist/delivery-outbox.d.ts +23 -1
  32. package/dist/delivery-outbox.js +114 -17
  33. package/dist/delivery-outbox.js.map +1 -1
  34. package/dist/doctor.d.ts +3 -0
  35. package/dist/doctor.js +9 -0
  36. package/dist/doctor.js.map +1 -1
  37. package/dist/event-log.d.ts +5 -0
  38. package/dist/event-log.js +13 -0
  39. package/dist/event-log.js.map +1 -1
  40. package/dist/fleet-context.d.ts +1 -1
  41. package/dist/fleet-lock.d.ts +6 -0
  42. package/dist/fleet-lock.js +19 -0
  43. package/dist/fleet-lock.js.map +1 -1
  44. package/dist/fleet-manager.d.ts +174 -2
  45. package/dist/fleet-manager.js +678 -68
  46. package/dist/fleet-manager.js.map +1 -1
  47. package/dist/fleet-process-handlers.d.ts +1 -0
  48. package/dist/fleet-process-handlers.js +9 -1
  49. package/dist/fleet-process-handlers.js.map +1 -1
  50. package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +11 -0
  51. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +4 -4
  52. package/dist/general-knowledge/skills/web-appearance/SKILL.md +7 -7
  53. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +109 -57
  54. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +110 -58
  55. package/dist/guest-seen-people.d.ts +38 -0
  56. package/dist/guest-seen-people.js +78 -0
  57. package/dist/guest-seen-people.js.map +1 -0
  58. package/dist/instance-config-impact.js +4 -1
  59. package/dist/instance-config-impact.js.map +1 -1
  60. package/dist/instance-lifecycle.d.ts +2 -0
  61. package/dist/instance-lifecycle.js +27 -1
  62. package/dist/instance-lifecycle.js.map +1 -1
  63. package/dist/locale.js +62 -12
  64. package/dist/locale.js.map +1 -1
  65. package/dist/log-service.js +1 -0
  66. package/dist/log-service.js.map +1 -1
  67. package/dist/mac-auto-restore.d.ts +117 -0
  68. package/dist/mac-auto-restore.js +294 -0
  69. package/dist/mac-auto-restore.js.map +1 -0
  70. package/dist/needs-you.js +1 -0
  71. package/dist/needs-you.js.map +1 -1
  72. package/dist/outbound-handlers.d.ts +2 -2
  73. package/dist/outbound-handlers.js +5 -5
  74. package/dist/outbound-handlers.js.map +1 -1
  75. package/dist/outbound-schemas.d.ts +2 -0
  76. package/dist/outbound-schemas.js +2 -0
  77. package/dist/outbound-schemas.js.map +1 -1
  78. package/dist/public-web-gateway.js +9 -1
  79. package/dist/public-web-gateway.js.map +1 -1
  80. package/dist/search/search-index.d.ts +10 -2
  81. package/dist/search/search-index.js +17 -4
  82. package/dist/search/search-index.js.map +1 -1
  83. package/dist/service-installer.d.ts +23 -1
  84. package/dist/service-installer.js +40 -11
  85. package/dist/service-installer.js.map +1 -1
  86. package/dist/session-oom-stop.d.ts +45 -0
  87. package/dist/session-oom-stop.js +259 -0
  88. package/dist/session-oom-stop.js.map +1 -0
  89. package/dist/session-oom.d.ts +13 -0
  90. package/dist/session-oom.js +119 -7
  91. package/dist/session-oom.js.map +1 -1
  92. package/dist/settings-api.d.ts +27 -1
  93. package/dist/settings-api.js +108 -2
  94. package/dist/settings-api.js.map +1 -1
  95. package/dist/settings-change.js +37 -3
  96. package/dist/settings-change.js.map +1 -1
  97. package/dist/settings-effect.js +5 -2
  98. package/dist/settings-effect.js.map +1 -1
  99. package/dist/spawn-gate.d.ts +2 -0
  100. package/dist/spawn-gate.js +17 -4
  101. package/dist/spawn-gate.js.map +1 -1
  102. package/dist/start-attempt.d.ts +15 -1
  103. package/dist/start-attempt.js +35 -5
  104. package/dist/start-attempt.js.map +1 -1
  105. package/dist/start-failure-chat.d.ts +41 -0
  106. package/dist/start-failure-chat.js +159 -0
  107. package/dist/start-failure-chat.js.map +1 -0
  108. package/dist/start-failure-notice.d.ts +4 -0
  109. package/dist/start-failure-notice.js +8 -0
  110. package/dist/start-failure-notice.js.map +1 -1
  111. package/dist/start-failure.d.ts +49 -0
  112. package/dist/start-failure.js +141 -1
  113. package/dist/start-failure.js.map +1 -1
  114. package/dist/telegram-api-root.d.ts +1 -0
  115. package/dist/telegram-api-root.js +40 -0
  116. package/dist/telegram-api-root.js.map +1 -0
  117. package/dist/turn-reply-guard.d.ts +25 -0
  118. package/dist/turn-reply-guard.js +111 -12
  119. package/dist/turn-reply-guard.js.map +1 -1
  120. package/dist/types.d.ts +21 -3
  121. package/dist/ui/backend-signin.js +5 -2
  122. package/dist/ui/chat-ambient.js +1 -1
  123. package/dist/ui/chat-effects.js +4 -4
  124. package/dist/ui/chat-render.js +5 -2
  125. package/dist/ui/chat-strings.js +2 -2
  126. package/dist/ui/chat-thread.js +2 -2
  127. package/dist/ui/first-run.js +2 -2
  128. package/dist/ui/fleet-canvas-view.js +114 -0
  129. package/dist/ui/fleet-canvas.js +15 -77
  130. package/dist/ui/fleet-office-editor.js +80 -5
  131. package/dist/ui/fleet-office-layout.js +197 -0
  132. package/dist/ui/fleet-office-model.js +6 -3
  133. package/dist/ui/fleet-office-pack.d.ts +39 -0
  134. package/dist/ui/fleet-office-pack.js +240 -29
  135. package/dist/ui/fleet-office-skin.js +130 -21
  136. package/dist/ui/fleet-office-store.js +75 -4
  137. package/dist/ui/fleet-office-view.js +154 -0
  138. package/dist/ui/fleet-office-zones.js +95 -33
  139. package/dist/ui/fleet-office.js +530 -94
  140. package/dist/ui/fleet-usage.js +7 -6
  141. package/dist/ui/panel-chat.js +20 -7
  142. package/dist/ui/panel-details.js +2 -2
  143. package/dist/ui/panel-fleet.js +11 -77
  144. package/dist/ui/panel-log.js +5 -5
  145. package/dist/ui/panel-settings.js +586 -164
  146. package/dist/ui/search-dialog.js +26 -7
  147. package/dist/ui/settings-appearance.js +82 -29
  148. package/dist/ui/settings-apply.js +42 -7
  149. package/dist/ui/settings-dialogs.js +68 -22
  150. package/dist/ui/settings-draft.js +51 -0
  151. package/dist/ui/settings-guests.js +244 -78
  152. package/dist/ui/settings-learn.js +2 -2
  153. package/dist/ui/settings-model.js +2 -0
  154. package/dist/ui/settings-registry.js +142 -0
  155. package/dist/ui/settings-strings.js +108 -44
  156. package/dist/ui/settings-web-authority.js +34 -23
  157. package/dist/ui/shared/app-i18n.js +14 -0
  158. package/dist/ui/shared/app-nav.js +10 -5
  159. package/dist/ui/shared/app-prefs.js +80 -2
  160. package/dist/ui/shared/app-route.js +39 -5
  161. package/dist/ui/shared/app-shell.js +80 -8
  162. package/dist/ui/shared/app-stream.js +2 -1
  163. package/dist/ui/shared/app.css +251 -20
  164. package/dist/ui/shared/app.js +3 -0
  165. package/dist/ui/shared/effect-stock-trust.js +1 -1
  166. package/dist/ui/shared/identity-color.js +4 -4
  167. package/dist/ui/shared/signin.js +2 -2
  168. package/dist/ui/shared/ui-dialog.js +6 -1
  169. package/dist/ui/shared/ui-icons.js +2 -0
  170. package/dist/ui/shared/usage-dialog.js +21 -11
  171. package/dist/ui/shared/view-strings.js +2 -0
  172. package/dist/ui/shared/web-authorize.js +13 -12
  173. package/dist/web-api.d.ts +9 -3
  174. package/dist/web-api.js +161 -6
  175. package/dist/web-api.js.map +1 -1
  176. package/dist/web-auth.js +7 -1
  177. package/dist/web-auth.js.map +1 -1
  178. package/dist/web-experimental.d.ts +1 -1
  179. package/dist/web-experimental.js +1 -1
  180. package/dist/web-experimental.js.map +1 -1
  181. package/dist/web-mod-agent.js +2 -2
  182. package/dist/web-mod-agent.js.map +1 -1
  183. package/dist/web-office-skins.d.ts +108 -0
  184. package/dist/web-office-skins.js +298 -0
  185. package/dist/web-office-skins.js.map +1 -0
  186. package/dist/web-office-store.d.ts +8 -2
  187. package/dist/web-office-store.js +37 -7
  188. package/dist/web-office-store.js.map +1 -1
  189. package/dist/web-settings-patch.d.ts +6 -0
  190. package/dist/web-settings-patch.js +61 -4
  191. package/dist/web-settings-patch.js.map +1 -1
  192. package/dist/web-shell-routes.d.ts +4 -2
  193. package/dist/web-shell-routes.js +17 -7
  194. package/dist/web-shell-routes.js.map +1 -1
  195. package/dist/working-recovery.d.ts +104 -0
  196. package/dist/working-recovery.js +263 -0
  197. package/dist/working-recovery.js.map +1 -0
  198. package/dist/working-state-journal.d.ts +70 -0
  199. package/dist/working-state-journal.js +106 -0
  200. package/dist/working-state-journal.js.map +1 -0
  201. 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); }
@@ -791,6 +893,7 @@ img.amb-p { object-fit: contain; }
791
893
  .composer-field { position: relative; flex: 1; min-width: 0; display: flex; }
792
894
  .composer-ph { position: absolute; left: var(--s-1); right: var(--s-1); top: 7px; pointer-events: none; color: var(--text-3);
793
895
  font-size: var(--fs-md); line-height: var(--lh-md); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
896
+ .composer-field textarea:not(:placeholder-shown) + .composer-ph { display: none; } /* text in the box, composing text too */
794
897
  .btn-send { width: 36px; padding: 0; }
795
898
  .btn-send .send-label { display: none; }
796
899
  /* A narrow phone: Stop reply beside Send leaves the message box no room — the label is for screen readers there. */
@@ -820,15 +923,45 @@ img.amb-p { object-fit: contain; }
820
923
  padding: var(--s-1) var(--s-2); border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--surface); color: var(--text);
821
924
  box-shadow: var(--shadow-2); }
822
925
  .webauth-tip[hidden] { display: none; }
823
- /* #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. */
824
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;
825
- 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); }
826
930
  .s-guests .guest-name span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
827
- .s-guests .guest-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--s-2); }
828
- .s-guests .guest-row { padding: var(--s-2) var(--s-3); border: 1px solid var(--border); border-radius: var(--r-sm); }
829
- .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; }
830
958
  .s-guests .guest-narrow-edit { margin-top: var(--s-2); }
831
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
+ }
832
965
  /* #1939 S2b: this browser's authorization, under the composer and in the session menu (one compact line). */
833
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); }
834
967
  .webauth-chip .webauth-ok { color: var(--ok); }
@@ -1191,11 +1324,14 @@ body.side-resizing, body.side-resizing * { cursor: col-resize !important; user-s
1191
1324
  .u-crow-name { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: var(--fw-semibold); color: var(--text); }
1192
1325
  .u-crow .u-plan { flex: 0 100 auto; min-width: 22px; max-width: max-content; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1193
1326
  padding-block: 0; line-height: 16px; font-size: 10.5px; }
1194
- .u-crow-reset { margin-left: auto; flex: none; padding-left: 6px; font-size: var(--fs-xs); color: var(--text-3); white-space: nowrap; font-variant-numeric: tabular-nums; }
1195
- .u-cm { display: grid; grid-template-columns: 2.6em minmax(0, 1fr) 3.4em; align-items: center; gap: 7px; min-width: 0; }
1327
+ /* Last known numbers: a small muted "cached at …" at the name line's end (why, in its tooltip). */
1328
+ .u-crow-stale { margin-left: auto; flex: none; padding-left: 6px; font-size: var(--fs-xs); color: var(--text-3); white-space: nowrap; font-variant-numeric: tabular-nums; }
1329
+ .u-cm { display: grid; grid-template-columns: 2.2em minmax(0, 1fr) 2.9em 3.7em; align-items: center; gap: 5px; min-width: 0; }
1330
+ /* Each window's own reset, after its percent: muted, tabular, right-aligned (an empty cell keeps the columns lined up). */
1331
+ .u-cm-reset { text-align: right; font-size: var(--fs-xs); color: var(--text-3); white-space: nowrap; font-variant-numeric: tabular-nums; overflow: hidden; }
1196
1332
  .u-cm-tag { font-size: var(--fs-xs); color: var(--text-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1197
1333
  /* #2240: an account of several pools (agy) names each pool's line: a wider label, ending in an ellipsis if it must. */
1198
- .u-crow.is-pooled .u-cm { grid-template-columns: minmax(0, 7.5em) minmax(0, 1fr) 3.4em; }
1334
+ .u-crow.is-pooled .u-cm { grid-template-columns: minmax(0, 7.0em) minmax(0, 1fr) 2.9em 3.7em; column-gap: 7px; }
1199
1335
  .u-cm-bar { height: 6px; border-radius: var(--r-full); background: var(--surface-2); overflow: hidden; }
1200
1336
  .u-cm-fill { display: block; height: 100%; width: 0; min-width: 2px; border-radius: var(--r-full); background: var(--accent); }
1201
1337
  .u-cm-fill.warn { background: var(--warn); } .u-cm-fill.crit { background: var(--danger); }
@@ -1204,6 +1340,8 @@ body.side-resizing, body.side-resizing * { cursor: col-resize !important; user-s
1204
1340
  .u-crow-note { font-size: var(--fs-xs); color: var(--text-3); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1205
1341
  .u-crow-note.is-alert::before { content: "●"; margin-right: 5px; color: var(--danger); }
1206
1342
  .u-crow.is-stale .u-crow-note.is-alert::before { color: var(--warn); }
1343
+ /* Last known numbers in compact: as the full card shows them (#2386), muted — not current. */
1344
+ .u-crow.is-stale .u-cm-pct { color: var(--text-3); } .u-crow.is-stale .u-cm-fill { background: var(--text-3); }
1207
1345
  @media (prefers-reduced-motion: no-preference) { .u-crow { transition: background-color 0.15s var(--ease); } }
1208
1346
  .u-rail-body { flex: 1; min-height: 0; overflow-y: auto; padding: 0 var(--s-3) var(--s-4); display: flex; flex-direction: column; gap: var(--s-3); }
1209
1347
  @media (max-width: 1199px) { .u-rail { display: none; } }
@@ -1218,7 +1356,7 @@ body.side-resizing, body.side-resizing * { cursor: col-resize !important; user-s
1218
1356
  .s-toolbar { display: flex; gap: var(--s-2); margin: var(--s-2) 0; }
1219
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); }
1220
1358
  .s-search:focus-within { box-shadow: 0 0 0 2px var(--focus); }
1221
- .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; }
1222
1360
  .s-list { display: flex; flex-direction: column; gap: 2px; margin-bottom: var(--s-4); }
1223
1361
  .s-group { margin-bottom: var(--s-2); }
1224
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);
@@ -1253,6 +1391,10 @@ body.side-resizing, body.side-resizing * { cursor: col-resize !important; user-s
1253
1391
  .grid2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s-3) var(--s-4); }
1254
1392
  .override { display: flex; flex-direction: column; gap: var(--s-1); }
1255
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); }
1256
1398
  .impact { font-size: 11px; font-weight: var(--fw-regular); color: var(--text-3); margin-left: var(--s-1); }
1257
1399
  .check .impact { margin-left: auto; }
1258
1400
  .feedback { font-size: var(--fs-xs); color: var(--text-2); }
@@ -1277,6 +1419,12 @@ body.side-resizing, body.side-resizing * { cursor: col-resize !important; user-s
1277
1419
  .btn.on { background: var(--surface-2); color: var(--text); font-weight: var(--fw-semibold); }
1278
1420
  .card h4 { font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: var(--text-2); }
1279
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; } }
1280
1428
  .s-key { display: flex; flex-direction: column; gap: var(--s-1); padding: var(--s-2) 0; border-top: 1px solid var(--border); }
1281
1429
  .s-key:first-of-type { border-top: 0; }
1282
1430
  .s-key-row { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; }
@@ -1310,6 +1458,7 @@ textarea.s-yaml { white-space: pre; overflow-x: auto; }
1310
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);
1311
1459
  background: var(--bg); border-top: 1px solid var(--border); box-shadow: var(--shadow-2); }
1312
1460
  .s-pending .grow { flex: 1; }
1461
+ .s-pending-actions { display: flex; align-items: center; gap: var(--s-3); margin-left: auto; }
1313
1462
  .s-op { border: 1px solid var(--border); }
1314
1463
  .s-op.failed { border-color: var(--danger-line); }
1315
1464
  .s-op-head { display: flex; align-items: center; gap: var(--s-2); }
@@ -1351,6 +1500,10 @@ textarea.s-yaml { white-space: pre; overflow-x: auto; }
1351
1500
  .op-line { display: flex; align-items: center; justify-content: center; gap: var(--s-2); }
1352
1501
  .op-line.ok { color: var(--ok); }
1353
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); }
1354
1507
  /* #2233: the backend sign-in dialog and Settings' list */
1355
1508
  .signin-field { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; margin: var(--s-2) 0; }
1356
1509
  .signin-label { color: var(--text-2); font-size: var(--fs-xs); min-width: 6em; }
@@ -1373,6 +1526,7 @@ textarea.s-yaml { white-space: pre; overflow-x: auto; }
1373
1526
  .shell:has(.s-pending) .pending-stack { bottom: calc(64px + 64px + var(--s-3)); }
1374
1527
  .shell:has(.s-pending) .toasts { bottom: auto; }
1375
1528
  .s-pending { padding: var(--s-2) var(--s-3); }
1529
+ .s-pending-actions { flex-basis: 100%; justify-content: flex-end; }
1376
1530
  }
1377
1531
 
1378
1532
  /* ── Needs you (#1408 step 4 = #1386 part b) ── */
@@ -1563,8 +1717,18 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
1563
1717
  .oc-mini { display: none; }
1564
1718
  }
1565
1719
  /* ── Fleet → Org chart → Office (fleet-office.js): the fleet as a pixel office on one <canvas>. Its size is set through
1566
- the CSSOM (never a style attribute, #1300); the stage scrolls, the page around it too. ── */
1567
- .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); }
1568
1732
  .oo-bar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); }
1569
1733
  .oo-pick { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-xs); color: var(--text-2); }
1570
1734
  .oo-pick .tb-select { max-width: 200px; }
@@ -1580,6 +1744,13 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
1580
1744
  .oe-label { font-size: var(--fs-xs); color: var(--text-2); }
1581
1745
  .oe-name { min-width: 0; width: 16em; max-width: 100%; }
1582
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; }
1583
1754
  .oe-members { display: flex; flex-wrap: wrap; gap: var(--s-3); }
1584
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; }
1585
1756
  .oe-group legend { font-size: var(--fs-xs); font-weight: var(--fw-semibold); color: var(--text-2); padding: 0; margin-bottom: 4px; }
@@ -1590,24 +1761,84 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
1590
1761
  .oo-people-list { list-style: none; margin: 0; padding: 0; }
1591
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; }
1592
1763
  .oo-note { font-size: var(--fs-xs); }
1593
- .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); }
1594
- .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
+ .oo-canvas { position: absolute; left: 0; top: 0; display: block; image-rendering: pixelated; cursor: grab; touch-action: none; }
1766
+ .oo-canvas:active { cursor: grabbing; }
1767
+ .oo-canvas:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
1595
1768
  .oo-canvas.hot { cursor: pointer; }
1769
+ /* The canvas controls, as the org chart canvas's (.oc-zoom, .oc-mini): over the stage's corners. */
1770
+ .oo-zoom { z-index: 6; }
1771
+ .oo-zoom .icon-btn { color: var(--text); }
1772
+ /* The overview's box (left, top, width, height) is set by the page, inside the stage and scaled to it (miniBox). */
1773
+ .oo-mini { z-index: 6; right: auto; bottom: auto; overflow: hidden; }
1774
+ .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;
1775
+ border-bottom: 1px solid var(--border); user-select: none; }
1776
+ .oo-zoom .oo-fill { font-size: 11px; font-weight: var(--fw-semibold); }
1777
+ .oo-zoom .oo-fill.on { background: var(--accent-dim); color: var(--text); }
1778
+ .oo-stage.editing .oo-mini { display: none; }
1779
+ .oo-mini-scene { fill: var(--bg); }
1780
+ .oo-mini-room { fill: var(--surface); stroke: var(--border-strong); stroke-width: 6; }
1781
+ .oo-mini-dot { fill: var(--text-3); }
1782
+ .oo-mini-dot.st-working { fill: var(--ok); }
1783
+ .oo-mini-dot.st-needs, .oo-mini-dot.st-stuck { fill: var(--warn); }
1784
+ .oo-mini-dot.st-crashed { fill: var(--danger); }
1785
+ .oo-mini-dot.match { fill: var(--focus); }
1596
1786
  .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;
1597
1787
  color: var(--text); background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md); box-shadow: var(--shadow-2); overflow-wrap: anywhere; }
1598
1788
  /* The desks as a list: for a screen reader, and shown (over the stage's corner) while the keyboard is in it. */
1599
1789
  .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; }
1600
- .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;
1790
+ .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;
1601
1791
  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; }
1602
1792
  .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); }
1603
1793
  .oo-desk:focus-visible { outline: none; border-color: var(--focus); box-shadow: inset 0 -2px 0 var(--focus); }
1794
+ /* #2513: Edit layout (fleet-office-layout.js) — an SVG over the canvas (placed and sized through the CSSOM), its 1280×720
1795
+ grid, the areas, corridors and a handle for each seat, item, the door and waypoint; its bar sticks to the stage's foot. */
1796
+ .oo-hide-asleep { font-size: var(--fs-xs); color: var(--text-2); }
1797
+ /* #2540: fullscreen. The container is the page's fullscreen element (the Fullscreen API: the top layer), or, where the page
1798
+ cannot take one (an iPhone) or the browser refused, "immersive": it covers the window. Only the stage shows (the bars,
1799
+ notes and Edit offices are left out), the safe areas kept clear; floating over it the zoom controls, the overview
1800
+ (not on a phone) and a compact office switcher. Immersive lifts the Fleet panel's band (stock content, level 1) over
1801
+ the header (2), the phone's nav (35) and the drawer and Needs-you stack (40); menus (60) and toasts (100) stay above. */
1802
+ .oo.fs { gap: 0; margin: 0; background: var(--bg); box-sizing: border-box;
1803
+ padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left); }
1804
+ .oo.fs > :not(.oo-stage) { display: none; }
1805
+ .oo.fs > .oo-stage { flex: 1 1 auto; min-height: 0; }
1806
+ .oo.fs.immersive { position: fixed; inset: 0; z-index: 50; width: 100vw; height: 100vh; width: 100dvw; height: 100dvh; }
1807
+ html.oo-immersive, html.oo-immersive body { overflow: hidden; }
1808
+ html.oo-immersive .p-fleet > .panel-body { z-index: 50; }
1809
+ .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); }
1810
+ .oo-fs-bar .tb-select { max-width: 180px; }
1811
+ .ol-overlay { position: absolute; left: 0; top: 0; z-index: 4; }
1812
+ .ol-svg { display: block; overflow: visible; touch-action: none; }
1813
+ .ol-grid { stroke: var(--text-3); stroke-opacity: 0.25; stroke-width: 1; }
1814
+ .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; }
1815
+ .ol-area { fill: none; stroke: var(--text-2); stroke-dasharray: 6 4; stroke-width: 1.5; }
1816
+ .ol-edge { stroke: var(--focus); stroke-width: 3; stroke-opacity: 0.7; }
1817
+ .ol-handle { cursor: grab; outline: none; }
1818
+ .ol-handle > circle, .ol-handle > rect { fill: var(--surface); stroke: var(--text); stroke-width: 2; }
1819
+ .ol-seat > circle { fill: var(--ok); }
1820
+ .ol-furniture > rect { fill: var(--warn); }
1821
+ .ol-door > rect { fill: var(--danger); }
1822
+ .ol-waypoint > circle { fill: var(--focus); }
1823
+ .ol-handle.on > circle, .ol-handle.on > rect, .ol-handle:focus-visible > circle, .ol-handle:focus-visible > rect { stroke: var(--focus); stroke-width: 4; }
1824
+ .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);
1825
+ background: var(--surface); border-top: 1px solid var(--border); }
1826
+ .ol-actions { display: inline-flex; gap: var(--s-2); margin-left: auto; }
1827
+ .ol-corridors { display: inline-flex; flex-wrap: wrap; gap: 4px; }
1828
+ .ol-msg { flex-basis: 100%; margin: 0; }
1604
1829
  @media (max-width: 640px) {
1605
- .oo-stage { height: calc(100dvh - 300px); min-height: 340px; }
1830
+ /* A phone: the bars at the column's narrower padding; the stage at least most of a screen; no overview (as the canvas). */
1831
+ .oc-office > .oc-bar { padding: var(--s-2) var(--s-3); }
1832
+ .oc-office > .oc-legend { margin: 0 var(--s-3) var(--s-2); }
1833
+ .oo > :not(.oo-stage) { margin-left: var(--s-3); margin-right: var(--s-3); }
1834
+ .oo-stage { min-height: max(300px, calc(100dvh - 300px)); }
1606
1835
  .oo-pick .tb-select { max-width: 150px; }
1836
+ .oo-mini { display: none; }
1837
+ .oo-zoom .icon-btn { width: 40px; height: 40px; }
1607
1838
  }
1608
1839
  /* A phone: Fleet's five tabs fit when only the open one shows its label (the others keep theirs for screen readers). */
1609
1840
  @media (max-width: 479px) {
1610
- .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; }
1841
+ .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; }
1611
1842
  }
1612
1843
 
1613
1844
  /* ── 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"/>`,
@@ -214,18 +214,27 @@ export function compactPools(p) {
214
214
  return [...pools.values()].map((x) => ({ ...x, worst: x.metrics.reduce((a, b) => ((b.used ?? 0) > (a.used ?? 0) ? b : a)) }));
215
215
  }
216
216
  const pctOf = (m) => Math.min(100, Math.max(0, m.used ?? 0));
217
+ /** When a window resets, at the clock ("14:05", "10/19 14:05"), for a tooltip; "" when unknown or already past. */
218
+ function resetClock(m, now = Date.now()) {
219
+ const at = new Date((m && m.resetsAt) || "").getTime();
220
+ return Number.isFinite(at) && at > now ? `↻ ${usageClock(m.resetsAt, now)}` : "";
221
+ }
217
222
  /**
218
- * One window on its own line: its few characters ("5h"), a full-width bar and the percent, coloured as the full card.
219
- * A pool's line (#2240): the pool's name in place of the window, its every window in the tooltip.
223
+ * One window on its own line: its few characters ("5h"), a full-width bar, the percent coloured as the full card, and
224
+ * when this window resets ("↻ 8m"; the user's ask, 2026-10-23: a week's reset was hidden behind the 5 h one's). A pool's
225
+ * line (#2240): the pool's name in place of the window, the reset of the window its percent is, every window (with its
226
+ * reset time) in the tooltip.
220
227
  */
221
228
  function CompactMeter({ m, tag, title }) {
222
229
  const bar = useRef(null);
223
230
  const pct = Math.min(100, Math.max(0, m.used ?? 0)), cls = level(pct);
224
231
  useLayoutEffect(() => { if (bar.current) bar.current.style.width = `${pct}%`; }, [pct]); // CSSOM (#1300)
225
232
  const word = cls === "crit" ? tn("usageNear") : cls === "warn" ? tn("usageHigh") : "";
226
- return html`<span class="u-cm" title=${title || `${usageText(m.label, m.labelI18n)}: ${pct.toFixed(0)}%${word ? ` (${word})` : ""}`}>
233
+ const reset = soonestReset([m]), clock = resetClock(m);
234
+ return html`<span class="u-cm" title=${title || `${usageText(m.label, m.labelI18n)}: ${pct.toFixed(0)}%${word ? ` (${word})` : ""}${clock ? ` · ${clock}` : ""}`}>
227
235
  <span class="u-cm-tag">${tag || windowTag(m)}</span><span class="u-cm-bar"><span ref=${bar} class=${`u-cm-fill ${cls}`}></span></span>
228
- <span class=${`u-cm-pct ${cls}`}>${pct.toFixed(0)}%</span></span>`;
236
+ <span class=${`u-cm-pct ${cls}`}>${pct.toFixed(0)}%</span>
237
+ <span class="u-cm-reset">${reset ? html`${reset.short}<span class="sr-only"> (${reset.full})</span>` : null}</span></span>`;
229
238
  }
230
239
  /** What a login says instead of numbers (not logged in, an error), or "" when it has numbers. */
231
240
  function rowNote(p) {
@@ -234,26 +243,27 @@ function rowNote(p) {
234
243
  return "";
235
244
  }
236
245
  /**
237
- * A login in three short lines, under its provider's name (the user's pick, 2026-10-22): its name and plan with the
238
- * soonest reset on the right; then its short window and its long one, each a labelled full-width bar and the percent.
246
+ * A login in three short lines, under its provider's name (the user's pick, 2026-10-22): its name and plan; then its
247
+ * short window and its long one, each a labelled full-width bar, the percent and that window's own reset.
239
248
  * Numbers not current, a failed read or no login: one muted line in place of the bars, what is wrong as its tooltip.
240
249
  * Pressed, it opens its full card.
241
250
  */
242
251
  export function CompactRow({ p, onExpand }) {
243
252
  const title = providerTitle(p), failed = p.status === "error" || p.status === "no-credentials";
244
- const pools = failed || p.stale ? null : compactPools(p);
245
- const metrics = failed || p.stale ? [] : pools ? pools.flatMap((x) => x.metrics) : compactMetrics(p);
253
+ // Last known numbers (stale, e.g. an expired token) show as the others do (the user, 2026-10-23; the full card has since
254
+ // #2386), with a small "cached at 07:47" on the name line; a window that reset since is left out (compactSlots).
255
+ const pools = failed ? null : compactPools(p);
256
+ const metrics = failed ? [] : pools ? pools.flatMap((x) => x.metrics) : compactMetrics(p);
246
257
  const note = rowNote(p), stale = p.stale ? staleNote(p) : null;
247
258
  const say = stale ? stale.mark : note || tn("usage.no_data");
248
259
  const detail = stale ? [stale.mark, stale.detail].filter(Boolean).join(" — ") : say;
249
- const reset = soonestReset(metrics);
250
260
  const name = title.profile || title.base;
251
261
  return html`<button type="button" class=${`u-crow${p.stale ? " is-stale" : ""}${pools ? " is-pooled" : ""}`} aria-expanded="false" title=${`${p.name} — ${tn("usageExpand")}`} onClick=${onExpand}>
252
262
  <span class="u-crow-head"><span class="u-crow-name">${name}</span>
253
263
  ${p.plan ? html`<span class="pill u-plan" title=${p.plan}><span aria-hidden="true">${planBadge(p.plan)}</span><span class="sr-only">${p.plan}</span></span>` : null}
254
- ${reset ? html`<span class="u-crow-reset" title=${reset.full}>${reset.short}<span class="sr-only"> (${reset.full})</span></span>` : null}</span>
264
+ ${stale && metrics.length ? html`<span class="u-crow-stale" title=${detail}>${p.cachedAt ? tn("usageCachedAt", { at: usageClock(p.cachedAt) }) : stale.mark}<span class="sr-only"> (${detail})</span></span>` : null}</span>
255
265
  ${pools ? pools.map((x) => html`<${CompactMeter} key=${x.pool.label} m=${x.worst} tag=${usageText(x.pool.label, x.pool.labelI18n)}
256
- title=${x.metrics.map((m) => `${usageText(m.label, m.labelI18n)}: ${pctOf(m).toFixed(0)}%`).join(" · ")} />`)
266
+ title=${x.metrics.map((m) => { const c = resetClock(m); return `${usageText(m.label, m.labelI18n)}: ${pctOf(m).toFixed(0)}%${c ? ` (${c})` : ""}`; }).join(" · ")} />`)
257
267
  : metrics.length ? metrics.map((m, j) => html`<${CompactMeter} key=${j} m=${m} />`)
258
268
  : html`<span class=${`u-crow-note${(stale || (p.status === "error" && !p.transient)) ? " is-alert" : ""}`} title=${detail}>${say}</span>`}
259
269
  </button>`;