@bongos/core 1.21.35 → 1.21.37

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 (78) hide show
  1. package/.bongos-core.json +100 -80
  2. package/clients/bongos-client/index.d.ts +3 -3
  3. package/docs/api/openapi.json +25 -35
  4. package/docs/api-reference.md +1 -1
  5. package/docs/copy-inventory.md +8 -8
  6. package/docs/copy-registry.json +8 -8
  7. package/docs/module-api-changelog.md +4 -0
  8. package/docs/page-inventory.json +4 -1
  9. package/docs/page-readings.json +569 -523
  10. package/modules/autonomy/matter-acts.js +41 -0
  11. package/modules/autonomy/routes/autonomy.js +2 -0
  12. package/modules/government/board.js +4 -0
  13. package/modules/government/db.js +14 -0
  14. package/modules/government/decision-board.js +90 -11
  15. package/modules/government/docket-vocab.js +5 -0
  16. package/modules/government/docket.js +11 -0
  17. package/modules/government/routes/government.js +2 -1
  18. package/modules/hall-ui/public/atlas.html +3 -3
  19. package/modules/hall-ui/public/blockers.html +3 -3
  20. package/modules/hall-ui/public/board-room.html +3 -3
  21. package/modules/hall-ui/public/city.html +3 -3
  22. package/modules/hall-ui/public/collab.html +3 -3
  23. package/modules/hall-ui/public/commands.html +3 -3
  24. package/modules/hall-ui/public/copy-desk.html +3 -3
  25. package/modules/hall-ui/public/craft-mode.js +44 -6
  26. package/modules/hall-ui/public/deploy.html +3 -3
  27. package/modules/hall-ui/public/diagrams.html +3 -3
  28. package/modules/hall-ui/public/drachmae.html +3 -3
  29. package/modules/hall-ui/public/fleet.html +3 -3
  30. package/modules/hall-ui/public/gate.html +3 -3
  31. package/modules/hall-ui/public/goal-map.html +3 -3
  32. package/modules/hall-ui/public/goals.html +3 -3
  33. package/modules/hall-ui/public/government.html +3 -3
  34. package/modules/hall-ui/public/idea.html +3 -3
  35. package/modules/hall-ui/public/ideas.html +3 -3
  36. package/modules/hall-ui/public/index.html +3 -3
  37. package/modules/hall-ui/public/modules.html +3 -3
  38. package/modules/hall-ui/public/primer.html +3 -3
  39. package/modules/hall-ui/public/profile.html +3 -3
  40. package/modules/hall-ui/public/project-settings.html +3 -3
  41. package/modules/hall-ui/public/ranks.html +3 -3
  42. package/modules/hall-ui/public/roadmap.html +3 -3
  43. package/modules/hall-ui/public/roster.html +3 -3
  44. package/modules/hall-ui/public/sessions.html +3 -3
  45. package/modules/hall-ui/public/settings.html +3 -3
  46. package/modules/hall-ui/public/street-card.js +62 -2
  47. package/modules/hall-ui/public/street-desk.js +101 -8
  48. package/modules/hall-ui/public/street-inside.js +6 -3
  49. package/modules/hall-ui/public/street.css +15 -0
  50. package/modules/hall-ui/public/street.html +3 -3
  51. package/modules/hall-ui/public/street.js +73 -4
  52. package/modules/hall-ui/public/street.states.json +124 -0
  53. package/modules/hall-ui/public/studio.html +3 -3
  54. package/modules/hall-ui/public/style.css +121 -2
  55. package/modules/hall-ui/public/task.html +3 -3
  56. package/modules/hall-ui/public/thinking.html +3 -3
  57. package/modules/hall-ui/public/tweak-editor.html +3 -3
  58. package/modules/hall-ui/public/watch.html +3 -3
  59. package/modules/hall-ui/public/work.html +3 -3
  60. package/modules/hall-ui/records/craft-mode.md +8 -0
  61. package/modules/ideas/governor-docket.js +91 -0
  62. package/modules/lifecycle/governor-docket.js +69 -0
  63. package/modules/lifecycle/matter-acts.js +63 -0
  64. package/modules/lifecycle/routes/done-when.js +26 -12
  65. package/modules/lifecycle/routes/gate-approvals.js +74 -59
  66. package/modules/lifecycle/routes/lifecycle.js +2 -0
  67. package/package-lock.json +2 -2
  68. package/package.json +1 -1
  69. package/release-notes.json +16 -0
  70. package/src/bongos/route-rank-check.js +2 -0
  71. package/src/bongos/routes/builders.js +56 -1
  72. package/src/module-api.js +1 -1
  73. package/tests/craft_mode_looks.mjs +170 -0
  74. package/tests/craft_mode_nav.mjs +7 -5
  75. package/tests/governor_matter_acts.mjs +241 -0
  76. package/tests/hall_street_desk.mjs +244 -4
  77. package/tests/hall_street_inside.mjs +3 -3
  78. package/tests/hall_tweak_editor.mjs +1 -1
@@ -225,9 +225,9 @@
225
225
 
226
226
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
227
227
 
228
- <script src="/builders/shell-trail.js?v=2026-10-03-nav"></script>
229
- <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
230
- <script src="/builders/shell.js?v=2026-10-03-nav"></script>
228
+ <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
229
+ <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
230
+ <script src="/builders/shell.js?v=2026-10-03-looks"></script>
231
231
  <script src="/builders/palette.js" defer></script>
232
232
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
233
233
  <!-- task 1003069 (R08) — the quick-filing form's routed result links a task
@@ -106,9 +106,9 @@
106
106
  <script src="/builders/brand-apply.js?v=2026-06-27-r75" defer></script>
107
107
  <!-- The app shell (sidebar + top bar + view router). Dependency-free; loaded
108
108
  BEFORE builders.js so the chrome ids exist when events are wired. -->
109
- <script src="/builders/shell-trail.js?v=2026-10-03-nav" defer></script>
110
- <script src="/builders/craft-mode.js?v=2026-10-03-nav" defer></script>
111
- <script src="/builders/shell.js?v=2026-10-03-nav" defer></script>
109
+ <script src="/builders/shell-trail.js?v=2026-10-03-looks" defer></script>
110
+ <script src="/builders/craft-mode.js?v=2026-10-03-looks" defer></script>
111
+ <script src="/builders/shell.js?v=2026-10-03-looks" defer></script>
112
112
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
113
113
  <script src="/builders/palette.js" defer></script>
114
114
  <!-- task 2166: the shared component kit (window.OTBKit) — pager/ledger/rail.
@@ -101,9 +101,9 @@
101
101
 
102
102
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
103
103
 
104
- <script src="/builders/shell-trail.js?v=2026-10-03-nav"></script>
105
- <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
106
- <script src="/builders/shell.js?v=2026-10-03-nav"></script>
104
+ <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
105
+ <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
106
+ <script src="/builders/shell.js?v=2026-10-03-looks"></script>
107
107
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
108
108
  <script src="/builders/palette.js" defer></script>
109
109
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
@@ -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-nav"></script>
119
- <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
120
- <script src="/builders/shell.js?v=2026-10-03-nav"></script>
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>
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-nav"></script>
210
- <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
211
- <script src="/builders/shell.js?v=2026-10-03-nav"></script>
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>
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-nav"></script>
62
- <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
63
- <script src="/builders/shell.js?v=2026-10-03-nav"></script>
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>
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-nav"></script>
81
- <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
82
- <script src="/builders/shell.js?v=2026-10-03-nav"></script>
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>
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-nav"></script>
45
- <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
46
- <script src="/builders/shell.js?v=2026-10-03-nav"></script>
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>
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-nav"></script>
57
- <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
58
- <script src="/builders/shell.js?v=2026-10-03-nav"></script>
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>
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-nav"></script>
115
- <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
116
- <script src="/builders/shell.js?v=2026-10-03-nav"></script>
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>
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-nav"></script>
439
- <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
440
- <script src="/builders/shell.js?v=2026-10-03-nav"></script>
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>
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. -->
@@ -512,6 +512,60 @@
512
512
  ACT_ROUTES.backup = { backup: function () { return post('/backup/trigger', {}); } };
513
513
  ACT_ROUTES.invite = { invite: function (id, inp) { return str(inp.option).trim() ? post('/access-requests/invite', { github_login: str(inp.option).trim() }) : null; } };
514
514
  ACT_ROUTES.autonomy_fence = ACT_ROUTES.autobongos_fence;
515
+ // The Planning desks (task 1004560). A blocker is decided by one of its chips,
516
+ // whose words are the resolution note; a criterion proposal ratifies in one
517
+ // tap; a Full Idea the board carried becomes a goal in the version its chip names.
518
+ ACT_ROUTES.blockers = {
519
+ decide: function (id, inp) { return str(inp.option).trim() ? post('/blockers/' + id + '/resolve', { resolution_note: 'Decided on the floor desk: ' + str(inp.option).trim() + '.' }) : null; },
520
+ };
521
+ // task 1004561: a builder's monthly limit (PATCH /builders/:id/budget; the
522
+ // 'none' chip clears it), and a sign-off undone (POST /done-when/:id/unsatisfy,
523
+ // the chip's words as the reason it logs).
524
+ ACT_ROUTES.builder_budget = {
525
+ budget: function (id, inp) { return inp.option ? call('PATCH', '/builders/' + id + '/budget', { monthly_budget_usd: inp.option === 'none' ? null : Number(inp.option) }) : null; },
526
+ };
527
+ ACT_ROUTES.signed_criterion = { unsign: function (id, inp) { return post('/done-when/' + id + '/unsatisfy', { reason: str(inp.label) || 'undone on the floor desk' }); } };
528
+ // The same acts at a building a sitting decides (task 1004561): filed as the
529
+ // building's matter, which carries the act out once it passes. Each returns
530
+ // { title, action } for POST /government/board/matters.
531
+ var COUNCIL_ACTS = {
532
+ autobongos_fence: {
533
+ switch_on: function () { return { title: 'Switch unattended building on', action: { type: 'switch_on' } }; },
534
+ switch_off: function () { return { title: 'Switch unattended building off', action: { type: 'switch_off' } }; },
535
+ allow: function (id, inp) { return inp.option ? { title: 'Allow ' + str(inp.label || 'goal ' + inp.option) + ' to build unattended', action: { type: 'allow_goal', goal_id: Number(inp.option) } } : null; },
536
+ },
537
+ criteria_uat: { force: function (id, inp, who) { return { title: 'Sign off: ' + who, action: { type: 'satisfy_criterion', criterion_id: Number(id) } }; } },
538
+ override_requests: { release: function (id, inp, who) { return { title: 'Release past its check: ' + who, action: { type: 'approve_override', request_id: Number(id) } }; } },
539
+ gate_approvals: { release: function (id, inp, who) { return { title: 'Release from the gate: ' + who, action: { type: 'approve_gate', pr: Number(id) } }; } },
540
+ builder_budget: {
541
+ budget: function (id, inp, who) { return inp.option ? { title: who + ': ' + str(inp.label || inp.option), action: { type: 'set_budget', builder_id: Number(id), monthly_usd: inp.option === 'none' ? null : Number(inp.option) } } : null; },
542
+ },
543
+ };
544
+ COUNCIL_ACTS.autonomy_fence = COUNCIL_ACTS.autobongos_fence;
545
+ COUNCIL_ACTS.autonomy_goal = { allow: COUNCIL_ACTS.autobongos_fence.allow };
546
+ function councilRequest(s, key, inp) {
547
+ var make = COUNCIL_ACTS[s.kind] && COUNCIL_ACTS[s.kind][key];
548
+ var m = make ? make(decoded(s.id), inp, str(s.who) || 'a matter') : null;
549
+ return m ? post('/government/board/matters', { area: s.council, title: str(m.title).slice(0, 200), action: m.action }) : null;
550
+ }
551
+ ACT_ROUTES.criterion_proposal = { ratify: function (id) { return post('/inbox/' + id + '/ratify', { notes_md: 'Ratified on the floor desk.' }); } };
552
+ ACT_ROUTES.full_idea = {
553
+ ratify_goal: function (id, inp) { return inp.option ? post('/inbox/' + id + '/ratify-goal', { target_version: str(inp.option), notes_md: 'Ratified on the floor desk.' }) : null; },
554
+ };
555
+ // The Priorities desk's save (task 1004560): the new order goes to
556
+ // POST /goals/reorder, or, where the decision table puts Planning to a sitting,
557
+ // it is filed as a Planning matter for the sitting to decide. save is the desk
558
+ // model's { set, ids, names, council }.
559
+ function orderRequest(save) {
560
+ var s = save || {};
561
+ var ids = (s.ids || []).map(Number).filter(function (n) { return n > 0 && Math.floor(n) === n; });
562
+ if (!ids.length) return null;
563
+ if (s.council) {
564
+ var lines = (s.names || []).map(function (n, i) { return (i + 1) + '. ' + str(n); });
565
+ return post('/government/board/matters', { area: 'planning', title: 'A new order for the goals', detail_md: 'Proposed on the floor desk, for version ' + str(s.set) + ':\n\n' + lines.join('\n') });
566
+ }
567
+ return post('/goals/reorder', { version_id: str(s.set), ordered_ids: ids });
568
+ }
515
569
  function decoded(encoded) { return decodeURIComponent(encoded); }
516
570
  // input: { reason, section } for object; { text } for revise.
517
571
  function request(cv, key, input) {
@@ -541,6 +595,7 @@
541
595
  if (key === 'fine') return post('/goals/' + enc(s.goalId) + '/requests/' + enc(s.id) + '/respond', { action: 'approve' });
542
596
  if (key === 'refuse') return post('/goals/' + enc(s.goalId) + '/requests/' + enc(s.id) + '/respond', { action: 'deny' });
543
597
  }
598
+ if (s.council && COUNCIL_ACTS[s.kind] && COUNCIL_ACTS[s.kind][key]) return councilRequest(s, key, inp);
544
599
  if (ACT_ROUTES[s.kind] && ACT_ROUTES[s.kind][key]) return ACT_ROUTES[s.kind][key](enc(s.id), inp);
545
600
  if (s.kind === 'override_requests' && key === 'release') return post('/override-requests/' + enc(s.id) + '/decide', { decision: 'approve' });
546
601
  if (s.kind === 'gate_approvals' && key === 'release') return post('/gate-approvals/' + enc(s.id) + '/approve', {});
@@ -565,6 +620,9 @@
565
620
  var r = res || {};
566
621
  var who = cv && cv.subject && cv.subject.who ? cv.subject.who : 'They';
567
622
  var b = cv ? cv.building : null;
623
+ if (cv && cv.subject && cv.subject.council && COUNCIL_ACTS[cv.subject.kind] && COUNCIL_ACTS[cv.subject.kind][key]) {
624
+ return { toast: 'Sent to the council. It is carried out when they pass it.', building: b, kind: 'light', close: false };
625
+ }
568
626
  if (key === 'for') {
569
627
  if (r.item_closed && r.outcome === 'passed') return { toast: 'Passed. It is in force from today.', building: b, kind: 'rise', close: false };
570
628
  if (r.item_closed) return { toast: 'The sitting closed: it went back to its author.', building: b, kind: 'settle', close: false };
@@ -594,8 +652,10 @@
594
652
  tier: '{who} has a new tier.', deactivate: '{who} is deactivated.', give: 'Rank given.', take: 'Rank taken back.',
595
653
  check: 'Checking it. The answer lands on this floor in a couple of minutes; nothing moves.', take_core: 'Taking it. The core moves on the runner\'s next tick.',
596
654
  rot: 'Rot timer set.', module_on: 'Turned on.', module_off: 'Turned off.', backup: 'Backing up now.', invite: '{who} is invited.',
655
+ decide: 'Decided. The work it held moves again.', ratify: 'Ratified. The promise is on its version.', ratify_goal: 'Ratified. It is a goal now.',
656
+ budget: '{who} has a new monthly limit.', unsign: 'Undone. The promise is open again.',
597
657
  };
598
- var ACT_KINDS = { check: 'light', rot: 'light', module_on: 'rise', module_off: 'settle', backup: 'light', confirm: 'settle', dispute: 'settle', fix: 'rise', remove: 'settle', keep: 'settle', signoff: 'rise', force: 'rise', switch_on: 'rise', switch_off: 'settle', allow: 'rise', disallow: 'settle', first: 'light', pause: 'settle', resume: 'rise', tier: 'light', deactivate: 'settle', give: 'light', take: 'settle' };
658
+ var ACT_KINDS = { budget: 'light', unsign: 'settle', decide: 'rise', ratify: 'rise', ratify_goal: 'rise', check: 'light', rot: 'light', module_on: 'rise', module_off: 'settle', backup: 'light', confirm: 'settle', dispute: 'settle', fix: 'rise', remove: 'settle', keep: 'settle', signoff: 'rise', force: 'rise', switch_on: 'rise', switch_off: 'settle', allow: 'rise', disallow: 'settle', first: 'light', pause: 'settle', resume: 'rise', tier: 'light', deactivate: 'settle', give: 'light', take: 'settle' };
599
659
 
600
660
  // A refused act, in words. The server's own message wins when it has one.
601
661
  var REFUSALS = {
@@ -786,7 +846,7 @@
786
846
 
787
847
  var api = {
788
848
  API: API, SECTIONS: SECTIONS, needsRoom: needsRoom, itemIdOf: itemIdOf, subjectKind: subjectKind,
789
- model: model, actsCard: actsCard, request: request, check: check, answer: answer, refusal: refusal, render: render,
849
+ model: model, actsCard: actsCard, request: request, orderRequest: orderRequest, check: check, answer: answer, refusal: refusal, render: render,
790
850
  memberWords: memberWords, windowWords: windowWords,
791
851
  // the wording change, for a drafted amendment's struck and inserted text (task 1004530)
792
852
  change: changeOf,
@@ -38,10 +38,20 @@
38
38
  // GovDesk.fit(s, n, words) the line formatter
39
39
  // GovDesk.breaks(desk) [] when the desk keeps the line rule
40
40
  // GovDesk.todayPips(n) the street header's meter
41
+ // GovDesk.moved(order, key, by) a desk order with one row moved
41
42
  // GovDesk.LIMITS, ACT_WORDS, CHIPS, NOUNS, CLEAR
42
43
  // A desk row is { key, matter, ini, name, fact, facts[], actions[{ key, label,
43
44
  // tone, go, ask }], chips { for, ask, options[{ key, label, input }] } | null,
44
45
  // wait, needs, card }: `card` is the GovMatterCard model the act is sent with.
46
+ //
47
+ // THE PLANNING DESKS (task 1004560). A row may carry `pips` [{ key, on }]: a
48
+ // goal's promises kept of total, or a vote's seats (filled = a yes). A row of an
49
+ // ordered set (the Priorities desk: `order` is its version) is moved with its
50
+ // two arrows; input.order is the order the viewer has made, and a changed order
51
+ // gives the desk a `save` { line, label, set, ids, names, council }: the
52
+ // council when the decision table puts Planning to a sitting that cannot act
53
+ // alone, so the save files a Planning matter instead of setting the order. A
54
+ // blocker's chips stand open (`show`): deciding it IS picking one.
45
55
 
46
56
  (function (root) {
47
57
  'use strict';
@@ -54,7 +64,7 @@
54
64
  nameWords: 3, name: 20, fact: 26, facts: 3, factLen: 24, actions: 2,
55
65
  tiles: 3, tileValue: 8, tileWords: 2, tileLabel: 14,
56
66
  status: 44, clearLine: 30, clearSub: 40, record: 4, recordLen: 30,
57
- chips: 5, chip: 16, todayTarget: 10,
67
+ chips: 5, chip: 16, todayTarget: 10, pips: 12, saveLine: 48,
58
68
  });
59
69
 
60
70
  // One line, never more: the house rule for every row, tile and status. A cap
@@ -105,15 +115,29 @@
105
115
  switch_on: 'switch on', switch_off: 'switch off', allow: 'allow', disallow: 'disallow', first: 'first',
106
116
  pause: 'pause', resume: 'resume', tier: 'tier', deactivate: 'deactivate', give: 'give', take: 'take back',
107
117
  check: 'check', take_core: 'take it', rot: 'set', module_on: 'turn on', module_off: 'turn off', backup: 'back up', invite: 'invite', open: 'open',
118
+ // the Planning desks (task 1004560)
119
+ decide: 'decide', ratify: 'ratify', ratify_goal: 'make goal', up: 'move up', down: 'move down',
120
+ // task 1004561: a builder's monthly limit, and a sign-off undone
121
+ budget: 'set limit', unsign: 'undo',
108
122
  });
109
123
  // What a row's chips ask, per act (a reason or an option is one tap, never text).
110
- const ASKS = Object.freeze({ check: 'check', take_core: 'move to', rot: 'stale after', module_off: 'sure', invite: 'their GitHub name', object: 'why', force: 'why', switch_off: 'why', pause: 'why', deactivate: 'why', signoff: 'confirm', allow: 'which goal', tier: 'which tier', give: 'to whom', take: 'from whom' });
124
+ const ASKS = Object.freeze({ budget: 'monthly limit', unsign: 'why', decide: 'pick one', ratify_goal: 'which version', check: 'check', take_core: 'move to', rot: 'stale after', module_off: 'sure', invite: 'their GitHub name', object: 'why', force: 'why', switch_off: 'why', pause: 'why', deactivate: 'why', signoff: 'confirm', allow: 'which goal', tier: 'which tier', give: 'to whom', take: 'from whom' });
111
125
  // How a floor counts rows nobody is waiting on (desk rows), by its desk.
112
126
  const DESK_NOUNS = Object.freeze({
113
127
  'people.builders': 'builders', 'people.ranks': 'ranks', 'disputes.confirmed': 'to fix',
114
128
  'automation.switch': 'switch', 'automation.goals': 'allowed', 'automation.runners': 'runners',
115
129
  'people.invite': 'invites', 'release.core': 'update', 'rules.settings': 'settings', 'rules.modules': 'modules', 'treasury.backup': 'backups',
130
+ 'planning.order': 'goals in order', 'planning.ratify': 'to ratify',
131
+ 'treasury.budgets': 'builders', 'review.signed': 'signed off',
116
132
  });
133
+ // Is this building decided by a sitting that cannot act alone (task 1004561)?
134
+ // The decision table's row, as the docket's buildings carry it: then a desk act
135
+ // is filed as the building's matter, and carried out once the sitting passes it.
136
+ function councilOf(docket, key) {
137
+ const b = docket && Array.isArray(docket.buildings) ? docket.buildings.find((x) => x && x.key === key) : null;
138
+ const d = b && b.decision ? b.decision : null;
139
+ return !!(d && d.sitting && ['decide', 'first'].indexOf(d.rule) < 0);
140
+ }
117
141
  // An objection's reason as one tap: the five sections the server accepts
118
142
  // (street-card.js SECTIONS), each in at most 16 characters.
119
143
  const CHIPS = Object.freeze({ scope: 'what it changes', purpose: 'its purpose', why_matters: 'why it matters', fit: 'how it fits', success: 'how to measure' });
@@ -185,6 +209,8 @@
185
209
  });
186
210
  }
187
211
  const cv = C && C.model ? C.model({ docket: ctx.docket, key: m.id, room: ctx.room, constitution: ctx.constitution, now: now, viewerId: ctx.viewerId }) : null;
212
+ // a release held for a sitting goes to it as a matter (task 1004561)
213
+ if (cv && cv.subject && cv.subject.itemId == null) cv.subject.council = councilOf(ctx.docket, cv.building) ? cv.building : null;
188
214
  const actions = [];
189
215
  let chips = null;
190
216
  if (cv && !cv.readOnly && !cv.loading) {
@@ -200,6 +226,8 @@
200
226
  return {
201
227
  key: str(m.id), matter: str(m.id), ini: initials(name), name: name, fact: fact, facts: facts,
202
228
  actions: actions, chips: chips, choices: chips ? { [chips.for]: chips } : {}, wait: wait, needs: stateOf(m) === 'needs', source: m.source, opened: m.opened_at || null, card: cv,
229
+ // a vote shows its seats: one pip each, filled for a yes
230
+ pips: cv && Array.isArray(cv.seats) && cv.seats.length ? cv.seats.slice(0, LIMITS.pips).map((s, i) => ({ key: 's' + i, on: s.pos === 'for' })) : null,
203
231
  };
204
232
  }
205
233
 
@@ -210,6 +238,8 @@
210
238
  // goes to the matter's own page (a sign-off that needs a recording).
211
239
  function actedRow(acts, card, ctx) {
212
240
  const titles = new Map((ctx.goals || []).map((g) => [str(g.id), str(g.title || 'goal ' + g.id)]));
241
+ // an act at a building a sitting decides is filed as its matter (task 1004561)
242
+ if (card && card.subject) card.subject.council = councilOf(ctx.docket, card.building) ? card.building : null;
213
243
  const actions = [];
214
244
  const choices = {};
215
245
  acts.forEach((a) => {
@@ -232,7 +262,9 @@
232
262
  actions.push({ key: a.key, label: ACT_WORDS[word], tone: actions.length ? 'quiet' : 'primary', go: goes ? card.href : null, ask: ask });
233
263
  });
234
264
  const first = actions.find((a) => a.ask);
235
- return { actions: actions, chips: first ? choices[first.key] : null, choices: choices, wait: actions.length ? '' : 'not from here', card: card };
265
+ // deciding a blocker is picking one of its options: its chips stand open
266
+ const show = first && first.key === 'decide' && choices.decide ? 'decide' : null;
267
+ return { actions: actions, chips: first ? choices[first.key] : null, choices: choices, show: show, wait: actions.length ? '' : 'not from here', card: card };
236
268
  }
237
269
 
238
270
  // A desk row (task 1004559): something on this floor the viewer may act on that
@@ -253,6 +285,8 @@
253
285
  return Object.assign(actedRow(d.acts || [], card || { href: null }, ctx), {
254
286
  key: str(d.id), matter: str(d.id), ini: initials(name), name: name, fact: fit(d.fact, LIMITS.fact), facts: facts,
255
287
  needs: d.needs === true, source: d.desk, opened: d.opened_at || null, desk: true, tool: d.tool === true, over: d.over || null,
288
+ order: d.set || null, goalId: d.set && ref.id != null ? str(ref.id) : null, full: full,
289
+ pips: d.pips ? Array.from({ length: Math.min(d.pips.of, LIMITS.pips) }, (_, i) => ({ key: 'p' + i, on: i < d.pips.on })) : null,
256
290
  });
257
291
  }
258
292
  // The desk rows of one floor: those of the desk its PLAN entry names.
@@ -292,13 +326,27 @@
292
326
  const spec = plan.floors[floor];
293
327
  const fl = I.floorFor(spec, { key: o.key, docket: docket, goals: o.goals || [], now: o.now || 0 });
294
328
  const ctx = { room: o.room, constitution: o.constitution, now: o.now || 0, viewerId: o.viewerId, done: o.done, goals: o.goals || [] };
295
- const rows = docket ? rowsFor(o.key, floor, docket, ctx) : [];
329
+ let rows = docket ? rowsFor(o.key, floor, docket, ctx) : [];
330
+ // the Priorities desk: the ordered rows in the order the viewer has made
331
+ const ordered = rows.filter((r) => r.order);
332
+ let save = null;
333
+ if (ordered.length > 1) {
334
+ const base = ordered.map((r) => r.key);
335
+ const want = Array.isArray(o.order) ? o.order.map(str) : null;
336
+ const mine = want && want.length === base.length && base.every((k) => want.indexOf(k) >= 0) ? want : base;
337
+ const sorted = mine.map((k) => ordered.find((r) => r.key === k));
338
+ sorted.forEach((r, i) => { r.pos = i + 1; r.top = i === 0; r.bottom = i === sorted.length - 1; });
339
+ rows = rows.filter((r) => !r.order).concat(sorted);
340
+ if (mine.join('|') !== base.join('|')) save = saveOf(docket, o.key, sorted);
341
+ }
296
342
  const bind = spec[2] || {};
297
343
  const src = bind.source ? bind.source : bind.floor === 'planning.ideas' ? 'idea' : floor === 0 ? 'board' : null;
298
344
  const needs = rows.filter((r) => r.needs);
299
345
  const noun = NOUNS[src] || 'open';
300
346
  // rows nobody waits on: 'moving' for matters, the desk's own noun for desk rows
301
- const moving = rows.length && rows.every((r) => r.desk) ? (DESK_NOUNS[rows[0].source] || 'here') : 'moving';
347
+ // (rows waiting on no one, when all of them are desk rows, count in their desk's noun: 'to ratify')
348
+ const rest = rows.filter((r) => !r.needs && !r.tool);
349
+ const moving = rest.length && rest.every((r) => r.desk) ? (DESK_NOUNS[rest[0].source] || 'here') : 'moving';
302
350
  const counted = rows.filter((r) => !r.tool);
303
351
  const oldestOf = (list) => list.reduce((a, r) => (r.opened && (!a || Date.parse(r.opened) < Date.parse(a)) ? r.opened : a), null);
304
352
  const cleared = !!o.cleared && rows.length === 0;
@@ -320,7 +368,13 @@
320
368
  const words = str(l).replace(/\s+/g, ' ').trim().split(' ').slice(0, LIMITS.tileWords).join(' ').replace(/[,.;:·-]+$/, '');
321
369
  tiles.push({ key: 't' + tiles.length, v: fit(val, LIMITS.tileValue), l: fit(words, LIMITS.tileLabel) });
322
370
  };
323
- if (rows.length) { tile(needs.length, noun); tile(counted.length - needs.length, moving); }
371
+ if (ordered.length > 1) {
372
+ // the order's one number: the share of its goals' promises kept
373
+ const on = ordered.reduce((a, r) => a + (r.pips || []).filter((p) => p.on).length, 0);
374
+ const of = ordered.reduce((a, r) => a + (r.pips || []).length, 0);
375
+ tile(needs.length, noun);
376
+ if (of) tile(Math.round((100 * on) / of) + '%', 'promises kept');
377
+ } else if (rows.length) { tile(needs.length, noun); tile(counted.length - needs.length, moving); }
324
378
  else if (fl.count) tile(fl.count, str(fl.line).replace(/^[$]?[0-9][0-9,.%]*\s*/, '') || 'on this floor');
325
379
  tile(history.length, 'on record');
326
380
  const record = (o.mine || []).map((t, i) => ({ key: 'm' + i, text: fit(t + ' · now', LIMITS.recordLen), mine: true }))
@@ -330,13 +384,50 @@
330
384
  key: o.key + ':' + floor, building: o.key, floor: floor, name: spec[0], sym: o.sym || { d: '', f: '' },
331
385
  status: fit(status, LIMITS.status), hot: hot,
332
386
  tiles: tiles.slice(0, LIMITS.tiles), rows: rows,
333
- quiet: quiet, cleared: cleared,
387
+ quiet: quiet, cleared: cleared, save: save,
334
388
  clearLine: fit(cleared ? (CLEAR[src] || 'Floor clear') : 'Nothing needs you here', LIMITS.clearLine),
335
389
  clearSub: fit(cleared ? 'lit up · ' + (o.today || 0) + ' decided today' : fl.line, LIMITS.clearSub),
336
390
  record: record,
337
391
  };
338
392
  }
339
393
 
394
+ // ---------- the Priorities desk's save ----------
395
+ // Who decides the order: the decision table's Planning row, as the docket's
396
+ // buildings carry it. A sitting whose rule cannot act alone means the save
397
+ // files a matter for it; the yes it needs is read from its seats.
398
+ function yesNeeded(rule, n) {
399
+ if (!(n > 0)) return null;
400
+ if (rule === 'majority') return Math.floor(n / 2) + 1;
401
+ if (rule === 'unanimous') return n;
402
+ if (rule === 'signoff') return 1;
403
+ return null;
404
+ }
405
+ function saveOf(docket, key, sorted) {
406
+ const b = docket && Array.isArray(docket.buildings) ? docket.buildings.find((x) => x && x.key === key) : null;
407
+ const d = b && b.decision ? b.decision : null;
408
+ const council = councilOf(docket, key);
409
+ const yes = council ? yesNeeded(d.rule, Number(b.holder_count)) : null;
410
+ const line = !council ? 'new order · saved for everyone'
411
+ : d.rule === 'consent' ? 'new order · to the council · unless one objects'
412
+ : 'new order · goes to the council' + (yes ? ' · ' + yes + ' yes needed' : '');
413
+ // what the toast says once it is sent: whole sentences, never the line cut up
414
+ const said = !council ? 'Saved. Goals are worked in this order now.'
415
+ : d.rule === 'consent' ? 'The new order is with the council. It passes unless someone objects.'
416
+ : 'The new order is with the council.' + (yes ? ' It needs ' + yes + ' yes.' : '');
417
+ return {
418
+ line: fit(line, LIMITS.saveLine), label: council ? 'send to council' : 'save order', council: council, said: said,
419
+ set: sorted[0].order, ids: sorted.map((r) => r.goalId), names: sorted.map((r) => r.full || r.name),
420
+ };
421
+ }
422
+ // An order with one row moved one place (by -1 up, +1 down); the ends hold.
423
+ function moved(order, key, by) {
424
+ const o = (order || []).slice();
425
+ const i = o.indexOf(key), j = i + by;
426
+ if (i < 0 || j < 0 || j >= o.length) return o;
427
+ o[i] = o[j]; o[j] = key;
428
+ return o;
429
+ }
430
+
340
431
  // ---------- the rule as a checker ----------
341
432
  function breaks(desk) {
342
433
  const out = [];
@@ -364,7 +455,9 @@
364
455
  if ((!c.field && !c.options.length) || c.options.length > LIMITS.chips || c.options.some((x) => !x.label || x.label.length > LIMITS.chip)) bad('chips', c.options.map((x) => x.label));
365
456
  });
366
457
  if (r.actions.some((a) => a.ask && !(r.choices || {})[a.key])) bad('an ask without its own chips', r.name);
458
+ if (r.pips && r.pips.length > LIMITS.pips) bad('more than twelve pips', r.pips.length);
367
459
  });
460
+ if (desk.save && (!oneLine(desk.save.line) || desk.save.line.length > LIMITS.saveLine)) bad('save line', desk.save.line);
368
461
  if (desk.record.length > LIMITS.record || desk.record.some((x) => !oneLine(x.text) || x.text.length > LIMITS.recordLen)) bad('record line', desk.record.map((x) => x.text));
369
462
  return out;
370
463
  }
@@ -379,7 +472,7 @@
379
472
 
380
473
  const api = {
381
474
  LIMITS: LIMITS, ACT_WORDS: ACT_WORDS, ASKS: ASKS, DESK_NOUNS: DESK_NOUNS, CHIPS: CHIPS, NOUNS: NOUNS, CLEAR: CLEAR,
382
- fit: fit, ageWord: ageWord, floorMatters: floorMatters, firstNeeds: firstNeeds, rowsFor: rowsFor, deskModel: deskModel, breaks: breaks, todayPips: todayPips,
475
+ fit: fit, ageWord: ageWord, floorMatters: floorMatters, firstNeeds: firstNeeds, rowsFor: rowsFor, deskModel: deskModel, breaks: breaks, todayPips: todayPips, moved: moved,
383
476
  };
384
477
  root.GovDesk = api;
385
478
  if (typeof module !== 'undefined' && module.exports) module.exports = api;
@@ -112,6 +112,8 @@
112
112
  const PLAN_SYMBOLS = {
113
113
  'Rules in force': 'gavel', 'New rules': 'wrench', 'Security reports': 'shield', 'Removals': 'removal', 'Grade appeals': 'scales',
114
114
  'Goals': 'flag', 'Promises': 'seal', 'Spending matters': 'coins',
115
+ // the Desks board's name for the blockers floor (task 1004560)
116
+ 'Waiting on you': 'hourglass',
115
117
  };
116
118
  function floorSymbol(name) {
117
119
  return PROTO_SYMBOLS[name] || PLAN_SYMBOLS[name] || 'crate';
@@ -346,7 +348,8 @@
346
348
  planning: {
347
349
  holds: 'Goals, and the promises that say when each one is done. Kept promises are what the project owes its users.', rule: 'majority', cap: 9,
348
350
  growth: 'When it fills, the next new goal opens a second planning office.', annex: 'room for a second planning office',
349
- floors: [['Goals', 'the open goals', { floor: 'planning.goals', goals: true }], ['Promises', 'what the goals say they deliver', { reading: 'promises' }], ['Ideas to ratify', 'full ideas waiting for a vote', { floor: 'planning.ideas' }], ['Priorities', 'the order goals are worked in', { floor: 'planning.priorities' }], ['Versions', 'what each version holds', { floor: 'planning.versions' }], ['Waiting on a decision', 'blockers holding work', { source: 'blockers' }]],
351
+ // the three desks first, as on the Desks board (task 1004560)
352
+ floors: [['Priorities', 'the order goals are worked in', { floor: 'planning.priorities', desk: 'planning.order' }], ['Ideas to ratify', 'ideas waiting to be ratified', { floor: 'planning.ideas', desk: 'planning.ratify' }], ['Waiting on you', 'blockers holding work', { source: 'blockers' }], ['Goals', 'the open goals', { floor: 'planning.goals', goals: true }], ['Promises', 'what the goals say they deliver', { reading: 'promises' }], ['Versions', 'what each version holds', { floor: 'planning.versions' }]],
350
353
  },
351
354
  automation: {
352
355
  holds: 'AI builders working on their own. You choose which goals they may build and how much they may spend, and you can stop them at any time.', rule: 'decide', cap: 6,
@@ -356,7 +359,7 @@
356
359
  review: {
357
360
  holds: 'Testing finished work and signing it off. Work does not count until someone who did not build it says it works.', rule: 'signoff', cap: 5,
358
361
  growth: 'When one queue of testers is not enough, an annex can take design reviews.', vacantGrowth: 'Nothing is built here until the charter says who holds the seat.', annex: 'room for an annex, such as design review',
359
- floors: [['Testers', 'who signs off finished work', { floor: 'review.testers' }], ['Waiting outside', 'finished work waiting for a tester', { source: 'criteria_uat', held: 'Being tested' }], ['Signed off', 'work a tester has signed off', { floor: 'review.signed_off' }]],
362
+ floors: [['Testers', 'who signs off finished work', { floor: 'review.testers' }], ['Waiting outside', 'finished work waiting for a tester', { source: 'criteria_uat', held: 'Being tested' }], ['Signed off', 'work a tester has signed off', { floor: 'review.signed_off', desk: 'review.signed' }]],
360
363
  },
361
364
  release: {
362
365
  holds: 'Putting finished work live, and taking updates to Bongos itself.', rule: 'decide', cap: 7,
@@ -366,7 +369,7 @@
366
369
  treasury: {
367
370
  holds: 'The budget, and who gets paid for what.', rule: 'majority', cap: 7,
368
371
  growth: 'Each new spending line takes a floor.', annex: 'room for an annex of spending lines',
369
- floors: [['Builder rewards', 'paid for shipped work', { floor: 'treasury.rewards' }], ['AI grading', 'the reviewer that grades every ship', { floor: 'treasury.grading' }], ['Hosting', 'servers and storage', { floor: 'treasury.hosting', desk: 'treasury.backup' }], ['Reserve', 'what is left this month', { reading: 'money' }], ['Spending matters', 'spending put to a vote', { source: 'treasury_spending' }]],
372
+ floors: [['Builder rewards', 'paid for shipped work', { floor: 'treasury.rewards' }], ['AI grading', 'the reviewer that grades every ship', { floor: 'treasury.grading' }], ['Hosting', 'servers and storage', { floor: 'treasury.hosting', desk: 'treasury.backup' }], ['Reserve', 'what is left this month', { reading: 'money', desk: 'treasury.budgets' }], ['Spending matters', 'spending put to a vote', { source: 'treasury_spending' }]],
370
373
  },
371
374
  });
372
375
  const MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
@@ -314,6 +314,18 @@ html[data-fullscreen="on"] .street-top { padding-right: 184px; }
314
314
  .desk-chip:hover { filter: brightness(1.08); }
315
315
  .desk-chip[disabled] { opacity: 0.55; cursor: progress; }
316
316
  .desk-wait { font-size: 13px; color: var(--ink-faint); border: 1px dashed var(--street-rule-strong); border-radius: 999px; padding: 6px 12px; white-space: nowrap; }
317
+ /* the Planning desks (task 1004560): a goal's place in the order, its promise
318
+ pips (or a vote's seats), the two arrows, and the save bar under a moved order */
319
+ .desk-pos { width: 22px; flex: none; font-size: 13px; color: var(--ink-faint); font-variant-numeric: tabular-nums; }
320
+ .desk-pips { display: flex; gap: 4px; flex: none; }
321
+ .desk-pip { width: 12px; height: 12px; border-radius: 3px; box-sizing: border-box; border: 1px solid var(--street-rule-strong); }
322
+ .desk-pip--on { background: var(--ink); border-color: var(--ink); }
323
+ .desk-arrow { width: 34px; height: 34px; flex: none; border-radius: 999px; border: 1px solid var(--street-rule-strong); background: none; color: var(--ink); cursor: pointer; display: inline-flex; align-items: center; justify-content: center; padding: 0; }
324
+ .desk-arrow svg { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
325
+ .desk-arrow[disabled] { opacity: 0.55; cursor: progress; }
326
+ .desk-arrow:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
327
+ .desk-save { display: flex; align-items: center; flex-wrap: wrap; gap: 10px 14px; padding: 16px 0 0; animation: street-enter 300ms cubic-bezier(.16,1,.3,1) both; }
328
+ .desk-save__line { font-size: 13.5px; color: var(--ink-soft); margin-right: auto; }
317
329
  .desk-facts, .desk-choices { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; padding: 10px 0 2px 52px; animation: street-enter 300ms cubic-bezier(.16,1,.3,1) both; }
318
330
  .desk-fact { font-size: 13px; color: var(--ink-soft); background: var(--street-wash); border-radius: 6px; padding: 6px 10px; white-space: nowrap; }
319
331
  .desk-choices__ask { font-size: 12.5px; color: var(--ink-faint); margin-right: 4px; }
@@ -373,6 +385,9 @@ html[data-fullscreen="on"] .street-top { padding-right: 184px; }
373
385
  .inside-gov__row { flex-wrap: wrap; row-gap: 4px; }
374
386
  .desk-row__main { flex-wrap: wrap; row-gap: 8px; }
375
387
  .desk-name { flex-basis: calc(100% - 52px); }
388
+ .desk-pos ~ .desk-name { flex-basis: calc(100% - 88px); }
389
+ .desk-pos ~ .desk-pips { margin-left: 88px; }
390
+ .desk-pips ~ .desk-acts { margin-left: auto; }
376
391
  .desk-acts { margin-left: 52px; }
377
392
  .desk-facts, .desk-choices { padding-left: 0; }
378
393
  .desk-tile { min-width: 0; padding: 10px 14px; }
@@ -208,9 +208,9 @@
208
208
  </div>
209
209
  </div>
210
210
 
211
- <script src="/builders/shell-trail.js?v=2026-10-03-nav"></script>
212
- <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
213
- <script src="/builders/shell.js?v=2026-10-03-nav"></script>
211
+ <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
212
+ <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
213
+ <script src="/builders/shell.js?v=2026-10-03-looks"></script>
214
214
  <script src="/builders/palette.js" defer></script>
215
215
  <script src="/builders/city-draw.js?v=2026-10-02-street1"></script>
216
216
  <script src="/builders/street-draw.js?v=2026-10-02-street1"></script>