@bongos/core 1.24.80 → 1.24.81

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 (106) hide show
  1. package/.bongos-core.json +125 -110
  2. package/docs/adr/0402-helm-is-the-halls-look-and-the-hall-is-dark-only.md +142 -0
  3. package/docs/adr/README.md +1 -0
  4. package/docs/copy-inventory.md +26 -31
  5. package/docs/copy-registry.json +29 -86
  6. package/docs/module-api-changelog.md +2 -0
  7. package/docs/page-inventory.json +1 -3
  8. package/docs/page-readings.json +94 -94
  9. package/modules/hall-ui/public/ai-studio.html +5 -3
  10. package/modules/hall-ui/public/ai-studio.states.json +1 -0
  11. package/modules/hall-ui/public/atlas.html +3 -3
  12. package/modules/hall-ui/public/atlas.states.json +1 -0
  13. package/modules/hall-ui/public/blockers.html +3 -3
  14. package/modules/hall-ui/public/board-room.html +3 -3
  15. package/modules/hall-ui/public/board-room.states.json +1 -0
  16. package/modules/hall-ui/public/city.css +1 -3
  17. package/modules/hall-ui/public/city.html +5 -8
  18. package/modules/hall-ui/public/city.js +10 -55
  19. package/modules/hall-ui/public/city.states.json +7 -32
  20. package/modules/hall-ui/public/collab.html +3 -3
  21. package/modules/hall-ui/public/collab.states.json +1 -0
  22. package/modules/hall-ui/public/commands.html +3 -3
  23. package/modules/hall-ui/public/commands.states.json +1 -0
  24. package/modules/hall-ui/public/controls.css +63 -11
  25. package/modules/hall-ui/public/copy-desk.html +3 -3
  26. package/modules/hall-ui/public/copy-desk.states.json +1 -0
  27. package/modules/hall-ui/public/deploy.html +3 -3
  28. package/modules/hall-ui/public/deploy.states.json +1 -0
  29. package/modules/hall-ui/public/goal-map.html +3 -3
  30. package/modules/hall-ui/public/goals.html +3 -3
  31. package/modules/hall-ui/public/goals.states.json +1 -0
  32. package/modules/hall-ui/public/government.html +3 -3
  33. package/modules/hall-ui/public/government.states.json +1 -0
  34. package/modules/hall-ui/public/idea.html +3 -3
  35. package/modules/hall-ui/public/idea.states.json +1 -0
  36. package/modules/hall-ui/public/ideas.html +3 -3
  37. package/modules/hall-ui/public/ideas.states.json +1 -0
  38. package/modules/hall-ui/public/index-founding.states.json +1 -0
  39. package/modules/hall-ui/public/index-genesis-decide.states.json +1 -0
  40. package/modules/hall-ui/public/index-genesis-demo.states.json +1 -0
  41. package/modules/hall-ui/public/index-genesis.states.json +1 -0
  42. package/modules/hall-ui/public/index.html +4 -4
  43. package/modules/hall-ui/public/index.states.json +1 -0
  44. package/modules/hall-ui/public/market-watch-item.html +3 -3
  45. package/modules/hall-ui/public/market-watch-item.states.json +1 -0
  46. package/modules/hall-ui/public/market-watch-rules.html +3 -3
  47. package/modules/hall-ui/public/market-watch-rules.states.json +1 -0
  48. package/modules/hall-ui/public/market-watch.html +3 -3
  49. package/modules/hall-ui/public/market-watch.states.json +1 -0
  50. package/modules/hall-ui/public/modules.html +3 -3
  51. package/modules/hall-ui/public/modules.states.json +1 -0
  52. package/modules/hall-ui/public/money.html +3 -3
  53. package/modules/hall-ui/public/money.states.json +1 -0
  54. package/modules/hall-ui/public/my-agents.html +3 -3
  55. package/modules/hall-ui/public/my-agents.states.json +1 -0
  56. package/modules/hall-ui/public/primer.html +3 -3
  57. package/modules/hall-ui/public/primer.states.json +1 -0
  58. package/modules/hall-ui/public/profile.html +3 -3
  59. package/modules/hall-ui/public/profile.states.json +1 -0
  60. package/modules/hall-ui/public/project-settings.html +3 -3
  61. package/modules/hall-ui/public/roster.html +3 -3
  62. package/modules/hall-ui/public/roster.states.json +1 -0
  63. package/modules/hall-ui/public/settings.html +3 -3
  64. package/modules/hall-ui/public/settings.states.json +1 -0
  65. package/modules/hall-ui/public/shell-game.css +87 -42
  66. package/modules/hall-ui/public/shell-game.js +41 -8
  67. package/modules/hall-ui/public/shell.js +8 -40
  68. package/modules/hall-ui/public/street-card.states.json +1 -0
  69. package/modules/hall-ui/public/street.css +1 -5
  70. package/modules/hall-ui/public/street.html +5 -9
  71. package/modules/hall-ui/public/street.js +8 -59
  72. package/modules/hall-ui/public/street.states.json +7 -32
  73. package/modules/hall-ui/public/studio.html +3 -3
  74. package/modules/hall-ui/public/studio.states.json +1 -0
  75. package/modules/hall-ui/public/style.css +124 -65
  76. package/modules/hall-ui/public/task.html +3 -3
  77. package/modules/hall-ui/public/task.states.json +1 -0
  78. package/modules/hall-ui/public/thinking.html +3 -3
  79. package/modules/hall-ui/public/thinking.states.json +1 -0
  80. package/modules/hall-ui/public/tweak-editor.html +3 -3
  81. package/modules/hall-ui/public/tweak-editor.states.json +1 -0
  82. package/modules/hall-ui/public/watch.html +3 -3
  83. package/modules/hall-ui/public/watch.states.json +1 -0
  84. package/modules/hall-ui/public/work-founding.states.json +1 -0
  85. package/modules/hall-ui/public/work.html +3 -3
  86. package/modules/hall-ui/public/work.states.json +1 -0
  87. package/modules/hall-ui/records/start-screen.md +6 -2
  88. package/modules/hall-ui/specimens/components.states.json +2 -1
  89. package/modules/ui-design/public/brand.html +2 -2
  90. package/package-lock.json +2 -2
  91. package/package.json +1 -1
  92. package/release-notes.json +6 -0
  93. package/scripts/hall-preview/README.md +2 -3
  94. package/src/bongos/serve-internal.js +7 -2
  95. package/src/hall-helm.js +79 -0
  96. package/src/module-api.js +1 -1
  97. package/tests/design_reference.mjs +1 -1
  98. package/tests/hall_builders_page_pass.mjs +2 -1
  99. package/tests/hall_city.mjs +15 -29
  100. package/tests/hall_controls.mjs +52 -4
  101. package/tests/hall_helm.mjs +187 -0
  102. package/tests/hall_record_world.mjs +4 -2
  103. package/tests/hall_start_screen.mjs +65 -2
  104. package/tests/hall_street.mjs +13 -26
  105. package/tests/hall_topbar_320.mjs +4 -2
  106. package/tests/nav_permission_atoms.mjs +1 -1
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "_": "Builders — this project's OWN roster, ranked by credits earned (task 1003441, goal 1000075). Was the #leaderboard hash view on index.html; /#leaderboard now redirects here. Deliberately UNGATED, because its one data source (GET /leaderboard) is deliberately unauthenticated and mirrors the public status surface — so unlike a gated page's states file the `out` state below renders the real page rather than a seal. Populated look: the hall-preview harness (scripts/hall-preview/fixtures/public__leaderboard.json, since task 1004437 the list the page reads: fifteen builders with avatars, ranks, karma, tokens and each one's roles, including an artist and an ideator with no AI sessions).",
3
3
  "page": "/builders/roster",
4
+ "modes": ["dark"],
4
5
  "_storage": "The hall asks a first-visit question (the session mode picker, craft-mode.js) over every page; seeding \"already asked\" renders the page a returning builder sees (task 1005080).",
5
6
  "storage": { "session": { "hall-craft-asked": "1" } },
6
7
  "surface": "hall-ui",
@@ -11,7 +11,7 @@
11
11
  <link rel="icon" href="/modules/ui-design/public/brand/favicon.svg" type="image/svg+xml">
12
12
  <link rel="preconnect" href="https://fonts.googleapis.com">
13
13
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
14
- <link rel="stylesheet" href="/builders/style.css?v=2026-11-10-options">
14
+ <link rel="stylesheet" href="/builders/style.css?v=2026-11-11-helm">
15
15
  <!-- The PANEL archetype (task 1003308): the row, the switch, the field, the
16
16
  chip control and the family's voices, shared with profile and pair.
17
17
  settings.css then holds only what this page
@@ -405,10 +405,10 @@
405
405
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
406
406
  chrome exists before the page scripts run. -->
407
407
  <script src="/builders/dom-utils.js?v=2026-11-05-numbers"></script>
408
- <script src="/builders/shell-game.js?v=2026-11-10-options"></script>
408
+ <script src="/builders/shell-game.js?v=2026-11-11-helm"></script>
409
409
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
410
410
  <script src="/builders/craft-mode.js?v=2026-11-10-visor"></script>
411
- <script src="/builders/shell.js?v=2026-11-10-character"></script>
411
+ <script src="/builders/shell.js?v=2026-11-11-helm"></script>
412
412
  <script src="/builders/hall-dialog.js"></script>
413
413
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
414
414
  <script src="/builders/palette.js" defer></script>
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "_": "The hall's SETTINGS page through modules/ui-design/kit/render.js (task 1003308). Every panel on it is a form the page script fills from a dozen /me/* reads, so the floors run against the hall-preview harness rather than the kit's own stub:\n\n node scripts/hall-preview/server.js --port 4621 --fixture-me\n node modules/ui-design/kit/render.js --states modules/hall-ui/public/settings.states.json --base-in http://builders.localhost:4621 --base-out http://builders.localhost:4621\n\nOne group renders at a time behind the section rail, so each rail group is its own state. There is no `out` state on purpose: settings.js answers a 401 by navigating to the sign-in flow, and a state whose page navigates away measures the destination, not this page (the not-signed-in gate panel is the non-401 failure render, pinned statically in tests/hall_settings_world.mjs).",
3
3
  "page": "/settings",
4
+ "modes": ["dark"],
4
5
  "_storage": "The hall asks a first-visit question (the session mode picker, craft-mode.js) over every page; seeding \"already asked\" renders the page a returning builder sees (task 1005080).",
5
6
  "storage": { "session": { "hall-craft-asked": "1" } },
6
7
  "surface": "hall-ui",
@@ -1,65 +1,110 @@
1
- /* shell-game.css — the game's start screen and load screen (task 1005143; shell-game.js start()).
2
- Both stand over the welcome's painting, darkened by the hall's scrim. The menu and the load
3
- screen are each a card of the hall's own tokens, so they follow the hall's theme and the city
4
- drawn in the load card sits on its own ground. Colours, sizes, corners and spacing are tokens;
5
- nothing here is typed in. Rules: modules/hall-ui/records/start-screen.md. */
1
+ /* shell-game.css — the game's start screen and load screen (task 1005143; shell-game.js start()),
2
+ in the Helm look (task 1005192, ADR 0402). Both stand over the welcome's painting. The start
3
+ screen is one full-height night panel on the left, closed left and right by an ice hairline:
4
+ the mark and the project's name in the name voice, the five choices as full-width rows, one
5
+ line at the foot. The load screen is the same panel language, centred: the week in the name
6
+ voice, one segment per week, the city, the tip. Colours, sizes, corners, spacing and the
7
+ panel's own measures are the hall tier's tokens (style.css); nothing here is typed in, and
8
+ there is no breakpoint: the panel is as wide as a phone and stands off the edge on a desk
9
+ (--helm-inset). Rules: modules/hall-ui/records/start-screen.md. */
6
10
  html.gs-open { overflow: hidden; }
7
11
  .gs {
8
12
  position: fixed; inset: 0; z-index: var(--ds-z-modal, 160); overflow: auto;
9
- display: flex; align-items: center; justify-content: center;
10
- padding: var(--ds-space-lg) var(--ds-space-gutter-narrow);
11
- background: var(--bg); color: var(--ink); font-family: var(--font-body);
12
- transition: opacity .5s var(--ds-ease);
13
+ display: flex; align-items: stretch; justify-content: flex-start;
14
+ padding: 0 0 0 var(--helm-inset);
15
+ background: var(--bg); color: var(--ink-soft); font-family: var(--font-body);
16
+ transition: opacity var(--ds-dur-slower) var(--ds-ease);
13
17
  }
14
18
  .gs--leaving { opacity: 0; pointer-events: none; }
19
+ .gs--load { align-items: center; justify-content: center; padding: var(--ds-space-lg) var(--ds-space-gutter-narrow); }
15
20
 
16
- /* the still: the painting, covering the screen, under the scrim so a card reads on any part of it */
21
+ /* the still: the painting, covering the screen. The start screen leaves it bare (the panel is
22
+ its own ground); the load screen lays the scrim over it so the centred panel reads anywhere. */
17
23
  .gs__art { position: absolute; inset: 0; }
18
- .gs__art::after { content: ""; position: absolute; inset: 0; background: var(--scrim); }
24
+ .gs--load .gs__art::after { content: ""; position: absolute; inset: 0; background: var(--scrim); }
19
25
  .gs__img { width: 100%; height: 100%; object-fit: cover; display: block; }
20
26
 
21
- /* the two cards */
27
+ /* the two panels: night over the art, an ice hairline left and right, no corner, no shadow */
22
28
  .gs__menu, .gs__load {
23
- position: relative; width: 100%; box-sizing: border-box; padding: var(--ds-space-md);
24
- background: var(--bg-card); border-radius: var(--ds-radius-panel); box-shadow: var(--ds-shadow-pop);
29
+ position: relative; box-sizing: border-box;
30
+ background: var(--night-glass);
31
+ border: 0 solid var(--rule); border-left-width: var(--line-strong); border-right-width: var(--line-strong);
25
32
  }
26
33
  .gs__menu[hidden], .gs__load[hidden] { display: none; }
27
34
 
28
- /* the start screen: the project's name over five choices, one column */
29
- .gs__menu { max-width: 24rem; }
35
+ /* the start screen: the mark and the name, the five choices, one line at the foot */
36
+ .gs__menu {
37
+ width: min(100%, var(--helm-panel-w)); min-height: 100%; flex: none;
38
+ display: flex; flex-direction: column; justify-content: space-between; gap: var(--ds-space-lg);
39
+ padding: var(--ds-space-xl) 0 var(--ds-space-lg);
40
+ }
41
+ .gs__top { display: flex; flex-direction: column; gap: var(--ds-space-xl); }
42
+ .gs__head { display: flex; flex-direction: column; gap: var(--ds-space-md); padding: 0 var(--ds-space-lg); }
43
+ .gs__mark { display: block; width: var(--helm-mark); height: var(--helm-mark); object-fit: contain; --mark-ink: var(--ink); }
30
44
  .gs__title {
31
- margin: var(--ds-space-xs) 0 var(--ds-space-md); text-align: center; text-wrap: balance;
32
- font-family: var(--font-display); font-size: var(--ds-text-title); font-weight: 700; line-height: 1.1;
45
+ margin: 0; overflow-wrap: anywhere;
46
+ font-family: var(--name-face); font-size: var(--ds-text-headline); font-weight: var(--name-weight);
47
+ letter-spacing: var(--name-space-wide); text-transform: var(--name-case); line-height: 1.15;
48
+ color: var(--ink); text-shadow: var(--name-glow);
33
49
  }
34
- .gs__items { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--ds-space-xs); }
50
+ .gs__items { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
35
51
  .gs__items[hidden] { display: none; }
36
- .gs__items--extras { margin-top: var(--ds-space-sm); flex-direction: row; flex-wrap: wrap; justify-content: center; }
37
52
  .gs__item {
38
- display: flex; align-items: center; justify-content: center; box-sizing: border-box;
39
- width: 100%; min-height: 48px; padding: var(--ds-space-xs) var(--ds-space-md);
40
- background: transparent; color: inherit; border: 1px solid var(--rule);
41
- border-radius: var(--ds-radius-pill); font: inherit; font-size: var(--ds-text-xl); font-weight: 600;
53
+ display: flex; align-items: center; justify-content: space-between; box-sizing: border-box;
54
+ width: 100%; min-height: var(--helm-row); padding: var(--ds-space-hair) var(--ds-space-lg);
55
+ background: transparent; color: var(--ink); border: 0; border-bottom: var(--line) solid var(--rule-soft);
56
+ font-family: var(--name-face); font-size: var(--ds-text-5xl); font-weight: 400;
57
+ letter-spacing: var(--name-space); text-transform: var(--name-case); text-align: left;
42
58
  text-decoration: none; cursor: pointer;
43
- transition: background-color .15s var(--ds-ease), border-color .15s var(--ds-ease);
59
+ transition: background-color var(--ds-dur-fast) var(--ds-ease);
60
+ }
61
+ .gs__items > li:last-child > .gs__item { border-bottom-color: transparent; }
62
+ .gs__item:hover { background: var(--glass); color: var(--ink); text-decoration: none; }
63
+ /* the ring sits inside the row: the focus colour on the edge, a band of the night inside it, so
64
+ one of the two reads on the accent bar as well as on the panel */
65
+ .gs__item:focus-visible { outline: 0; border-radius: var(--press-radius); box-shadow: var(--press-focus); }
66
+ /* Start is the one thing lit: the accent bar under its dark ink, with the key that takes it */
67
+ .gs__item[data-gs="start"] {
68
+ background: var(--accent); color: var(--accent-on); border-bottom-color: transparent;
69
+ font-weight: var(--name-weight-lit); box-shadow: var(--accent-glow);
70
+ }
71
+ .gs__item[data-gs="start"]:hover { background: var(--accent-lift); color: var(--accent-on); }
72
+ .gs__item[data-gs="start"]:focus-visible { box-shadow: var(--press-focus), var(--accent-glow); }
73
+ .gs__item[data-gs="start"]::after {
74
+ content: "Enter"; padding: 0 var(--ds-space-hair);
75
+ border: var(--line-strong) solid currentColor;
76
+ font-size: var(--ds-text-md); letter-spacing: var(--label-space);
77
+ }
78
+ /* Extras: three quieter rows, stepped in under the menu */
79
+ .gs__items--extras { border-top: var(--line) solid var(--rule-soft); }
80
+ .gs__item--extra {
81
+ min-height: var(--ds-space-xl); padding-left: var(--ds-space-xl);
82
+ font-size: var(--ds-text-xl); letter-spacing: var(--name-space-tight); color: var(--ink-soft);
44
83
  }
45
- .gs__item:hover { background: var(--bg); border-color: var(--ink-soft); text-decoration: none; }
46
- .gs__item:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }
47
- /* Start is the one filled choice, as the hall's primary pill is */
48
- .gs__item[data-gs="start"] { background: var(--accent); color: var(--accent-on); border-color: transparent; }
49
- .gs__item[data-gs="start"]:hover { background: var(--accent-lift); border-color: transparent; }
50
- .gs__item--extra { width: auto; min-height: 40px; font-size: var(--ds-text-md); font-weight: 500; }
51
- .gs__note { margin: var(--ds-space-md) 0 0; text-align: center; font-size: var(--ds-text-md); color: var(--ink-soft); }
84
+ /* the foot: one line said to you, over the strong hairline */
85
+ .gs__foot { padding: var(--ds-space-md) var(--ds-space-lg) 0; border-top: var(--line-strong) solid var(--rule); }
86
+ .gs__line, .gs__note { margin: 0; font-size: var(--ds-text-lg); line-height: 1.5; text-wrap: pretty; color: var(--ink-soft); }
87
+ .gs__note { margin-top: var(--ds-space-xs); color: var(--ink); }
52
88
  .gs__note[hidden] { display: none; }
53
89
 
54
- /* the load screen: the city building up, the week it is showing, and the tip under it */
55
- .gs__load { max-width: 44rem; display: flex; flex-direction: column; gap: var(--ds-space-sm); text-align: center; }
56
- .gs__city { display: block; width: 100%; height: auto; max-height: 56vh; }
57
- .gs__city[hidden], .gs__when[hidden] { display: none; }
58
- .gs__when { margin: 0; font-size: var(--ds-text-sm); color: var(--ink-soft); font-variant-numeric: tabular-nums; }
59
- .gs__tip { margin: 0; font-size: var(--ds-text-lg); line-height: 1.45; text-wrap: pretty; }
90
+ /* the load screen: the week it is showing, a segment per week, the city, and the tip under it */
91
+ .gs__load {
92
+ width: 100%; max-width: 44rem; padding: var(--ds-space-lg) var(--ds-space-md);
93
+ display: flex; flex-direction: column; gap: var(--ds-space-sm);
94
+ }
95
+ .gs__city { display: block; width: 100%; height: auto; max-height: 52vh; }
96
+ .gs__city[hidden], .gs__when[hidden], .gs__segs[hidden] { display: none; }
97
+ .gs__when {
98
+ margin: 0; color: var(--ink); font-variant-numeric: tabular-nums;
99
+ font-family: var(--name-face); font-size: var(--ds-text-xl); font-weight: var(--name-weight);
100
+ letter-spacing: var(--name-space); text-transform: var(--name-case);
101
+ }
102
+ .gs__tip {
103
+ margin: 0; padding-top: var(--ds-space-sm); border-top: var(--line) solid var(--rule-soft);
104
+ font-size: var(--ds-text-lg); line-height: 1.5; text-wrap: pretty; color: var(--ink-soft);
105
+ }
60
106
  .gs__tip b {
61
- display: inline-block; margin-right: var(--ds-space-xs); padding: 0 var(--ds-space-xs);
62
- border: 1px solid var(--rule); border-radius: var(--ds-radius-pill);
63
- font-size: var(--ds-text-2xs); font-weight: 600; letter-spacing: .08em; text-transform: uppercase;
64
- color: var(--ink-soft); vertical-align: middle;
107
+ display: block; margin-bottom: var(--ds-space-hair);
108
+ font-family: var(--name-face); font-size: var(--label-size); font-weight: var(--name-weight);
109
+ letter-spacing: var(--label-space); text-transform: var(--name-case); color: var(--ink);
65
110
  }
@@ -91,22 +91,54 @@
91
91
  if (m.href) return '<li><a' + inner + ' href="' + esc(m.href) + '">' + esc(m.label) + '</a></li>';
92
92
  return '<li><button type="button"' + inner + (m.id === 'extras' ? ' aria-expanded="false" aria-controls="gs-extras"' : '') + '>' + esc(m.label) + '</button></li>';
93
93
  }
94
+ // The project's own logo where it has one (ADR 0358), else the shared mark, as the sidebar draws it.
95
+ function markHtml() {
96
+ var theme = (window.__BRANDING__ && window.__BRANDING__.theme) || {};
97
+ if (theme.logo) return '<img class="gs__mark" src="' + esc(theme.logo) + '" alt="" decoding="async">';
98
+ return '<svg class="gs__mark" viewBox="0 0 200 200" aria-hidden="true"><use href="/modules/ui-design/public/brand/mark.svg#mark"/></svg>';
99
+ }
100
+ // The Helm panel (task 1005192, ADR 0402): the mark and the name, the choices, one line at the foot.
94
101
  function screenHtml(o) {
95
102
  return '<div class="gs__art" aria-hidden="true">' + (o.art ? '<img class="gs__img" alt="" src="' + esc(o.art) + '" decoding="async">' : '') + '</div>' +
96
103
  '<div class="gs__menu">' +
97
- '<h2 class="gs__title" id="gs-title">' + esc(o.name || 'This project') + '</h2>' +
98
- '<ul class="gs__items">' + MENU.map(itemHtml).join('') + '</ul>' +
99
- '<ul class="gs__items gs__items--extras" id="gs-extras" hidden>' + EXTRAS.map(function (x) {
100
- return '<li><a class="gs__item gs__item--extra" href="' + esc(x.href) + '">' + esc(x.label) + '</a></li>';
101
- }).join('') + '</ul>' +
102
- '<p class="gs__note" id="gs-note" role="status" hidden></p>' +
104
+ '<div class="gs__top">' +
105
+ '<div class="gs__head">' + markHtml() +
106
+ '<h2 class="gs__title" id="gs-title">' + esc(o.name || 'This project') + '</h2>' +
107
+ '</div>' +
108
+ '<div class="gs__list">' +
109
+ '<ul class="gs__items">' + MENU.map(itemHtml).join('') + '</ul>' +
110
+ '<ul class="gs__items gs__items--extras" id="gs-extras" hidden>' + EXTRAS.map(function (x) {
111
+ return '<li><a class="gs__item gs__item--extra" href="' + esc(x.href) + '">' + esc(x.label) + '</a></li>';
112
+ }).join('') + '</ul>' +
113
+ '</div>' +
114
+ '</div>' +
115
+ '<div class="gs__foot">' +
116
+ '<p class="gs__line" id="gs-line">' + esc(tipAt(read('localStorage', TIP_KEY))) + '</p>' +
117
+ '<p class="gs__note" id="gs-note" role="status" hidden></p>' +
118
+ '</div>' +
103
119
  '</div>' +
104
120
  '<div class="gs__load" id="gs-load" role="status" hidden>' +
105
- '<svg class="gs__city" id="gs-city" role="img" aria-label="The project city building up, week by week" hidden></svg>' +
106
121
  '<p class="gs__when" id="gs-when" hidden></p>' +
122
+ '<ol class="gs__segs segs" id="gs-segs" aria-hidden="true" hidden></ol>' +
123
+ '<svg class="gs__city" id="gs-city" role="img" aria-label="The project city building up, week by week" hidden></svg>' +
107
124
  '<p class="gs__tip"><b>Tip</b><span id="gs-tip"></span></p>' +
108
125
  '</div>';
109
126
  }
127
+ // One segment per week of the history: filled up to the week being shown. The words beside
128
+ // them (#gs-when) say the same thing, so the row is hidden from a screen reader.
129
+ // Counted the way whenLabel words it, so the row and the sentence never disagree.
130
+ function segsDone(weeks, t) {
131
+ var n = Math.max(0, Math.round(Number(weeks) || 0));
132
+ return Math.max(0, n - Math.round(Math.max(0, n - 1 - (Number(t) || 0))));
133
+ }
134
+ function drawSegs(weeks, t) {
135
+ var box = root && root.querySelector('#gs-segs'), n = Math.max(0, Math.round(Number(weeks) || 0));
136
+ if (!box || n < 2) return;
137
+ while (box.children.length < n) box.appendChild(document.createElement('li'));
138
+ var done = segsDone(n, t);
139
+ for (var i = 0; i < box.children.length; i++) box.children[i].className = i < done ? 'seg seg--done' : 'seg';
140
+ box.hidden = false;
141
+ }
110
142
 
111
143
  var root = null, opts = null, timers = [], phase = null;
112
144
  function later(ms, fn) { timers.push(setTimeout(fn, ms)); }
@@ -159,6 +191,7 @@
159
191
  svg.removeAttribute('hidden');
160
192
  when.textContent = whenLabel(model.W, t);
161
193
  when.hidden = false;
194
+ drawSegs(model.W, t);
162
195
  }
163
196
  // One week after another, as state: each stop is a whole drawing, so nothing here animates.
164
197
  function runHistory(model) {
@@ -325,6 +358,6 @@
325
358
  } catch (_) { /* a href the browser cannot read: leave it alone */ }
326
359
  }, true);
327
360
  }
328
- window.OTBShellGame = { on: on, set: set, hint: hint, start: start, tipAt: tipAt, historySteps: historySteps, whenLabel: whenLabel, MENU: MENU, TIPS: TIPS,
361
+ window.OTBShellGame = { on: on, set: set, hint: hint, start: start, tipAt: tipAt, historySteps: historySteps, whenLabel: whenLabel, segsDone: segsDone, MENU: MENU, TIPS: TIPS,
329
362
  embedded: function () { return embedded; }, menuPath: menuPath };
330
363
  })();
@@ -538,7 +538,6 @@
538
538
  '</button>' +
539
539
  (CRAFT ? CRAFT.switchHtml() : '') +
540
540
  '<button class="zoom-toggle" id="zoom-toggle" type="button" aria-label="Text size"></button>' +
541
- '<button class="theme-toggle" id="theme-toggle" type="button" aria-label="Switch theme"></button>' +
542
541
  // task 1004327: the full-screen switch, LAST so it sits in the corner the
543
542
  // collapsed bar keeps in full screen — the one control that stays put.
544
543
  // task 1004430: on every page now, unless the page opts out.
@@ -646,44 +645,21 @@
646
645
  window.setTimeout(function () { if (!e.defaultPrevented && isFullscreen()) setFullscreen(false); }, 0);
647
646
  }
648
647
 
649
- // ---------- theme (light / dark) ----------
650
- // The pack's :root override carries only light values; the dark neutrals live
651
- // in style.css under html[data-theme="dark"] (higher specificity than :root,
652
- // so they win over the pack while the instance accent survives). Stored
653
- // preference beats the OS setting; no stored preference follows it live.
654
- var THEME_KEY = 'hall-theme';
655
- var themeMedia = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
656
-
657
- function storedTheme() {
658
- try { var v = window.localStorage.getItem(THEME_KEY); return v === 'dark' || v === 'light' ? v : null; } catch (_) { return null; }
659
- }
660
- function effectiveTheme() {
661
- return storedTheme() || (themeMedia && themeMedia.matches ? 'dark' : 'light');
662
- }
648
+ // ---------- theme: the hall is dark only (task 1005192, ADR 0402) ----------
649
+ // Helm is the hall's one look, so there is no light/dark switch and no stored
650
+ // choice: the server stamps html[data-theme="dark"] on every hall page
651
+ // (src/hall-helm.js) and this sets it again, for a page read without that stamp.
652
+ // The old 'hall-theme' preference is never read. Scripts that only READ the
653
+ // attribute (the city, the sky, the atlas) keep working: they always read dark.
663
654
  function applyTheme() {
664
- var t = effectiveTheme();
665
- document.documentElement.dataset.theme = t;
666
- var btn = document.getElementById('theme-toggle');
667
- if (btn) {
668
- var toDark = t !== 'dark';
669
- btn.setAttribute('aria-label', toDark ? 'Switch to dark theme' : 'Switch to light theme');
670
- btn.title = btn.getAttribute('aria-label');
671
- btn.innerHTML = toDark
672
- ? '<svg viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="M14.5 10.8A6 6 0 0 1 7.2 3.5a6 6 0 1 0 7.3 7.3Z"/></svg>'
673
- : '<svg viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><circle cx="9" cy="9" r="3.5"/><path d="M9 1.5v2m0 11v2M1.5 9h2m11 0h2M3.7 3.7l1.4 1.4m7.8 7.8 1.4 1.4m0-10.6-1.4 1.4M5.1 12.9l-1.4 1.4"/></svg>';
674
- }
675
- }
676
- function toggleTheme() {
677
- var next = effectiveTheme() === 'dark' ? 'light' : 'dark';
678
- try { window.localStorage.setItem(THEME_KEY, next); } catch (_) { /* private mode */ }
679
- applyTheme();
655
+ document.documentElement.dataset.theme = 'dark';
680
656
  }
681
657
 
682
658
  // ---------- zoom (accessibility: enlarge the whole page) ----------
683
659
  // task 1475 (idea 280): a one-click "make everything bigger" control for
684
660
  // readability. Cycles 100% -> 125% -> 150% -> 100% via CSS `zoom` on <html>,
685
661
  // which enlarges px AND rem sizes uniformly (a font-size bump would miss the
686
- // hall's many px values). Persisted per-browser like the theme preference.
662
+ // hall's many px values). Persisted per-browser.
687
663
  var ZOOM_KEY = 'hall-zoom';
688
664
  var ZOOM_STEPS = [1, 1.25, 1.5];
689
665
  function storedZoom() {
@@ -1326,8 +1302,6 @@
1326
1302
  if (document.documentElement.classList.contains('drawer-open')) closeDrawer();
1327
1303
  else openDrawer();
1328
1304
  });
1329
- var theme = document.getElementById('theme-toggle');
1330
- if (theme) theme.addEventListener('click', toggleTheme);
1331
1305
  var zoom = document.getElementById('zoom-toggle');
1332
1306
  if (zoom) zoom.addEventListener('click', cycleZoom);
1333
1307
  var full = document.getElementById('fullscreen-toggle');
@@ -1335,9 +1309,6 @@
1335
1309
  full.addEventListener('click', function () { setFullscreen(!isFullscreen()); });
1336
1310
  window.addEventListener('keydown', onFullscreenKey);
1337
1311
  }
1338
- if (themeMedia && themeMedia.addEventListener) {
1339
- themeMedia.addEventListener('change', function () { if (!storedTheme()) applyTheme(); });
1340
- }
1341
1312
  var scrim = document.getElementById('app-scrim');
1342
1313
  if (scrim) scrim.addEventListener('click', closeDrawer);
1343
1314
  var sidebar = document.getElementById('app-sidebar');
@@ -1469,9 +1440,6 @@
1469
1440
  closeDrawer: closeDrawer,
1470
1441
  isFullscreen: isFullscreen,
1471
1442
  setFullscreen: setFullscreen,
1472
- // task 1004544: a page with its own light/dark switch (the governor city
1473
- // and street) repaints the top bar's after writing 'hall-theme'.
1474
- applyTheme: applyTheme,
1475
1443
  // task 1004477: a page that renders an exit after boot asks the shell to fill it.
1476
1444
  fillExits: TRAIL.fillExits,
1477
1445
  positions: function () { return access.positions; }, // task 1004713: seats held (null while unknown)
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "_": "THE MATTER CARD THAT ACTS (task 1004528, goal 1000125) on the street page, through modules/ui-design/kit/render.js. Kept beside street.states.json rather than inside it, so the street and its interiors (task 1004526) and the card each own their states. The populated card needs the hall-preview harness, whose government__docket.json puts the review-seat amendment on the board source (board:47) and whose government__board__items.json carries its ballot (item 47), with a canned answer for a vote:\n\n node scripts/hall-preview/server.js --port 4625 --fixture-me\n node modules/ui-design/kit/render.js --states modules/hall-ui/public/street-card.states.json --base-in http://localhost:4625 --base-out http://localhost:4625\n\nTask 1004566 adds a card per kind the docket carries (blocker, red-team report, core update, module re-check, a Full Idea and the review queue), each compact and with \"more\" open; the harness fixture carries them. Against the kit stub there is no docket and no pin, so these states are for the harness.",
3
3
  "page": "/builders/street",
4
+ "modes": ["dark"],
4
5
  "_storage": "The hall asks a first-visit question (the session mode picker, craft-mode.js) over every page; seeding \"already asked\" renders the page a returning builder sees (task 1005080).",
5
6
  "storage": { "session": { "hall-craft-asked": "1" } },
6
7
  "surface": "hall-ui",
@@ -128,8 +128,6 @@ html[data-fullscreen="on"] .street-top { padding-right: 184px; }
128
128
  .street-hud__dot--hot { background: var(--accent); }
129
129
  .street-hud__dot--moving { width: 6px; height: 6px; border: 0; background: var(--ink); }
130
130
  .street-hud__n { font-size: 24px; font-weight: 300; font-variant-numeric: tabular-nums; }
131
- .street-mode { margin-left: 36px; flex: none; background: none; border: 1px solid var(--street-rule-strong); border-radius: 999px; height: 36px; padding: 0 14px; display: flex; align-items: center; gap: 8px; cursor: pointer; color: var(--ink); font-family: var(--font-body); font-size: 13px; text-transform: lowercase; }
132
- .street-mode svg { display: block; stroke-width: 1.4; }
133
131
 
134
132
  /* the street band: the prototype's 1440-wide artboard, scaled by street.js */
135
133
  /* The living street's parallax (task 1004685) is Keith Clark's, sideways and
@@ -309,20 +307,18 @@ html[data-fullscreen="on"] .street-top { padding-right: 184px; }
309
307
  .street-card__lines li { padding: 8px 0; border-top: 1px solid var(--street-rule); font-size: 14px; line-height: 1.4; }
310
308
  .street-card__result { margin: 0; font-size: 15px; line-height: 1.45; }
311
309
 
312
- .street-mark:focus-visible, .street-mode:focus-visible, .street-pin:focus-visible, .street-door:focus-visible, .street-sign:focus-visible, .street-card__close:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
310
+ .street-mark:focus-visible, .street-pin:focus-visible, .street-door:focus-visible, .street-sign:focus-visible, .street-card__close:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
313
311
 
314
312
  /* narrower: the strip tightens, then wraps; the readings scroll sideways */
315
313
  @media (max-width: 1180px) {
316
314
  .street-hud { gap: 20px; }
317
315
  .street-hud__n { font-size: 20px; }
318
- .street-mode { margin-left: 20px; }
319
316
  }
320
317
  /* narrow: two rows inside the 80 px top-bar floor, the wordmark and the switch
321
318
  on the first, the three figures on the second as their own sideways strip */
322
319
  @media (max-width: 900px) {
323
320
  .street-top { height: auto; max-height: 80px; display: grid; grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "brand mode" "hud hud"; row-gap: 2px; padding: 5px 16px; }
324
321
  .street-top__brand { grid-area: brand; }
325
- .street-mode { grid-area: mode; margin-left: 0; height: 34px; }
326
322
  .street-hud { grid-area: hud; margin-left: 0; gap: 18px; min-width: 0; overflow-x: auto; overflow-y: hidden; white-space: nowrap; height: 30px; }
327
323
  .street-hud__item { gap: 6px; flex: none; }
328
324
  .street-hud .street-label { font-size: 11px; }
@@ -10,8 +10,8 @@
10
10
  <!-- The faces (Manrope, JetBrains Mono) come from the branding pack's font
11
11
  links, injected into this head by the server like every hall page's
12
12
  (tests/hall_tokens.mjs: no hall page hardcodes a web-font stylesheet). -->
13
- <link rel="stylesheet" href="/builders/style.css?v=2026-11-10-options">
14
- <link rel="stylesheet" href="/builders/street.css?v=2026-10-09-living3">
13
+ <link rel="stylesheet" href="/builders/style.css?v=2026-11-11-helm">
14
+ <link rel="stylesheet" href="/builders/street.css?v=2026-11-11-helm">
15
15
  <link rel="stylesheet" href="/builders/street-card.css?v=2026-10-04-record1">
16
16
  <link rel="stylesheet" href="/builders/street-deck.css?v=2026-10-17-tidy">
17
17
  </head>
@@ -92,10 +92,6 @@
92
92
  <span class="street-hud__n" id="hudShare">—</span>
93
93
  </div>
94
94
  </div>
95
- <button type="button" class="street-mode" id="streetMode" aria-label="Switch to light mode" title="Switch to light mode">
96
- <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" aria-hidden="true"><path id="streetModeIcon" d=""></path></svg>
97
- <span id="streetModeWord">light</span>
98
- </button>
99
95
  </div>
100
96
 
101
97
  <div class="street-window" id="streetWindow">
@@ -229,10 +225,10 @@
229
225
  </div>
230
226
 
231
227
  <script src="/builders/dom-utils.js?v=2026-11-05-numbers"></script>
232
- <script src="/builders/shell-game.js?v=2026-11-10-options"></script>
228
+ <script src="/builders/shell-game.js?v=2026-11-11-helm"></script>
233
229
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
234
230
  <script src="/builders/craft-mode.js?v=2026-11-10-visor"></script>
235
- <script src="/builders/shell.js?v=2026-11-10-character"></script>
231
+ <script src="/builders/shell.js?v=2026-11-11-helm"></script>
236
232
  <script src="/builders/hall-dialog.js"></script>
237
233
  <script src="/builders/palette.js" defer></script>
238
234
  <script src="/builders/gov-living.js?v=2026-10-09-positions1"></script>
@@ -246,6 +242,6 @@
246
242
  <script src="/builders/street-amend.js?v=2026-10-02-amend1"></script>
247
243
  <script src="/builders/street-deck.js?v=2026-10-13-glass1"></script>
248
244
  <script src="/builders/street-office.js?v=2026-10-17-tidy"></script>
249
- <script src="/builders/street.js?v=2026-10-17-tidy"></script>
245
+ <script src="/builders/street.js?v=2026-11-11-helm"></script>
250
246
  </body>
251
247
  </html>
@@ -28,10 +28,8 @@
28
28
  // A feed that fails leaves its part out (no pins, no districts, "—" for the
29
29
  // share) and the page says the docket could not be read; it never draws sample data.
30
30
  //
31
- // THE SWITCH. The hall's own preference ('hall-theme', else the OS) and the
32
- // hall's own attribute (html[data-theme]), shared with the city and every
33
- // hall page. The drawing's faces are drawing data in the mode, so a toggle
34
- // redraws the street from the last feeds (no refetch).
31
+ // THE MODE. The hall is dark only (task 1005192, ADR 0402), so the street has
32
+ // no light/dark switch and reads no stored choice: it draws its night faces.
35
33
  //
36
34
  // MOTION is CSS only (street.css); nothing here animates. The scale write on
37
35
  // resize is layout, not motion.
@@ -64,9 +62,6 @@
64
62
  'use strict';
65
63
  var S = window.GovStreet;
66
64
  var SVG_NS = 'http://www.w3.org/2000/svg';
67
- var THEME_KEY = 'hall-theme';
68
- var SUN = 'M7 1.5V3M7 11v1.5M1.5 7H3M11 7h1.5M3.1 3.1l1 1M9.9 9.9l1 1M3.1 10.9l1-1M9.9 4.1l1-1M9.5 7A2.5 2.5 0 1 1 4.5 7A2.5 2.5 0 1 1 9.5 7Z';
69
- var MOON = 'M11.5 8.6A5 5 0 0 1 5.4 2.5A5 5 0 1 0 11.5 8.6Z';
70
65
  var FEEDS = {
71
66
  docket: '/api/bongos/government/docket',
72
67
  goals: '/api/bongos/goals',
@@ -88,7 +83,6 @@
88
83
  var CARD_W = 390;
89
84
 
90
85
  var $ = function (id) { return document.getElementById(id); };
91
- var media = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
92
86
  var data = { docket: null, goals: [], rollups: [], earnings: null, loaded: false, tasks: {}, room: null, constitution: null };
93
87
  // ask: the act whose chips stand open; more: the detail is open in place;
94
88
  // pick: which of the review queue the queue's card shows (task 1004566)
@@ -108,52 +102,12 @@
108
102
  var today = { day: '', n: 0 };
109
103
  var todayDrawn = null; // the count the meter's pips last drew: they are rebuilt only when it moves
110
104
 
111
- // ---------- light / dark ----------
112
- var chosen = null;
113
- function storedTheme() {
114
- try { var v = window.localStorage.getItem(THEME_KEY); if (v === 'dark' || v === 'light') return v; } catch (_) { /* blocked */ }
115
- return chosen;
116
- }
117
- function effectiveTheme() { return storedTheme() || (media && media.matches ? 'dark' : 'light'); }
118
- function paintSwitch(t) {
119
- var btn = $('streetMode');
120
- var label = t === 'dark' ? 'Switch to light mode' : 'Switch to dark mode';
121
- if (btn) { btn.setAttribute('aria-label', label); btn.setAttribute('title', label); }
122
- var icon = $('streetModeIcon');
123
- if (icon) icon.setAttribute('d', t === 'dark' ? SUN : MOON);
124
- var word = $('streetModeWord');
125
- if (word) word.textContent = t === 'dark' ? 'light' : 'dark';
126
- }
105
+ // ---------- the mode: dark only (task 1005192, ADR 0402) ----------
127
106
  function applyTheme() {
128
- var t = effectiveTheme();
129
- document.documentElement.setAttribute('data-theme', t);
130
- paintSwitch(t);
131
- return t;
132
- }
133
- function toggleTheme() {
134
- var next = effectiveTheme() === 'dark' ? 'light' : 'dark';
135
- chosen = next;
136
- try { window.localStorage.setItem(THEME_KEY, next); } catch (_) { /* private mode: this page only */ }
137
- painted = applyTheme();
138
- draw();
139
- var shell = window.OTBShell;
140
- if (shell && typeof shell.applyTheme === 'function' && storedTheme() === painted) shell.applyTheme();
141
- }
142
- // IN THE HALL (task 1004544) the shell's top bar carries the hall's own theme
143
- // switch beside this one, as on the city. Both write the same 'hall-theme'
144
- // preference and html[data-theme]; each repaints when the other flips it.
145
- var painted = null;
146
- function watchShellTheme() {
147
- if (typeof MutationObserver !== 'function') return;
148
- new MutationObserver(function () {
149
- var t = document.documentElement.getAttribute('data-theme');
150
- if ((t === 'dark' || t === 'light') && t !== painted) { painted = t; chosen = t; paintSwitch(t); draw(); }
151
- }).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
152
- }
153
- function currentMode() {
154
- var t = document.documentElement.getAttribute('data-theme');
155
- return t === 'light' || t === 'dark' ? t : effectiveTheme();
107
+ document.documentElement.setAttribute('data-theme', 'dark');
108
+ return 'dark';
156
109
  }
110
+ function currentMode() { return 'dark'; }
157
111
 
158
112
  // ---------- the address ----------
159
113
  function here() { return S.parseHash(window.location && window.location.hash); }
@@ -316,7 +270,6 @@
316
270
  function draw() {
317
271
  var mode = currentMode();
318
272
  view.mode = mode;
319
- paintSwitch(mode);
320
273
  var h = here();
321
274
  var inside = $('streetInside');
322
275
  var sizerEl = $('streetSizer');
@@ -1465,8 +1418,6 @@
1465
1418
  }
1466
1419
 
1467
1420
  function boot() {
1468
- var btn = $('streetMode');
1469
- if (btn) btn.addEventListener('click', toggleTheme);
1470
1421
  var close = $('streetCardClose');
1471
1422
  if (close) close.addEventListener('click', function () { closeCard(true); });
1472
1423
  window.addEventListener('keydown', onKey);
@@ -1479,8 +1430,7 @@
1479
1430
  if (am) am.addEventListener('click', amend);
1480
1431
  var win = $('streetWindow');
1481
1432
  if (win) win.addEventListener('scroll', placeCard);
1482
- if (media && media.addEventListener) media.addEventListener('change', function () { if (!storedTheme()) { applyTheme(); draw(); } });
1483
- painted = applyTheme();
1433
+ applyTheme();
1484
1434
  today = readToday();
1485
1435
  // the decision deck (#deck) and the government inside each building, over the
1486
1436
  // street's own senders (street-deck.js, street-office.js; task 1004685)
@@ -1490,10 +1440,9 @@
1490
1440
  // an answer in the deck moves the header's counts as it moves the deck's
1491
1441
  if (window.GovFeeds) window.GovFeeds.onChange(function () { if (data.loaded) draw(); });
1492
1442
  draw();
1493
- watchShellTheme();
1494
1443
  return load();
1495
1444
  }
1496
1445
 
1497
- window.GovStreetPage = { boot: boot, draw: draw, load: load, fit: fit, openCard: openCard, closeCard: closeCard, actOn: actOn, pickFloor: pickFloor, deskAct: deskAct, goStreet: goStreet, amend: amend, fileDraft: fileDraft, FEEDS: FEEDS, THEME_KEY: THEME_KEY };
1446
+ window.GovStreetPage = { boot: boot, draw: draw, load: load, fit: fit, openCard: openCard, closeCard: closeCard, actOn: actOn, pickFloor: pickFloor, deskAct: deskAct, goStreet: goStreet, amend: amend, fileDraft: fileDraft, FEEDS: FEEDS };
1498
1447
  window.GovStreetPage.ready = boot();
1499
1448
  }());