@bongos/core 1.25.7 → 1.25.9

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.
@@ -895,7 +895,7 @@ html[data-theme="dark"] .brand-mark__mark { --mark-ink: #e2e1e4; }
895
895
  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
896
896
  with the count over its word, the one accent button (its name may take two lines; its key is in the sheet), and of
897
897
  the key hints only Actions, as its dots (every other hint is a row in its sheet). */
898
- html[data-game="on"] #visor { --visor-inset-bottom: 52px; }
898
+ :root[data-game="on"] { --visor-inset-bottom: 52px; --visor-foot-room: 112px; }
899
899
  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; }
900
900
  html[data-game="on"] .visor__foot { grid-template-columns: auto minmax(0, 1fr) auto; column-gap: var(--ds-space-hair); }
901
901
  html[data-game="on"] .visor__me, html[data-game="on"] .visor__hints { transform: none; }
@@ -914,7 +914,7 @@ html[data-theme="dark"] .brand-mark__mark { --mark-ink: #e2e1e4; }
914
914
  html[data-game="on"] .visor__who, html[data-game="on"] .visor__hint { background: var(--night-plate); box-shadow: none; -webkit-backdrop-filter: none; backdrop-filter: none; }
915
915
  html[data-game="on"] .visor__hint--sheet > :not(.visor__dots) { position: absolute; width: 1px; height: 1px; padding: 0; border: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
916
916
  html[data-game="on"] .visor__main .visor__key { display: none; }
917
- html[data-game="on"] .visor__sheet { bottom: calc(var(--visor-inset-bottom) + 112px); max-height: 60vh; }
917
+ html[data-game="on"] .visor__sheet { max-height: 60vh; }
918
918
  }
919
919
  /* task 1004424 (BV2.PS10): THE FOUNDING MARK. A pill in .rank-badge's grammar (same
920
920
  height, type and radius) so it sits beside a rank without competing with it; the
@@ -10,11 +10,15 @@
10
10
  the night, and the place, each hint and your credits stand on a small night plate. Colours, sizes and corners
11
11
  are the hall tier's tokens; nothing here is typed in. Record: modules/hall-ui/records/visor.md. */
12
12
  .visor { display: none; }
13
+ /* where the glass sits, so a scene's pop-up, and a window the glass opens, can keep clear of
14
+ 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. */
16
+ html[data-game="on"] {
17
+ --visor-arc: 64px; --visor-inset-top: 88px; --visor-inset-bottom: 76px; --visor-foot-room: 100px;
18
+ --visor-inset-right: max(var(--ds-space-gutter-narrow), var(--helm-inset)); --visor-inset-left: var(--visor-inset-right);
19
+ }
13
20
  html[data-game="on"] .visor:not([hidden]) {
14
21
  display: block; position: fixed; inset: 0; z-index: 4; pointer-events: none;
15
- /* where the glass sits, so a scene's pop-up can keep clear of it (ADR 0399 §2.3) */
16
- --visor-arc: 64px; --visor-inset-top: 88px; --visor-inset-bottom: 76px;
17
- --visor-inset-right: max(var(--ds-space-gutter-narrow), var(--helm-inset)); --visor-inset-left: var(--visor-inset-right);
18
22
  color: var(--ink); font-family: var(--font-body);
19
23
  }
20
24
 
@@ -187,9 +191,9 @@ html[data-game="on"] .visor:not([hidden]) {
187
191
  panel between two strong hairlines, standing over the hints that opened it, clear of the foot
188
192
  row even when the hints have wrapped onto a second line. */
189
193
  .visor__sheet {
190
- position: absolute; right: var(--visor-inset-right); bottom: calc(var(--visor-inset-bottom) + 100px); z-index: 1;
194
+ position: absolute; right: var(--visor-inset-right); bottom: calc(var(--visor-inset-bottom) + var(--visor-foot-room)); z-index: 1;
191
195
  pointer-events: auto; box-sizing: border-box; width: min(18rem, calc(100vw - 2 * var(--ds-space-gutter-narrow)));
192
- max-height: calc(100% - var(--visor-inset-top) - var(--visor-inset-bottom) - 100px); overflow-y: auto;
196
+ max-height: calc(100% - var(--visor-inset-top) - var(--visor-inset-bottom) - var(--visor-foot-room)); overflow-y: auto;
193
197
  padding: var(--ds-space-sm); background: var(--bg-card);
194
198
  border: 0 solid var(--rule); border-left-width: var(--line-strong); border-right-width: var(--line-strong);
195
199
  }
@@ -237,5 +241,29 @@ html[data-game="on"] .visor:not([hidden]) {
237
241
  .visor__focus .visor__key { color: var(--ink-soft); background: none; }
238
242
  .visor__main--lead[aria-busy="true"] { cursor: progress; background: var(--accent-lift); }
239
243
 
244
+ /* A WINDOW THE GLASS OPENS OPENS IN THE GLASS (task 1005253, ADR 0403; owner 2026-11-13). The
245
+ options window and your profile (shell-game.js openOverlay, .opt-window) and the game menu
246
+ (openMenu, .game-menu) are reached from the visor, so they never cover it: in game mode each
247
+ fits between the top arc and the foot row, and the visor is drawn over its scrim, undimmed,
248
+ as the frame you see it through. While one is up the glass is a picture: nothing on it takes
249
+ a pointer (the window is modal and keeps the keyboard), the sight leaves the window's middle,
250
+ and the glass's own pop-ups (the sheet, the scene's pick, your card) step aside. */
251
+ html.visor-on .opt-window, html.visor-on .game-menu {
252
+ box-sizing: border-box;
253
+ padding: var(--visor-inset-top) var(--visor-inset-right) calc(var(--visor-inset-bottom) + var(--visor-foot-room)) var(--visor-inset-left);
254
+ }
255
+ html.visor-on .opt-window__box { width: min(1040px, 100%); height: min(720px, 100%); }
256
+ html.visor-on .game-menu__box { max-height: 100%; }
257
+ html.visor-on:is(.opt-window-open, .game-menu-open) .visor:not([hidden]) { z-index: calc(var(--ds-z-modal, 160) + 1); }
258
+ html.visor-on:is(.opt-window-open, .game-menu-open) .visor * { pointer-events: none; }
259
+ html.visor-on:is(.opt-window-open, .game-menu-open) :is(.visor__sight, .visor__sheet, .visor__focus, .visor__card) { visibility: hidden; }
260
+ /* THE GAME MENU IS THE ONE THING IN FOCUS (owner 2026-11-13): the place behind it is frosted
261
+ (the hall's glass blur, under the glass, so the visor's lines stay sharp), and everything on
262
+ the glass but its two arcs steps back, so the eye goes to the menu. The arcs stay as the frame
263
+ the menu is held in. */
264
+ html.visor-on .game-menu { -webkit-backdrop-filter: blur(var(--glass-blur)); backdrop-filter: blur(var(--glass-blur)); }
265
+ html.visor-on .visor > :not(.visor__frame, .visor__notch) { transition: opacity var(--ds-dur-fast) var(--ds-ease); }
266
+ html.visor-on.game-menu-open .visor > :not(.visor__frame, .visor__notch) { opacity: .25; }
267
+
240
268
  /* a phone (640px and narrower): the rules live in style.css’s shared phone block, so the
241
269
  visor adds no breakpoint of its own (the design-count ratchet). */
@@ -28,6 +28,6 @@ The menu is a Helm screen, written from the hall tier's tokens alone (`settings.
28
28
  ## The options window (owner, 2026-11-11: "WoW sizing, an overlay")
29
29
 
30
30
  - **In game mode the menu opens over the place, not as a page.** The game menu's Options row (task 1005250, `records/game-menu.md`; until then Escape itself), or a click on any link to `/settings` (the profile chip), draws one window (`shell-game.js` `openOverlay`, `window.OTBOptionsOverlay`), built on the shared dialog (`OTBDialog.bind`: Escape, scrim, close button, focus trap and return), with the page behind made `inert` while it is up, holding the menu framed with `?embed=1`; the place stays drawn behind it. Escape, the scrim or the × shuts it; `options-menu.js` `close()` shuts the window when it is in one. A page that cannot draw it falls back to the menu page. A plain visit to `/settings` is the page, as before.
31
- - **Window sized, see-through.** The window is `min(1040px, …) × min(720px, …)` of glass at 48% of the ground with a light blur, over the shared `--scrim` at a third. Every embedded screen is drawn at `zoom: 0.82` with no ground of its own (`style.css`), and the menu inside it takes smaller words (`settings.css`, `html[data-embed="on"]`); a window-high length divides by the zoom (`--opt-vh`).
31
+ - **Window sized, see-through.** The window is `min(1040px, …) × min(720px, …)` of glass at 48% of the ground with a light blur, over the shared `--scrim` at a third. Every embedded screen is drawn at `zoom: 0.82` with no ground of its own (`style.css`), and the menu inside it takes smaller words (`settings.css`, `html[data-embed="on"]`); a window-high length divides by the zoom (`--opt-vh`). In game mode with the visor up the window fits inside the glass, between the top arc and the foot row, and never covers the visor (task 1005253, `records/visor.md`).
32
32
  - **One review palette.** A framed screen docks no artist palette of its own (`craft-mode.js` `applyDocked`); the page around it has one.
33
33
  - **Tests:** `tests/hall_options_menu.mjs` ("away from the menu, Escape opens the options as a window…"), `tests/page_notes_panel.mjs` (the frame docks no panel).
@@ -6,6 +6,14 @@ 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
+ ## A window the glass opens opens in the glass (task 1005253, owner 2026-11-13)
10
+
11
+ - **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.
12
+ - **The rule.** A menu or window the visor opens never covers the visor. In game mode (`html.visor-on`) the window fits between the top arc (`--visor-inset-top`) and the foot row (`--visor-inset-bottom` + `--visor-foot-room`), inside the side insets, and the visor is drawn over the window's scrim, undimmed, as the frame it is seen through (`visor.css`). While a window is up the glass is a picture: none of it takes a pointer (the window is modal and keeps the keyboard), the sight leaves the window's middle, and the glass's own pop-ups (the sheet, the scene's pick, your card) step aside. Shut, the glass goes back to z 4.
13
+ - **The game menu is the one thing in focus** (owner, same day: "a frosted glass or blur… so the user focuses specifically on the game menu"). While it is up the place behind it is frosted (`backdrop-filter` with the hall's `--glass-blur`, on `.game-menu`, so it is under the glass and the visor's lines stay sharp), and everything on the glass but its two arcs and the notch fades to a quarter. The options and profile windows are glass of their own and keep the visor whole around them.
14
+ - **The glass's measures are on the root now** (`html[data-game="on"]`, and the phone block's `:root[data-game="on"]`), not on `#visor`, because the window is not inside the visor. The sheet reads `--visor-foot-room` too, so the sheet and a window keep the same clearance over the foot row. Classic mode keeps the centred window.
15
+ - **Tests:** `tests/hall_visor_profile.mjs` ("a window the visor opens sits inside the glass…", at a desk and a phone width).
16
+
9
17
  ## One owner per key (task 1005248, owner 2026-11-12)
10
18
 
11
19
  - **The bug.** The Artist saw Esc twice: "Options" on the glass, then in the Actions sheet both Close and the Options row said Esc.
@@ -134,6 +134,9 @@ function githubChecklist(status, appSlug, facts = {}) {
134
134
  // what "is alive" waits for — device flow is optional by design
135
135
  connected: approve === 'done' && install === 'done' && signIn === 'done',
136
136
  wired_elsewhere: elsewhere,
137
+ // GitHub sent the founder back from the install page (or they asked for a check) and
138
+ // the runner has not confirmed it yet: the page shows the step as being checked
139
+ install_checking: install !== 'done' && !!(facts.installReportedAt || facts.checkRequestedAt),
137
140
  };
138
141
  }
139
142
 
@@ -533,7 +533,10 @@ body:has(#view-new:not([hidden])) footer.foot{position:relative;z-index:1;backgr
533
533
  .setFact .optional{margin-left:8px;font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-faint);}
534
534
  .setFact.done .d{background:color-mix(in srgb,var(--ok) 12%,transparent);color:var(--ok);border-color:var(--ok);}
535
535
  .setFact.done .t,.setFact.now .t,.setFact.you .t{color:var(--ink);}
536
- .setFact.now .d{border:2px solid var(--accent);}
536
+ .setFact.now .d{border:2px solid var(--accent);border-top-color:transparent;animation:factSpin 1s linear infinite;}
537
+ /* a loading wheel is essential, so it turns even with reduced motion (slower, no pulse) */
538
+ @media (prefers-reduced-motion:reduce){.setFact.now .d{animation-duration:2s;}}
539
+ @keyframes factSpin{to{transform:rotate(360deg)}}
537
540
  .setFact.you .d{background:color-mix(in srgb,var(--accent) 16%,transparent);border-color:color-mix(in srgb,var(--accent) 60%,transparent);color:var(--ink);}
538
541
  .setFact.bad .d{background:color-mix(in srgb,var(--err) 10%,transparent);color:var(--err);border-color:var(--err);}
539
542
  .setFact.bad .t{color:var(--err);}
@@ -548,8 +551,7 @@ body:has(#view-new:not([hidden])) footer.foot{position:relative;z-index:1;backgr
548
551
  .setFact .acctPick .acctTyped{margin-top:10px;}
549
552
  .setFact .acctPick .hint{min-height:0;}
550
553
  @media (prefers-reduced-motion:no-preference){
551
- .setFact.now .d{animation:factBreathe 1.4s ease-in-out infinite,factSpin 1s linear infinite;border-top-color:transparent;}
552
- @keyframes factSpin{to{transform:rotate(360deg)}}
554
+ .setFact.now .d{animation:factBreathe 1.4s ease-in-out infinite,factSpin 1s linear infinite;}
553
555
  .setNow.working::after{content:"";display:inline-block;width:1.2em;text-align:left;animation:workDots 1.4s steps(4,end) infinite;}
554
556
  @keyframes workDots{0%{content:""}25%{content:"."}50%{content:".."}75%{content:"..."}}
555
557
  @keyframes factBreathe{0%,100%{border-color:var(--accent)}50%{border-color:color-mix(in srgb,var(--accent) 35%,transparent)}}
@@ -6933,6 +6935,7 @@ summary{min-height:24px;padding:3px 0;}
6933
6935
  sentence and carries the action that fixes it, with the app GitHub already made, never a
6934
6936
  second one. live.pickUp is the server's word for what that action does (github-setup.js
6935
6937
  pickUpFor); live.verifying is whether the runner is still checking. */
6938
+ var installOpened = false; /* this page sent the founder to GitHub's install page */
6936
6939
  var ghTabTrouble = null; /* 'behind' | 'closed': what this page saw of the GitHub tab it opened */
6937
6940
  var NO_SECOND_APP = 'Nothing is lost, and no second app will be made.';
6938
6941
  function projectName() { return state.name || state.slug || 'your project'; }
@@ -7067,6 +7070,10 @@ summary{min-height:24px;padding:3px 0;}
7067
7070
  /* the runner stopped checking and GitHub never confirmed the install: the link opens the
7068
7071
  SAME app's install page, and the click asks the runner to look again */
7069
7072
  else if (live.pickUp === 'check') { install.state = 'bad'; install.sub = 'GitHub stopped before the app was on ' + (repo || 'your repo') + '.'; install.right = checked; install.act = 'pick-up-link'; install.href = installUrl; }
7073
+ /* the founder came back from GitHub's install page (GitHub reported it, or they pressed
7074
+ the link here): the runner is checking with GitHub, so the step spins until it ticks,
7075
+ and the link stays in case they never finished */
7076
+ else if ((live.setup && live.setup.install_checking) || installOpened) { install.state = 'now'; install.sub = (repo ? repo + '. ' : '') + 'Checking with GitHub. This ticks by itself.'; install.right = checked; install.act = 'install-link'; install.href = installUrl; }
7070
7077
  else { install.state = 'you'; install.right = 'not yet seen'; install.act = 'install-link'; install.href = installUrl; }
7071
7078
 
7072
7079
  var signIn = { key: 'sign_in', title: 'Builders can sign in' };
@@ -7313,6 +7320,7 @@ summary{min-height:24px;padding:3px 0;}
7313
7320
  var ib = $('installBtn'), instRow = gh[1];
7314
7321
  var instHref = instRow && instRow.state !== 'done' && homeUp && (instRow.act === 'install-link' || instRow.act === 'pick-up-link') ? installHrefOf(instRow) : '';
7315
7322
  if (instHref) ib.setAttribute('href', instHref); else ib.removeAttribute('href');
7323
+ if (!ib.getAttribute('data-wired')) { ib.setAttribute('data-wired', '1'); ib.addEventListener('click', function () { installOpened = true; paintFacts(); }); }
7316
7324
  ib.hidden = !instHref;
7317
7325
  paintSetDone(ready, home, gh);
7318
7326
  tipsRun(!ready);
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.25.7",
3
+ "version": "1.25.9",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.25.7",
9
+ "version": "1.25.9",
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.7",
3
+ "version": "1.25.9",
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",
@@ -11753,5 +11753,12 @@
11753
11753
  "id": "1005219",
11754
11754
  "text": "Opening the builders' hall now starts on the Game or Classic choice. Signed out, your pick takes you to sign in and then straight to that menu."
11755
11755
  }
11756
+ ],
11757
+ "1.25.8": [],
11758
+ "1.25.9": [
11759
+ {
11760
+ "id": "1005253",
11761
+ "text": "In game mode, the windows the visor opens (Options, your profile, the game menu) now open inside the visor instead of covering it, and the game menu blurs the place behind it and quiets the visor so it is the one thing in focu"
11762
+ }
11756
11763
  ]
11757
11764
  }
package/src/module-api.js CHANGED
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
81
81
  // MAJOR (see allowBoxScope below): passes the request through untouched.
82
82
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
83
83
 
84
- const CORE_VERSION = '1.25.7'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.25.9'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
85
85
 
86
86
  // A namespaced logger so a module's log lines are attributable + consistent.
87
87
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -35,7 +35,7 @@ const SCRIPT = HUB.slice(HUB.lastIndexOf('<script>'), HUB.lastIndexOf('</script>
35
35
 
36
36
  test('each step reads only what the server has seen — the honest "not yet" states included', () => {
37
37
  const none = GS.githubChecklist(null, null);
38
- assert.deepEqual(none, { approve: 'todo', install: 'todo', sign_in: 'todo', device_flow: 'todo', connected: false, wired_elsewhere: false }, 'never started');
38
+ assert.deepEqual(none, { approve: 'todo', install: 'todo', sign_in: 'todo', device_flow: 'todo', connected: false, wired_elsewhere: false, install_checking: false }, 'never started');
39
39
  assert.equal(GS.githubChecklist('pending', null).approve, 'now', 'waiting for the approval on GitHub');
40
40
  for (const s of ['code_received', 'converting']) {
41
41
  const c = GS.githubChecklist(s, null);
@@ -327,3 +327,10 @@ test('the device-flow finding lives in the direction doc, and the code points at
327
327
  assert.match(doc, /Device flow cannot be switched on for the founder \(spike, task 1004649\)/);
328
328
  assert.match(fs.readFileSync(path.join(ROOT, 'modules', 'provisioning', 'github-setup.js'), 'utf8'), /docs\/design\/project-startup-direction\.md/);
329
329
  });
330
+
331
+ test('back from the install page, the install step reads as being checked until GitHub confirms it', () => {
332
+ const { githubChecklist } = require('../modules/provisioning/github-setup.js');
333
+ assert.equal(githubChecklist('done', 'kestrel', {}).install_checking, false, 'not reported yet');
334
+ assert.equal(githubChecklist('done', 'kestrel', { installReportedAt: '2026-10-25T10:00:00Z' }).install_checking, true, 'reported, not yet seen');
335
+ assert.equal(githubChecklist('done', 'kestrel', { installReportedAt: '2026-10-25T10:00:00Z', installedAt: '2026-10-25T10:01:00Z' }).install_checking, false, 'seen: done');
336
+ });
@@ -53,7 +53,7 @@ test('the project saying sign-in works ticks sign-in, whatever the attempt row s
53
53
 
54
54
  test('without that answer, nothing changes: a pending attempt still waits on GitHub', () => {
55
55
  const c = gs.githubChecklist('pending', null, {});
56
- assert.deepEqual(c, { approve: 'now', install: 'todo', sign_in: 'todo', device_flow: 'todo', connected: false, wired_elsewhere: false });
56
+ assert.deepEqual(c, { approve: 'now', install: 'todo', sign_in: 'todo', device_flow: 'todo', connected: false, wired_elsewhere: false, install_checking: false });
57
57
  });
58
58
 
59
59
  test('with our app recorded, the answer ticks sign-in and the install keeps its own fact', () => {
@@ -93,6 +93,35 @@ test('Escape in a game place opens the game menu, not the options; Escape again
93
93
  await ctx.close();
94
94
  });
95
95
 
96
+ // task 1005253 (owner, 2026-11-13): the menu is in the glass and is the one thing in focus. The
97
+ // place behind it is frosted, the visor stays over it with only its arcs at full strength, and
98
+ // it ends above the foot row.
99
+ test('the game menu is the one thing in focus: the place frosted, the glass quiet but for its arcs', { skip }, async () => {
100
+ const { ctx, page, errors } = await open();
101
+ await escape(page);
102
+ assert.equal(await menuUp(page), true);
103
+ await page.waitForTimeout(300); // the glass's fade
104
+ const f = await page.evaluate(() => {
105
+ const cs = (s) => getComputedStyle(document.querySelector(s));
106
+ return {
107
+ blur: cs('.game-menu').backdropFilter,
108
+ visorOver: Number(cs('#visor').zIndex) > Number(cs('.game-menu').zIndex),
109
+ foot: Number(cs('.visor__foot').opacity), arc: Number(cs('.visor__frame--top').opacity),
110
+ clear: document.querySelector('.game-menu__box').getBoundingClientRect().bottom <= document.querySelector('.visor__foot').getBoundingClientRect().top,
111
+ };
112
+ });
113
+ assert.match(f.blur, /^blur\(\d/, 'the place behind is frosted');
114
+ assert.ok(f.visorOver, 'the glass stays over the menu\'s scrim');
115
+ assert.ok(f.foot < 0.5, 'the buttons and words on the glass step back');
116
+ assert.equal(f.arc, 1, 'the arcs hold the menu at full strength');
117
+ assert.ok(f.clear, 'the menu ends above the foot row');
118
+ await escape(page);
119
+ await page.waitForTimeout(300);
120
+ assert.equal(await page.evaluate(() => Number(getComputedStyle(document.querySelector('.visor__foot')).opacity)), 1, 'shut, the glass is whole again');
121
+ assert.deepEqual(errors, []);
122
+ await ctx.close();
123
+ });
124
+
96
125
  test('Return to game shuts the menu; Options opens the options window in its place', { skip }, async () => {
97
126
  const { ctx, page, errors } = await open();
98
127
  await escape(page);
@@ -65,7 +65,7 @@ const SCENE = `<script>
65
65
  window.__visorScene = { name: 'test', place: 'Test city', actions: [], keys: [] };
66
66
  </script>`;
67
67
  const PAGE = (withWindow) => `<!doctype html><html data-theme="dark" data-faces="helm"><head><meta charset="utf-8">
68
- <link rel="stylesheet" href="/builders/style.css"><link rel="stylesheet" href="/builders/visor.css"></head>
68
+ <link rel="stylesheet" href="/tokens.css"><link rel="stylesheet" href="/builders/style.css"><link rel="stylesheet" href="/builders/visor.css"></head>
69
69
  <body>${SCENE}<script src="/builders/hall-dialog.js"></script>${withWindow ? '<script src="/builders/shell-game.js"></script>' : ''}<script src="/builders/visor.js"></script></body></html>`;
70
70
 
71
71
  async function open({ withWindow = true } = {}) {
@@ -73,6 +73,7 @@ async function open({ withWindow = true } = {}) {
73
73
  await ctx.route(ORIGIN + '/**', (route) => {
74
74
  const p = new URL(route.request().url()).pathname;
75
75
  if (p === '/') return route.fulfill({ contentType: 'text/html', body: PAGE(withWindow) });
76
+ if (p === '/tokens.css') return route.fulfill({ contentType: 'text/css', body: fs.readFileSync(path.join(ROOT, 'modules/ui-design/public/tokens.css')) }); // the design tokens the hall loads first
76
77
  if (p === '/profile') return route.fulfill({ contentType: 'text/html', body: '<!doctype html><title>profile</title><p id="pp">your profile</p><script src="/builders/shell-game.js"></script>' }); // the real page carries the game shell, which hands an Escape up
77
78
  const f = path.join(HALL, p.replace(/^\/builders\//, ''));
78
79
  if (p.startsWith('/builders/') && TYPES[path.extname(p)] && fs.existsSync(f)) return route.fulfill({ contentType: TYPES[path.extname(p)], body: fs.readFileSync(f) });
@@ -133,6 +134,44 @@ test('clicking you opens your whole profile in the window over the place; Escape
133
134
  await ctx.close();
134
135
  });
135
136
 
137
+ // task 1005253 (owner, 2026-11-13): a window the glass opens opens IN the glass. It never covers
138
+ // the visor: it sits under the top arc and above the foot row, the visor is drawn over its scrim,
139
+ // and while it is up nothing on the glass takes a pointer and the glass's own pop-ups step aside.
140
+ test('a window the visor opens sits inside the glass, and the visor stays drawn around it', { skip }, async () => {
141
+ for (const viewport of [{ width: 1280, height: 800 }, { width: 390, height: 844 }]) {
142
+ const { ctx, page, errors } = await open();
143
+ await page.setViewportSize(viewport);
144
+ await page.evaluate(() => window.OTBOptionsOverlay.open(''));
145
+ await page.waitForFunction(() => window.OTBOptionsOverlay.isOpen());
146
+ const g = await page.evaluate(() => {
147
+ const r = (s) => document.querySelector(s).getBoundingClientRect();
148
+ const box = r('.opt-window__box'); const foot = r('.visor__foot'); const main = r('.visor__main');
149
+ const z = (s) => Number(getComputedStyle(document.querySelector(s)).zIndex);
150
+ // what is on top at the centre of the accent button: the glass, not the window
151
+ const hit = document.elementsFromPoint(main.left + main.width / 2, main.top + main.height / 2);
152
+ return {
153
+ top: box.top, bottom: box.bottom, footTop: foot.top, place: getComputedStyle(document.documentElement).getPropertyValue('--visor-inset-top').trim(),
154
+ visorOver: z('#visor') > z('.opt-window'),
155
+ buttonUnder: hit.some((el) => el.closest && el.closest('.opt-window__box')),
156
+ mainPointer: getComputedStyle(document.querySelector('.visor__main')).pointerEvents,
157
+ sight: getComputedStyle(document.querySelector('.visor__sight')).visibility,
158
+ };
159
+ });
160
+ assert.ok(g.bottom <= g.footTop, `${viewport.width}px: the window ends above the foot row (${g.bottom} > ${g.footTop})`);
161
+ assert.equal(g.top, parseFloat(g.place), `${viewport.width}px: the window starts under the top arc`);
162
+ assert.ok(g.visorOver, 'the glass is drawn over the window\'s scrim');
163
+ assert.ok(!g.buttonUnder, 'the window covers none of the foot row');
164
+ assert.equal(g.mainPointer, 'none', 'the glass is a picture while the window is up');
165
+ assert.equal(g.sight, 'hidden', 'the sight leaves the window\'s middle');
166
+ await page.evaluate(() => window.OTBOptionsOverlay.close());
167
+ await page.waitForFunction(() => !window.OTBOptionsOverlay.isOpen());
168
+ assert.equal(await page.evaluate(() => getComputedStyle(document.querySelector('#visor')).zIndex), '4', 'shut, the glass goes back under the pop-ups');
169
+ assert.equal(await page.evaluate(() => getComputedStyle(document.querySelector('.visor__main')).pointerEvents), 'auto');
170
+ assert.deepEqual(errors, []);
171
+ await ctx.close();
172
+ }
173
+ });
174
+
136
175
  test('with no window to draw, you are still a link to your profile', { skip }, async () => {
137
176
  const { ctx, page, errors } = await open({ withWindow: false });
138
177
  const r = await page.evaluate(() => {