@bongos/core 1.24.51 → 1.24.53

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 (95) hide show
  1. package/.bongos-core.json +125 -95
  2. package/docs/copy-inventory.md +225 -242
  3. package/docs/copy-registry.json +278 -452
  4. package/docs/file-map.md +1 -1
  5. package/docs/module-api-changelog.md +4 -0
  6. package/docs/page-inventory.json +33 -0
  7. package/docs/page-readings.json +515 -531
  8. package/modules/discord/webhook.js +28 -6
  9. package/modules/hall-ui/public/ai-studio.html +1 -0
  10. package/modules/hall-ui/public/approval-queue.css +3 -11
  11. package/modules/hall-ui/public/approval-queue.js +13 -20
  12. package/modules/hall-ui/public/atlas.html +1 -0
  13. package/modules/hall-ui/public/blockers.html +1 -0
  14. package/modules/hall-ui/public/board-room.html +1 -0
  15. package/modules/hall-ui/public/bulk-toolbar.js +5 -1
  16. package/modules/hall-ui/public/city.html +1 -0
  17. package/modules/hall-ui/public/collab.html +1 -0
  18. package/modules/hall-ui/public/commands.html +1 -0
  19. package/modules/hall-ui/public/controls.css +100 -0
  20. package/modules/hall-ui/public/copy-desk.css +7 -11
  21. package/modules/hall-ui/public/copy-desk.html +17 -16
  22. package/modules/hall-ui/public/copy-desk.js +13 -9
  23. package/modules/hall-ui/public/deploy.html +1 -0
  24. package/modules/hall-ui/public/goal-inbox.js +32 -17
  25. package/modules/hall-ui/public/goal-map.html +1 -0
  26. package/modules/hall-ui/public/goals-page.js +7 -1
  27. package/modules/hall-ui/public/goals.html +1 -0
  28. package/modules/hall-ui/public/government.html +1 -0
  29. package/modules/hall-ui/public/government.js +10 -2
  30. package/modules/hall-ui/public/hall-dialog.js +255 -0
  31. package/modules/hall-ui/public/hall-kit.js +9 -2
  32. package/modules/hall-ui/public/idea.html +1 -0
  33. package/modules/hall-ui/public/ideas.html +1 -0
  34. package/modules/hall-ui/public/index.html +1 -0
  35. package/modules/hall-ui/public/market-watch-item.html +2 -1
  36. package/modules/hall-ui/public/market-watch-lib.js +16 -9
  37. package/modules/hall-ui/public/market-watch-rules.html +1 -0
  38. package/modules/hall-ui/public/market-watch.css +2 -3
  39. package/modules/hall-ui/public/market-watch.html +2 -1
  40. package/modules/hall-ui/public/modules.html +1 -0
  41. package/modules/hall-ui/public/money.html +1 -0
  42. package/modules/hall-ui/public/my-agents.html +1 -0
  43. package/modules/hall-ui/public/oversight.css +3 -33
  44. package/modules/hall-ui/public/primer.html +1 -0
  45. package/modules/hall-ui/public/profile.html +1 -0
  46. package/modules/hall-ui/public/project-settings.html +1 -0
  47. package/modules/hall-ui/public/roster.html +1 -0
  48. package/modules/hall-ui/public/settings-ai-keys.js +5 -1
  49. package/modules/hall-ui/public/settings-passkeys.js +5 -1
  50. package/modules/hall-ui/public/settings-sessions.js +10 -2
  51. package/modules/hall-ui/public/settings.html +1 -0
  52. package/modules/hall-ui/public/settings.js +5 -1
  53. package/modules/hall-ui/public/street.html +1 -0
  54. package/modules/hall-ui/public/studio.css +6 -32
  55. package/modules/hall-ui/public/studio.html +4 -3
  56. package/modules/hall-ui/public/studio.js +40 -35
  57. package/modules/hall-ui/public/style.css +2 -8
  58. package/modules/hall-ui/public/task.html +1 -0
  59. package/modules/hall-ui/public/task.js +26 -12
  60. package/modules/hall-ui/public/thinking.html +1 -0
  61. package/modules/hall-ui/public/tweak-editor.html +1 -0
  62. package/modules/hall-ui/public/watch.html +1 -0
  63. package/modules/hall-ui/public/work.html +1 -0
  64. package/modules/hall-ui/public/work.js +7 -1
  65. package/modules/public-landing/public/index.html +11 -8
  66. package/modules/ui-design/public/brand/og-card.png +0 -0
  67. package/package-lock.json +2 -2
  68. package/package.json +1 -1
  69. package/public-docs/index.html +107 -12
  70. package/release-notes.json +12 -0
  71. package/scripts/bongos/gen-brand.js +40 -6
  72. package/scripts/discord/lib.js +17 -3
  73. package/scripts/status-mirror-sync.js +11 -7
  74. package/src/bongos/serve-internal.js +3 -2
  75. package/src/error-page.js +120 -0
  76. package/src/link-preview.js +71 -0
  77. package/src/module-api.js +1 -1
  78. package/src/platform-server.js +22 -8
  79. package/tests/brand_surfaces.mjs +213 -0
  80. package/tests/bulk_toolbar.mjs +1 -1
  81. package/tests/discord_board_broadcast.mjs +23 -10
  82. package/tests/docs_openapi_route.mjs +2 -1
  83. package/tests/hall_approval_queue.mjs +8 -2
  84. package/tests/hall_audit.mjs +1 -1
  85. package/tests/hall_city.mjs +1 -1
  86. package/tests/hall_collab_world.mjs +9 -6
  87. package/tests/hall_dialog.mjs +401 -0
  88. package/tests/hall_error_envelope.mjs +1 -1
  89. package/tests/hall_oversight_world.mjs +9 -7
  90. package/tests/hall_record_world.mjs +1 -1
  91. package/tests/hall_street.mjs +1 -1
  92. package/tests/hall_studio_home.mjs +1 -1
  93. package/tests/home_attention_cards.mjs +2 -0
  94. package/tests/kind_axis_unset.mjs +1 -1
  95. package/tests/market_watch_pages.mjs +3 -1
@@ -71,16 +71,36 @@ function capContent(text) {
71
71
  return String(text).slice(0, 2000);
72
72
  }
73
73
 
74
+ // THE BRAND ON A POST (task 1005091). A broadcast goes out as an EMBED, not as bare
75
+ // message text: the same words as the embed's description, with the pack's accent as
76
+ // the embed's colour bar, so every post in a channel reads as the product's. The
77
+ // description cap is 4096 (an embed's own limit). Mentions inside an embed never ring,
78
+ // which is #871's rule anyway; allowed_mentions stays parse:[] belt-and-braces.
79
+ // The pack's accent as Discord's integer colour, or null (no bar) when it is not a hex.
80
+ function brandColor(brand) {
81
+ let accent = '';
82
+ try { accent = ((brand || api.clientBranding()).theme || {}).colors.accent || ''; } catch { accent = ''; }
83
+ const m = /^#([0-9a-f]{6})$/i.exec(String(accent).trim());
84
+ return m ? parseInt(m[1], 16) : null;
85
+ }
86
+ // The body of a broadcast post: one embed carrying the text, coloured with the accent.
87
+ function embedBody(text, brand) {
88
+ const embed = { description: String(text).slice(0, 4096) };
89
+ const color = brandColor(brand);
90
+ if (color !== null) embed.color = color;
91
+ return { embeds: [embed], allowed_mentions: { parse: [] } };
92
+ }
93
+
74
94
  // R22 (the board mirror) needs the id of the message it just posted so the close
75
95
  // can be edited onto it, so the POST asks for the created message back
76
96
  // (`?wait=true` — Discord then answers 200 + the message JSON instead of 204 with
77
97
  // no body). The extra field is additive: every existing caller ignores the return.
78
- async function postCore(channel, text, allowedMentions) {
98
+ async function postCore(channel, body) {
79
99
  const url = resolveWebhookUrl(channel);
80
100
  const res = await fetch(`${url}${url.includes('?') ? '&' : '?'}wait=true`, {
81
101
  method: 'POST',
82
102
  headers: { 'Content-Type': 'application/json' },
83
- body: JSON.stringify({ content: capContent(text), allowed_mentions: allowedMentions }),
103
+ body: JSON.stringify(body),
84
104
  });
85
105
  if (!res.ok) {
86
106
  const body = await res.text().catch(() => '');
@@ -96,7 +116,7 @@ async function postCore(channel, text, allowedMentions) {
96
116
  // attacker-influenceable; allowed_mentions:{parse:[]} renders text verbatim but
97
117
  // fires zero notifications. (Discord defaults to parsing mentions when omitted.)
98
118
  async function postWebhook(channel, text) {
99
- return postCore(channel, text, { parse: [] });
119
+ return postCore(channel, embedBody(text));
100
120
  }
101
121
 
102
122
  // postWebhookMentions — the ONE sanctioned exception to #871's blanket mention
@@ -104,10 +124,12 @@ async function postWebhook(channel, text) {
104
124
  // notice"). Still parse:[] — nothing typed INTO the content can ever ping — but
105
125
  // an explicit `users` allowlist lets exactly the named ids ring. The ids must
106
126
  // come from the board-membership resolve (server data), NEVER from message
107
- // text; the cap of 20 is a sanity bound far above any real board.
127
+ // text; the cap of 20 is a sanity bound far above any real board. It stays
128
+ // MESSAGE TEXT, not an embed: a mention inside an embed never rings, and ringing
129
+ // is this post's whole job (R22's edit rewrites the same text in place).
108
130
  async function postWebhookMentions(channel, text, userIds = []) {
109
131
  const users = [...new Set(userIds.filter((u) => /^\d+$/.test(String(u))))].slice(0, 20).map(String);
110
- return postCore(channel, text, { parse: [], users });
132
+ return postCore(channel, { content: capContent(text), allowed_mentions: { parse: [], users } });
111
133
  }
112
134
 
113
135
  // editWebhook — rewrite a message this webhook already posted, in place (R22 of
@@ -149,5 +171,5 @@ function hasWebhook(channel) {
149
171
 
150
172
  module.exports = {
151
173
  CHANNELS, WEBHOOKS_PATH, loadWebhooks, resolveWebhookUrl, hasWebhook,
152
- postWebhook, postWebhookMentions, editWebhook,
174
+ postWebhook, postWebhookMentions, editWebhook, embedBody,
153
175
  };
@@ -90,6 +90,7 @@
90
90
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
91
91
  <script src="/builders/craft-mode.js?v=2026-11-05-goals"></script>
92
92
  <script src="/builders/shell.js?v=2026-11-07-brand-mark"></script>
93
+ <script src="/builders/hall-dialog.js"></script>
93
94
  <script src="/builders/palette.js" defer></script>
94
95
  <script src="/builders/tweak-editor-lib.js?v=2026-09-28-tw23"></script>
95
96
  <script src="/builders/studio-lib.js?v=2026-10-09-day"></script>
@@ -26,7 +26,6 @@
26
26
  --aq-glass: rgba(255, 255, 255, .075);
27
27
  --aq-glass-edge: rgba(255, 255, 255, .18);
28
28
  --aq-glass-rim: rgba(255, 255, 255, .22);
29
- --aq-drop: rgba(0, 0, 0, .55);
30
29
  --aq-ink: #ffffff;
31
30
  --aq-ink-soft: #c0c0c0;
32
31
  --aq-ink-faint: #a8a8a8;
@@ -44,14 +43,8 @@
44
43
  --aq-rule: rgba(255, 255, 255, .12);
45
44
  --aq-face: var(--font-body, Manrope, "Segoe UI", system-ui, sans-serif);
46
45
  --aq-mono: var(--font-mono, "JetBrains Mono", ui-monospace, Consolas, monospace);
47
- position: fixed;
48
- inset: 0;
49
- z-index: var(--z-modal, 160);
50
- display: flex;
51
- align-items: center;
52
- justify-content: center;
53
- padding: 24px;
54
- box-sizing: border-box;
46
+ /* the layer, the placement and the gutter are the shared .hall-scrim's
47
+ (controls.css, task 1005085); only the artboard's tint is its own */
55
48
  background: var(--aq-scrim);
56
49
  color: var(--aq-ink);
57
50
  font-family: var(--aq-face);
@@ -60,7 +53,6 @@ html[data-theme="dark"] .aq-overlay { --aq-scrim: rgba(4, 5, 9, .42); }
60
53
  @media (prefers-color-scheme: dark) {
61
54
  html:not([data-theme]) .aq-overlay { --aq-scrim: rgba(4, 5, 9, .42); }
62
55
  }
63
- .aq-overlay[hidden] { display: none; }
64
56
  html.aq-open { overflow: hidden; }
65
57
 
66
58
  /* FITS THE SCREEN (task 1004381). The artboard's 1180 by 820 is a ceiling, never
@@ -80,7 +72,7 @@ html.aq-open { overflow: hidden; }
80
72
  border: 1px solid var(--aq-glass-edge);
81
73
  backdrop-filter: blur(30px) saturate(160%);
82
74
  -webkit-backdrop-filter: blur(30px) saturate(160%);
83
- box-shadow: inset 0 1px 0 var(--aq-glass-rim), 0 40px 90px var(--aq-drop);
75
+ box-shadow: inset 0 1px 0 var(--aq-glass-rim), var(--ds-shadow-pop);
84
76
  display: flex;
85
77
  flex-direction: column;
86
78
  gap: 16px;
@@ -79,11 +79,16 @@
79
79
  };
80
80
 
81
81
  const scrim = document.createElement('div');
82
- scrim.className = 'aq-overlay';
82
+ // The shared overlay layer (controls.css .hall-scrim, task 1005085); the
83
+ // glass inside is the artboard's own.
84
+ scrim.className = 'hall-scrim aq-overlay';
83
85
  scrim.hidden = true;
84
86
  scrim.innerHTML = '<div class="aq-dialog" role="dialog" aria-modal="true" aria-labelledby="aq-title" tabindex="-1"></div>';
85
87
  root.appendChild(scrim);
86
88
  const dialog = scrim.querySelector('.aq-dialog');
89
+ // The hall's one dialog behaviour (hall-dialog.js, task 1005085): Escape, the
90
+ // focus trap, a click on the scrim, the scroll lock and focus back to the opener.
91
+ const modal = window.OTBDialog.bind(scrim, { panel: dialog, lockClass: 'aq-open', initialFocus: () => dialog, onClose: closed });
87
92
 
88
93
  const current = () => state.queue[state.idx] || null;
89
94
 
@@ -296,14 +301,11 @@
296
301
  // `pageId` (optional) lands on that page's card when it is in the queue, so a
297
302
  // row elsewhere that says "needs to be reviewed" opens its own card.
298
303
  async function open(pageId, { reload = true } = {}) {
299
- state.opener = document.activeElement;
300
304
  state.open = true;
301
305
  state.error = null;
302
306
  state.noteOpen = false;
303
- scrim.hidden = false;
304
- document.documentElement.classList.add('aq-open');
305
307
  paint();
306
- dialog.focus();
308
+ modal.open();
307
309
  if (reload || !state.loaded) await loadQueue();
308
310
  if (typeof pageId === 'string') {
309
311
  const at = state.queue.findIndex((q) => q.page_id === pageId);
@@ -315,17 +317,17 @@
315
317
  if (first) first.focus();
316
318
  }
317
319
 
318
- function close() {
320
+ function close() { modal.close(); }
321
+
322
+ // Whatever closed it (the close button, Escape, the scrim), the queue settles here.
323
+ function closed() {
319
324
  state.open = false;
320
- scrim.hidden = true;
321
- document.documentElement.classList.remove('aq-open');
322
325
  // A decided page leaves the queue for good; the next open starts fresh.
323
326
  if (state.outcome.size) {
324
327
  state.queue = state.queue.filter((q) => !state.outcome.has(q.task_id));
325
328
  state.outcome.clear();
326
329
  state.idx = 0;
327
330
  }
328
- if (state.opener && state.opener.focus) state.opener.focus();
329
331
  }
330
332
 
331
333
  // A switch repaints the dialog, so the pressed pill is focused again: a
@@ -338,7 +340,6 @@
338
340
  }
339
341
 
340
342
  scrim.addEventListener('click', (ev) => {
341
- if (ev.target === scrim) return close();
342
343
  const t = ev.target.closest('button');
343
344
  if (!t || t.disabled) return undefined;
344
345
  if (('close' in t.dataset)) return close();
@@ -353,17 +354,9 @@
353
354
  return undefined;
354
355
  });
355
356
 
357
+ // Escape and the Tab trap are the shared dialog's (OTBDialog.bind above).
356
358
  scrim.addEventListener('keydown', (ev) => {
357
- if (ev.key === 'Escape') { ev.preventDefault(); close(); return; }
358
- if (ev.key === 'Enter' && ev.target && ev.target.id === 'aq-note') { ev.preventDefault(); decide('sent'); return; }
359
- if (ev.key !== 'Tab') return;
360
- // Keep focus inside the dialog while it is open (aria-modal).
361
- const f = [...dialog.querySelectorAll('button:not([disabled]), input, [tabindex="0"]')];
362
- if (!f.length) return;
363
- const first = f[0];
364
- const last = f[f.length - 1];
365
- if (ev.shiftKey && document.activeElement === first) { ev.preventDefault(); last.focus(); }
366
- else if (!ev.shiftKey && document.activeElement === last) { ev.preventDefault(); first.focus(); }
359
+ if (ev.key === 'Enter' && ev.target && ev.target.id === 'aq-note') { ev.preventDefault(); decide('sent'); }
367
360
  });
368
361
 
369
362
  scrim.addEventListener('input', (ev) => {
@@ -93,6 +93,7 @@
93
93
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
94
94
  <script src="/builders/craft-mode.js?v=2026-11-05-goals"></script>
95
95
  <script src="/builders/shell.js?v=2026-11-07-brand-mark"></script>
96
+ <script src="/builders/hall-dialog.js"></script>
96
97
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
97
98
  <script src="/builders/palette.js" defer></script>
98
99
  <script src="/builders/atlas.js?v=2026-09-02-index" defer></script>
@@ -62,6 +62,7 @@
62
62
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
63
63
  <script src="/builders/craft-mode.js?v=2026-11-05-goals"></script>
64
64
  <script src="/builders/shell.js?v=2026-11-07-brand-mark"></script>
65
+ <script src="/builders/hall-dialog.js"></script>
65
66
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
66
67
  <script src="/builders/palette.js" defer></script>
67
68
  <!-- the branding pack's currency label, loaded BEFORE blockers.js. -->
@@ -64,6 +64,7 @@
64
64
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
65
65
  <script src="/builders/craft-mode.js?v=2026-11-05-goals"></script>
66
66
  <script src="/builders/shell.js?v=2026-11-07-brand-mark"></script>
67
+ <script src="/builders/hall-dialog.js"></script>
67
68
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
68
69
  <script src="/builders/palette.js" defer></script>
69
70
  <script src="/builders/hall-kit.js"></script>
@@ -246,8 +246,12 @@
246
246
  if (!entries.length) return;
247
247
  const label = entries.length === 1 ? `#${entries[0][1]}` : `${entries.length} tasks`;
248
248
  const pronoun = entries.length === 1 ? 'It returns' : 'They return';
249
- if (!window.confirm(`Release ${label}? ${pronoun} to the queue and no ${curLabel()} are earned.`)) return;
250
249
  const btn = el('release');
250
+ if (!(await window.OTBDialog.confirm({
251
+ title: `Release ${label}?`,
252
+ body: `${pronoun} to the queue and no ${curLabel()} are earned.`,
253
+ confirmLabel: 'Release', danger: true, trigger: btn,
254
+ }))) return;
251
255
  if (btn) { btn.disabled = true; btn.textContent = 'Releasing…'; }
252
256
  try {
253
257
  const out = await claimAction.releaseMany(entries);
@@ -109,6 +109,7 @@
109
109
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers" defer></script>
110
110
  <script src="/builders/craft-mode.js?v=2026-11-05-goals" defer></script>
111
111
  <script src="/builders/shell.js?v=2026-11-07-brand-mark" defer></script>
112
+ <script src="/builders/hall-dialog.js" defer></script>
112
113
  <script src="/builders/palette.js" defer></script>
113
114
  <script src="/builders/gov-living.js?v=2026-10-09-positions1" defer></script>
114
115
  <script src="/builders/gov-feeds.js?v=2026-10-09-living3" defer></script>
@@ -163,6 +163,7 @@
163
163
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
164
164
  <script src="/builders/craft-mode.js?v=2026-11-05-goals"></script>
165
165
  <script src="/builders/shell.js?v=2026-11-07-brand-mark"></script>
166
+ <script src="/builders/hall-dialog.js"></script>
166
167
  <script src="/builders/palette.js" defer></script>
167
168
  <script src="/builders/collab-lib.js?v=2026-09-24-archive"></script>
168
169
  <script src="/builders/collab.js?v=2026-09-24-archive"></script>
@@ -55,6 +55,7 @@
55
55
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
56
56
  <script src="/builders/craft-mode.js?v=2026-11-05-goals"></script>
57
57
  <script src="/builders/shell.js?v=2026-11-07-brand-mark"></script>
58
+ <script src="/builders/hall-dialog.js"></script>
58
59
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
59
60
  <script src="/builders/palette.js" defer></script>
60
61
  <script src="/builders/hall-kit.js"></script>
@@ -221,3 +221,103 @@ select:disabled, input[type="text"]:disabled, input[type="search"]:disabled, tex
221
221
  cursor: not-allowed;
222
222
  }
223
223
  select option { background: var(--bg-card); color: var(--ink); }
224
+
225
+ /* ---------- the danger button ----------
226
+ The one fill a confirm spends on an act that cannot be taken back (delete a
227
+ rank, end every session). The status red, with the card's own ground as its
228
+ text: --danger is measured at 4.5:1 against --bg-card in both modes
229
+ (tests/hall_tokens.mjs), so the pair holds whichever way round it is read. */
230
+ .btn--danger {
231
+ color: var(--bg-card);
232
+ background: var(--danger);
233
+ border-color: transparent;
234
+ }
235
+ .btn--danger:hover:not(:disabled) {
236
+ color: var(--bg-card);
237
+ background: color-mix(in srgb, var(--danger) 86%, var(--ink));
238
+ text-decoration: none;
239
+ }
240
+
241
+ /* ---------- the dialog (task 1005085) ----------
242
+ THE HALL'S ONE DIALOG. hall-dialog.js builds it (OTBDialog.open / .confirm)
243
+ or binds a page's own <dialog class="hall-dialog"> (OTBDialog.bind). It is
244
+ DESIGN.md's panel lifted over the page: bg-card, one hairline, the panel
245
+ corner, and the one shadow the hall spends, on an element that floats over
246
+ the scrim. A native dialog opens with showModal(), so it sits in the top
247
+ layer; the z-index is the ladder's modal step for any reader that does not.
248
+ .hall-scrim is the same layer for a div overlay (the approval queue's and the
249
+ studio's glass): fixed over the page, centred, darkened by --scrim. */
250
+ .hall-dialog {
251
+ z-index: var(--ds-z-modal);
252
+ box-sizing: border-box;
253
+ width: min(480px, calc(100vw - 2 * var(--ds-space-sm)));
254
+ max-height: calc(100dvh - 2 * var(--ds-space-md));
255
+ overflow-y: auto;
256
+ margin: auto;
257
+ padding: var(--ds-space-md);
258
+ color: var(--ink);
259
+ background: var(--bg-card);
260
+ border: var(--line) solid var(--rule);
261
+ border-radius: var(--ds-radius-panel);
262
+ box-shadow: var(--ds-shadow-pop);
263
+ }
264
+ .hall-dialog--wide { width: min(720px, calc(100vw - 2 * var(--ds-space-sm))); }
265
+ .hall-dialog::backdrop { background: var(--scrim); }
266
+ .hall-scrim {
267
+ position: fixed;
268
+ inset: 0;
269
+ z-index: var(--ds-z-modal);
270
+ display: flex;
271
+ align-items: center;
272
+ justify-content: center;
273
+ box-sizing: border-box;
274
+ padding: var(--ds-space-md);
275
+ background: var(--scrim);
276
+ }
277
+ .hall-scrim[hidden] { display: none; }
278
+ .hall-scrim > .hall-dialog { max-height: 100%; }
279
+ .hall-dialog__head {
280
+ display: flex;
281
+ align-items: flex-start;
282
+ gap: var(--ds-space-xs);
283
+ margin: 0 0 var(--ds-space-xs);
284
+ }
285
+ .hall-dialog__title {
286
+ flex: 1;
287
+ margin: 0;
288
+ font-family: var(--font-display);
289
+ font-size: var(--ds-text-3xl);
290
+ font-weight: 500;
291
+ line-height: 1.25;
292
+ color: var(--ink);
293
+ }
294
+ .hall-dialog__x {
295
+ flex: none;
296
+ min-width: 32px;
297
+ min-height: 32px;
298
+ margin: -4px -6px 0 0;
299
+ padding: 0;
300
+ font: inherit;
301
+ font-size: var(--ds-text-4xl);
302
+ line-height: 1;
303
+ color: var(--ink-soft);
304
+ background: transparent;
305
+ border: 0;
306
+ border-radius: var(--radius-pill);
307
+ cursor: pointer;
308
+ }
309
+ .hall-dialog__x:hover { color: var(--ink); }
310
+ .hall-dialog__body p {
311
+ margin: 0 0 var(--ds-space-xs);
312
+ font-size: var(--ds-text-md);
313
+ line-height: 1.5;
314
+ color: var(--ink-soft);
315
+ }
316
+ .hall-dialog__body p:last-child { margin-bottom: 0; }
317
+ .hall-dialog__actions {
318
+ display: flex;
319
+ flex-wrap: wrap;
320
+ justify-content: flex-end;
321
+ gap: var(--ds-space-xs);
322
+ margin-top: var(--ds-space-md);
323
+ }
@@ -2,9 +2,9 @@
2
2
  R02 of goal 1000074), rebuilt on the hall's row archetype by task 1003471:
3
3
  the counts are the shell's glass .stat-row, every list is a run of
4
4
  oversight.css's .ov-row, the filters and the search are the kit's .filters,
5
- the two dialogs are the family's .ov-modal on a native <dialog>. What is
6
- left here is what only this page has: the quoted string as evidence, the
7
- reason as prose, the places a duplicate lives, the dialog's scrim.
5
+ the three dialogs are the hall's one dialog (controls.css .hall-dialog,
6
+ task 1005085). What is left here is what only this page has: the quoted
7
+ string as evidence, the reason as prose, the places a duplicate lives.
8
8
 
9
9
  px and tokens throughout, and state is COLOUR — the pill's verification
10
10
  vocabulary — never opacity. Opacity dims the panel behind the text as well
@@ -67,16 +67,12 @@
67
67
  .cd-find .filter input[type="search"] { width: 100%; }
68
68
  .cd-search-count { margin: 0 0 14px; }
69
69
 
70
- /* The three dialogs are the family modal on a native <dialog>: bg-card, one rule
71
- hairline, no shadow. The backdrop is a SCRIM — it has to darken what is behind
72
- it in EITHER mode, so it can take no ground token and no mix of --ink (which is
73
- white on black). That is why it was a literal; task 1003495 gave the value a
74
- home in style.css as --scrim instead, so the sheet keeps none. */
75
- .cd-dialog { width: min(480px, calc(100vw - 32px)); color: var(--ink); }
76
- .cd-dialog::backdrop { background: var(--scrim); }
70
+ /* The three dialogs are the hall's one dialog (controls.css .hall-dialog: the
71
+ panel, the scrim, the shadow, task 1005085). What stays is the form inside. */
72
+ .cd-dialog .cd-dialog__target { margin: var(--ds-space-xs) 0 0; font-size: var(--ds-text-sm-plus); line-height: 1.5; color: var(--ink-soft); }
77
73
  .cd-dialog .ov-form { margin: 14px 0 0; }
78
74
  .cd-dialog textarea { resize: vertical; line-height: 1.5; }
79
- .ov-modal .cd-dialog__err { min-height: 18px; margin: 8px 0 0; font-size: 13px; line-height: 1.45; color: var(--danger); }
75
+ .cd-dialog .cd-dialog__err { min-height: 18px; margin: 8px 0 0; font-size: 13px; line-height: 1.45; color: var(--danger); }
80
76
 
81
77
  /* The placeholder rule, shown only for a sentence that has holes in it (task
82
78
  1003114 / R03). It is a CONSTRAINT, not a footnote — losing a hole is the one
@@ -77,10 +77,10 @@
77
77
  <!-- The flag composer. A dialog rather than an inline field because the reason is
78
78
  the point: a one-line input invites "typo", and the queue is only worth
79
79
  reading if the reasons are. A native <dialog> (its own focus trap and
80
- Escape) dressed as the family's confirm modal. -->
81
- <dialog id="cd-dialog" class="ov-modal cd-dialog">
80
+ Escape) dressed as the hall's one dialog (controls.css .hall-dialog, task 1005085). -->
81
+ <dialog id="cd-dialog" class="hall-dialog cd-dialog" aria-labelledby="cd-dialog-h">
82
82
  <form method="dialog" id="cd-form">
83
- <h3 class="scroll__h" id="cd-dialog-h">Flag this copy</h3>
83
+ <h3 class="hall-dialog__title" id="cd-dialog-h">Flag this copy</h3>
84
84
  <p class="cd-dialog__target" id="cd-dialog-target"></p>
85
85
  <div class="ov-form">
86
86
  <div class="ov-field ov-field--wide">
@@ -103,17 +103,17 @@
103
103
  <p class="ov-form__why">The artist reads this instead of guessing. A pointer with no judgement costs them a look and tells them nothing.</p>
104
104
  </div>
105
105
  <p class="cd-dialog__err" id="cd-dialog-err" role="alert"></p>
106
- <div class="ov-modal__actions">
107
- <button type="button" class="btn-ghost" id="cd-cancel">Cancel</button>
108
- <button type="button" class="btn-accent" id="cd-submit">File the flag</button>
106
+ <div class="hall-dialog__actions">
107
+ <button type="button" class="btn btn--ghost" id="cd-cancel">Cancel</button>
108
+ <button type="button" class="btn" id="cd-submit">File the flag</button>
109
109
  </div>
110
110
  </form>
111
111
  </dialog>
112
112
 
113
113
  <!-- Closing a flag asks for a note for the same reason opening one does. -->
114
- <dialog id="cd-close-dialog" class="ov-modal cd-dialog">
114
+ <dialog id="cd-close-dialog" class="hall-dialog cd-dialog" aria-labelledby="cd-close-h">
115
115
  <form method="dialog" id="cd-close-form">
116
- <h3 class="scroll__h">Close this flag</h3>
116
+ <h3 class="hall-dialog__title" id="cd-close-h">Close this flag</h3>
117
117
  <p class="cd-dialog__target" id="cd-close-target"></p>
118
118
  <div class="ov-form">
119
119
  <div class="ov-field ov-field--wide">
@@ -135,9 +135,9 @@
135
135
  <p class="ov-form__why">The next person to notice this sentence reads your note instead of re-litigating it.</p>
136
136
  </div>
137
137
  <p class="cd-dialog__err" id="cd-close-err" role="alert"></p>
138
- <div class="ov-modal__actions">
139
- <button type="button" class="btn-ghost" id="cd-close-cancel">Cancel</button>
140
- <button type="button" class="btn-accent" id="cd-close-submit">Close it</button>
138
+ <div class="hall-dialog__actions">
139
+ <button type="button" class="btn btn--ghost" id="cd-close-cancel">Cancel</button>
140
+ <button type="button" class="btn" id="cd-close-submit">Close it</button>
141
141
  </div>
142
142
  </form>
143
143
  </dialog>
@@ -147,9 +147,9 @@
147
147
  wrong, a proposal says this is what it should say instead, and one of them
148
148
  creates work on the board. The textarea opens PREFILLED with the current
149
149
  wording — an artist edits a sentence, they do not retype it from memory. -->
150
- <dialog id="cd-propose-dialog" class="ov-modal cd-dialog">
150
+ <dialog id="cd-propose-dialog" class="hall-dialog cd-dialog" aria-labelledby="cd-propose-h">
151
151
  <form method="dialog" id="cd-propose-form">
152
- <h3 class="scroll__h">Propose new wording</h3>
152
+ <h3 class="hall-dialog__title" id="cd-propose-h">Propose new wording</h3>
153
153
  <p class="cd-dialog__target" id="cd-propose-target"></p>
154
154
  <div class="ov-form">
155
155
  <div class="ov-field ov-field--wide">
@@ -168,9 +168,9 @@
168
168
  <p class="ov-form__why">Nothing changes yet. This files a task carrying your wording; whoever claims it lands the diff, and it ships graded like every other change.</p>
169
169
  </div>
170
170
  <p class="cd-dialog__err" id="cd-propose-err" role="alert"></p>
171
- <div class="ov-modal__actions">
172
- <button type="button" class="btn-ghost" id="cd-propose-cancel">Cancel</button>
173
- <button type="button" class="btn-accent" id="cd-propose-submit">File the change</button>
171
+ <div class="hall-dialog__actions">
172
+ <button type="button" class="btn btn--ghost" id="cd-propose-cancel">Cancel</button>
173
+ <button type="button" class="btn" id="cd-propose-submit">File the change</button>
174
174
  </div>
175
175
  </form>
176
176
  </dialog>
@@ -181,6 +181,7 @@
181
181
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
182
182
  <script src="/builders/craft-mode.js?v=2026-11-05-goals"></script>
183
183
  <script src="/builders/shell.js?v=2026-11-07-brand-mark"></script>
184
+ <script src="/builders/hall-dialog.js"></script>
184
185
  <script src="/builders/palette.js" defer></script>
185
186
  <script src="/builders/hall-kit.js"></script>
186
187
  <script src="/builders/copy-desk.js?v=2026-09-01-redesign"></script>
@@ -354,6 +354,10 @@
354
354
  // the composer
355
355
  // ---------------------------------------------------------------------------
356
356
 
357
+ // The three dialogs are the hall's one dialog (hall-dialog.js, task 1005085):
358
+ // the shared Escape, focus trap, scrim click and focus return.
359
+ const dialog = (sel) => window.OTBDialog.bind($(sel));
360
+
357
361
  function openComposer(t) {
358
362
  target = t;
359
363
  $('#cd-dialog-target').innerHTML = t.string_id
@@ -361,7 +365,7 @@
361
365
  : `${pill('', 'whole surface')} <span class="ov-key ov-key--faint">${escapeHtml(t.surface)}</span>`;
362
366
  $('#cd-reason').value = '';
363
367
  $('#cd-dialog-err').textContent = '';
364
- $('#cd-dialog').showModal();
368
+ dialog('#cd-dialog').open();
365
369
  }
366
370
 
367
371
  async function submitFlag() {
@@ -387,7 +391,7 @@
387
391
  $('#cd-dialog-err').textContent = d.what_this_means || d.why || err.message || 'Could not file the flag.';
388
392
  return;
389
393
  }
390
- $('#cd-dialog').close();
394
+ dialog('#cd-dialog').close();
391
395
  toast('Flagged. It is in the queue.');
392
396
  await loadQueue();
393
397
  if ($('#cd-search').value.trim()) await runSearch();
@@ -423,7 +427,7 @@
423
427
  + `Keep all ${n} — move them if the new sentence needs them elsewhere, but do not remove one: `
424
428
  + 'the values come from the code, and dropping one is a code change.';
425
429
  }
426
- $('#cd-propose-dialog').showModal();
430
+ dialog('#cd-propose-dialog').open();
427
431
  }
428
432
 
429
433
  async function submitProposal() {
@@ -463,7 +467,7 @@
463
467
  err.textContent = d.what_this_means || d.why || e.message || 'Could not file the change.';
464
468
  return;
465
469
  }
466
- $('#cd-propose-dialog').close();
470
+ dialog('#cd-propose-dialog').close();
467
471
  const task = (res.data && res.data.task) || {};
468
472
  toast(res.data && res.data.created === false
469
473
  ? `Already filed — task #${task.id} carries this exact change.`
@@ -486,7 +490,7 @@
486
490
  $('#cd-close-note').value = '';
487
491
  $('#cd-close-task').value = '';
488
492
  $('#cd-close-err').textContent = '';
489
- $('#cd-close-dialog').showModal();
493
+ dialog('#cd-close-dialog').open();
490
494
  }
491
495
 
492
496
  async function submitClose() {
@@ -506,7 +510,7 @@
506
510
  $('#cd-close-err').textContent = d.what_this_means || d.why || err.message || 'Could not close the flag.';
507
511
  return;
508
512
  }
509
- $('#cd-close-dialog').close();
513
+ dialog('#cd-close-dialog').close();
510
514
  toast('Closed.');
511
515
  await loadQueue();
512
516
  }
@@ -560,11 +564,11 @@
560
564
  if (closeBtn) openCloser(closeBtn.dataset.flag);
561
565
  });
562
566
 
563
- $('#cd-cancel').addEventListener('click', () => $('#cd-dialog').close());
567
+ $('#cd-cancel').addEventListener('click', () => dialog('#cd-dialog').close());
564
568
  $('#cd-submit').addEventListener('click', submitFlag);
565
- $('#cd-propose-cancel').addEventListener('click', () => $('#cd-propose-dialog').close());
569
+ $('#cd-propose-cancel').addEventListener('click', () => dialog('#cd-propose-dialog').close());
566
570
  $('#cd-propose-submit').addEventListener('click', submitProposal);
567
- $('#cd-close-cancel').addEventListener('click', () => $('#cd-close-dialog').close());
571
+ $('#cd-close-cancel').addEventListener('click', () => dialog('#cd-close-dialog').close());
568
572
  $('#cd-close-submit').addEventListener('click', submitClose);
569
573
 
570
574
  (async () => {
@@ -82,6 +82,7 @@
82
82
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
83
83
  <script src="/builders/craft-mode.js?v=2026-11-05-goals"></script>
84
84
  <script src="/builders/shell.js?v=2026-11-07-brand-mark"></script>
85
+ <script src="/builders/hall-dialog.js"></script>
85
86
  <script src="/builders/palette.js" defer></script>
86
87
  <script src="/builders/hall-kit.js"></script>
87
88
  <!-- the shared ask card the move is drawn with (task 1004829) -->