@bongos/core 1.25.24 → 1.25.26

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.
@@ -89,8 +89,9 @@ html:has(body[data-page="city"]) { height: 100%; }
89
89
  .city-main { position: fixed; top: var(--topbar-h, 56px); left: var(--sidebar-w-rail, 64px); right: 0; bottom: 0; max-width: none; margin: 0; contain: layout paint; z-index: 1; }
90
90
  html[data-fullscreen="on"] .city-main { top: 0; left: 0; }
91
91
  /* tasks 1005148, 1005195 and 1005209: with the visor up (game mode), the door (now its line alone) rises clear of the
92
- foot row, whose one button is centred too, even when the key hints wrap onto a second line. */
93
- html.visor-on .city-door { bottom: 224px; }
92
+ foot row, whose one button is centred too. Task 1005266: the hints are keys alone, so they no
93
+ longer wrap onto a second line, and the row sits --visor-drop lower (visor.css; 0 on a phone). */
94
+ html.visor-on .city-door { bottom: calc(180px - var(--visor-drop, 0px)); }
94
95
  /* task 1005209 (ADR 0403): with the visor up the door's button and the archived links are the
95
96
  visor's (D on the glass, the rest in its Actions sheet), so the scene carries none of its own. */
96
97
  html.visor-on .city-start, html.visor-on .city-archived { display: none; }
@@ -940,7 +940,8 @@ html[data-theme="dark"] .brand-mark__mark { --mark-ink: #e2e1e4; }
940
940
  and the shell’s corner buttons, so the top arc, its notch and the place all step aside. The foot row keeps its three parts in one line and nothing leans: the sprite
941
941
  with the count over its word, the one accent button (its name may take two lines; its key is in the sheet), and of
942
942
  the key hints only Actions, as its dots (every other hint is a row in its sheet). */
943
- :root[data-game="on"] { --visor-inset-bottom: 52px; --visor-foot-room: 112px; }
943
+ :root[data-game="on"] { --visor-inset-bottom: 52px; --visor-foot-room: 112px; --visor-arc-foot: 8px; }
944
+ html[data-game="on"].visor-on { --visor-drop: 0px; } /* task 1005266: a phone's foot row did not move */
944
945
  html[data-game="on"] .visor__place, html[data-game="on"] .visor__progress, html[data-game="on"] .visor__notch, html[data-game="on"] .visor__frame--top, html[data-game="on"] .visor__name { display: none; }
945
946
  html[data-game="on"] .visor__foot { grid-template-columns: auto minmax(0, 1fr) auto; column-gap: var(--ds-space-hair); }
946
947
  html[data-game="on"] .visor__me, html[data-game="on"] .visor__hints { transform: none; }
@@ -309,12 +309,14 @@ html[data-fullscreen="on"] .sky-stage .top { padding-right: 184px; }
309
309
  clear of the glass, as ADR 0399 §2.3 asks of a scene's pop-ups: the top row, the composer, the
310
310
  jump field and the two side panels step below the compass and the place line, and the foot and
311
311
  the tour rise above the visor's corner pills. Task 1005203 put the version's progress under
312
- the place, so the top offsets grew by its 24px. Outside game mode nothing here applies. */
313
- html.visor-on .top { padding-top: calc(84px + env(safe-area-inset-top, 0px)); }
314
- html.visor-on .composer, html.visor-on .jump { top: calc(140px + env(safe-area-inset-top, 0px)); }
315
- html.visor-on .panel, html.visor-on .room { top: calc(204px + env(safe-area-inset-top, 0px)); bottom: calc(200px + env(safe-area-inset-bottom, 0px)); }
316
- html.visor-on .foot { padding-bottom: calc(132px + env(safe-area-inset-bottom, 0px)); }
317
- html.visor-on .tour, html.visor-on .start { bottom: calc(210px + env(safe-area-inset-bottom, 0px)); }
312
+ the place, so the top offsets grew by its 24px. Task 1005266 made the glass slimmer: the top
313
+ band is 40px shorter, and the foot row sits --visor-drop lower (visor.css; 0 on a phone, whose
314
+ row did not move). Outside game mode nothing here applies. */
315
+ html.visor-on .top { padding-top: calc(44px + env(safe-area-inset-top, 0px)); }
316
+ html.visor-on .composer, html.visor-on .jump { top: calc(100px + env(safe-area-inset-top, 0px)); }
317
+ html.visor-on .panel, html.visor-on .room { top: calc(164px + env(safe-area-inset-top, 0px)); bottom: calc(200px - var(--visor-drop, 0px) + env(safe-area-inset-bottom, 0px)); }
318
+ html.visor-on .foot { padding-bottom: calc(132px - var(--visor-drop, 0px) + env(safe-area-inset-bottom, 0px)); }
319
+ html.visor-on .tour, html.visor-on .start { bottom: calc(210px - var(--visor-drop, 0px) + env(safe-area-inset-bottom, 0px)); }
318
320
  /* task 1005210 (ADR 0403): with the visor up the sky carries no control of its own. The top
319
321
  row's lens switch and pills are rows in the visor's Actions sheet; the composer and the jump
320
322
  field open from the visor ("Write a thought" on /, "Jump" on J) and close again when left
@@ -8,13 +8,22 @@
8
8
  hour's sky runs to every edge of the view, so the glass has no bands and no vignette). The
9
9
  marks carry themselves: every line is drawn over a wider stroke of the night, a key sits on a chip of
10
10
  the night, and the place, each hint and your credits stand on a small night plate. Colours, sizes and corners
11
- are the hall tier's tokens; nothing here is typed in. Record: modules/hall-ui/records/visor.md. */
11
+ are the hall tier's tokens; nothing here is typed in. Record: modules/hall-ui/records/visor.md.
12
+ IT RESTS WHILE YOU LOOK AND WAKES SLIM WHEN YOU ACT (task 1005266, owner 2026-11-13: the glass
13
+ took a third of a laptop's height). Awake, the top arc is the version's progress in four
14
+ pieces, the place and the percentage share one small line, and the foot row is smaller and
15
+ sits lower. At rest (.visor--rest, set by visor.js on a desk with a pointer) only the arcs,
16
+ the sight and your picture stay. A phone is as it was (style.css's shared phone block). */
12
17
  .visor { display: none; }
18
+ /* how far the foot row dropped (task 1005266), for a scene's own layers that sat above the old
19
+ row (thinking.css, city.css). A phone's row did not move, so the phone block sets it to 0. */
20
+ html.visor-on { --visor-drop: 48px; }
13
21
  /* where the glass sits, so a scene's pop-up, and a window the glass opens, can keep clear of
14
22
  it (ADR 0399 §2.3). On the root, not the visor, because the options window is not inside
15
- it. --visor-foot-room is the foot row's height with the hints wrapped onto a second line. */
23
+ it. --visor-foot-room is the foot row's height (task 1005266: the hints are keys alone, so
24
+ the row is one line), and --visor-arc-foot is where the foot arc sits under it. */
16
25
  html[data-game="on"] {
17
- --visor-arc: 64px; --visor-inset-top: 88px; --visor-inset-bottom: 76px; --visor-foot-room: 100px;
26
+ --visor-arc: 44px; --visor-inset-top: 48px; --visor-inset-bottom: 36px; --visor-foot-room: 56px; --visor-arc-foot: 10px;
18
27
  --visor-inset-right: max(var(--ds-space-gutter-narrow), var(--helm-inset)); --visor-inset-left: var(--visor-inset-right);
19
28
  }
20
29
  html[data-game="on"] .visor:not([hidden]) {
@@ -41,43 +50,44 @@ html[data-game="on"] .visor:not([hidden]) {
41
50
  top arc, its notch and the place all sit inside the top inset, above where a scene starts its
42
51
  own top row; the foot arc runs under the foot row. */
43
52
  .visor__frame { position: absolute; left: 0; width: 100%; overflow: visible; }
44
- .visor__frame--top { top: calc(var(--visor-arc) - 50px); height: 42px; }
45
- .visor__frame--foot { bottom: calc(var(--visor-inset-bottom) - 44px); height: 18px; }
53
+ .visor__frame--top { top: calc(var(--visor-arc) - 36px); height: 42px; }
54
+ .visor__frame--foot { bottom: var(--visor-arc-foot); height: 18px; }
46
55
  .visor__frame path { vector-effect: non-scaling-stroke; }
47
56
  .visor__line { fill: none; stroke: var(--char, var(--ink-soft)); stroke-opacity: .7; stroke-width: var(--line-strong); }
48
57
  .visor__line--under { stroke: var(--bg); stroke-opacity: 1; stroke-width: 5px; }
49
58
  .visor__dot { fill: var(--char, var(--ink-soft)); stroke: var(--bg); stroke-width: var(--line-strong); }
50
59
  .visor__notch, .visor__sight { position: absolute; left: 50%; transform: translate(-50%, -50%); overflow: visible; }
51
- .visor__notch { top: calc(var(--visor-arc) - 36px); width: 12px; height: 9px; }
60
+ .visor__notch { top: calc(var(--visor-arc) - 22px); width: 12px; height: 9px; }
61
+ /* the top arc as the version's progress (task 1005266): the same curve in four pieces. A quarter
62
+ built is the soft ink, the quarter being built is the character's colour, the rest are faint.
63
+ At rest the one plain arc stands in for them. */
64
+ .visor__seg--done { stroke: var(--ink-soft); stroke-opacity: 1; stroke-width: 3px; }
65
+ .visor__seg--now { stroke-opacity: 1; stroke-width: 3px; }
66
+ .visor__seg--todo { stroke: var(--ink-soft); stroke-opacity: .35; }
67
+ .visor--rest .visor__seg, .visor:not(.visor--rest) .visor__frame--segs .visor__arc { display: none; }
52
68
  .visor__sight { top: 50%; width: 24px; height: 24px; }
53
69
 
54
70
  /* type over the scene stands on a small plate of the night, cut like everything pressable and
55
71
  sized to its words: the place, each key hint, and your name and credits. No band, no halo. */
56
72
  .visor__place, .visor__who, .visor__hint { background: var(--night-plate); clip-path: var(--cut-path-sm); }
57
73
  /* the name voice, for every name on the glass */
58
- .visor__place, .visor__name, .visor__unit, .visor__main, .visor__hint, .visor__key, .visor__sheet-title, .visor__cap {
74
+ .visor__place, .visor__name, .visor__unit, .visor__main, .visor__hint, .visor__key, .visor__sheet-title, .visor__cap, .visor__hints::after {
59
75
  font-family: var(--name-face); font-weight: var(--name-weight); text-transform: var(--name-case);
60
76
  }
61
77
 
62
- /* the place, under the top arc */
78
+ /* the place and the version's reading, one small line under the top arc (task 1005266) */
79
+ .visor__top {
80
+ position: absolute; left: 50%; top: calc(var(--visor-arc) - 18px); transform: translateX(-50%);
81
+ display: flex; align-items: center; gap: var(--ds-space-hair); white-space: nowrap;
82
+ }
63
83
  .visor__place {
64
- position: absolute; left: 50%; top: calc(var(--visor-arc) - 26px); transform: translateX(-50%); margin: 0;
65
- font-size: var(--ds-text-2xl); letter-spacing: var(--name-space-wide); line-height: 1.2;
66
- color: var(--ink-soft); white-space: nowrap; padding: 0 var(--ds-space-xs);
84
+ margin: 0; font-size: var(--ds-text-xs); letter-spacing: var(--name-space); line-height: 1;
85
+ color: var(--ink); white-space: nowrap; padding: var(--ds-space-hair) var(--ds-space-xs);
67
86
  }
68
87
 
69
- /* the version's progress under the place (task 1005203): the kit's four segments, then the same
70
- reading in words (a row of bars is never the only reading). Done quarters are ice, the quarter
71
- being built is the accent, the rest are outlines; each segment wears an outline of the night so it
72
- reads over a midday sky. It sits under the arc band, inside the top inset (--visor-inset-top). */
73
- .visor__progress {
74
- position: absolute; left: 50%; top: calc(var(--visor-arc) + 6px); transform: translateX(-50%);
75
- display: flex; align-items: center; gap: var(--ds-space-xs); white-space: nowrap;
76
- }
77
- .visor__segs { flex-wrap: nowrap; }
78
- .visor__segs .seg { border-color: var(--ink-soft); outline: var(--line-strong) solid var(--night-plate); }
79
- .visor__segs .seg--done { background: var(--ink-soft); border-color: transparent; }
80
- .visor__segs .seg--now { background: var(--accent); border-color: transparent; }
88
+ /* the version's progress beside the place (task 1005203; task 1005266 moved the four segments
89
+ onto the top arc): the same reading in words, since a row of bars is never the only reading. */
90
+ .visor__progress { display: flex; align-items: center; white-space: nowrap; }
81
91
  .visor__progress-words {
82
92
  margin: 0; font-family: var(--name-face); font-weight: var(--name-weight); text-transform: var(--name-case);
83
93
  font-size: var(--ds-text-xs); letter-spacing: var(--name-space); line-height: 1; color: var(--ink-soft);
@@ -93,6 +103,11 @@ html[data-game="on"] .visor:not([hidden]) {
93
103
  background: var(--smoke-sheen), var(--smoke); box-shadow: var(--smoke-edge);
94
104
  -webkit-backdrop-filter: var(--smoke-blur); backdrop-filter: var(--smoke-blur);
95
105
  }
106
+ /* a key's name (task 1005266) is a plate like the four above, so it is the same pane */
107
+ .visor__hints[data-name]::after {
108
+ background: var(--smoke-sheen), var(--smoke); box-shadow: var(--smoke-edge);
109
+ -webkit-backdrop-filter: var(--smoke-blur); backdrop-filter: var(--smoke-blur);
110
+ }
96
111
  }
97
112
 
98
113
  /* the foot row: you on the left, the one thing to press in the middle, the key hints on the
@@ -136,23 +151,23 @@ html[data-game="on"] .visor:not([hidden]) {
136
151
  .visor__card-open { font-size: var(--ds-text-sm); color: var(--accent-ink); }
137
152
  .visor__card-name, .visor__card-open, .visor__fact-k { font-family: var(--name-face); font-weight: var(--name-weight); text-transform: var(--name-case); letter-spacing: var(--name-space); }
138
153
  .visor__sprite {
139
- flex: none; box-sizing: border-box; width: 56px; height: 56px; object-fit: cover; image-rendering: pixelated;
154
+ flex: none; box-sizing: border-box; width: 40px; height: 40px; object-fit: cover; image-rendering: pixelated;
140
155
  background: var(--bg-card); border: var(--line-strong) solid var(--char, var(--ink-soft)); clip-path: var(--cut-path-sm);
141
156
  }
142
157
  /* your name and credits stand in a box of the night, cut like the sprite beside it */
143
158
  .visor__who { display: flex; flex-direction: column; gap: 2px; padding: var(--ds-space-hair) var(--ds-space-xs); }
144
159
  .visor__name { font-size: var(--ds-text-xs); letter-spacing: var(--name-space); line-height: 1; color: var(--char, var(--ink-soft)); }
145
160
  .visor__credits { display: flex; align-items: baseline; gap: var(--ds-space-xs); white-space: nowrap; }
146
- .visor__num { font-family: var(--name-face); font-size: var(--ds-text-headline); font-weight: 400; line-height: 1; color: var(--ink); font-variant-numeric: tabular-nums; }
147
- .visor__unit { font-size: var(--ds-text-md); letter-spacing: var(--name-space); color: var(--ink-soft); }
161
+ .visor__num { font-family: var(--name-face); font-size: var(--ds-text-4xl); font-weight: 400; line-height: 1; color: var(--ink); font-variant-numeric: tabular-nums; }
162
+ .visor__unit { font-size: var(--ds-text-2xs); letter-spacing: var(--name-space); color: var(--ink-soft); }
148
163
 
149
164
  /* the one accent button: the cut corner, the dark ink, the key that takes it */
150
165
  .visor__main {
151
166
  pointer-events: auto; box-sizing: border-box; justify-self: center;
152
167
  display: inline-flex; align-items: center; justify-content: space-between; gap: var(--ds-space-sm);
153
- min-height: 56px; padding: var(--ds-space-hair) var(--ds-space-sm) var(--ds-space-hair) var(--ds-space-md);
168
+ min-height: 44px; padding: var(--ds-space-hair) var(--ds-space-xs) var(--ds-space-hair) var(--ds-space-sm);
154
169
  background: var(--accent); color: var(--accent-on); border: 0; border-radius: var(--press-radius); clip-path: var(--cut-path);
155
- font-size: var(--ds-text-3xl); font-weight: var(--name-weight-lit); letter-spacing: var(--name-space-tight); line-height: 1.1;
170
+ font-size: var(--ds-text-xl); font-weight: var(--name-weight-lit); letter-spacing: var(--name-space-tight); line-height: 1.1;
156
171
  text-decoration: none; cursor: pointer; transition: background-color var(--ds-dur-fast) var(--ds-ease);
157
172
  }
158
173
  .visor__main:hover { background: var(--accent-lift); color: var(--accent-on); text-decoration: none; }
@@ -160,18 +175,30 @@ html[data-game="on"] .visor:not([hidden]) {
160
175
  /* the key hints: a key and a name, no box round the pair. The only other parts of the glass
161
176
  that take a pointer. */
162
177
  .visor__hints {
163
- justify-self: end; display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: center; gap: 0 var(--ds-space-md);
178
+ position: relative; justify-self: end; display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: center; gap: 0 var(--ds-space-hair);
164
179
  transform: perspective(900px) rotateY(-16deg); transform-origin: right center;
165
180
  }
166
181
  .visor__hint {
167
182
  pointer-events: auto; box-sizing: border-box; display: inline-flex; align-items: center; gap: var(--ds-space-hair);
168
- min-height: 44px; padding: 0 var(--ds-space-xs) 0 var(--ds-space-hair); border: 0; border-radius: var(--press-radius);
183
+ min-height: 44px; padding: 0 var(--ds-space-xs); border: 0; border-radius: var(--press-radius);
169
184
  color: var(--ink-soft); font-size: var(--ds-text-lg); letter-spacing: var(--name-space-tight); white-space: nowrap;
170
185
  text-decoration: none; cursor: pointer;
171
186
  transition: color var(--ds-dur-fast) var(--ds-ease);
172
187
  }
173
188
  .visor__hint:hover { color: var(--ink); text-decoration: none; }
174
189
  .visor__hint[hidden] { display: none; }
190
+ /* a hint is its key alone (task 1005266). Its name stays in the page for a screen reader, and
191
+ when you point at the key or reach it with the keyboard the name stands on one small plate
192
+ above the row (visor.js nameHint sets data-name), so naming a key never moves another. The
193
+ place's own lead action keeps its words: they are the place's call to you (ADR 0403). */
194
+ .visor__hint:not(.visor__hint--place) > span:not(.visor__dots) {
195
+ position: absolute; width: 1px; height: 1px; padding: 0; border: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap;
196
+ }
197
+ .visor__hints[data-name]::after {
198
+ content: attr(data-name); position: absolute; right: 0; bottom: calc(100% + var(--ds-space-hair)); pointer-events: none;
199
+ padding: var(--ds-space-hair) var(--ds-space-xs); background: var(--night-plate); clip-path: var(--cut-path-sm);
200
+ color: var(--ink); font-size: var(--ds-text-sm); letter-spacing: var(--name-space-tight); line-height: 1.2; white-space: nowrap;
201
+ }
175
202
  /* Actions on a phone: three dots on the same chip of the night a key sits on (style.css shows it) */
176
203
  .visor__dots {
177
204
  display: none; box-sizing: content-box; width: 14px; height: 4px; padding: 10px 5px; border: var(--line-strong) solid currentColor;
@@ -219,7 +246,7 @@ html[data-game="on"] .visor:not([hidden]) {
219
246
  hairlines like the sheet, never wider than a short paragraph. The words are the scene's facts;
220
247
  the status dot is the hall's status vocabulary (--ok / --warn / --info), never a colour of its own. */
221
248
  .visor__focus {
222
- position: absolute; left: 50%; bottom: calc(var(--visor-inset-bottom) + 72px); transform: translateX(-50%);
249
+ position: absolute; left: 50%; bottom: calc(var(--visor-inset-bottom) + 56px); transform: translateX(-50%);
223
250
  pointer-events: auto; box-sizing: border-box; width: min(20rem, calc(100vw - 2 * var(--ds-space-gutter-narrow)));
224
251
  padding: var(--ds-space-xs) var(--ds-space-sm) var(--ds-space-sm); background: var(--bg-card);
225
252
  border: 0 solid var(--rule); border-left-width: var(--line-strong); border-right-width: var(--line-strong);
@@ -241,6 +268,13 @@ html[data-game="on"] .visor:not([hidden]) {
241
268
  .visor__focus .visor__key { color: var(--ink-soft); background: none; }
242
269
  .visor__main--lead[aria-busy="true"] { cursor: progress; background: var(--accent-lift); }
243
270
 
271
+ /* RESTING (task 1005266): while you look, pan and scroll, everything but the arcs, the sight and
272
+ your picture fades out; visor.js wakes it when you reach for the bottom of the view, press a
273
+ visor key or tab onto it. The parts stay in the page (a key press, a screen reader and the
274
+ keyboard still reach them), and nothing moves, so waking is a fade and never a jump. */
275
+ .visor__top, .visor__notch, .visor__who, .visor__main, .visor__hints { transition: opacity var(--ds-dur-base) var(--ds-ease); }
276
+ .visor--rest .visor__top, .visor--rest .visor__notch, .visor--rest .visor__who, .visor--rest .visor__main, .visor--rest .visor__hints { opacity: 0; }
277
+
244
278
  /* A WINDOW THE GLASS OPENS OPENS IN THE GLASS (task 1005253, ADR 0403; owner 2026-11-13). The
245
279
  options window and your profile (shell-game.js openOverlay, .opt-window) and the game menu
246
280
  (openMenu, .game-menu) are reached from the visor, so they never cover it: in game mode each
@@ -179,9 +179,87 @@
179
179
  }),
180
180
  keys: (Array.isArray(s.keys) ? s.keys : []).filter(function (k) { return k && k.label && k.key; }),
181
181
  };
182
+ // Arriving at a place shows the whole glass for a moment; a scene that only registers
183
+ // again (a toggle's row changed) does not.
184
+ if (scene.name !== arrived) { arrived = scene.name; wake(ARRIVE_FOR); }
182
185
  render();
183
186
  }
184
187
 
188
+ // ---------- resting and waking (task 1005266, owner 2026-11-13) ----------
189
+ // The glass took a third of a laptop's height, so while you look, pan and scroll it RESTS:
190
+ // only the arcs, the sight and your picture stay (visor.css .visor--rest). It WAKES when you
191
+ // reach for it: the pointer near the bottom of the view or on a control, a visor key, the
192
+ // keyboard's focus coming onto it, the scene picking something, and for a moment on
193
+ // arriving at a place. It stays awake while the sheet or a scene's card is open, while the
194
+ // pointer is still there and while the keyboard is on it; it rests the moment you press or
195
+ // wheel on the scene, and after a few idle seconds. Only on a desk with a pointer: a touch
196
+ // screen has nothing to reach with, so a phone or tablet keeps the whole glass.
197
+ var REST_AFTER = 3000, ARRIVE_FOR = 4000, REACH = 160;
198
+ var awake = true, reaching = false, restTimer = null, arrived = null, until = 0;
199
+ // The two questions are asked of the browser once; each list keeps its own answer current,
200
+ // so the pointer's every move reads two booleans and builds nothing.
201
+ var mqPointer = null, mqPhone = null;
202
+ function canRest() {
203
+ if (typeof window.matchMedia !== 'function') return false;
204
+ if (!mqPointer) { mqPointer = window.matchMedia('(hover: hover) and (pointer: fine)'); mqPhone = window.matchMedia('(max-width: 640px)'); }
205
+ return mqPointer.matches && !mqPhone.matches;
206
+ }
207
+ function keyboardOn() { return !!(root && document.activeElement && root.contains(document.activeElement)); }
208
+ // A window the glass opened (the options, your profile, the game menu; task 1005253) holds it too.
209
+ function windowUp() { var c = document.documentElement.classList; return !!(c && (c.contains('opt-window-open') || c.contains('game-menu-open'))); }
210
+ function held() { return !!sheet || !!focus || reaching || keyboardOn() || windowUp(); }
211
+ function resting() { return !awake && canRest(); }
212
+ function paintRest() { if (root && root.classList) root.classList.toggle('visor--rest', resting()); }
213
+ // Waking only moves the hour it may rest at; one clock is ever running, so a pointer
214
+ // crossing the bottom of the view repaints nothing and starts no timer of its own.
215
+ function wake(ms) {
216
+ var was = awake;
217
+ awake = true;
218
+ if (!was) paintRest();
219
+ if (typeof setTimeout !== 'function') return; // no clock (a test's bare page): it stays awake
220
+ until = Math.max(until, Date.now() + (ms || REST_AFTER));
221
+ if (restTimer == null) restTimer = setTimeout(settle, ms || REST_AFTER);
222
+ }
223
+ function settle() {
224
+ restTimer = null;
225
+ var left = until - Date.now();
226
+ if (held()) { restTimer = setTimeout(settle, REST_AFTER); return; }
227
+ if (left > 0) { restTimer = setTimeout(settle, left); return; }
228
+ awake = false;
229
+ paintRest();
230
+ }
231
+ // You went back to the world: rest at once, and hand the keyboard back with it.
232
+ function rest() {
233
+ if (sheet || focus) return;
234
+ clearTimeout(restTimer);
235
+ restTimer = null;
236
+ until = 0;
237
+ if (keyboardOn() && document.activeElement.blur) document.activeElement.blur();
238
+ awake = false;
239
+ reaching = false;
240
+ paintRest();
241
+ }
242
+ // A key hint is its key alone; pointing at one, or reaching it with the keyboard, names it
243
+ // on the one plate above the row (visor.css .visor__hints[data-name]). Only where there is a
244
+ // pointer to hover with: a tap must not leave a name standing.
245
+ function nameHint(e) {
246
+ var box = root && root.querySelector('.visor__hints');
247
+ if (!box) return;
248
+ var h = (e.type === 'mouseover' || e.type === 'focusin') && e.target && e.target.closest ? e.target.closest('.visor__hint:not(.visor__hint--place)') : null;
249
+ var label = h ? h.querySelector('span:not(.visor__dots)') : null;
250
+ var hover = typeof window.matchMedia === 'function' && window.matchMedia('(hover: hover)').matches;
251
+ if (label && hover) box.setAttribute('data-name', label.textContent);
252
+ else box.removeAttribute('data-name');
253
+ }
254
+ function onRoot(e) { return !!(root && e.target && e.target.nodeType === 1 && root.contains(e.target)); }
255
+ function onPointerMove(e) {
256
+ if (!shown() || e.buttons || e.pointerType === 'touch') return; // a drag is looking, not reaching
257
+ reaching = onRoot(e) || e.clientY >= window.innerHeight - REACH;
258
+ if (reaching) wake();
259
+ }
260
+ function onPointerDown(e) { if (!shown()) return; if (onRoot(e)) wake(); else rest(); }
261
+ function onWheel(e) { if (shown() && !onRoot(e)) rest(); }
262
+
185
263
  // ---------- the keys: one resolver, so no key ever means two things (task 1005248) ----------
186
264
  // Every key the visor shows or binds comes from here, and here hands each key out once.
187
265
  // A key in ACTIONS or a scene's registration is a RECOMMENDATION: when it is already taken
@@ -239,6 +317,7 @@
239
317
  }
240
318
  if (focus && !was) document.addEventListener('keydown', onFocusKey, true);
241
319
  if (!focus && was) document.removeEventListener('keydown', onFocusKey, true);
320
+ wake(); // the place has something for you; the card holds the glass awake while it is up
242
321
  render();
243
322
  }
244
323
  // Closed by the person (Escape or the close mark): the scene is told, so it unpicks.
@@ -288,8 +367,26 @@
288
367
  // The two arcs are each a 1000×100 box stretched across the stage (preserveAspectRatio
289
368
  // none), with non-scaling strokes so each stays one hairline at any size. The notch and the
290
369
  // sight are drawn at their own size, so neither is ever stretched.
370
+ // One piece of the top arc, from t0 to t1 along it (task 1005266): the same curve, cut.
371
+ var ARC = [[76, 100], [500, -100], [924, 100]];
372
+ function arcPiece(t0, t1) {
373
+ var at = function (t, i) { var u = 1 - t; return u * u * ARC[0][i] + 2 * u * t * ARC[1][i] + t * t * ARC[2][i]; };
374
+ var mid = function (i) { return (1 - t0) * ((1 - t1) * ARC[0][i] + t1 * ARC[1][i]) + t0 * ((1 - t1) * ARC[1][i] + t1 * ARC[2][i]); };
375
+ var n = function (v) { return String(Math.round(v * 10) / 10); };
376
+ return 'M' + n(at(t0, 0)) + ' ' + n(at(t0, 1)) + 'Q' + n(mid(0)) + ' ' + n(mid(1)) + ' ' + n(at(t1, 0)) + ' ' + n(at(t1, 1));
377
+ }
378
+ // The top arc is the version's progress while the glass is awake: four pieces, one per
379
+ // quarter, coloured by segStates (visor.css .visor__seg--*). With no version to read, and
380
+ // at rest, it is the one plain arc.
381
+ function arcSegs() {
382
+ if (!progress) return '';
383
+ return segStates(progress.pct).map(function (s, i) {
384
+ return twice('<path class="visor__line visor__seg visor__seg--' + s + '" d="' + arcPiece(i / 4 + 0.006, (i + 1) / 4 - 0.006) + '"/>');
385
+ }).join('');
386
+ }
291
387
  function frameSvg() {
292
- return svgOpen('visor__frame visor__frame--top', '0 0 1000 100', true) + twice('<path class="visor__line" d="M76 100Q500 -100 924 100"/>') + '</svg>' +
388
+ return svgOpen('visor__frame visor__frame--top' + (progress ? ' visor__frame--segs' : ''), '0 0 1000 100', true) +
389
+ twice('<path class="visor__line visor__arc" d="M76 100Q500 -100 924 100"/>') + arcSegs() + '</svg>' +
293
390
  svgOpen('visor__frame visor__frame--foot', '0 0 1000 100', true) + twice('<path class="visor__line" d="M76 0Q500 200 924 0"/>') + '</svg>' +
294
391
  svgOpen('visor__notch', '0 0 12 9') + twice('<path class="visor__line" d="M0 0l6 9l6 -9"/>') + '</svg>' +
295
392
  svgOpen('visor__sight', '0 0 24 24') + twice('<circle class="visor__line" cx="12" cy="12" r="11"/>') + '<circle class="visor__dot" cx="12" cy="12" r="2"/></svg>';
@@ -320,8 +417,7 @@
320
417
  var sprite = C && typeof C.portraitSrc === 'function' ? C.portraitSrc(id) : (C && typeof C.spriteSrc === 'function' ? C.spriteSrc(id) : '');
321
418
  var n = credits();
322
419
  return frameSvg() +
323
- (scene.place ? '<p class="visor__place">' + esc(scene.place) + '</p>' : '') +
324
- progressHtml() +
420
+ '<div class="visor__top">' + (scene.place ? '<p class="visor__place">' + esc(scene.place) + '</p>' : '') + progressHtml() + '</div>' +
325
421
  focusHtml() +
326
422
  '<div class="visor__foot">' +
327
423
  '<a class="visor__me" data-visor-me href="' + esc(hallHref('/profile')) + '">' +
@@ -355,13 +451,12 @@
355
451
  '</span>' +
356
452
  '<span class="visor__card-open">Open your profile</span></span>';
357
453
  }
358
- // The kit's segments (controls.css .seg), always beside the words that say the same thing.
454
+ // The version's reading in words, beside the place; the four segments are the top arc
455
+ // (arcSegs, task 1005266), so the bars are never the only reading.
359
456
  function progressHtml() {
360
457
  if (!progress) return '';
361
- var segs = segStates(progress.pct).map(function (s) { return '<li class="seg' + (s === 'todo' ? '' : ' seg--' + s) + '"></li>'; }).join('');
362
458
  var said = progress.id + (progress.name ? ', ' + progress.name : '') + ': ' + progress.pct + '% built';
363
459
  return '<div class="visor__progress" data-visor-progress title="' + esc(said) + '">' +
364
- '<ol class="segs visor__segs" aria-hidden="true">' + segs + '</ol>' +
365
460
  '<p class="visor__progress-words"><span class="sr-only">' + esc(said) + '</span>' +
366
461
  '<span aria-hidden="true">' + esc(progress.id) + ' · ' + progress.pct + '%</span></p></div>';
367
462
  }
@@ -397,8 +492,13 @@
397
492
  root.setAttribute('role', 'region');
398
493
  root.setAttribute('aria-label', 'Visor');
399
494
  root.addEventListener('click', onClick);
495
+ root.addEventListener('focusin', function (e) { wake(); nameHint(e); });
496
+ root.addEventListener('focusout', nameHint);
497
+ root.addEventListener('mouseover', nameHint);
498
+ root.addEventListener('mouseout', nameHint);
400
499
  document.body.appendChild(root);
401
500
  }
501
+ paintRest();
402
502
  var open = !!sheet;
403
503
  closeSheet(false);
404
504
  root.hidden = false;
@@ -475,6 +575,9 @@
475
575
  function onClick(e) {
476
576
  var t = e.target && e.target.closest ? e.target : null;
477
577
  if (!t) return;
578
+ // A press that lands on a control you could not see wakes the glass and does nothing else;
579
+ // your picture is drawn at rest, so it is exempt.
580
+ if (resting() && !t.closest('[data-visor-me]')) { e.preventDefault(); wake(); return; }
478
581
  if (t.closest('[data-visor-close]')) { closeSheet(true); return; }
479
582
  if (t.closest('[data-visor-unfocus]')) { dropFocus(); return; }
480
583
  if (t.closest('[data-visor-lead]')) { if (focus && focus.lead && !focus.lead.busy) { closeSheet(false); focus.lead.run(); } return; }
@@ -533,6 +636,7 @@
533
636
  var hit = keyFor(character(), e);
534
637
  if (!hit) return;
535
638
  e.preventDefault();
639
+ wake(); // a visor key is reaching for it
536
640
  if (hit === 'sheet') { if (sheet) closeSheet(true); else openSheet(); return; }
537
641
  closeSheet(false);
538
642
  run(hit);
@@ -540,6 +644,12 @@
540
644
 
541
645
  function start() {
542
646
  window.addEventListener('keydown', onKey);
647
+ // Resting and waking: all three listen in capture and are passive, so they see a press or
648
+ // a wheel a scene stops, and never slow or change one (ADR 0399 §2.1: the glass is not layout).
649
+ window.addEventListener('pointermove', onPointerMove, { capture: true, passive: true });
650
+ window.addEventListener('pointerdown', onPointerDown, { capture: true, passive: true });
651
+ window.addEventListener('wheel', onWheel, { capture: true, passive: true });
652
+ wake(ARRIVE_FOR);
543
653
  // A scene whose script ran before this file left its registration here.
544
654
  if (window.__visorScene && !scene) registerScene(window.__visorScene);
545
655
  // The character and the credits settle when /me answers (craft-mode.js), after this file.
@@ -560,6 +670,11 @@
560
670
  open: openSheet,
561
671
  close: function () { closeSheet(true); },
562
672
  isOpen: function () { return !!sheet; },
673
+ // resting and waking (task 1005266): a scene may wake the glass; the tests read and drive it
674
+ wake: wake,
675
+ rest: rest,
676
+ resting: resting,
677
+ arcPiece: arcPiece,
563
678
  // pure parts, for the tests
564
679
  ACTIONS: ACTIONS,
565
680
  actionsFor: actionsFor,
@@ -6,6 +6,20 @@ Covers: `visor.js` + `visor.css`, loaded on Home, `/city`, `/goal-map` and `/thi
6
6
 
7
7
  The visor is the control and the scene is the view: every main call to action in game mode is on the glass (the accent button, the key hints, the Actions sheet), and a scene carries none of its own. A scene that has something to do registers it with `OTBVisor.scene({ actions, keys })`. ADR 0403 §3 lists the scenes that do not keep it yet; the Governor City keeps it since task 1005209 the sky since task 1005210, and Home's gallery since task 1005211 (below).
8
8
 
9
+ ## It rests while you look and wakes slim when you act (task 1005266, owner 2026-11-13)
10
+
11
+ The owner: the design and the theme are right, but the glass took too much of the view (231 of a laptop's 720px of height). He chose from a six-way mock-up (private page, linked on the task): rest by default, and a slimmer glass when you need commands. Where this section and an older one below disagree about a size or where a part sits, this section is current. No action, key, id or data source changed.
12
+
13
+ - **Awake, it is slimmer.** The top arc is the version's progress: the same curve in four pieces (`arcSegs`, coloured by `segStates`: built is the soft ink, being built is the character's colour, the rest faint). The place and the reading in words share one small line under it (`.visor__top`); the separate row of bars is gone. The foot row is smaller and sits lower: picture 40px, button 44px, credits at `--ds-text-4xl`. Insets: `--visor-arc` 44px, `--visor-inset-top` 48px, `--visor-inset-bottom` 36px, and the foot arc at `--visor-arc-foot`.
14
+ - **A key hint is its key alone.** Its name stays in the page for a screen reader; pointing at the key, or reaching it with the keyboard, shows the name on one plate above the row (`nameHint`, `.visor__hints[data-name]::after`), so naming a key never moves another. Tried first and dropped: widening the hint on hover, which moved the row under the pointer and lost the click. The place's lead action (`.visor__hint--place`) keeps its words: they are the place's call to you (ADR 0403).
15
+ - **At rest** (`.visor--rest`) only the arcs (one plain top arc), the sight and your picture are drawn. The rest fades out and stays in the page, so a key, the keyboard and a screen reader still reach it. Nothing moves, so waking is a fade.
16
+ - **It wakes** when the pointer is within 160px of the bottom of the view or on a control, on a visor key, when keyboard focus comes onto it, when a scene hands it a pick (`focus`), and for 4s on arriving at a place. **It stays awake** while the sheet or a pick's card is open, while the pointer is still there, and while the keyboard is on it. **It rests** at once on a press or a wheel on the scene (handing the keyboard back), and after 3s idle. A press that lands on a control you could not see only wakes it. A scene may call `OTBVisor.wake()`.
17
+ - **Only on a desk with a pointer** (`(hover: hover) and (pointer: fine)`, wider than 640px). A phone or tablet has nothing to reach with and keeps the whole glass; a phone's layout is as it was.
18
+ - **Scenes.** `html.visor-on` carries `--visor-drop` (48px; 0 on a phone): how far the foot row dropped. The sky's bottom offsets and the city's door subtract it; the sky's top offsets are 40px smaller.
19
+ - **Not done:** the sky's start panel and the city's wordmark were placed for the old top band and were not re-laid; arrow-key panning does not rest the glass (the idle timer does).
20
+
21
+ Tests: `tests/hall_visor.mjs` (rest, wake, the drag that is not a reach, the sheet holding it awake, the unseen control, the key's name, the arc's pieces, a phone).
22
+
9
23
  ## A window the glass opens opens in the glass (task 1005253, owner 2026-11-13)
10
24
 
11
25
  - **The bug.** The options window and your profile window (`shell-game.js` `openOverlay`, `.opt-window`), and the game menu (`openMenu`, `.game-menu`, task 1005250), are reached from the visor, yet they opened as a box over the whole screen and covered the visor itself: the arcs, the place, your credits, the accent button and the hints.
@@ -1558,6 +1558,12 @@ summary{min-height:24px;padding:3px 0;}
1558
1558
  <svg class="gh" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
1559
1559
  Continue with GitHub
1560
1560
  </a>
1561
+ <!-- No GitHub account yet (task 1005283): the same sign-in, opened on GitHub's
1562
+ sign-up page (signup=1), which offers "Continue with Google" and hands the
1563
+ sign-in straight back here once the account exists. Bongos cannot make a
1564
+ GitHub account for anyone; GitHub can, in one step, with Google. -->
1565
+ <p class="finePrint" id="wizWhyGh">Your project’s files live in a GitHub repository, so it needs a GitHub account.</p>
1566
+ <p class="finePrint" id="wizNoGh">No GitHub account? <a id="wizSignup" href="/api/bongos/auth/web/start?via=github&amp;signup=1&amp;return=/projects%3Fview%3Dnew&amp;repos=1">Make one free with Google</a>. On GitHub’s page choose <strong>Continue with Google</strong>, and you come straight back here.</p>
1561
1567
  <!-- Informed consent BEFORE GitHub's own wording (ADR 0155): the scope the
1562
1568
  button asks for reads as "all your repositories" on GitHub's screen, so
1563
1569
  the reason is stated here, on the path every founder actually takes. -->
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.25.24",
3
+ "version": "1.25.26",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.25.24",
9
+ "version": "1.25.26",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.25.24",
3
+ "version": "1.25.26",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -11912,5 +11912,17 @@
11912
11912
  "id": "1005107",
11913
11913
  "text": "A founder can now go back and change their Genesis plan after saving it, and change any earlier stage's answers until the Board Room has voted that stage through — a voted stage shows its answers with a plain reason why they a"
11914
11914
  }
11915
+ ],
11916
+ "1.25.25": [
11917
+ {
11918
+ "id": "1005283",
11919
+ "text": "Someone with no GitHub account can now start a project. The creation page explains why GitHub is needed and offers a 'Make one free with Google' link. GitHub's own sign-up page opens, they continue with their Google account, a"
11920
+ }
11921
+ ],
11922
+ "1.25.26": [
11923
+ {
11924
+ "id": "1005266",
11925
+ "text": "In game mode the visor now steps out of the way while you look around and fades back in, slimmer than before, when you reach for a command."
11926
+ }
11915
11927
  ]
11916
11928
  }
@@ -291,6 +291,22 @@ function buildAuthorizeUrl(state, redirectUri, { repos = false, switchAccount =
291
291
  return u.toString();
292
292
  }
293
293
 
294
+ // GitHub's own sign-up page, for a person with no GitHub account (task 1005283). It is the page GitHub's sign-in screen links as "Create an account"
295
+ // during an OAuth sign-in: `return_to` carries the authorize step back, so once the
296
+ // account exists (GitHub's sign-up offers "Continue with Google") GitHub resumes the
297
+ // SAME sign-in — same state, same scope — and our callback lands the person where they
298
+ // started. Only github.com's own authorize path is ever handed on; anything else is null
299
+ // and the caller falls back to the plain authorize redirect.
300
+ function githubSignupUrl(authorizeUrl) {
301
+ let u;
302
+ try { u = new URL(String(authorizeUrl)); } catch { return null; }
303
+ if (u.protocol !== 'https:' || u.host !== 'github.com' || u.pathname !== '/login/oauth/authorize') return null;
304
+ const signup = new URL('https://github.com/signup');
305
+ signup.searchParams.set('return_to', u.pathname + u.search);
306
+ signup.searchParams.set('source', 'oauth');
307
+ return signup.toString();
308
+ }
309
+
294
310
  async function completeWebFlow({ code, redirectUri, userAgent, captureRepoToken = false }) {
295
311
  const { accessToken, scope: grantedScope } = await exchangeCodeForToken(code, redirectUri);
296
312
  const ghUser = await fetchGithubUser(accessToken);
@@ -386,6 +402,7 @@ async function completeWebFlow({ code, redirectUri, userAgent, captureRepoToken
386
402
  // slashes on origin are trimmed so `https://hub/` and `https://hub` behave alike.
387
403
 
388
404
  module.exports = {
405
+ githubSignupUrl,
389
406
  buildAuthorizeUrl,
390
407
  completeWebFlow,
391
408
  exchangeCodeForToken,
@@ -58,7 +58,7 @@ const { branding, userAgent } = require('../branding');
58
58
 
59
59
  const { SCOPE, authConfigured, grantedScopeCoversRepos, idpConfigured, idpEnabled, loadIdpConfig, scopeForWebFlow } = require('./auth-config.js');
60
60
  const { firstAdminLogin, firstTimeAdmissionBlocked, instanceNeedsFirstAdmin, joinabilityMode, maybeSeatFirstAdmin, membershipKind, notifyHubOfInvite, openEnrollmentEnabled, readAppStatusFromHub, readApplicantProfileFromHub, readApplicantViewFromHub, webAdmissionStatus } = require('./auth-admission.js');
61
- const { buildAuthorizeUrl, completeWebFlow, pollDeviceFlow, startDeviceFlow } = require('./auth-github.js');
61
+ const { buildAuthorizeUrl, githubSignupUrl, completeWebFlow, pollDeviceFlow, startDeviceFlow } = require('./auth-github.js');
62
62
  const { buildIdpAuthorizeUrl, completeDeviceExchange, completeIdpFlow, idpAuthorizeUrl, loadHubPublicKey, logoutTokenJtiSeenAndRecord, readApplicationVouch, verifyHubAssertion, verifyHubLogoutToken } = require('./auth-idp.js');
63
63
  const { addDiscordGuildMember, buildDiscordAuthorizeUrl, discordAuthorizeUrl, discordBotConfigured, discordConfigured, exchangeDiscordCode, fetchDiscordUser, isValidDiscordUserId, loadDiscordBotToken, loadDiscordCreds, removeDiscordGuildMember } = require('./auth-discord.js');
64
64
 
@@ -854,6 +854,7 @@ module.exports = {
854
854
  startDeviceFlow,
855
855
  pollDeviceFlow,
856
856
  buildAuthorizeUrl,
857
+ githubSignupUrl,
857
858
  scopeForWebFlow,
858
859
  grantedScopeCoversRepos,
859
860
  buildIdpAuthorizeUrl,