@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
@@ -82,7 +82,8 @@
82
82
  var view = { open: null, opener: null, model: null, scale: 1, mode: null, inside: null, insideModel: null };
83
83
  // the desk's own state: rows you acted on this visit, what you did per floor,
84
84
  // the floors you cleared, the row whose facts or chips are open
85
- var desk = { done: {}, mine: {}, cleared: {}, open: null, ask: null, busy: false, roomAsked: false, model: null };
85
+ // order: the Priorities desk's order as the viewer has moved it, per floor (task 1004560)
86
+ var desk = { done: {}, mine: {}, cleared: {}, order: {}, open: null, ask: null, busy: false, roomAsked: false, model: null };
86
87
  var TODAY_KEY = 'gov-desk-today';
87
88
  var today = { day: '', n: 0 };
88
89
 
@@ -553,7 +554,7 @@
553
554
  var D = window.GovDesk, st = view.inside;
554
555
  if (!D || !st) return;
555
556
  var k = st.key + ':' + bv.selFloor;
556
- var dm = D.deskModel({ key: st.key, floor: bv.selFloor, docket: data.docket, goals: (data.goals || []).filter(function (g) { return g && g.status === 'open'; }), now: Date.now(), room: data.room, constitution: data.constitution, done: desk.done, mine: desk.mine[k] || [], cleared: !!desk.cleared[k], today: today.n, sym: bv.sym });
557
+ var dm = D.deskModel({ key: st.key, floor: bv.selFloor, docket: data.docket, goals: (data.goals || []).filter(function (g) { return g && g.status === 'open'; }), now: Date.now(), room: data.room, constitution: data.constitution, done: desk.done, mine: desk.mine[k] || [], cleared: !!desk.cleared[k], today: today.n, sym: bv.sym, order: desk.order[k] || null });
557
558
  desk.model = dm;
558
559
  el.className = 'inside-open desk' + (dm.quiet ? ' desk--quiet' : '');
559
560
  el.setAttribute('data-desk', dm.key);
@@ -595,6 +596,14 @@
595
596
  dm.rows.forEach(function (r) { list.appendChild(deskRow(r)); });
596
597
  el.appendChild(list);
597
598
  }
599
+ // a changed order: what saving does, then undo and save (task 1004560)
600
+ if (dm.save) {
601
+ var bar = node('div', 'desk-save');
602
+ bar.appendChild(node('span', 'desk-save__line', dm.save.line));
603
+ bar.appendChild(chipBtn('desk-chip--quiet', 'undo', function () { desk.order[k] = null; draw(); }));
604
+ bar.appendChild(chipBtn('desk-chip--primary', dm.save.label, function () { saveOrder(dm, k); }));
605
+ el.appendChild(bar);
606
+ }
598
607
  // the record line: what you just did here, then what this building closed
599
608
  var rec = node('div', 'desk-record');
600
609
  rec.appendChild(node('span', 'desk-record__label', 'record'));
@@ -606,6 +615,7 @@
606
615
  var row = node('div', 'desk-row' + (r.needs ? ' desk-row--needs' : ''));
607
616
  row.setAttribute('data-row', r.key);
608
617
  var main = node('div', 'desk-row__main');
618
+ if (r.pos) main.appendChild(node('span', 'desk-pos', String(r.pos)));
609
619
  main.appendChild(node('span', 'desk-ini', r.ini));
610
620
  var name = node('button', 'desk-name');
611
621
  name.setAttribute('type', 'button');
@@ -615,10 +625,33 @@
615
625
  name.appendChild(node('span', 'desk-name__f', r.fact));
616
626
  name.addEventListener('click', function () { desk.open = desk.open === r.key ? null : r.key; desk.ask = null; draw(); focusRow(r.key, 'name'); });
617
627
  main.appendChild(name);
628
+ // promises kept of total, or a vote's seats: one pip each
629
+ if (r.pips && r.pips.length) {
630
+ var pips = node('span', 'desk-pips');
631
+ pips.setAttribute('aria-hidden', 'true');
632
+ r.pips.forEach(function (p) { pips.appendChild(node('span', 'desk-pip' + (p.on ? ' desk-pip--on' : ''))); });
633
+ main.appendChild(pips);
634
+ }
618
635
  var acts = node('span', 'desk-acts');
619
636
  if (r.wait) acts.appendChild(node('span', 'desk-wait', r.wait));
637
+ if (r.order) {
638
+ // the order's two arrows: the move is the viewer's until saved
639
+ [['up', -1, 'M6 15l6-6 6 6'], ['down', 1, 'M6 9l6 6 6-6']].forEach(function (m) {
640
+ if (!r.actions.some(function (a) { return a.key === m[0]; })) return;
641
+ var b = node('button', 'desk-arrow');
642
+ b.setAttribute('type', 'button');
643
+ b.setAttribute('aria-label', 'move ' + r.name + ' ' + m[0]);
644
+ // the top goal cannot go up, nor the last down
645
+ if (desk.busy || (m[1] < 0 && r.top) || (m[1] > 0 && r.bottom)) b.setAttribute('disabled', '');
646
+ b.appendChild(icon(m[2], null, 14));
647
+ b.addEventListener('click', function () { moveRow(r, m[1]); });
648
+ acts.appendChild(b);
649
+ });
650
+ }
620
651
  // the primary button sits rightmost, as on the board
621
- r.actions.slice().reverse().forEach(function (a) {
652
+ // a row whose chips stand open is decided by its chip alone: no button for it
653
+ if (!r.order) r.actions.slice().reverse().forEach(function (a) {
654
+ if (r.show && a.key === r.show) return;
622
655
  acts.appendChild(chipBtn('desk-chip--' + a.tone, a.label, function () {
623
656
  if (a.go) { window.location.href = a.go; return; }
624
657
  if (a.ask) { var at = r.key + '|' + a.key; desk.ask = desk.ask === at ? null : at; desk.open = null; draw(); focusRow(r.key, 'chip'); return; }
@@ -633,6 +666,8 @@
633
666
  row.appendChild(facts);
634
667
  }
635
668
  var asked = desk.ask && desk.ask.indexOf(r.key + '|') === 0 ? (r.choices || {})[desk.ask.slice(r.key.length + 1)] : null;
669
+ // a blocker's options stand open: deciding it is picking one
670
+ if (!asked && r.show) asked = (r.choices || {})[r.show] || null;
636
671
  if (asked) {
637
672
  var chips = node('div', 'desk-choices');
638
673
  chips.appendChild(node('span', 'desk-choices__ask', asked.ask));
@@ -653,6 +688,35 @@
653
688
  }
654
689
  return row;
655
690
  }
691
+ // The Priorities desk (task 1004560): a move is kept in this visit until saved.
692
+ function moveRow(r, by) {
693
+ var D = window.GovDesk, st = view.inside;
694
+ if (!D || !st || !desk.model) return;
695
+ var k = st.key + ':' + (view.insideModel ? view.insideModel.selFloor : 0);
696
+ var now = desk.model.rows.filter(function (x) { return x.order; }).map(function (x) { return x.key; });
697
+ desk.order[k] = D.moved(now, r.key, by);
698
+ draw();
699
+ focusRow(r.key, 'arrow');
700
+ }
701
+ // Save: the order goes to POST /goals/reorder, or is filed for the council;
702
+ // the server's own checks decide either way.
703
+ function saveOrder(dm, k) {
704
+ var C = window.GovMatterCard;
705
+ var req = C && C.orderRequest ? C.orderRequest(dm.save) : null;
706
+ if (!req || desk.busy) return Promise.resolve(false);
707
+ desk.busy = true;
708
+ draw();
709
+ return send(req).then(function (out) {
710
+ desk.busy = false;
711
+ if (!out.ok) { say(C.refusal(out.status, out.body)); draw(); return false; }
712
+ desk.order[k] = null;
713
+ desk.mine[k] = [dm.save.council ? 'order sent to council' : 'order set'].concat(desk.mine[k] || []);
714
+ bumpToday();
715
+ say(dm.save.said);
716
+ draw();
717
+ return load().then(function () { return true; });
718
+ });
719
+ }
656
720
  function deskRowsEl() {
657
721
  var st = view.insideModel;
658
722
  var on = $(st && st.flipA ? 'insideOpenA' : 'insideOpenB');
@@ -667,6 +731,7 @@
667
731
  if (rows[i].getAttribute('data-row') !== key) continue;
668
732
  var main = rows[i].children[0];
669
733
  var target = what === 'chip' && rows[i].children[1] ? rows[i].children[1].children[1] : main && main.children[1];
734
+ if (what === 'arrow' && main) target = main.querySelector ? main.querySelector('.desk-arrow') : target;
670
735
  if (target && target.focus) target.focus();
671
736
  return;
672
737
  }
@@ -685,6 +750,8 @@
685
750
  switch_on: 'switched on', switch_off: 'switched off', allow: 'allowed', disallow: 'disallowed', first: 'put first',
686
751
  pause: 'paused', resume: 'resumed', tier: 'new tier', deactivate: 'deactivated', give: 'gave', take: 'took back',
687
752
  check: 'checked', rot: 'set', module_on: 'turned on', module_off: 'turned off', backup: 'backed up', invite: 'invited',
753
+ decide: 'decided', ratify: 'ratified', ratify_goal: 'made a goal of',
754
+ budget: 'set the limit of', unsign: 'undid',
688
755
  };
689
756
  // One act from a row: checked, sent through the card's own request (the
690
757
  // server's permission checks decide), then the row clears, the floor counts
@@ -699,7 +766,9 @@
699
766
  var req = C.request(r.card, key, input);
700
767
  var settle = function (body) {
701
768
  var a = C.answer(r.card, key, body);
702
- desk.mine[k] = [DONE_WORDS[key] + ' ' + r.name].concat(desk.mine[k] || []);
769
+ // an act at a building a sitting decides went to it as a matter (task 1004561)
770
+ var sent = r.card.subject && r.card.subject.council && req && /\/government\/board\/matters$/.test(req.url);
771
+ desk.mine[k] = [(sent ? 'sent to council:' : DONE_WORDS[key]) + ' ' + r.name].concat(desk.mine[k] || []);
703
772
  desk.done[r.matter] = true;
704
773
  desk.open = null; desk.ask = null;
705
774
  if (desk.model && !desk.model.rows.some(function (x) { return x.matter !== r.matter; })) desk.cleared[k] = true;
@@ -681,6 +681,130 @@
681
681
  ".desk-rows"
682
682
  ]
683
683
  }
684
+ },
685
+ "desk-planning-priorities": {
686
+ "auth": true,
687
+ "_": "Task 1004560: Planning, Priorities. The open goals in order with promise pips and two arrows; one goal moved down, so the save bar says the new order goes to the council and how many yes it needs, with undo.",
688
+ "url": "/builders/street#building=planning",
689
+ "actions": [
690
+ [
691
+ "wait",
692
+ 900
693
+ ],
694
+ [
695
+ "clickIfVisible",
696
+ "[data-craft-close]"
697
+ ],
698
+ [
699
+ "wait",
700
+ 300
701
+ ],
702
+ [
703
+ "click",
704
+ "[data-floor=\"0\"]"
705
+ ],
706
+ [
707
+ "wait",
708
+ 600
709
+ ],
710
+ [
711
+ "click",
712
+ ".desk-arrow[aria-label$=\" down\"]"
713
+ ],
714
+ [
715
+ "wait",
716
+ 400
717
+ ]
718
+ ],
719
+ "expect": {
720
+ "visible": [
721
+ ".desk-rows",
722
+ ".desk-pips",
723
+ ".desk-save"
724
+ ]
725
+ }
726
+ },
727
+ "desk-planning-ideas": {
728
+ "auth": true,
729
+ "_": "Task 1004560: Planning, Ideas to ratify. A Full Idea in its vote with seat pips, a promise to ratify in one tap, and a carried Full Idea made a goal from a version chip.",
730
+ "url": "/builders/street#building=planning",
731
+ "actions": [
732
+ [
733
+ "wait",
734
+ 900
735
+ ],
736
+ [
737
+ "clickIfVisible",
738
+ "[data-craft-close]"
739
+ ],
740
+ [
741
+ "wait",
742
+ 300
743
+ ],
744
+ [
745
+ "click",
746
+ "[data-floor=\"1\"]"
747
+ ],
748
+ [
749
+ "wait",
750
+ 600
751
+ ],
752
+ [
753
+ "focus",
754
+ ".desk-rows .desk-row:last-child .desk-name"
755
+ ],
756
+ [
757
+ "wait",
758
+ 300
759
+ ]
760
+ ],
761
+ "expect": {
762
+ "visible": [
763
+ ".desk-rows"
764
+ ]
765
+ }
766
+ },
767
+ "desk-planning-blockers": {
768
+ "auth": true,
769
+ "_": "Task 1004560: Planning, Waiting on you. Each blocker decided by one of its options as chips standing open; go ahead, not now, need more when it lists none.",
770
+ "url": "/builders/street#building=planning",
771
+ "actions": [
772
+ [
773
+ "wait",
774
+ 900
775
+ ],
776
+ [
777
+ "clickIfVisible",
778
+ "[data-craft-close]"
779
+ ],
780
+ [
781
+ "wait",
782
+ 300
783
+ ],
784
+ [
785
+ "click",
786
+ "[data-floor=\"2\"]"
787
+ ],
788
+ [
789
+ "wait",
790
+ 600
791
+ ],
792
+ [
793
+ "focus",
794
+ ".desk-rows .desk-row:last-child .desk-name"
795
+ ],
796
+ [
797
+ "wait",
798
+ 300
799
+ ]
800
+ ],
801
+ "expect": {
802
+ "visible": [
803
+ ".desk-rows",
804
+ ".desk-choices",
805
+ ".desk-status--hot"
806
+ ]
807
+ }
684
808
  }
685
809
  },
686
810
  "reducedMotion": {
@@ -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-nav"></script>
180
- <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
181
- <script src="/builders/shell.js?v=2026-10-03-nav"></script>
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>
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
@@ -1206,8 +1206,8 @@ html[data-fullscreen="on"][data-fullscreen-plain="on"] .app-content { padding-to
1206
1206
  /* THE CRAFT MODE (task 1004492, craft-mode.js). The top bar's switch: one pill
1207
1207
  holding the four crafts and Legacy, the current one lit. Narrower windows
1208
1208
  drop the words, and a phone shows only the current mode (or "Mode"), which
1209
- opens the picker. The picker below asks once per session. Neither paints a
1210
- mode's colour — the looks are their own task (1004495). */
1209
+ opens the picker. The picker below asks once per session. Each mode's look
1210
+ (its colour on the current pill) is THE CRAFT MODE LOOKS, further down. */
1211
1211
  .craft-switch {
1212
1212
  display: inline-flex;
1213
1213
  align-items: center;
@@ -1431,6 +1431,125 @@ html.craft-picker-open { overflow: hidden; }
1431
1431
  .craft-picker__legacy { height: auto; min-height: 40px; white-space: normal; text-align: left; }
1432
1432
  }
1433
1433
 
1434
+ /* THE CRAFT MODE LOOKS (task 1004495; direction docs/design/craft-mode-direction.md).
1435
+ Three modes wear a look: Artistry, Ideating, Governing. Building, Legacy and no
1436
+ mode wear none — nothing below names them, so they render as the hall always
1437
+ did (tests/craft_mode_looks.mjs pins it). --craft marks WHERE you are, never
1438
+ what to press: the accent stays the action colour in every mode. Light values
1439
+ clear 5.4:1 on the hall's paper; the dark values are the prototype's. Light
1440
+ keeps the paper ground; only dark changes the ground and the glass. */
1441
+ html[data-craft="artistry"] { --craft: #2e3c6e; --craft-on: #f1e6d2; }
1442
+ html[data-craft="ideating"] { --craft: #7a5a0c; --craft-on: #fdf6dd; }
1443
+ html[data-craft="governing"] { --craft: #33479e; --craft-on: #eef1ff; }
1444
+ html[data-theme="dark"][data-craft="artistry"] {
1445
+ --craft: #f1e6d2; --craft-on: #0c1122;
1446
+ --bg: #07080d; --bg-frame: #0a0b12;
1447
+ --glass: rgba(150, 170, 230, .11); --glass-strong: rgba(170, 190, 240, .17);
1448
+ --glass-edge: rgba(241, 230, 210, .16);
1449
+ }
1450
+ html[data-theme="dark"][data-craft="ideating"] {
1451
+ --craft: #f2d27a; --craft-on: #1d1604;
1452
+ --bg: #05060d;
1453
+ --glass: rgba(140, 150, 225, .10); --glass-strong: rgba(160, 170, 240, .16);
1454
+ --glass-edge: rgba(242, 210, 122, .16);
1455
+ }
1456
+ html[data-theme="dark"][data-craft="governing"] {
1457
+ --craft: #aabeff; --craft-on: #0a1030;
1458
+ --glass-edge: rgba(170, 190, 255, .16);
1459
+ }
1460
+ /* The shared treatment: the switch's current pill, the current tab and the top
1461
+ bar's hairline take the mode colour. */
1462
+ html:is([data-craft="artistry"], [data-craft="ideating"], [data-craft="governing"]) .craft-switch__opt[aria-pressed="true"] {
1463
+ background: var(--craft); color: var(--craft-on);
1464
+ }
1465
+ html:is([data-craft="artistry"], [data-craft="ideating"], [data-craft="governing"]) .nav-item--active {
1466
+ background: color-mix(in srgb, var(--craft) 14%, transparent); color: var(--craft);
1467
+ }
1468
+ html:is([data-craft="artistry"], [data-craft="ideating"], [data-craft="governing"]) .nav-item--active .nav-icon,
1469
+ html:is([data-craft="artistry"], [data-craft="ideating"], [data-craft="governing"]) .nav-item--active .nav-item__label { color: var(--craft); }
1470
+ html:is([data-craft="artistry"], [data-craft="ideating"], [data-craft="governing"]) .nav-item--active::before { background: var(--craft); }
1471
+ html:is([data-craft="artistry"], [data-craft="ideating"], [data-craft="governing"]) .app-topbar {
1472
+ border-bottom-color: color-mix(in srgb, var(--craft) 30%, transparent);
1473
+ }
1474
+ /* The header mark: the mode's own words beside the page title (craft-mode.js
1475
+ applyLook). Hidden on a phone — the switch's one pill already says the mode,
1476
+ and the bar has no room for both. */
1477
+ .craft-eyebrow {
1478
+ font-family: var(--label-face);
1479
+ font-size: 11px;
1480
+ font-weight: 500;
1481
+ letter-spacing: .14em;
1482
+ text-transform: uppercase;
1483
+ color: var(--craft);
1484
+ white-space: nowrap;
1485
+ flex-shrink: 0;
1486
+ }
1487
+ @media (max-width: 640px) { .craft-eyebrow { display: none; } }
1488
+ /* The signature: the mode's door scene (craft-mode.js artHtml, the same art the
1489
+ picker draws) framed under the brand, 40px in the closed rail and 64px open.
1490
+ The 110px-tall scene is scaled to the plate's height and widened to fill it,
1491
+ never redrawn: each scene's pieces are placed from its edges or its centre. */
1492
+ .craft-plate {
1493
+ position: relative;
1494
+ height: 40px;
1495
+ margin: 10px 8px 0;
1496
+ border-radius: var(--radius-sm);
1497
+ overflow: hidden;
1498
+ flex-shrink: 0;
1499
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--craft) 30%, transparent);
1500
+ }
1501
+ .craft-plate .craft-art {
1502
+ position: absolute;
1503
+ left: 0;
1504
+ top: 0;
1505
+ width: calc(100% / .364);
1506
+ height: 110px;
1507
+ transform: scale(.364);
1508
+ transform-origin: 0 0;
1509
+ }
1510
+ .craft-plate .craft-art__city { top: 0; }
1511
+ .app-sidebar:hover .craft-plate,
1512
+ .app-sidebar:focus-within .craft-plate { height: 64px; margin: 10px 16px 0; }
1513
+ .app-sidebar:hover .craft-plate .craft-art,
1514
+ .app-sidebar:focus-within .craft-plate .craft-art { width: calc(100% / .582); transform: scale(.582); }
1515
+ @media (max-width: 768px) {
1516
+ .craft-plate { height: 64px; margin: 10px 16px 0; }
1517
+ .craft-plate .craft-art { width: calc(100% / .582); transform: scale(.582); }
1518
+ }
1519
+ /* The plate follows the theme (owner, 2026-10-03): light shows each scene's day. */
1520
+ html[data-theme="light"] .craft-plate .craft-art--artistry { background: #dfe3ef; }
1521
+ html[data-theme="light"] .craft-plate .craft-art__dot--star { background: #2e3c6e; }
1522
+ html[data-theme="light"] .craft-plate .craft-art__moon { background: #f3c98b; box-shadow: 0 0 0 9px rgba(243, 201, 139, .28); }
1523
+ html[data-theme="light"] .craft-plate .craft-art__ground { background: #c3c9dc; }
1524
+ html[data-theme="light"] .craft-plate .craft-art__house { background: #8d97b8; }
1525
+ html[data-theme="light"] .craft-plate .craft-art--ideating { background: #f6f0dc; }
1526
+ html[data-theme="light"] .craft-plate .craft-art__dot--gold { background: #c9a23a; }
1527
+ html[data-theme="light"] .craft-plate .craft-art__dot--glow { background: #b8891c; box-shadow: 0 0 10px rgba(184, 137, 28, .45); }
1528
+ html[data-theme="light"] .craft-plate .craft-art__dot--proto { box-shadow: inset 0 0 0 1px rgba(122, 90, 12, .6); }
1529
+ html[data-theme="light"] .craft-plate .craft-art__ring { box-shadow: inset 0 0 0 1px rgba(122, 90, 12, .35); }
1530
+ html[data-theme="light"] .craft-plate .craft-art--governing { background: #e9ecf6; }
1531
+ html[data-theme="light"] .craft-plate .craft-art__city { filter: invert(1) hue-rotate(180deg); }
1532
+ /* Ideating's star dust: a few faint fixed specks across the rail itself. */
1533
+ html[data-craft="ideating"] .app-sidebar {
1534
+ background-image:
1535
+ radial-gradient(1px 1px at 14px 40px, rgba(242, 210, 122, .5), transparent),
1536
+ radial-gradient(1px 1px at 46px 130px, rgba(242, 210, 122, .35), transparent),
1537
+ radial-gradient(1.5px 1.5px at 120px 90px, rgba(242, 210, 122, .4), transparent),
1538
+ radial-gradient(1px 1px at 200px 190px, rgba(242, 210, 122, .35), transparent),
1539
+ radial-gradient(1px 1px at 30px 230px, rgba(242, 210, 122, .3), transparent),
1540
+ radial-gradient(1.5px 1.5px at 170px 260px, rgba(242, 210, 122, .3), transparent);
1541
+ background-size: 246px 280px;
1542
+ }
1543
+ html[data-theme="light"][data-craft="ideating"] .app-sidebar {
1544
+ background-image:
1545
+ radial-gradient(1px 1px at 14px 40px, rgba(184, 137, 28, .45), transparent),
1546
+ radial-gradient(1px 1px at 46px 130px, rgba(184, 137, 28, .3), transparent),
1547
+ radial-gradient(1.5px 1.5px at 120px 90px, rgba(184, 137, 28, .35), transparent),
1548
+ radial-gradient(1px 1px at 200px 190px, rgba(184, 137, 28, .3), transparent),
1549
+ radial-gradient(1px 1px at 30px 230px, rgba(184, 137, 28, .25), transparent),
1550
+ radial-gradient(1.5px 1.5px at 170px 260px, rgba(184, 137, 28, .25), transparent);
1551
+ }
1552
+
1434
1553
  /* THE BACK LINK (task 1004430): at the head of the bar when the page was
1435
1554
  reached from inside another hall page. 24px floor on both axes. */
1436
1555
  .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-nav"></script>
58
- <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
59
- <script src="/builders/shell.js?v=2026-10-03-nav"></script>
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>
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-nav"></script>
142
- <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
143
- <script src="/builders/shell.js?v=2026-10-03-nav"></script>
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>
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>
@@ -142,9 +142,9 @@
142
142
 
143
143
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
144
144
 
145
- <script src="/builders/shell-trail.js?v=2026-10-03-nav"></script>
146
- <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
147
- <script src="/builders/shell.js?v=2026-10-03-nav"></script>
145
+ <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
146
+ <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
147
+ <script src="/builders/shell.js?v=2026-10-03-looks"></script>
148
148
  <script src="/builders/palette.js" defer></script>
149
149
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
150
150
  <script src="/builders/brand-holes.js?v=2026-09-29-aqfix"></script>
@@ -162,9 +162,9 @@
162
162
 
163
163
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
164
164
  chrome exists before the page script runs. -->
165
- <script src="/builders/shell-trail.js?v=2026-10-03-nav"></script>
166
- <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
167
- <script src="/builders/shell.js?v=2026-10-03-nav"></script>
165
+ <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
166
+ <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
167
+ <script src="/builders/shell.js?v=2026-10-03-looks"></script>
168
168
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
169
169
  <script src="/builders/palette.js" defer></script>
170
170
  <!-- shared frontend toolkit (window.OTB) — loaded before watch.js, which pulls from it. -->
@@ -171,9 +171,9 @@
171
171
 
172
172
  <!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
173
173
  time so the chrome exists before the deferred page scripts execute. -->
174
- <script src="/builders/shell-trail.js?v=2026-10-03-nav"></script>
175
- <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
176
- <script src="/builders/shell.js?v=2026-10-03-nav"></script>
174
+ <script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
175
+ <script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
176
+ <script src="/builders/shell.js?v=2026-10-03-looks"></script>
177
177
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
178
178
  <script src="/builders/palette.js" defer></script>
179
179
  <!-- shared frontend toolkit (window.OTB) — loaded FIRST so work.js can pull from it. -->
@@ -27,3 +27,11 @@ Covers: every shelled hall page (`craft-mode.js`, `shell.js`, `style.css`), and
27
27
  - **Government shows only in Governing**, and outside that group Governing is the shortest list (Blockers, Board Room). Building is every tab except Government and the Board Room; Artistry is Studio, Goals, Your thinking; Ideating is Sky, Your thinking, Goals. The prototype names rooms that do not exist yet (Store, Page reviews, Treasury); a list names only real NAV items, and `tests/craft_mode_nav.mjs` requires every NAV item to sit in some mode.
28
28
  - **The mode only ever takes away, and never through `hidden`.** The access gate owns `hidden`; `applyNav()` marks what the mode leaves out with `[data-craft-off]`, which `style.css` hides. So a mode can never show a tab the gate hid, and switching mode just moves the attribute. A group head goes too once nothing under it shows.
29
29
  - **Two callers.** `apply()` runs it on every mode change (live, no reload), and `shell.js` `applyAccess` runs it right after its gate loop, so a later `/me` answer (a promotion, a permission) brings a head back with its tabs. Hidden tabs still open by their URL; nothing is enforced (ADR 0016).
30
+
31
+ ## Each mode's look (task 1004495)
32
+
33
+ - **Three modes wear a look; Building, Legacy and no mode wear none.** Artistry, Ideating and Governing each set `--craft` (where you are) and `--craft-on` (text on it) on `html[data-craft="<mode>"]`, with a dark twin on `html[data-theme="dark"][data-craft="<mode>"]` that also darkens the ground and tints the glass (Artistry, Ideating). Values and contrast are the direction's, `docs/design/craft-mode-direction.md`. No rule in `style.css` names `building` or `legacy`, and none matches any mode at all; `tests/craft_mode_looks.mjs` pins both, and re-measures every colour pair.
34
+ - **The accent stays the action colour.** `--craft` paints only the switch's current pill, the current tab (tint, icon, label, marker) and the top bar's hairline.
35
+ - **Two pieces are drawn by `craft-mode.js` `applyLook()`, on every mode change:** the eyebrow beside `#topbar-title` (`LOOKS`: "the studio · night", "the sky", "the city"; hidden at <=640px, where it overflowed phones) and the scene plate under `.app-sidebar__brand` (40px closed rail, 64px open). The plate's markup IS `artHtml(mode)`, the picker's door art, exported rather than copied; light mode recolours it to each scene's day (the city is the dark one through `invert(1) hue-rotate(180deg)`). It does not animate.
36
+ - **Always-present tabs widened** (owner, 2026-10-03): `ALWAYS_TABS` is Builders, Collab and Profile, so every mode keeps the You and Community groups.
37
+ - **Governing's header tallies** ("needs you · moving") are task 1004563, not this look.
@@ -19,6 +19,7 @@
19
19
 
20
20
  const api = require('../../src/module-api');
21
21
  const blockers = require('./blockers');
22
+ const ratify = require('./ratify');
22
23
 
23
24
  const POINT = 'governor.docket';
24
25
  const DOCKET_BLOCKER_LIMIT = 50;
@@ -41,9 +42,96 @@ function blockerDocketMatter(b, mayResolve) {
41
42
  tally: null,
42
43
  href: `/blockers#blocker-${b.id}`,
43
44
  ref: { type: 'blocker', id: b.id },
45
+ // the floor desk's one act (task 1004560): decide, by one of its options
46
+ ...(mayResolve ? { acts: [{ key: 'decide', options: blockerOptions(b.body_md) }] } : {}),
44
47
  };
45
48
  }
46
49
 
50
+ // A blocker's options as one-tap chips (task 1004560). Its own, when its body
51
+ // lists them as "Option A: …" lines (two to five of them); otherwise the three
52
+ // answers every blocker takes. The chosen key becomes the resolution note, so
53
+ // a key is the option's own words (at most 64 characters) and the label its
54
+ // first words. Pure.
55
+ const DEFAULT_CHOICES = Object.freeze(['go ahead', 'not now', 'need more']);
56
+ const OPTION_LINE = /^\s*(?:[-*]|\d+[.)])?\s*(?:\*\*)?option\s+[A-Za-z0-9]{1,2}(?:\*\*)?\s*[:.)—-]+\s*(?:\*\*)?(.+)$/i;
57
+ function plainWords(s) { return String(s).replace(/[*_`[\]]+/g, '').replace(/\([^)]*\)/g, '').replace(/\s+/g, ' ').trim(); }
58
+ function shortLabel(s) {
59
+ let out = '';
60
+ for (const w of s.split(' ')) { const next = out ? out + ' ' + w : w; if (next.length > 16) break; out = next; }
61
+ return out || s.slice(0, 16);
62
+ }
63
+ function blockerOptions(bodyMd) {
64
+ const found = [];
65
+ for (const line of String(bodyMd || '').split(/\r?\n/)) {
66
+ const m = OPTION_LINE.exec(line);
67
+ if (!m) continue;
68
+ const text = plainWords(m[1]).split(/\s[—–-]\s|[.;:]\s/)[0].trim();
69
+ if (text && !found.some((o) => o.key === text.slice(0, 64))) found.push({ key: text.slice(0, 64), label: shortLabel(text) });
70
+ }
71
+ if (found.length >= 2 && found.length <= 5) return found;
72
+ return DEFAULT_CHOICES.map((t) => ({ key: t, label: t }));
73
+ }
74
+
75
+ // The Ideas-to-ratify desk (task 1004560): the ideas a ratifier may take from
76
+ // the floor, under idea.ratify (both ratify routes' atom). A criterion proposal
77
+ // ratifies in one tap (POST /inbox/:id/ratify). A Full Idea the board has
78
+ // carried becomes a goal (POST /inbox/:id/ratify-goal) with its target version
79
+ // as a chip — only when its answers compose a goal on their own; one that needs
80
+ // the ratifier to write its criterion opens its page instead. A Full Idea still
81
+ // in its sitting is not here: it is the board's vote row (ADR 0175 §8). Pure.
82
+ function ratifyDeskRows(ideas, versions, passed) {
83
+ const chips = (versions || []).slice(0, 5).map((v) => ({ key: String(v.id), label: String(v.id) }));
84
+ const out = [];
85
+ for (const i of ideas || []) {
86
+ if (!i || i.status !== 'open') continue;
87
+ const href = `/idea/${i.id}`;
88
+ if (i.kind === 'criterion-proposal') {
89
+ let p = null;
90
+ try { p = ratify.parseProposalBody(i.body_md); } catch (_) { p = null; }
91
+ if (!p || !p.target_version) continue;
92
+ out.push({
93
+ id: `p${i.id}`, name: String(i.title || 'a promise'), ref: { type: 'criterion_proposal', id: i.id }, href, opened_at: i.created_at,
94
+ fact: `a promise for ${p.target_version}`, facts: [String(p.criterion_md || '').slice(0, 80)],
95
+ acts: [{ key: 'ratify' }, { key: 'open' }],
96
+ });
97
+ continue;
98
+ }
99
+ if (i.grade !== 'full' || !passed.has(String(i.id))) continue;
100
+ const composed = ratify.composeGoalFromFullIdea({ ideaId: i.id, templateKey: i.template_key, fieldValues: i.field_values });
101
+ const fits = composed && composed.ok && chips.length > 0;
102
+ out.push({
103
+ id: `g${i.id}`, name: String(i.title || 'a full idea'), ref: { type: 'full_idea', id: i.id }, href, opened_at: i.created_at,
104
+ fact: 'the board carried it', facts: [fits ? 'becomes a goal' : 'needs its page to finish'],
105
+ acts: fits ? [{ key: 'ratify_goal', options: chips }, { key: 'open' }] : [{ key: 'open' }],
106
+ });
107
+ }
108
+ return out;
109
+ }
110
+ // The desk reads the oldest 30 open proposals and Full Ideas (older first, so
111
+ // nothing waits forever behind newer ones); which Full Ideas the board carried
112
+ // is one read for all of them (government.boardPassedIdeaIds).
113
+ const RATIFY_IDEAS_LIMIT = 30;
114
+ async function readRatifyDesk() {
115
+ const { rows: ideas } = await api.pool.query(
116
+ `SELECT i.id, i.title, i.kind, i.status, i.body_md, i.created_at,
117
+ COALESCE(g.grade, 'quick') AS grade, g.template_key, g.field_values
118
+ FROM idea_inbox i LEFT JOIN ideas_grades g ON g.idea_id = i.id
119
+ WHERE i.status = 'open' AND (i.kind = 'criterion-proposal' OR g.grade = 'full')
120
+ ORDER BY i.created_at, i.id LIMIT $1`,
121
+ [RATIFY_IDEAS_LIMIT],
122
+ );
123
+ const government = api.resolveOptional && api.resolveOptional('government');
124
+ const passed = new Set();
125
+ const full = ideas.filter((i) => i.grade === 'full').map((i) => i.id);
126
+ if (full.length && government && typeof government.boardPassedIdeaIds === 'function') {
127
+ (await government.boardPassedIdeaIds(full)).forEach((id) => passed.add(String(id)));
128
+ }
129
+ const { rows: versions } = passed.size
130
+ ? await api.pool.query(`SELECT id, name FROM versions WHERE status IN ('building', 'planning') ORDER BY (status = 'building') DESC, id LIMIT 5`)
131
+ : { rows: [] };
132
+ return ratifyDeskRows(ideas, versions, passed);
133
+ }
134
+
47
135
  // The rules-at-work reading from this month's routed filings. Pure.
48
136
  function rulesReading(settledThisMonth) {
49
137
  const n = Number(settledThisMonth) || 0;
@@ -114,6 +202,7 @@ function contributeIdeasToDocket() {
114
202
  return rows.map((b) => blockerDocketMatter(b, mayResolve));
115
203
  },
116
204
  });
205
+ api.contribute(POINT, { desk: 'planning.ratify', gate: { permission: 'idea.ratify' }, read: () => readRatifyDesk() });
117
206
  // A count, no rows: any builder may read it.
118
207
  api.contribute(POINT, {
119
208
  reading: 'rules',
@@ -127,6 +216,8 @@ function contributeIdeasToDocket() {
127
216
  module.exports = {
128
217
  contributeIdeasToDocket,
129
218
  blockerDocketMatter,
219
+ blockerOptions,
220
+ ratifyDeskRows,
130
221
  rulesReading,
131
222
  rulesFloor,
132
223
  rulesHistory,