@bongos/core 1.24.10 → 1.24.12

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 (96) hide show
  1. package/.bongos-core.json +171 -86
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +12 -0
  4. package/clients/bongos-client/index.cjs +12 -0
  5. package/clients/bongos-client/index.d.ts +18 -1
  6. package/clients/bongos-client/index.mjs +12 -0
  7. package/docs/api/openapi.json +304 -5
  8. package/docs/api-reference.md +14 -4
  9. package/docs/architecture.md +4 -0
  10. package/docs/copy-inventory.md +154 -76
  11. package/docs/copy-registry.json +923 -125
  12. package/docs/module-api-changelog.md +4 -0
  13. package/docs/page-inventory.json +68 -4
  14. package/docs/page-readings.json +873 -819
  15. package/modules/government/decision-rules.js +1 -0
  16. package/modules/hall-ui/public/ai-studio.html +1 -1
  17. package/modules/hall-ui/public/atlas.html +1 -1
  18. package/modules/hall-ui/public/blockers.html +1 -1
  19. package/modules/hall-ui/public/board-room.html +1 -1
  20. package/modules/hall-ui/public/city.html +1 -1
  21. package/modules/hall-ui/public/collab.html +1 -1
  22. package/modules/hall-ui/public/commands.html +1 -1
  23. package/modules/hall-ui/public/copy-desk.html +1 -1
  24. package/modules/hall-ui/public/craft-mode.js +2 -2
  25. package/modules/hall-ui/public/deploy.html +1 -1
  26. package/modules/hall-ui/public/diagrams.html +1 -1
  27. package/modules/hall-ui/public/drachmae.html +1 -1
  28. package/modules/hall-ui/public/fleet.html +1 -1
  29. package/modules/hall-ui/public/gate.html +1 -1
  30. package/modules/hall-ui/public/goal-map.html +1 -1
  31. package/modules/hall-ui/public/goals.html +1 -1
  32. package/modules/hall-ui/public/government.html +1 -1
  33. package/modules/hall-ui/public/idea.html +1 -1
  34. package/modules/hall-ui/public/ideas.html +1 -1
  35. package/modules/hall-ui/public/index.html +1 -1
  36. package/modules/hall-ui/public/market-watch-item.html +83 -0
  37. package/modules/hall-ui/public/market-watch-item.js +150 -0
  38. package/modules/hall-ui/public/market-watch-item.states.json +143 -0
  39. package/modules/hall-ui/public/market-watch-lib.js +243 -0
  40. package/modules/hall-ui/public/market-watch-rules.html +1 -1
  41. package/modules/hall-ui/public/market-watch-settings.html +1 -1
  42. package/modules/hall-ui/public/market-watch.css +169 -0
  43. package/modules/hall-ui/public/market-watch.html +111 -0
  44. package/modules/hall-ui/public/market-watch.js +129 -0
  45. package/modules/hall-ui/public/market-watch.states.json +14 -0
  46. package/modules/hall-ui/public/modules.html +1 -1
  47. package/modules/hall-ui/public/my-agents.html +1 -1
  48. package/modules/hall-ui/public/primer.html +1 -1
  49. package/modules/hall-ui/public/profile.html +1 -1
  50. package/modules/hall-ui/public/project-settings.css +12 -0
  51. package/modules/hall-ui/public/project-settings.html +1 -1
  52. package/modules/hall-ui/public/project-settings.js +63 -1
  53. package/modules/hall-ui/public/ranks.html +1 -1
  54. package/modules/hall-ui/public/roadmap.html +1 -1
  55. package/modules/hall-ui/public/roster.html +1 -1
  56. package/modules/hall-ui/public/sessions.html +1 -1
  57. package/modules/hall-ui/public/settings.html +1 -1
  58. package/modules/hall-ui/public/shell.js +3 -2
  59. package/modules/hall-ui/public/street.html +1 -1
  60. package/modules/hall-ui/public/studio.html +1 -1
  61. package/modules/hall-ui/public/task.html +1 -1
  62. package/modules/hall-ui/public/task.js +34 -1
  63. package/modules/hall-ui/public/thinking.html +1 -1
  64. package/modules/hall-ui/public/tweak-editor.html +1 -1
  65. package/modules/hall-ui/public/watch.html +1 -1
  66. package/modules/hall-ui/public/work.html +1 -1
  67. package/modules/hall-ui/records/market-watch.md +40 -0
  68. package/modules/lifecycle/migrations/lifecycle_018_pay_terms.sql +64 -0
  69. package/modules/lifecycle/pay-terms.js +171 -0
  70. package/modules/lifecycle/routes/project-settings.js +94 -0
  71. package/modules/lifecycle/routes/task-write-routes.js +35 -4
  72. package/modules/market-watch/db.js +10 -8
  73. package/modules/market-watch/module.json +2 -1
  74. package/modules/market-watch/pages.js +323 -0
  75. package/modules/market-watch/proposals.js +66 -4
  76. package/modules/market-watch/routes/pages.js +116 -0
  77. package/package-lock.json +2 -2
  78. package/package.json +1 -1
  79. package/release-notes.json +12 -0
  80. package/scripts/bongos/government-walk.js +1 -0
  81. package/scripts/bongos/run-unit-tests.js +4 -0
  82. package/scripts/bongos/start.js +42 -2
  83. package/src/bongos/module-scope-map.js +1 -0
  84. package/src/module-api.js +1 -1
  85. package/tests/fixtures/market-watch-seed-rows.cjs +60 -0
  86. package/tests/government_act_alone_buildings.mjs +1 -0
  87. package/tests/hall_nav.mjs +4 -1
  88. package/tests/hall_page_gate_map.mjs +4 -0
  89. package/tests/market_watch_module.mjs +2 -1
  90. package/tests/market_watch_pages.mjs +356 -0
  91. package/tests/market_watch_rules.mjs +63 -3
  92. package/tests/nav_permission_atoms.mjs +1 -1
  93. package/tests/patch_gate_ordering.mjs +3 -0
  94. package/tests/pay_terms.mjs +254 -0
  95. package/tests/pay_terms_db.mjs +86 -0
  96. package/tests/project_settings_page.mjs +25 -6
@@ -0,0 +1,243 @@
1
+ // market-watch-lib.js — the drawing shared by the Market Watch home and item pages
2
+ // (task 1004939): the landscape scope, sparklines, confidence bars, type pills, the
3
+ // next-sweep countdown and the propose-a-change dialog. Pure markup builders, so
4
+ // tests/market_watch_pages.mjs runs them on the seed-built fixtures. Every string that
5
+ // came from a member or the research goes through the hall's one escaper
6
+ // (dom-utils.js, window.OTB.escapeHtml) where it becomes markup.
7
+ (function (root) {
8
+ 'use strict';
9
+
10
+ var esc = function (s) { return root.OTB.escapeHtml(s); };
11
+ // Only an http(s) link becomes an href; anything else renders as a dead link.
12
+ function safeHref(u) { return /^https?:\/\//i.test(String(u || '')) ? esc(u) : '#'; }
13
+ function itemHref(id) { return '/market-watch-item?id=' + encodeURIComponent(String(id)); }
14
+
15
+ var KIND_ICON = { competitor: '◎', partner: '✦', topic: '#' };
16
+ var KIND_PLURAL = { competitor: 'competitors', partner: 'partners', topic: 'topics' };
17
+ var CONF = ['', 'low', 'medium', 'high'];
18
+
19
+ function confDots(c) {
20
+ c = Number(c) || 0;
21
+ var bars = '';
22
+ for (var i = 1; i <= 3; i += 1) bars += '<i class="' + (i <= c ? 'on' : '') + '"></i>';
23
+ return '<span class="mw-conf' + (c === 1 ? ' mw-conf--low' : '') + '" title="confidence: ' + CONF[c] + '" aria-label="confidence ' + CONF[c] + '">' + bars + '</span>';
24
+ }
25
+ function typePill(t) { return '<span class="mw-pill mw-pill--' + esc(t) + '">' + esc(t) + '</span>'; }
26
+ function initials(name) {
27
+ var w = String(name || '').replace(/\(.*?\)/g, '').trim().split(/\s+/).filter(Boolean);
28
+ return ((w[0] || '?').charAt(0) + (w[1] ? w[1].charAt(0) : (w[0] || '').charAt(1))).toUpperCase();
29
+ }
30
+ function fmtDay(iso, long) {
31
+ var d = new Date(String(iso).slice(0, 10) + 'T12:00:00Z');
32
+ if (isNaN(d)) return '';
33
+ return d.toLocaleDateString('en-GB', long ? { day: 'numeric', month: 'short', year: 'numeric', timeZone: 'UTC' } : { day: 'numeric', month: 'short', timeZone: 'UTC' });
34
+ }
35
+
36
+ // spark(values, w, h, label) — the kit's sparkline (OTBKit.sparklineHtml), sized for a
37
+ // card row. A null point is a run that did not score the dimension: it is dropped rather
38
+ // than drawn as zero, which the kit would otherwise do.
39
+ function spark(vals, w, h, label) {
40
+ var pts = (vals || []).filter(function (v) { return v != null; });
41
+ return '<span class="mw-spark" style="width:' + Number(w) + 'px;height:' + Number(h) + 'px">'
42
+ + root.OTBKit.sparklineHtml(pts, { width: Number(w), height: Number(h), ariaLabel: label }) + '</span>';
43
+ }
44
+
45
+ // jitter — a fixed nudge per dot so two items on the same score do not hide each other.
46
+ function jit(i, k) { return ((i * 37 + k * 17) % 11 - 5) / 22; }
47
+
48
+ // scopeSvg(home, xKey, yKey) — the landscape: every competitor as a dot on the two
49
+ // dimensions the viewer picked; this week's reported items glow; "us" holds the far
50
+ // corner, as in the mockup.
51
+ function scopeSvg(home, xKey, yKey) {
52
+ var S = 600, p = 40;
53
+ var sx = function (v) { return p + (v / 10) * (S - 2 * p); };
54
+ var sy = function (v) { return S - p - (v / 10) * (S - 2 * p); };
55
+ var hot = {};
56
+ (home.hot || []).forEach(function (id) { hot[String(id)] = true; });
57
+ var label = function (k) { var a = (home.axes || []).filter(function (x) { return x.key === k; })[0]; return a ? a.label : k; };
58
+ var g = '<svg viewBox="0 0 ' + S + ' ' + S + '" role="img" aria-label="The landscape: ' + esc(label(xKey)) + ' across, ' + esc(label(yKey)) + ' up">';
59
+ g += '<defs><linearGradient id="mw-sg" x1="0" x2="1"><stop offset="0" stop-color="var(--accent)" stop-opacity="0"/><stop offset="1" stop-color="var(--accent)" stop-opacity=".22"/></linearGradient></defs>';
60
+ [1, 2, 3, 4].forEach(function (k) { g += '<circle cx="' + sx(10) + '" cy="' + sy(10) + '" r="' + (k * 130) + '" fill="none" stroke="var(--rule-soft)" stroke-dasharray="3 5"/>'; });
61
+ g += '<line x1="' + sx(5) + '" x2="' + sx(5) + '" y1="' + sy(0) + '" y2="' + sy(10) + '" stroke="var(--rule)"/><line y1="' + sy(5) + '" y2="' + sy(5) + '" x1="' + sx(0) + '" x2="' + sx(10) + '" stroke="var(--rule)"/>';
62
+ var cx0 = sx(10), cy0 = sy(10);
63
+ g += '<g class="mw-sweep" style="transform-origin:' + cx0 + 'px ' + cy0 + 'px"><path d="M' + cx0 + ' ' + cy0 + ' L' + (cx0 - 560) + ' ' + cy0 + ' A560 560 0 0 1 ' + (cx0 - 560 * Math.cos(0.5)).toFixed(1) + ' ' + (cy0 + 560 * Math.sin(0.5)).toFixed(1) + 'Z" fill="url(#mw-sg)" transform="rotate(180 ' + cx0 + ' ' + cy0 + ')"/></g>';
64
+ var plotted = 0;
65
+ (home.dots || []).forEach(function (d, i) {
66
+ var vx = xKey === 'overall' ? d.overall : d.scores[xKey];
67
+ var vy = yKey === 'overall' ? d.overall : d.scores[yKey];
68
+ if (vx == null || vy == null) return;
69
+ plotted += 1;
70
+ var cx = sx(Math.min(10, Math.max(0, Number(vx) + jit(i, 1)))).toFixed(1);
71
+ var cy = sy(Math.min(10, Math.max(0, Number(vy) + jit(i, 2)))).toFixed(1);
72
+ var h = !!hot[String(d.id)];
73
+ var r = (3 + (Number(d.overall) || 0) * 0.7).toFixed(1);
74
+ g += '<g class="mw-blip" data-id="' + esc(d.id) + '">';
75
+ if (h) g += '<circle class="mw-ping" cx="' + cx + '" cy="' + cy + '" r="6" fill="none" stroke="var(--accent)" stroke-width="2"/>';
76
+ g += '<circle cx="' + cx + '" cy="' + cy + '" r="' + r + '" fill="' + (h ? 'var(--accent)' : 'var(--ink-soft)') + '" fill-opacity="' + (h ? 1 : 0.4) + '"><title>' + esc(d.name) + ' · ' + esc(d.overall) + '/10 like us</title></circle>';
77
+ if (h || Number(d.overall) >= 6) g += '<text x="' + (Number(cx) + 9) + '" y="' + (Number(cy) + 4) + '" font-size="12" fill="var(--ink-soft)" font-family="var(--font-body)">' + esc(String(d.name).split(' ')[0]) + '</text>';
78
+ g += '</g>';
79
+ });
80
+ g += '<rect x="' + (cx0 - 9) + '" y="' + (cy0 - 9) + '" width="18" height="18" rx="4" fill="var(--ink)"/><text x="' + (cx0 - 14) + '" y="' + (cy0 + 24) + '" text-anchor="end" font-size="12" font-weight="700" fill="var(--ink)">us</text>';
81
+ g += '<text x="' + (S - p) + '" y="' + (S - 12) + '" text-anchor="end" font-size="12" fill="var(--ink-faint)">' + esc(label(xKey)) + ' →</text><text x="12" y="' + (p - 14) + '" font-size="12" fill="var(--ink-faint)">↑ ' + esc(label(yKey)) + '</text>';
82
+ if (!plotted) g += '<text x="' + (S / 2) + '" y="' + (S / 2) + '" text-anchor="middle" font-size="14" fill="var(--ink-faint)">No competitor is scored on these two yet.</text>';
83
+ return g + '</svg>';
84
+ }
85
+
86
+ // nextSweep(home, now) → { big, small } for the countdown panel.
87
+ function nextSweep(home, now) {
88
+ var ns = home.next_sweep || {};
89
+ if (!ns.enabled) return { big: 'paused', small: 'the watch is switched off' };
90
+ if (!ns.due) return { big: 'due now', small: 'starts on the next hourly check' };
91
+ var due = new Date(ns.due);
92
+ var ms = due - (now || new Date());
93
+ if (!(ms > 0)) return { big: 'due now', small: 'starts on the next hourly check' };
94
+ var d = Math.floor(ms / 864e5), h = Math.floor((ms % 864e5) / 36e5), m = Math.floor((ms % 36e5) / 6e4);
95
+ var big = d > 0 ? d + 'd ' + h + 'h' : h > 0 ? h + 'h ' + m + 'm' : m + 'm';
96
+ var small = due.toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric', month: 'short' }) + ' · ' + due.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
97
+ return { big: big, small: small };
98
+ }
99
+
100
+ function money(v) { return '$' + (Number(v) || 0).toFixed(2); }
101
+
102
+ // The facts an item carries, in words. Research keys get their label; anything else
103
+ // is shown under its own key with underscores as spaces.
104
+ var FACT_LABEL = { pricing: 'Pricing', funding: 'Funding', open_source_self_host: 'Self-host', partner_candidate: 'Also a partner?', rank: 'Partner rank', quote: 'In their words', unverified: 'Unverified', watch_sources: 'Sources to watch' };
105
+ function factRows(facts) {
106
+ var out = [];
107
+ Object.keys(facts || {}).forEach(function (k) {
108
+ if (k === 'quote_url') return;
109
+ var v = facts[k];
110
+ if (v == null || v === '' || (Array.isArray(v) && !v.length)) return;
111
+ if (typeof v === 'boolean') v = v ? 'yes' : 'no';
112
+ else if (Array.isArray(v)) v = v.join('; ');
113
+ else if (typeof v === 'object') return;
114
+ out.push([FACT_LABEL[k] || k.replace(/_/g, ' '), String(v)]);
115
+ });
116
+ return out;
117
+ }
118
+
119
+ // ---- per-viewer conveniences (never the record) --------------------------------
120
+ function store(key, fallback) {
121
+ try { var v = JSON.parse(root.localStorage.getItem(key)); return v == null ? fallback : v; } catch (e) { return fallback; }
122
+ }
123
+ function save(key, v) { try { root.localStorage.setItem(key, JSON.stringify(v)); } catch (e) { /* private mode */ } }
124
+ var pins = {
125
+ all: function () { var v = store('mw-pins', []); return Array.isArray(v) ? v : []; },
126
+ has: function (id) { return pins.all().some(function (p) { return String(p.id) === String(id); }); },
127
+ toggle: function (item) {
128
+ var list = pins.all().filter(function (p) { return String(p.id) !== String(item.id); });
129
+ var on = list.length === pins.all().length;
130
+ if (on) list.unshift({ id: item.id, name: item.name, kind: item.kind });
131
+ save('mw-pins', list.slice(0, 12));
132
+ return on;
133
+ },
134
+ };
135
+ var reads = {
136
+ get: function () { var v = store('mw-read', []); return Array.isArray(v) ? v.map(String) : []; },
137
+ add: function (id) { var v = reads.get(); if (v.indexOf(String(id)) < 0) v.push(String(id)); save('mw-read', v.slice(-200)); },
138
+ };
139
+
140
+ // ---- the propose dialog ------------------------------------------------------
141
+ // One <dialog> for every proposal either page files. `mode` is 'change' (the item
142
+ // page's four kinds), 'stop' (stop tracking) or 'scout' (a new item). It posts to
143
+ // POST /market-watch/proposals and resolves with the server's answer.
144
+ var CHANGE_KINDS = [
145
+ ['rescore', 'Re-score a dimension'],
146
+ ['move_kind', 'Move to partner / competitor'],
147
+ ['add_source', 'Add a source to watch'],
148
+ ['correct_entry', 'Correct an entry'],
149
+ ];
150
+ function dialogHtml(mode, ctx) {
151
+ ctx = ctx || {};
152
+ var it = ctx.item || {};
153
+ var h = '<form method="dialog" novalidate>';
154
+ if (mode === 'scout') {
155
+ h += '<span class="mw-label">send a new scout</span>';
156
+ h += '<label>What should we watch?<select name="scout"><option value="competitor">A competitor</option><option value="partner">A partner</option><option value="topic">A topic</option></select></label>';
157
+ h += '<label>Its name<input name="name" maxlength="120" autocomplete="off" required></label>';
158
+ h += '<label>A link, if it has one<input name="url" type="url" maxlength="500" placeholder="https://" autocomplete="off"></label>';
159
+ } else if (mode === 'stop') {
160
+ h += '<span class="mw-label">stop tracking</span>';
161
+ h += '<p class="mw-quiet" style="margin:0 0 12px">Asks the government to take ' + esc(it.name) + ' off the watch. Its case file stays readable.</p>';
162
+ } else {
163
+ h += '<span class="mw-label">propose a change</span>';
164
+ h += '<label>What should change?<select name="kind">';
165
+ CHANGE_KINDS.forEach(function (k) {
166
+ if (k[0] === 'move_kind' && it.kind === 'topic') return;
167
+ var lbl = k[0] === 'move_kind' ? (it.kind === 'partner' ? 'Move to competitor' : 'Move to partner') : k[1];
168
+ h += '<option value="' + k[0] + '">' + esc(lbl) + '</option>';
169
+ });
170
+ h += '</select></label>';
171
+ h += '<div data-for="rescore"><div class="mw-row2"><label>Which score<select name="dimension">';
172
+ (ctx.dimensions || []).forEach(function (d) { h += '<option value="' + esc(d.key) + '">' + esc(d.label) + (d.latest != null ? ' (now ' + esc(d.latest) + ')' : '') + '</option>'; });
173
+ h += '<option value="overall">Overall' + (it.overall != null ? ' (now ' + esc(it.overall) + ')' : '') + '</option>';
174
+ h += '</select></label><label>New score<input name="value" type="number" min="0" max="10" step="0.5" inputmode="decimal"></label></div></div>';
175
+ h += '<div data-for="add_source" hidden><label>The source<input name="url" type="url" maxlength="500" placeholder="https://" autocomplete="off"></label></div>';
176
+ h += '<div data-for="correct_entry" hidden><label>Which entry<select name="log_entry_id">';
177
+ (ctx.entries || []).forEach(function (e) { h += '<option value="' + esc(e.id) + '">' + esc(fmtDay(e.found_on)) + ' · ' + esc(String(e.takeaway).slice(0, 70)) + '</option>'; });
178
+ h += '</select></label></div>';
179
+ }
180
+ h += '<label>Why, in one line<input name="reason" maxlength="280" autocomplete="off" required placeholder="' + (mode === 'scout' ? 'e.g. they launched an agent marketplace this week' : mode === 'stop' ? 'e.g. they shut down in September' : 'e.g. they now pay maintainers, so economy should be 4') + '"></label>';
181
+ h += '<p class="mw-dialog__err" role="alert"></p>';
182
+ h += '<div class="mw-dialog__acts"><button class="btn btn--ghost" value="cancel" type="button" data-cancel>cancel</button><button class="btn" type="submit">send to the government</button></div>';
183
+ return h + '</form>';
184
+ }
185
+
186
+ // bodyFor(mode, form, item) — what the dialog posts.
187
+ function bodyFor(mode, f, item) {
188
+ var v = function (n) { var el = f.elements[n]; return el ? String(el.value || '').trim() : ''; };
189
+ if (mode === 'scout') return { kind: 'add_item', scout: v('scout'), name: v('name'), url: v('url'), reason: v('reason') };
190
+ if (mode === 'stop') return { kind: 'remove_item', item_id: item.id, reason: v('reason') };
191
+ var kind = v('kind');
192
+ var b = { kind: kind, item_id: item.id, reason: v('reason') };
193
+ if (kind === 'rescore') { b.dimension = v('dimension'); b.value = v('value'); }
194
+ if (kind === 'add_source') b.url = v('url');
195
+ if (kind === 'correct_entry') b.log_entry_id = v('log_entry_id');
196
+ return b;
197
+ }
198
+
199
+ function openPropose(mode, ctx, onDone) {
200
+ var doc = root.document;
201
+ var dlg = doc.getElementById('mw-propose');
202
+ dlg.innerHTML = dialogHtml(mode, ctx);
203
+ var f = dlg.querySelector('form');
204
+ var err = dlg.querySelector('.mw-dialog__err');
205
+ var sync = function () {
206
+ var k = f.elements.kind ? f.elements.kind.value : '';
207
+ Array.prototype.forEach.call(dlg.querySelectorAll('[data-for]'), function (el) { el.hidden = el.getAttribute('data-for') !== k; });
208
+ };
209
+ if (f.elements.kind) f.elements.kind.addEventListener('change', sync);
210
+ sync();
211
+ dlg.querySelector('[data-cancel]').addEventListener('click', function () { dlg.close(); });
212
+ f.addEventListener('submit', function (ev) {
213
+ ev.preventDefault();
214
+ var body = bodyFor(mode, f, ctx.item || {});
215
+ if (!body.reason) { err.textContent = 'Say why, in one line.'; return; }
216
+ var btn = f.querySelector('[type=submit]');
217
+ btn.disabled = true;
218
+ err.textContent = '';
219
+ root.fetch('/api/bongos/market-watch/proposals', {
220
+ method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
221
+ }).then(function (r) {
222
+ return r.json().catch(function () { return {}; }).then(function (j) { return { status: r.status, ok: r.ok, data: j }; });
223
+ }).then(function (res) {
224
+ btn.disabled = false;
225
+ if (res.ok) { dlg.close(); onDone(res.data); return; }
226
+ var e = (res.data && res.data.error) || {};
227
+ err.textContent = res.status === 401 ? 'Sign in again to send this.' : (e.message && e.message !== e.code ? e.message : 'That did not send. Try again in a moment.');
228
+ }).catch(function () { btn.disabled = false; err.textContent = 'Could not reach the server.'; });
229
+ });
230
+ dlg.showModal();
231
+ var first = f.querySelector('select, input');
232
+ if (first) first.focus();
233
+ }
234
+
235
+ var api = {
236
+ esc: esc, safeHref: safeHref, itemHref: itemHref, confDots: confDots, typePill: typePill, initials: initials,
237
+ fmtDay: fmtDay, spark: spark, scopeSvg: scopeSvg, nextSweep: nextSweep, money: money, factRows: factRows,
238
+ pins: pins, reads: reads, dialogHtml: dialogHtml, bodyFor: bodyFor, openPropose: openPropose,
239
+ KIND_ICON: KIND_ICON, KIND_PLURAL: KIND_PLURAL,
240
+ };
241
+ root.OTBMarketWatch = api;
242
+ if (typeof module !== 'undefined' && module.exports) module.exports = api;
243
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -43,7 +43,7 @@
43
43
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
44
44
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
45
45
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
46
- <script src="/builders/shell.js?v=2026-10-30-market-watch"></script>
46
+ <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></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/hall-kit.js"></script>
@@ -105,7 +105,7 @@
105
105
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
106
106
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
107
107
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
108
- <script src="/builders/shell.js?v=2026-10-30-market-watch"></script>
108
+ <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
109
109
  <script src="/builders/palette.js" defer></script>
110
110
  <script src="/builders/hall-kit.js"></script>
111
111
  <script src="/builders/market-watch-settings.js"></script>
@@ -0,0 +1,169 @@
1
+ /* market-watch.css — the Market Watch home (the watchtower desk, option D) and the
2
+ tracked-item page (the dossier, option B). Task 1004939; the designs of record are
3
+ docs/design/mocks/market-watch/home.html and item.html. Every colour is a hall token
4
+ from the branding pack, never the mock's literal hex: the mocks drew with the same
5
+ fifteen, so the look carries over and a host's palette still wins. */
6
+
7
+ .mw-label {
8
+ display: block;
9
+ margin: 0 0 8px;
10
+ font-family: var(--label-face);
11
+ font-size: var(--label-size);
12
+ letter-spacing: var(--label-space);
13
+ text-transform: var(--label-case);
14
+ color: var(--ink-faint);
15
+ font-weight: 500;
16
+ }
17
+ .mw-num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
18
+ .mw-one { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
19
+ .mw-panel { background: var(--bg-card); border-radius: var(--radius); padding: 12px; }
20
+ .mw-quiet { color: var(--ink-faint); font-size: 12.5px; }
21
+ .mw-off { max-width: var(--measure); }
22
+ .mw-off p { margin: 0 0 6px; color: var(--ink-soft); }
23
+
24
+ /* ---- the watchtower desk ---------------------------------------------------- */
25
+ .mw-desk { display: grid; grid-template-columns: 260px minmax(0, 1fr) 320px; gap: 16px; align-items: start; }
26
+ @media (max-width: 1180px) { .mw-desk { grid-template-columns: 1fr 1fr; } .mw-scope-wrap { grid-column: 1 / -1; order: -1; } }
27
+ @media (max-width: 680px) { .mw-desk { grid-template-columns: minmax(0, 1fr); } }
28
+ .mw-side { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
29
+
30
+ .mw-watched { display: flex; align-items: center; gap: 6px; margin: 0 0 10px; font-size: 12.5px; color: var(--ink-soft); }
31
+ .mw-watched b { color: var(--ink); font-weight: 600; }
32
+ .mw-scout { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 12px; background: var(--glass); margin-bottom: 8px; white-space: nowrap; }
33
+ .mw-scout__ic { width: 30px; height: 30px; flex: none; border-radius: 9px; display: grid; place-items: center; background: var(--bg-card); font-size: 15px; color: var(--ink-soft); }
34
+ .mw-scout__t { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; line-height: 1.3; }
35
+ .mw-scout__lv { font-size: 11.5px; color: var(--ink-faint); }
36
+ .mw-badge { min-width: 24px; height: 24px; border-radius: 12px; background: var(--accent); color: var(--accent-on); font-size: 12px; font-weight: 700; display: grid; place-items: center; padding: 0 7px; }
37
+ .mw-badge--none { background: var(--glass-strong); color: var(--ink-faint); }
38
+ .mw-send { width: 100%; margin-top: 4px; }
39
+ .mw-streak { display: flex; gap: 4px; margin-top: 6px; }
40
+ .mw-streak i { flex: 1; max-width: 40px; height: 18px; border-radius: 5px; background: var(--accent); opacity: .85; }
41
+ .mw-streak i.no { background: var(--glass-strong); opacity: 1; }
42
+ .mw-pins { margin-top: 14px; }
43
+ .mw-pin-row { display: flex; align-items: center; gap: 8px; min-height: 32px; border-top: 1px solid var(--rule-soft); font-size: 13px; }
44
+ .mw-pin-row:first-of-type { border-top: 0; }
45
+ .mw-pin-row a { color: var(--ink); text-decoration: none; padding: 4px 0; }
46
+ .mw-pin-row a:hover { text-decoration: underline; }
47
+
48
+ .mw-scope-tools { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; font-size: 12.5px; color: var(--ink-faint); margin-bottom: 8px; }
49
+ .mw-scope-tools select { font: inherit; color: var(--ink); background: var(--bg-card); border: 1px solid var(--rule); border-radius: var(--radius-sm); padding: 4px 8px; min-height: 28px; max-width: 46%; }
50
+ .mw-scope-tools .mw-sp { flex: 1; }
51
+ .mw-key { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
52
+ .mw-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex: none; }
53
+ .mw-scope { position: relative; aspect-ratio: 1 / 1; max-height: 560px; margin: 0 auto; border-radius: 18px; background: var(--bg-frame); overflow: hidden; }
54
+ .mw-scope svg { position: absolute; inset: 0; width: 100%; height: 100%; }
55
+ .mw-scope .mw-blip { cursor: pointer; }
56
+ .mw-open { margin: 10px 0 0; }
57
+ .mw-open select { flex: 1 1 200px; min-width: 0; max-width: 360px; }
58
+ .mw-sweep { animation: mw-sweep 6s linear infinite; }
59
+ @keyframes mw-sweep { to { transform: rotate(360deg); } }
60
+ .mw-ping { animation: mw-ping 1.8s ease-out infinite; }
61
+ @keyframes mw-ping { 0% { r: 4; opacity: .9; } 100% { r: 16; opacity: 0; } }
62
+ @media (prefers-reduced-motion: reduce) { .mw-sweep, .mw-ping { animation: none; } }
63
+
64
+ .mw-fuel { display: flex; gap: 10px; align-items: flex-end; margin-top: 4px; }
65
+ .mw-count { font-size: 34px; font-weight: 300; letter-spacing: -.03em; line-height: 1.1; font-family: var(--font-display); }
66
+ .mw-when { color: var(--ink-faint); font-size: 12px; }
67
+ .mw-tank { width: 44px; height: 90px; border-radius: 10px; border: 2px solid var(--rule); position: relative; overflow: hidden; margin: 0 auto; }
68
+ .mw-tank i { position: absolute; left: 0; right: 0; bottom: 0; background: var(--accent); }
69
+ .mw-left { font-size: 11px; color: var(--ink-faint); margin-top: 4px; text-align: center; white-space: nowrap; }
70
+
71
+ .mw-xp { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; font-size: 12px; color: var(--ink-faint); }
72
+ .mw-xp__b { flex: 1; height: 8px; border-radius: 8px; background: var(--glass-strong); overflow: hidden; }
73
+ .mw-xp__b i { display: block; height: 100%; background: var(--accent); transition: width .3s; }
74
+ .mw-rep { display: flex; align-items: center; gap: 8px; min-height: 40px; border-top: 1px solid var(--rule-soft); white-space: nowrap; }
75
+ .mw-rep:first-child { border-top: 0; }
76
+ .mw-rep__ic { width: 16px; flex: none; text-align: center; color: var(--ink-faint); }
77
+ .mw-rep__t { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; font-size: 13px; color: var(--ink-soft); text-decoration: none; padding: 6px 0; }
78
+ .mw-rep__t b { color: var(--ink); font-weight: 600; }
79
+ .mw-rep__t:hover { color: var(--ink); }
80
+ .mw-rep.done { opacity: .45; }
81
+ .mw-rep button { flex: none; min-width: 44px; min-height: 26px; border: 1px solid var(--rule-soft); background: transparent; color: var(--ink-soft); border-radius: var(--radius-pill); padding: 0 10px; cursor: pointer; font: inherit; font-size: 12px; }
82
+ .mw-rep button:hover { border-color: var(--ink-faint); color: var(--ink); }
83
+
84
+ /* ---- the dossier ------------------------------------------------------------- */
85
+ .mw-back { display: inline-flex; align-items: center; min-height: 28px; margin: 0 0 10px; font-size: 13px; color: var(--ink-faint); text-decoration: none; }
86
+ .mw-back:hover { color: var(--ink); }
87
+ .mw-wrap { display: grid; grid-template-columns: 340px minmax(0, 1fr); gap: 24px; align-items: start; }
88
+ @media (max-width: 1000px) { .mw-wrap { grid-template-columns: minmax(0, 1fr); } .mw-card { position: static; max-width: 440px; width: 100%; } }
89
+ .mw-card { border-radius: 20px; overflow: hidden; background: var(--bg-card); position: sticky; top: calc(var(--topbar-h) + 16px); box-shadow: var(--shadow-pop); }
90
+ .mw-holo { position: relative; height: 150px; display: flex; align-items: flex-end; padding: 14px 16px; color: var(--void-ink);
91
+ background:
92
+ radial-gradient(120% 90% at 20% 10%, color-mix(in srgb, var(--sea) 55%, transparent) 0, transparent 50%),
93
+ radial-gradient(90% 90% at 85% 30%, color-mix(in srgb, var(--accent) 60%, transparent) 0, transparent 55%),
94
+ radial-gradient(90% 90% at 50% 110%, color-mix(in srgb, var(--sea) 35%, transparent) 0, transparent 60%),
95
+ var(--void); }
96
+ .mw-holo__ini { position: absolute; right: 14px; top: 12px; font: 300 54px/1 var(--font-display); letter-spacing: -.04em; opacity: .85; }
97
+ .mw-holo__kind { position: absolute; left: 14px; top: 14px; font: 500 11px/1 var(--font-mono); padding: 5px 8px; border-radius: 999px; background: color-mix(in srgb, var(--void) 55%, transparent); color: var(--void-ink); }
98
+ .mw-holo__nm { font-weight: 600; font-size: 17px; text-shadow: 0 1px 6px var(--void); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
99
+ .mw-lvl { display: flex; align-items: center; gap: 14px; padding: 14px 16px; border-bottom: 1px solid var(--rule-soft); }
100
+ .mw-ring { width: 62px; height: 62px; border-radius: 50%; display: grid; place-items: center; flex: none; background: conic-gradient(var(--accent) calc(var(--p) * 1%), var(--glass-strong) 0); }
101
+ .mw-ring b { width: 48px; height: 48px; border-radius: 50%; background: var(--bg-card); display: grid; place-items: center; font: 600 18px var(--font-body); }
102
+ .mw-vtag { font-weight: 600; color: var(--accent-ink); }
103
+ .mw-stats { padding: 8px 16px 4px; }
104
+ .mw-st { display: grid; grid-template-columns: minmax(0, 1fr) 70px; gap: 10px; align-items: center; padding: 4px 0; font-size: 12.5px; }
105
+ .mw-st .kit-bars { --kit-bars-label-w: 128px; margin: 0; }
106
+ .mw-spark { display: block; flex: none; }
107
+ .mw-spark .kit-spark { width: 100%; height: 100%; }
108
+ .mw-badges { display: flex; gap: 6px; flex-wrap: wrap; padding: 10px 16px 14px; }
109
+ .mw-bdg { font: 500 11px var(--font-mono); padding: 5px 8px; border-radius: 8px; background: var(--glass); }
110
+ .mw-bdg--hot { background: var(--accent-tint); color: var(--accent-ink); }
111
+ .mw-acts { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; padding: 0 16px 16px; }
112
+ .mw-acts .btn, .mw-acts .btn--ghost { width: 100%; min-width: 0; }
113
+ .mw-acts .mw-wide { grid-column: 1 / -1; }
114
+ .mw-acts [aria-pressed="true"] { background: var(--accent-tint); color: var(--accent-ink); border-color: transparent; }
115
+
116
+ .mw-sec { margin-bottom: 22px; }
117
+ .mw-why { padding: 14px 16px; font-size: 14.5px; line-height: 1.5; }
118
+ .mw-why small { display: block; color: var(--ink-faint); font-size: 12px; margin-top: 6px; }
119
+ .mw-facts { padding: 0; }
120
+ .mw-f { display: grid; grid-template-columns: 120px minmax(0, 1fr); gap: 12px; padding: 9px 16px; border-top: 1px solid var(--rule-soft); font-size: 13px; }
121
+ .mw-f:first-child { border-top: 0; }
122
+ .mw-f > span:first-child { color: var(--ink-faint); }
123
+
124
+ .mw-src { color: var(--ink-faint); text-decoration: none; border-bottom: 1px dotted var(--rule); font-size: 12.5px; white-space: nowrap; display: inline-block; padding: 4px 0; max-width: 180px; overflow: hidden; text-overflow: ellipsis; vertical-align: middle; }
125
+ .mw-src:hover { color: var(--ink); }
126
+ .mw-pill { display: inline-flex; align-items: center; font: 500 11px/1 var(--font-mono); padding: 4px 8px; border-radius: 999px; white-space: nowrap; color: var(--ink-soft); background: var(--glass-strong); }
127
+ .mw-pill--insight { color: var(--accent-ink); background: var(--accent-tint); }
128
+ .mw-pill--news { color: var(--sea); background: color-mix(in srgb, var(--sea) 16%, var(--bg-card)); }
129
+ .mw-conf { display: inline-flex; gap: 2px; align-items: center; }
130
+ .mw-conf i { width: 5px; height: 11px; border-radius: 2px; background: var(--rule); }
131
+ .mw-conf i.on { background: var(--ink-soft); }
132
+ .mw-conf--low i.on { background: var(--terracotta); }
133
+
134
+ .mw-log { overflow: hidden; padding: 0; }
135
+ .mw-log table { width: 100%; border-collapse: collapse; font-size: 13px; }
136
+ .mw-log th { text-align: left; padding: 0; border-bottom: 1px solid var(--rule-soft); white-space: nowrap; }
137
+ .mw-log th button, .mw-log th span { display: flex; align-items: center; width: 100%; min-height: 34px; padding: 0 10px; background: none; border: 0; text-align: left; font: 500 10.5px var(--font-mono); letter-spacing: .06em; text-transform: uppercase; color: var(--ink-faint); }
138
+ .mw-log th button { cursor: pointer; }
139
+ .mw-log th button:hover { color: var(--ink); }
140
+ .mw-log th[aria-sort] button { color: var(--ink); }
141
+ .mw-log th[aria-sort="descending"] button::after { content: " \2193"; }
142
+ .mw-log th[aria-sort="ascending"] button::after { content: " \2191"; }
143
+ .mw-log td { padding: 9px 10px; border-bottom: 1px solid var(--rule-soft); vertical-align: middle; }
144
+ .mw-log tr:last-child td { border-bottom: 0; }
145
+ .mw-log tr.pin td:first-child { box-shadow: inset 3px 0 0 var(--accent); }
146
+ .mw-log td.mw-tk { max-width: 0; width: 100%; }
147
+ .mw-date, .mw-run { font: 400 12px var(--font-mono); color: var(--ink-faint); white-space: nowrap; }
148
+ @media (max-width: 760px) {
149
+ .mw-log thead { display: none; }
150
+ .mw-log tr { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 4px 10px; padding: 10px 12px; border-bottom: 1px solid var(--rule-soft); }
151
+ .mw-log tr:last-child { border-bottom: 0; }
152
+ .mw-log td { padding: 0; border: 0; }
153
+ .mw-log td.mw-tk { grid-column: 1 / -1; grid-row: 2; max-width: none; }
154
+ .mw-log td.mw-c-run, .mw-log td.mw-c-conf { display: none; }
155
+ .mw-log tr.pin { box-shadow: inset 3px 0 0 var(--accent); }
156
+ .mw-log tr.pin td:first-child { box-shadow: none; }
157
+ .mw-f { grid-template-columns: 96px minmax(0, 1fr); }
158
+ }
159
+
160
+ /* ---- the propose dialog (both pages) ---------------------------------------- */
161
+ .mw-dialog { border: 0; border-radius: var(--radius); background: var(--bg-card); color: var(--ink); padding: 20px; width: min(440px, calc(100vw - 32px)); box-shadow: var(--shadow-pop); }
162
+ .mw-dialog::backdrop { background: var(--scrim); }
163
+ .mw-dialog label { display: block; font-size: 13px; color: var(--ink-soft); margin-bottom: 12px; }
164
+ .mw-dialog input, .mw-dialog select { display: block; width: 100%; margin-top: 6px; font: inherit; color: var(--ink); background: var(--bg); border: 1px solid var(--rule); border-radius: var(--radius-sm); padding: 8px 9px; min-height: 36px; }
165
+ .mw-dialog .mw-row2 { display: grid; grid-template-columns: minmax(0, 1fr) 90px; gap: 8px; }
166
+ .mw-dialog__acts { display: flex; gap: 8px; justify-content: flex-end; margin-top: 4px; }
167
+ .mw-dialog__err { color: var(--danger); font-size: 12.5px; margin: 0 0 8px; min-height: 1em; }
168
+ .mw-filter { margin: 0 0 8px; }
169
+ .mw-rules-link { display: inline-flex; align-items: center; min-height: 28px; margin-top: 12px; font-size: 12.5px; color: var(--ink-soft); }
@@ -0,0 +1,111 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Market Watch — {{worldName}}</title>
7
+ <meta name="description" content="The project's scouts on its competitors, partners and topics: the landscape, the next sweep, the budget left and this week's reports.">
8
+ <link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
12
+ <link rel="stylesheet" href="/builders/market-watch.css">
13
+ </head>
14
+ <body data-page="market-watch" data-page-title="Market Watch">
15
+
16
+ <div class="app">
17
+ <aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
18
+ <div class="app-scrim" id="app-scrim" hidden></div>
19
+ <div class="app-body">
20
+ <header class="app-topbar" id="app-topbar"></header>
21
+ <div class="app-content">
22
+ <!-- The page's one h1, for screen readers: no page shows its head (task 1004836). -->
23
+ <div class="page-head page-head--room">
24
+ <h1 class="page-head__h">Market Watch</h1>
25
+ </div>
26
+
27
+ <main id="hall-main">
28
+ <p class="mw-quiet" id="mw-loading">Loading the watch…</p>
29
+
30
+ <!-- Market Watch is off on this project: a quiet line, never an error. -->
31
+ <section class="mw-panel mw-off" id="mw-off" hidden>
32
+ <span class="mw-label">market watch</span>
33
+ <p>Market Watch is off on this project.</p>
34
+ <p class="mw-quiet">An owner can turn it on from <a href="/modules">Modules</a>.</p>
35
+ </section>
36
+
37
+ <section class="mw-panel mw-off" id="mw-fail" hidden>
38
+ <p id="mw-fail-text">Could not read the watch. Try reloading the page.</p>
39
+ </section>
40
+
41
+ <div class="mw-desk" id="mw-desk" hidden>
42
+ <section class="mw-panel" aria-labelledby="mw-scouts-h">
43
+ <span class="mw-label" id="mw-scouts-h">your scouts</span>
44
+ <p class="mw-watched" id="mw-watched"></p>
45
+ <div id="mw-scouts"></div>
46
+ <button class="btn btn--ghost mw-send" type="button" id="mw-send">+ send a new scout</button>
47
+ <span class="mw-label" style="margin-top:16px">streak</span>
48
+ <div class="mw-streak" id="mw-streak"></div>
49
+ <a class="mw-rules-link" href="/market-watch-rules">the watch rules, and what members proposed →</a>
50
+ <div class="mw-pins" id="mw-pins" hidden>
51
+ <span class="mw-label">pinned</span>
52
+ <div id="mw-pin-list"></div>
53
+ </div>
54
+ </section>
55
+
56
+ <section class="mw-panel mw-scope-wrap" aria-label="The landscape">
57
+ <div class="mw-scope-tools">
58
+ <label for="mw-ax">across</label> <select id="mw-ax"></select>
59
+ <label for="mw-ay">up</label> <select id="mw-ay"></select>
60
+ <span class="mw-sp"></span>
61
+ <span class="mw-key"><i class="mw-dot" style="background:var(--accent)"></i> new signal</span>
62
+ <span class="mw-key"><i class="mw-dot" style="background:var(--ink)"></i> us</span>
63
+ </div>
64
+ <div class="mw-scope" id="mw-scope"></div>
65
+ <div class="mw-scope-tools mw-open">
66
+ <label for="mw-open">open a case file</label> <select id="mw-open"></select>
67
+ </div>
68
+ </section>
69
+
70
+ <div class="mw-side">
71
+ <section class="mw-panel" aria-labelledby="mw-sweep-h">
72
+ <span class="mw-label" id="mw-sweep-h">next sweep</span>
73
+ <div class="mw-fuel">
74
+ <div>
75
+ <div class="mw-count mw-num" id="mw-cd"></div>
76
+ <div class="mw-when" id="mw-when"></div>
77
+ </div>
78
+ <span class="mw-sp" style="flex:1"></span>
79
+ <div>
80
+ <div class="mw-tank" id="mw-tank"><i id="mw-tank-fill"></i></div>
81
+ <div class="mw-left mw-num" id="mw-left"></div>
82
+ </div>
83
+ </div>
84
+ </section>
85
+ <section class="mw-panel" aria-labelledby="mw-reps-h">
86
+ <span class="mw-label" id="mw-reps-h">reports in · this week</span>
87
+ <div class="mw-xp"><span class="mw-num" id="mw-xpn"></span><span class="mw-xp__b"><i id="mw-xpb" style="width:0"></i></span><span>read</span></div>
88
+ <div id="mw-reps"></div>
89
+ </section>
90
+ </div>
91
+ </div>
92
+ </main>
93
+
94
+ <footer class="app-foot" id="app-foot"></footer>
95
+ </div>
96
+ </div>
97
+ </div>
98
+
99
+ <dialog class="mw-dialog" id="mw-propose" aria-label="Propose a change"></dialog>
100
+ <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
101
+
102
+ <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
103
+ <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
104
+ <script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
105
+ <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
106
+ <script src="/builders/palette.js" defer></script>
107
+ <script src="/builders/hall-kit.js"></script>
108
+ <script src="/builders/market-watch-lib.js"></script>
109
+ <script src="/builders/market-watch.js"></script>
110
+ </body>
111
+ </html>