@bongos/core 1.21.40 → 1.21.41

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 (72) hide show
  1. package/.bongos-core.json +128 -68
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +16 -0
  4. package/clients/bongos-client/index.cjs +16 -0
  5. package/clients/bongos-client/index.d.ts +25 -0
  6. package/clients/bongos-client/index.mjs +16 -0
  7. package/docs/api/openapi.json +492 -3
  8. package/docs/api-reference.md +13 -1
  9. package/docs/copy-inventory.md +29 -12
  10. package/docs/copy-registry.json +177 -14
  11. package/docs/file-map.md +1 -0
  12. package/docs/module-api-changelog.md +2 -0
  13. package/docs/page-readings.json +229 -229
  14. package/modules/hall-ui/public/atlas.html +3 -3
  15. package/modules/hall-ui/public/blockers.html +3 -3
  16. package/modules/hall-ui/public/board-room.html +3 -3
  17. package/modules/hall-ui/public/city.html +3 -3
  18. package/modules/hall-ui/public/collab.html +3 -3
  19. package/modules/hall-ui/public/commands.html +3 -3
  20. package/modules/hall-ui/public/copy-desk.html +3 -3
  21. package/modules/hall-ui/public/craft-mode.js +62 -0
  22. package/modules/hall-ui/public/deploy.html +3 -3
  23. package/modules/hall-ui/public/diagrams.html +3 -3
  24. package/modules/hall-ui/public/drachmae.html +3 -3
  25. package/modules/hall-ui/public/fleet.html +3 -3
  26. package/modules/hall-ui/public/gate.html +3 -3
  27. package/modules/hall-ui/public/goal-map.html +3 -3
  28. package/modules/hall-ui/public/goals.html +3 -3
  29. package/modules/hall-ui/public/government.html +3 -3
  30. package/modules/hall-ui/public/idea.html +3 -3
  31. package/modules/hall-ui/public/ideas.html +3 -3
  32. package/modules/hall-ui/public/index.html +3 -3
  33. package/modules/hall-ui/public/modules.html +3 -3
  34. package/modules/hall-ui/public/page-notes.js +320 -0
  35. package/modules/hall-ui/public/primer.html +3 -3
  36. package/modules/hall-ui/public/profile.html +3 -3
  37. package/modules/hall-ui/public/project-settings.html +3 -3
  38. package/modules/hall-ui/public/ranks.html +3 -3
  39. package/modules/hall-ui/public/roadmap.html +3 -3
  40. package/modules/hall-ui/public/roster.html +3 -3
  41. package/modules/hall-ui/public/sessions.html +3 -3
  42. package/modules/hall-ui/public/settings.html +3 -3
  43. package/modules/hall-ui/public/street.html +3 -3
  44. package/modules/hall-ui/public/studio.html +3 -3
  45. package/modules/hall-ui/public/style.css +144 -0
  46. package/modules/hall-ui/public/task.html +3 -3
  47. package/modules/hall-ui/public/thinking.html +3 -3
  48. package/modules/hall-ui/public/tweak-editor.html +3 -3
  49. package/modules/hall-ui/public/watch.html +3 -3
  50. package/modules/hall-ui/public/work.html +3 -3
  51. package/modules/hall-ui/records/page-notes.md +16 -0
  52. package/modules/page-notes/CLAUDE.md +15 -0
  53. package/modules/page-notes/db.js +147 -0
  54. package/modules/page-notes/migrations/page_notes_001_notes.sql +68 -0
  55. package/modules/page-notes/module.json +13 -0
  56. package/modules/page-notes/notes.js +174 -0
  57. package/modules/page-notes/routes/page-notes.js +172 -0
  58. package/modules/page-notes/split.js +68 -0
  59. package/modules/provisioning/starter-bundles.js +1 -0
  60. package/modules/ui-design/kit/serve.js +1 -1
  61. package/package-lock.json +2 -2
  62. package/package.json +1 -1
  63. package/release-notes.json +6 -0
  64. package/scripts/gds/publish-manifest.js +1 -0
  65. package/src/bongos/module-scope-map.js +4 -0
  66. package/src/bongos/routes.js +4 -1
  67. package/src/module-api.js +1 -1
  68. package/tests/hall_tweak_editor.mjs +1 -1
  69. package/tests/module_loader.mjs +1 -1
  70. package/tests/page_notes.mjs +289 -0
  71. package/tests/page_notes_db.mjs +140 -0
  72. package/tests/page_notes_panel.mjs +145 -0
@@ -0,0 +1,320 @@
1
+ // page-notes.js — the Artistry palette's feedback doc (task 1004496, goal
2
+ // 1000095 WA6). Loaded on demand by craft-mode.js: when the palette button is
3
+ // pressed (open) or a page arrives with ?note=N (openNote). Never on page load.
4
+ //
5
+ // A side sheet over the page: write a doc about THIS page (as long as it needs
6
+ // to be), add reference images, and "Turn into ideas" — the server's AI split
7
+ // files each point as its own idea under your name, and the doc stays as the
8
+ // page's reference. Below the writing area, the page's earlier docs; each opens
9
+ // in place. The draft saves as you type, so closing loses nothing.
10
+ //
11
+ // The walls are the server's (modules/page-notes): a draft is its author's, a
12
+ // submitted doc is any signed-in builder's. This file only draws.
13
+ (function () {
14
+ 'use strict';
15
+
16
+ var API = '/api/bongos/page-notes';
17
+ var IMAGE_RE = /!\[([^\]\n]{0,120})\]\(\/api\/bongos\/page-notes\/images\/(\d{1,18})\)/g;
18
+ var MAX_IMAGE_BYTES = 4 * 1024 * 1024;
19
+ var MAX_IMAGES = 8;
20
+ var TYPES = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
21
+
22
+ var sheet = null, returnFocus = null, state = null, saveTimer = null, pollTimer = null;
23
+
24
+ function esc(s) {
25
+ return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
26
+ return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
27
+ });
28
+ }
29
+ function page() {
30
+ var b = document.body || { dataset: {} };
31
+ var slug = String((b.dataset && b.dataset.page) || 'home').toLowerCase().replace(/[^a-z0-9_-]/g, '-').slice(0, 64) || 'home';
32
+ var title = (b.dataset && b.dataset.pageTitle) || String(document.title || '').split(/\s[—-]\s/)[0] || slug;
33
+ var path = String((window.location && window.location.pathname) || '/').replace(/[^A-Za-z0-9/_.-]/g, '').slice(0, 200) || '/';
34
+ return { id: 'builders:' + slug, path: path, title: title.slice(0, 120) };
35
+ }
36
+ function api(method, path, body) {
37
+ var opts = { method: method, credentials: 'same-origin', headers: {} };
38
+ if (body !== undefined) { opts.headers['content-type'] = 'application/json'; opts.body = JSON.stringify(body); }
39
+ return fetch(API + path, opts).then(function (r) {
40
+ return r.json().catch(function () { return {}; }).then(function (j) {
41
+ if (!r.ok) {
42
+ var e = new Error((j && j.error && j.error.message) || (r.status === 401 ? 'Sign in to write page feedback.' : 'That did not work. Try again in a moment.'));
43
+ e.status = r.status;
44
+ throw e;
45
+ }
46
+ return j;
47
+ });
48
+ });
49
+ }
50
+ function day(iso) {
51
+ var d = iso ? new Date(iso) : null;
52
+ return d && !isNaN(d) ? d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) : '';
53
+ }
54
+ function plural(n, one, many) { return n + ' ' + (n === 1 ? one : many); }
55
+
56
+ // The doc, read: headings, lists, paragraphs, and only this module's own images.
57
+ function renderBody(md) {
58
+ var out = [], para = [], list = [];
59
+ function flushPara() { if (para.length) { out.push('<p>' + para.join('<br>') + '</p>'); para = []; } }
60
+ function flushList() { if (list.length) { out.push('<ul>' + list.map(function (x) { return '<li>' + x + '</li>'; }).join('') + '</ul>'); list = []; } }
61
+ function inline(line) {
62
+ return esc(line).replace(IMAGE_RE, function (_m, alt, id) {
63
+ return '<img class="pn-doc__img" src="' + API + '/images/' + id + '" alt="' + alt + '" loading="lazy">';
64
+ });
65
+ }
66
+ String(md || '').split('\n').forEach(function (raw) {
67
+ var line = raw.replace(/\s+$/, '');
68
+ var h = /^(#{1,3})\s+(.*)$/.exec(line);
69
+ var li = /^\s*[-*]\s+(.*)$/.exec(line);
70
+ if (!line.trim()) { flushPara(); flushList(); return; }
71
+ if (h) { flushPara(); flushList(); out.push('<h3 class="pn-doc__h">' + inline(h[2]) + '</h3>'); return; }
72
+ if (li) { flushPara(); list.push(inline(li[1])); return; }
73
+ flushList();
74
+ para.push(inline(line));
75
+ });
76
+ flushPara(); flushList();
77
+ return out.join('');
78
+ }
79
+
80
+ // ---------- the sheet ----------
81
+ function shellHtml(p) {
82
+ return '<div class="pn__panel" role="dialog" aria-modal="true" aria-labelledby="pn-title">' +
83
+ '<div class="pn__head">' +
84
+ '<p class="pn__eyebrow">Page feedback · ' + esc(p.title) + '</p>' +
85
+ '<h2 class="pn__title" id="pn-title">What should this page be?</h2>' +
86
+ '<button class="pn__close" type="button" data-pn-close aria-label="Close">' +
87
+ '<svg viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M5 5l8 8M13 5l-8 8"/></svg></button>' +
88
+ '</div>' +
89
+ '<div class="pn__body" id="pn-body"><p class="pn__quiet">Opening your doc…</p></div>' +
90
+ '</div>';
91
+ }
92
+ function writeHtml() {
93
+ var n = state.note, imgs = state.images;
94
+ return '<label class="pn__label" for="pn-text">Your feedback</label>' +
95
+ '<textarea class="pn__text" id="pn-text" rows="14" placeholder="Write what you see on this page and what it should be. Long is fine: each separate point becomes its own idea.">' + esc(n.body_md) + '</textarea>' +
96
+ '<div class="pn__tools">' +
97
+ '<label class="pn__add"><input type="file" id="pn-file" accept="' + TYPES.join(',') + '" multiple>Add images</label>' +
98
+ '<span class="pn__quiet" id="pn-count">' + imgs.length + ' of ' + MAX_IMAGES + ' images</span>' +
99
+ '<span class="pn__quiet pn__saved" id="pn-saved" aria-live="polite"></span>' +
100
+ '</div>' +
101
+ (imgs.length ? '<div class="pn__thumbs">' + imgs.map(function (i) {
102
+ return '<img class="pn__thumb" src="' + esc(i.url) + '" alt="' + esc(i.name || 'reference image') + '">';
103
+ }).join('') + '</div>' : '') +
104
+ '<p class="pn__error" id="pn-error" role="alert"></p>' +
105
+ '<div class="pn__foot">' +
106
+ '<p class="pn__hint">When you submit, an AI splits this into separate ideas, filed under your name. The doc stays here as this page\'s reference.</p>' +
107
+ '<button class="pn__submit" type="button" data-pn-submit>Turn into ideas</button>' +
108
+ '</div>' +
109
+ listHtml();
110
+ }
111
+ function listHtml() {
112
+ var rows = state.list || [];
113
+ return '<section class="pn__list" aria-labelledby="pn-list-h"><h3 class="pn__list-h" id="pn-list-h">This page\'s reference docs</h3>' +
114
+ (rows.length ? '<ul class="pn__rows">' + rows.map(function (r) {
115
+ return '<li><button class="pn__row" type="button" data-pn-note="' + esc(r.id) + '">' +
116
+ '<span class="pn__row-meta">' + esc(r.author_name) + ' · ' + esc(day(r.submitted_at)) + ' · ' +
117
+ (r.status === 'filed' ? esc(plural(r.idea_count, 'idea', 'ideas')) : 'splitting…') + '</span>' +
118
+ '<span class="pn__row-text">' + esc(String(r.excerpt || '').replace(IMAGE_RE, '[image]').replace(/\s+/g, ' ').slice(0, 160)) + '</span>' +
119
+ '</button></li>';
120
+ }).join('') + '</ul>' : '<p class="pn__quiet">None yet. Yours will be the first.</p>') +
121
+ '</section>';
122
+ }
123
+ function ideasHtml(ideas) {
124
+ if (!ideas || !ideas.length) return '';
125
+ return '<ul class="pn__ideas">' + ideas.map(function (i) {
126
+ return '<li><a href="/#/idea/' + esc(i.id) + '">' + esc(i.title) + '</a></li>';
127
+ }).join('') + '</ul>';
128
+ }
129
+ function readHtml(doc) {
130
+ var n = doc.note;
131
+ var filed = n.status === 'filed';
132
+ return '<button class="pn__back" type="button" data-pn-back>← ' + (state && state.note ? 'Back to your doc' : 'This page\'s docs') + '</button>' +
133
+ '<p class="pn__row-meta">' + esc(n.author_name) + ' · ' + esc(day(n.submitted_at)) + ' · ' + esc(n.page_title || n.page_id) + '</p>' +
134
+ '<div class="pn-doc">' + renderBody(n.body_md) + '</div>' +
135
+ (filed
136
+ ? '<h3 class="pn__list-h">It became ' + esc(plural(doc.ideas.length, 'idea', 'ideas')) + '</h3>' + ideasHtml(doc.ideas)
137
+ : '<p class="pn__quiet">Being split into ideas…</p>');
138
+ }
139
+ function doneHtml(doc) {
140
+ return '<p class="pn__done">Filed as ' + esc(plural(doc.ideas.length, 'idea', 'ideas')) + ' under your name.</p>' +
141
+ ideasHtml(doc.ideas) +
142
+ '<p class="pn__hint">The doc is now this page\'s reference. Every idea links back to it.</p>' +
143
+ '<button class="pn__back" type="button" data-pn-new>Write another</button>';
144
+ }
145
+
146
+ function body() { return sheet && sheet.querySelector('#pn-body'); }
147
+ function setError(msg) { var e = sheet && sheet.querySelector('#pn-error'); if (e) e.textContent = msg || ''; }
148
+ function setSaved(msg) { var e = sheet && sheet.querySelector('#pn-saved'); if (e) e.textContent = msg || ''; }
149
+
150
+ function setTitle(text) { var h = sheet && sheet.querySelector('#pn-title'); if (h) h.textContent = text; }
151
+ function showWrite() {
152
+ var b = body();
153
+ if (!b) return;
154
+ setTitle('What should this page be?');
155
+ b.innerHTML = writeHtml();
156
+ var t = b.querySelector('#pn-text');
157
+ if (t) { t.addEventListener('input', queueSave); t.focus(); }
158
+ var f = b.querySelector('#pn-file');
159
+ if (f) f.addEventListener('change', function () { addImages(Array.prototype.slice.call(f.files || [])); f.value = ''; });
160
+ }
161
+ function currentText() { var t = sheet && sheet.querySelector('#pn-text'); return t ? t.value : (state && state.note ? state.note.body_md : ''); }
162
+ function queueSave() {
163
+ setSaved('');
164
+ if (saveTimer) clearTimeout(saveTimer);
165
+ saveTimer = setTimeout(save, 700);
166
+ }
167
+ function save() {
168
+ if (saveTimer) { clearTimeout(saveTimer); saveTimer = null; }
169
+ if (!state || !state.note || state.note.status !== 'draft') return Promise.resolve();
170
+ var text = currentText();
171
+ if (text === state.note.body_md) return Promise.resolve();
172
+ return api('PATCH', '/' + state.note.id, { body_md: text }).then(function () {
173
+ state.note.body_md = text;
174
+ setSaved('Saved');
175
+ }, function (e) { setError(e.message); });
176
+ }
177
+ function readFile(file) {
178
+ return new Promise(function (resolve, reject) {
179
+ var r = new FileReader();
180
+ r.onload = function () { resolve(String(r.result || '')); };
181
+ r.onerror = function () { reject(new Error('That file could not be read.')); };
182
+ r.readAsDataURL(file);
183
+ });
184
+ }
185
+ function addImages(files) {
186
+ setError('');
187
+ var chain = Promise.resolve();
188
+ files.forEach(function (file) {
189
+ chain = chain.then(function () {
190
+ if (TYPES.indexOf(file.type) === -1) throw new Error('Images only: PNG, JPG, GIF or WebP.');
191
+ if (file.size > MAX_IMAGE_BYTES) throw new Error('Each image can be up to 4 MB.');
192
+ if (state.images.length >= MAX_IMAGES) throw new Error('A doc can carry up to ' + MAX_IMAGES + ' images.');
193
+ return readFile(file).then(function (data) {
194
+ return api('POST', '/' + state.note.id + '/images', { content_type: file.type, data: data, name: String(file.name || '').slice(0, 120) });
195
+ }).then(function (j) {
196
+ state.images.push(j.image);
197
+ var name = String(j.image.name || 'image').replace(/[\]\[()]/g, '');
198
+ var t = sheet.querySelector('#pn-text');
199
+ var text = t ? t.value : '';
200
+ var at = t && typeof t.selectionStart === 'number' ? t.selectionStart : text.length;
201
+ var md = (at && text[at - 1] !== '\n' ? '\n' : '') + '![' + name + '](' + j.image.url + ')\n';
202
+ state.note.body_md = text;
203
+ var next = text.slice(0, at) + md + text.slice(at);
204
+ return api('PATCH', '/' + state.note.id, { body_md: next }).then(function () {
205
+ state.note.body_md = next;
206
+ showWrite();
207
+ setSaved('Saved');
208
+ });
209
+ });
210
+ });
211
+ });
212
+ chain.catch(function (e) { setError(e.message); });
213
+ }
214
+ function submit() {
215
+ setError('');
216
+ var btn = sheet.querySelector('[data-pn-submit]');
217
+ if (btn) btn.disabled = true;
218
+ save().then(function () {
219
+ return api('POST', '/' + state.note.id + '/submit');
220
+ }).then(function () {
221
+ var b = body();
222
+ if (b) b.innerHTML = '<p class="pn__done" aria-live="polite">Splitting your feedback into ideas…</p>';
223
+ poll(state.note.id, 0);
224
+ }, function (e) { if (btn) btn.disabled = false; setError(e.message); });
225
+ }
226
+ function poll(id, tries) {
227
+ pollTimer = setTimeout(function () {
228
+ api('GET', '/' + id).then(function (doc) {
229
+ if (!sheet) return;
230
+ if (doc.note.status === 'filed') { state.note = null; body().innerHTML = doneHtml(doc); refreshList(); return; }
231
+ if (tries < 60) poll(id, tries + 1);
232
+ else body().innerHTML = '<p class="pn__quiet">Still splitting. It will appear in this page\'s docs when it is done.</p>';
233
+ }, function () { if (tries < 60) poll(id, tries + 1); });
234
+ }, tries ? 2000 : 1200);
235
+ }
236
+ function refreshList() {
237
+ return api('GET', '?page_id=' + encodeURIComponent(page().id)).then(function (j) { if (state) state.list = j.notes || []; }, function () { /* the list is a convenience */ });
238
+ }
239
+ function openDraft() {
240
+ var p = page();
241
+ return Promise.all([
242
+ api('POST', '', { page_id: p.id, page_path: p.path, page_title: p.title }),
243
+ refreshList(),
244
+ ]).then(function (r) {
245
+ state.note = r[0].note;
246
+ state.images = r[0].images || [];
247
+ showWrite();
248
+ }, function (e) {
249
+ var b = body();
250
+ if (b) b.innerHTML = '<p class="pn__error" role="alert">' + esc(e.message) + '</p>';
251
+ });
252
+ }
253
+ function showNote(id) {
254
+ var b = body();
255
+ setTitle('Feedback on this page');
256
+ if (b) b.innerHTML = '<p class="pn__quiet">Opening the doc…</p>';
257
+ return save().then(function () { return api('GET', '/' + id); }).then(function (doc) {
258
+ if (b) { b.innerHTML = readHtml(doc); var back = b.querySelector('[data-pn-back]'); if (back) back.focus(); }
259
+ }, function (e) {
260
+ if (b) b.innerHTML = '<p class="pn__error" role="alert">' + esc(e.message) + '</p><button class="pn__back" type="button" data-pn-back>← Back</button>';
261
+ });
262
+ }
263
+
264
+ function onClick(e) {
265
+ if (e.target === sheet) { close(); return; }
266
+ var btn = e.target.closest && e.target.closest('button');
267
+ if (!btn) return;
268
+ if (btn.hasAttribute('data-pn-close')) close();
269
+ else if (btn.hasAttribute('data-pn-submit')) submit();
270
+ else if (btn.hasAttribute('data-pn-note')) showNote(btn.getAttribute('data-pn-note'));
271
+ else if (btn.hasAttribute('data-pn-back') || btn.hasAttribute('data-pn-new')) {
272
+ if (state.note && !btn.hasAttribute('data-pn-new')) showWrite();
273
+ else { body().innerHTML = '<p class="pn__quiet">Opening your doc…</p>'; openDraft(); }
274
+ }
275
+ }
276
+ function onKey(e) {
277
+ if (!sheet) return;
278
+ if (e.key === 'Escape') { e.preventDefault(); close(); return; }
279
+ if (e.key !== 'Tab') return;
280
+ var f = Array.prototype.slice.call(sheet.querySelectorAll('button, textarea, input, a[href]')).filter(function (x) { return !x.disabled; });
281
+ if (!f.length) return;
282
+ if (e.shiftKey && document.activeElement === f[0]) { e.preventDefault(); f[f.length - 1].focus(); }
283
+ else if (!e.shiftKey && document.activeElement === f[f.length - 1]) { e.preventDefault(); f[0].focus(); }
284
+ }
285
+ function mountSheet() {
286
+ if (sheet) return false;
287
+ returnFocus = document.activeElement;
288
+ state = { note: null, images: [], list: [] };
289
+ sheet = document.createElement('div');
290
+ sheet.className = 'pn';
291
+ sheet.id = 'page-notes';
292
+ sheet.innerHTML = shellHtml(page());
293
+ sheet.addEventListener('click', onClick);
294
+ document.addEventListener('keydown', onKey, true);
295
+ document.body.appendChild(sheet);
296
+ document.documentElement.classList.add('pn-open');
297
+ var c = sheet.querySelector('[data-pn-close]');
298
+ if (c) c.focus();
299
+ return true;
300
+ }
301
+ function close() {
302
+ if (!sheet) return;
303
+ save();
304
+ if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; }
305
+ document.removeEventListener('keydown', onKey, true);
306
+ if (sheet.parentNode) sheet.parentNode.removeChild(sheet);
307
+ sheet = null;
308
+ document.documentElement.classList.remove('pn-open');
309
+ if (returnFocus && returnFocus.focus) { try { returnFocus.focus(); } catch (_) { /* gone */ } }
310
+ returnFocus = null;
311
+ }
312
+
313
+ window.OTBPageNotes = {
314
+ open: function () { if (mountSheet()) openDraft(); },
315
+ openNote: function (id) { if (mountSheet()) refreshList().then(function () { return showNote(id); }); },
316
+ close: close,
317
+ renderBody: renderBody,
318
+ page: page,
319
+ };
320
+ })();
@@ -115,9 +115,9 @@
115
115
 
116
116
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
117
117
 
118
- <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
119
- <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
120
- <script src="/builders/shell.js?v=2026-10-03-looks"></script>
118
+ <script src="/builders/shell-trail.js?v=2026-10-04-palette"></script>
119
+ <script src="/builders/craft-mode.js?v=2026-10-04-palette"></script>
120
+ <script src="/builders/shell.js?v=2026-10-04-palette"></script>
121
121
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
122
122
  <script src="/builders/palette.js" defer></script>
123
123
  <!-- the shared markdown reader (window.OTBMdReader) — primer.js renders through
@@ -206,9 +206,9 @@
206
206
 
207
207
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
208
208
  chrome exists before the page script runs. -->
209
- <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
210
- <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
211
- <script src="/builders/shell.js?v=2026-10-03-looks"></script>
209
+ <script src="/builders/shell-trail.js?v=2026-10-04-palette"></script>
210
+ <script src="/builders/craft-mode.js?v=2026-10-04-palette"></script>
211
+ <script src="/builders/shell.js?v=2026-10-04-palette"></script>
212
212
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
213
213
  <script src="/builders/palette.js" defer></script>
214
214
  <!-- shared frontend toolkit (window.OTB) — loaded before profile.js, which pulls from it. -->
@@ -58,9 +58,9 @@
58
58
 
59
59
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
60
60
 
61
- <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
62
- <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
63
- <script src="/builders/shell.js?v=2026-10-03-looks"></script>
61
+ <script src="/builders/shell-trail.js?v=2026-10-04-palette"></script>
62
+ <script src="/builders/craft-mode.js?v=2026-10-04-palette"></script>
63
+ <script src="/builders/shell.js?v=2026-10-04-palette"></script>
64
64
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
65
65
  <script src="/builders/palette.js" defer></script>
66
66
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
@@ -77,9 +77,9 @@
77
77
 
78
78
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
79
79
  chrome exists before the page script runs. -->
80
- <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
81
- <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
82
- <script src="/builders/shell.js?v=2026-10-03-looks"></script>
80
+ <script src="/builders/shell-trail.js?v=2026-10-04-palette"></script>
81
+ <script src="/builders/craft-mode.js?v=2026-10-04-palette"></script>
82
+ <script src="/builders/shell.js?v=2026-10-04-palette"></script>
83
83
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
84
84
  <script src="/builders/palette.js" defer></script>
85
85
  <!-- shared frontend toolkit (window.OTB) — loaded before ranks.js, which pulls from it. -->
@@ -41,9 +41,9 @@
41
41
 
42
42
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
43
43
 
44
- <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
45
- <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
46
- <script src="/builders/shell.js?v=2026-10-03-looks"></script>
44
+ <script src="/builders/shell-trail.js?v=2026-10-04-palette"></script>
45
+ <script src="/builders/craft-mode.js?v=2026-10-04-palette"></script>
46
+ <script src="/builders/shell.js?v=2026-10-04-palette"></script>
47
47
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
48
48
  <script src="/builders/palette.js" defer></script>
49
49
  <script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
@@ -53,9 +53,9 @@
53
53
 
54
54
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
55
55
  chrome exists before the page script runs. -->
56
- <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
57
- <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
58
- <script src="/builders/shell.js?v=2026-10-03-looks"></script>
56
+ <script src="/builders/shell-trail.js?v=2026-10-04-palette"></script>
57
+ <script src="/builders/craft-mode.js?v=2026-10-04-palette"></script>
58
+ <script src="/builders/shell.js?v=2026-10-04-palette"></script>
59
59
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
60
60
  <script src="/builders/palette.js" defer></script>
61
61
  <!-- shared frontend toolkit (window.OTB — rank labels, number formats) + the
@@ -111,9 +111,9 @@
111
111
 
112
112
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
113
113
  chrome exists before the page script runs. -->
114
- <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
115
- <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
116
- <script src="/builders/shell.js?v=2026-10-03-looks"></script>
114
+ <script src="/builders/shell-trail.js?v=2026-10-04-palette"></script>
115
+ <script src="/builders/craft-mode.js?v=2026-10-04-palette"></script>
116
+ <script src="/builders/shell.js?v=2026-10-04-palette"></script>
117
117
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
118
118
  <script src="/builders/palette.js" defer></script>
119
119
  <!-- shared frontend toolkit (window.OTB) — loaded BEFORE sessions.js so it can pull from it. -->
@@ -435,9 +435,9 @@
435
435
 
436
436
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
437
437
  chrome exists before the page scripts run. -->
438
- <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
439
- <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
440
- <script src="/builders/shell.js?v=2026-10-03-looks"></script>
438
+ <script src="/builders/shell-trail.js?v=2026-10-04-palette"></script>
439
+ <script src="/builders/craft-mode.js?v=2026-10-04-palette"></script>
440
+ <script src="/builders/shell.js?v=2026-10-04-palette"></script>
441
441
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
442
442
  <script src="/builders/palette.js" defer></script>
443
443
  <!-- shared frontend toolkit (window.OTB) — loaded before consuming IIFEs. -->
@@ -203,9 +203,9 @@
203
203
  </div>
204
204
  </div>
205
205
 
206
- <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
207
- <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
208
- <script src="/builders/shell.js?v=2026-10-03-looks"></script>
206
+ <script src="/builders/shell-trail.js?v=2026-10-04-palette"></script>
207
+ <script src="/builders/craft-mode.js?v=2026-10-04-palette"></script>
208
+ <script src="/builders/shell.js?v=2026-10-04-palette"></script>
209
209
  <script src="/builders/palette.js" defer></script>
210
210
  <script src="/builders/city-draw.js?v=2026-10-02-street1"></script>
211
211
  <script src="/builders/street-draw.js?v=2026-10-02-street1"></script>
@@ -176,9 +176,9 @@
176
176
 
177
177
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
178
178
 
179
- <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
180
- <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
181
- <script src="/builders/shell.js?v=2026-10-03-looks"></script>
179
+ <script src="/builders/shell-trail.js?v=2026-10-04-palette"></script>
180
+ <script src="/builders/craft-mode.js?v=2026-10-04-palette"></script>
181
+ <script src="/builders/shell.js?v=2026-10-04-palette"></script>
182
182
  <script src="/builders/palette.js" defer></script>
183
183
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
184
184
  <!-- The kit, for its ONE renderer: the full-size ship-time visual figure
@@ -1550,6 +1550,150 @@ html[data-theme="light"][data-craft="ideating"] .app-sidebar {
1550
1550
  radial-gradient(1.5px 1.5px at 170px 260px, rgba(184, 137, 28, .25), transparent);
1551
1551
  }
1552
1552
 
1553
+ /* THE MODE TOOLS (task 1004496, craft-mode.js TOOLS): a round button beside the
1554
+ switch, in the mode's colour, shown only in its own mode. Artistry's palette
1555
+ opens the page feedback doc (page-notes.js). */
1556
+ .craft-tool {
1557
+ display: inline-flex;
1558
+ align-items: center;
1559
+ justify-content: center;
1560
+ width: 32px; height: 32px;
1561
+ padding: 0;
1562
+ border: 0;
1563
+ border-radius: var(--radius-pill);
1564
+ background: none;
1565
+ color: var(--craft, var(--ink-soft));
1566
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--craft, var(--ink)) 40%, transparent);
1567
+ cursor: pointer;
1568
+ flex-shrink: 0;
1569
+ }
1570
+ .craft-tool:hover { background: color-mix(in srgb, var(--craft, var(--ink)) 12%, transparent); }
1571
+ .craft-tool:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
1572
+ .craft-tool[aria-disabled="true"] { opacity: .45; cursor: default; }
1573
+ .craft-tool__icon { width: 16px; height: 16px; flex-shrink: 0; }
1574
+ /* With room, the tool says what it does, so it cannot be read as the mode's own
1575
+ pill (same palette glyph); under 1180px it folds to the icon, as the switch does. */
1576
+ .craft-tool__label { font-size: 13px; white-space: nowrap; }
1577
+ @media (min-width: 1181px) {
1578
+ .craft-tool { width: auto; gap: 6px; padding: 0 12px 0 10px; }
1579
+ }
1580
+ @media (max-width: 1180px) { .craft-tool__label { display: none; } }
1581
+
1582
+ /* The page feedback doc: a sheet from the right over a scrim. */
1583
+ html.pn-open { overflow: hidden; }
1584
+ .pn {
1585
+ position: fixed;
1586
+ inset: 0;
1587
+ z-index: var(--z-modal);
1588
+ display: flex;
1589
+ justify-content: flex-end;
1590
+ background: var(--scrim);
1591
+ }
1592
+ .pn__panel {
1593
+ position: relative;
1594
+ width: min(560px, 100%);
1595
+ height: 100%;
1596
+ box-sizing: border-box;
1597
+ display: flex;
1598
+ flex-direction: column;
1599
+ background: var(--bg-card);
1600
+ color: var(--ink);
1601
+ box-shadow: 0 0 0 1px var(--glass-edge), var(--shadow-pop);
1602
+ }
1603
+ .pn__head { position: relative; padding: 22px 64px 14px 24px; border-bottom: 1px solid var(--rule-soft); }
1604
+ .pn__eyebrow {
1605
+ margin: 0 0 6px;
1606
+ font-family: var(--label-face);
1607
+ font-size: 11px;
1608
+ letter-spacing: .12em;
1609
+ text-transform: uppercase;
1610
+ color: var(--craft, var(--ink-faint));
1611
+ }
1612
+ .pn__title { margin: 0; font-family: var(--font-display); font-weight: 400; font-size: 22px; letter-spacing: -0.02em; }
1613
+ .pn__close {
1614
+ position: absolute; top: 14px; right: 14px;
1615
+ display: inline-flex; align-items: center; justify-content: center;
1616
+ width: 40px; height: 40px; padding: 0; border: 0; border-radius: var(--radius-pill);
1617
+ background: none; color: var(--ink-soft); cursor: pointer;
1618
+ }
1619
+ .pn__close svg { width: 18px; height: 18px; }
1620
+ .pn__close:hover { color: var(--ink); background: var(--glass); }
1621
+ .pn__body { flex: 1; min-height: 0; overflow-y: auto; padding: 18px 24px 28px; display: flex; flex-direction: column; gap: 12px; }
1622
+ .pn__label { font-size: 13px; color: var(--ink-soft); }
1623
+ .pn__text {
1624
+ width: 100%;
1625
+ min-height: 260px;
1626
+ box-sizing: border-box;
1627
+ padding: 14px 16px;
1628
+ border: 1px solid var(--rule);
1629
+ border-radius: var(--radius-sm);
1630
+ background: var(--bg);
1631
+ color: var(--ink);
1632
+ font: inherit;
1633
+ font-size: 15px;
1634
+ line-height: 1.55;
1635
+ resize: vertical;
1636
+ }
1637
+ .pn__text:focus { outline: 2px solid var(--focus); outline-offset: 1px; }
1638
+ .pn__tools { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; }
1639
+ .pn__add {
1640
+ position: relative;
1641
+ display: inline-flex; align-items: center;
1642
+ min-height: 34px; padding: 0 14px;
1643
+ border-radius: var(--radius-pill);
1644
+ box-shadow: inset 0 0 0 1px var(--rule);
1645
+ font-size: 13.5px; color: var(--ink); cursor: pointer;
1646
+ }
1647
+ .pn__add:hover { background: var(--glass); }
1648
+ .pn__add:focus-within { outline: 2px solid var(--focus); outline-offset: 2px; }
1649
+ .pn__add input { position: absolute; inset: 0; opacity: 0; cursor: pointer; width: 100%; }
1650
+ .pn__quiet { margin: 0; font-size: 13px; color: var(--ink-faint); }
1651
+ .pn__saved { margin-left: auto; }
1652
+ .pn__thumbs { display: grid; grid-template-columns: repeat(auto-fill, minmax(88px, 1fr)); gap: 8px; }
1653
+ .pn__thumb { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; border-radius: 8px; background: var(--bg-deep); }
1654
+ .pn__error { margin: 0; font-size: 13.5px; color: var(--danger); }
1655
+ .pn__error:empty { display: none; }
1656
+ .pn__foot { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 16px; padding-top: 4px; }
1657
+ .pn__hint { margin: 0; font-size: 13px; line-height: 1.45; color: var(--ink-soft); }
1658
+ .pn__foot .pn__hint { flex: 1 1 240px; }
1659
+ .pn__submit {
1660
+ min-height: 40px; padding: 0 20px; border: 0; border-radius: var(--radius-pill);
1661
+ background: var(--accent); color: var(--accent-on); font: inherit; font-weight: 500; cursor: pointer;
1662
+ }
1663
+ .pn__submit:hover { background: var(--accent-lift); }
1664
+ .pn__submit:disabled { opacity: .55; cursor: default; }
1665
+ .pn__submit:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
1666
+ .pn__list { margin-top: 14px; padding-top: 16px; border-top: 1px solid var(--rule-soft); }
1667
+ .pn__list-h {
1668
+ margin: 0 0 8px;
1669
+ font-family: var(--label-face); font-size: var(--label-size); letter-spacing: var(--label-space);
1670
+ text-transform: var(--label-case); color: var(--ink-faint); font-weight: 400;
1671
+ }
1672
+ .pn__rows, .pn__ideas { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
1673
+ .pn__row {
1674
+ display: grid; gap: 2px; width: 100%; padding: 10px 12px; border: 0; border-radius: var(--radius-sm);
1675
+ background: none; color: var(--ink); font: inherit; text-align: left; cursor: pointer;
1676
+ }
1677
+ .pn__row:hover { background: var(--glass); }
1678
+ .pn__row:focus-visible, .pn__back:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
1679
+ .pn__row-meta { font-size: 12px; color: var(--ink-faint); }
1680
+ .pn__row-text { font-size: 14px; color: var(--ink-soft); }
1681
+ .pn__ideas a { display: inline-block; padding: 6px 0; color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
1682
+ .pn__back {
1683
+ align-self: flex-start; min-height: 32px; padding: 0 12px; margin-left: -12px; border: 0;
1684
+ border-radius: var(--radius-pill); background: none; color: var(--ink-soft); font: inherit; font-size: 13.5px; cursor: pointer;
1685
+ }
1686
+ .pn__back:hover { color: var(--ink); background: var(--glass); }
1687
+ .pn__done { margin: 0; font-size: 16px; }
1688
+ .pn-doc { font-size: 15px; line-height: 1.6; overflow-wrap: anywhere; }
1689
+ .pn-doc p, .pn-doc ul { margin: 0 0 12px; }
1690
+ .pn-doc__h { margin: 16px 0 6px; font-size: 16px; font-weight: 500; }
1691
+ .pn-doc__img { display: block; max-width: 100%; height: auto; margin: 8px 0; border-radius: 8px; }
1692
+ @media (max-width: 640px) {
1693
+ .pn__head { padding: 18px 60px 12px 16px; }
1694
+ .pn__body { padding: 14px 16px 24px; }
1695
+ }
1696
+
1553
1697
  /* THE BACK LINK (task 1004430): at the head of the bar when the page was
1554
1698
  reached from inside another hall page. 24px floor on both axes. */
1555
1699
  .topbar-back {
@@ -54,9 +54,9 @@
54
54
 
55
55
  <!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
56
56
  time so the chrome exists before the deferred page scripts execute. -->
57
- <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
58
- <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
59
- <script src="/builders/shell.js?v=2026-10-03-looks"></script>
57
+ <script src="/builders/shell-trail.js?v=2026-10-04-palette"></script>
58
+ <script src="/builders/craft-mode.js?v=2026-10-04-palette"></script>
59
+ <script src="/builders/shell.js?v=2026-10-04-palette"></script>
60
60
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
61
61
  <script src="/builders/palette.js" defer></script>
62
62
  <!-- shared frontend toolkit (window.OTB) — loaded FIRST so task.js can pull from it. -->
@@ -138,9 +138,9 @@
138
138
  </div>
139
139
  </div>
140
140
 
141
- <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
142
- <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
143
- <script src="/builders/shell.js?v=2026-10-03-looks"></script>
141
+ <script src="/builders/shell-trail.js?v=2026-10-04-palette"></script>
142
+ <script src="/builders/craft-mode.js?v=2026-10-04-palette"></script>
143
+ <script src="/builders/shell.js?v=2026-10-04-palette"></script>
144
144
  <script src="/builders/palette.js" defer></script>
145
145
 
146
146
  <script src="/builders/idea-objects.js?v=2026-09-27-split"></script>