@bongos/core 1.19.1051 → 1.19.1053

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 (56) hide show
  1. package/.bongos-core.json +66 -61
  2. package/docs/copy-inventory.md +138 -146
  3. package/docs/copy-registry.json +294 -458
  4. package/docs/module-api-changelog.md +4 -0
  5. package/modules/hall-ui/public/atlas.html +1 -1
  6. package/modules/hall-ui/public/blockers.html +1 -1
  7. package/modules/hall-ui/public/board-room.html +1 -1
  8. package/modules/hall-ui/public/collab.html +1 -1
  9. package/modules/hall-ui/public/copy-desk.html +1 -1
  10. package/modules/hall-ui/public/deploy.html +1 -1
  11. package/modules/hall-ui/public/diagrams.html +1 -1
  12. package/modules/hall-ui/public/drachmae.html +1 -1
  13. package/modules/hall-ui/public/fleet.html +1 -1
  14. package/modules/hall-ui/public/gate.html +1 -1
  15. package/modules/hall-ui/public/goal-map.html +18 -24
  16. package/modules/hall-ui/public/goal-map.states.json +2 -2
  17. package/modules/hall-ui/public/goals.html +1 -1
  18. package/modules/hall-ui/public/government.html +1 -1
  19. package/modules/hall-ui/public/idea.html +1 -1
  20. package/modules/hall-ui/public/ideas.html +1 -1
  21. package/modules/hall-ui/public/index.html +1 -1
  22. package/modules/hall-ui/public/modules.html +1 -1
  23. package/modules/hall-ui/public/primer.html +1 -1
  24. package/modules/hall-ui/public/profile.html +1 -1
  25. package/modules/hall-ui/public/project-settings.html +1 -1
  26. package/modules/hall-ui/public/ranks.html +1 -1
  27. package/modules/hall-ui/public/roadmap.html +1 -1
  28. package/modules/hall-ui/public/roster.html +1 -1
  29. package/modules/hall-ui/public/sessions.html +1 -1
  30. package/modules/hall-ui/public/settings.html +1 -1
  31. package/modules/hall-ui/public/shell.js +77 -1
  32. package/modules/hall-ui/public/sky-data.js +34 -19
  33. package/modules/hall-ui/public/sky-interaction.js +36 -30
  34. package/modules/hall-ui/public/sky-layout.js +11 -6
  35. package/modules/hall-ui/public/sky-panel.js +3 -2
  36. package/modules/hall-ui/public/sky-tour.js +37 -26
  37. package/modules/hall-ui/public/studio.html +10 -4
  38. package/modules/hall-ui/public/studio.states.json +76 -1
  39. package/modules/hall-ui/public/style.css +53 -0
  40. package/modules/hall-ui/public/task.html +1 -1
  41. package/modules/hall-ui/public/thinking.css +56 -16
  42. package/modules/hall-ui/public/thinking.html +52 -41
  43. package/modules/hall-ui/public/thinking.states.json +20 -3
  44. package/modules/hall-ui/public/watch.html +1 -1
  45. package/modules/hall-ui/public/work.html +1 -1
  46. package/modules/hall-ui/records/sky.md +20 -0
  47. package/package-lock.json +2 -2
  48. package/package.json +1 -1
  49. package/release-notes.json +12 -0
  50. package/src/module-api.js +1 -1
  51. package/tests/goal_map_page.mjs +54 -28
  52. package/tests/hall_audit.mjs +2 -1
  53. package/tests/hall_fullscreen.mjs +196 -0
  54. package/tests/hall_sky.mjs +49 -12
  55. package/tests/helpers.mjs +2 -1
  56. package/tests/nav_permission_atoms.mjs +1 -1
@@ -1,13 +1,17 @@
1
1
  /* modules/hall-ui/public/thinking.css — THE SKY's one sheet (task 1004231 / sky
2
2
  part 3 of 5, goal 1000095; ADR 0331). The <style> of
3
3
  docs/design/mocks/sky/sky.template.html, verbatim: the owner adopted the
4
- prototype as the design of record and this sheet is its pixel contract. It
5
- deliberately does NOT load the hall's style.css or its tokens — the sky is
6
- its own surface (black void, Manrope, JetBrains Mono for figures), and the
7
- template's values are the contract. Keep it one file (the task says so).
4
+ prototype as the design of record and this sheet is its pixel contract. The
5
+ sky is its own surface (black void, Manrope, JetBrains Mono for figures),
6
+ and the template's values are the contract. Keep it one file (the task says so).
8
7
  ONE SHEET, TWO DOORS (part 4, task 1004232): /thinking and /goal-map are the
9
- same page opened in different lenses, so both bodies are named below and
10
- nothing in here is per-page. */
8
+ same page opened in different lenses, so both are named below.
9
+ IN THE HALL (task 1004327): /thinking now loads style.css and sits in the
10
+ hall chrome; /goal-map is still standalone. So the sky's tokens and ground
11
+ sit on /thinking's STAGE (main.sky-stage) instead of its body — the chrome
12
+ around the stage keeps the hall's own palette in both modes — and the sky's
13
+ few global rules are written :where()-scoped, so they cost no specificity
14
+ (goal-map renders exactly as before) and never reach the chrome. */
11
15
  /* The template declared these on :root. They sit on the page's body here,
12
16
  with the template's values untouched, because the server splices the
13
17
  branding pack's own :root (--ink, --accent, --rule …) into <head> AFTER this
@@ -17,7 +21,7 @@
17
21
  its own — tests/hall_reading_world.mjs; color-scheme inherits from body
18
22
  too). Same reason `body` below is qualified: a pack backdrop is written as
19
23
  `body{background:…}` and the void must win. */
20
- body[data-page="thinking"], body[data-page="goal-map"] {
24
+ body[data-page="goal-map"], body[data-page="thinking"] .sky-stage {
21
25
  color-scheme: dark;
22
26
  --void: #000000; --ink: #ffffff; --ink-soft: #c0c0c0; --ink-faint: #a8a8a8; --ink-dim: #65656a; --ink-ghost: #3a3a3f;
23
27
  --rule: rgba(255,255,255,.16); --rule-soft: rgba(255,255,255,.08); --glass: rgba(255,255,255,.075); --glass-strong: rgba(255,255,255,.14);
@@ -26,12 +30,46 @@ body[data-page="thinking"], body[data-page="goal-map"] {
26
30
  --face: Manrope, "Segoe UI", system-ui, -apple-system, sans-serif;
27
31
  }
28
32
  html, body { height: 100%; }
29
- body[data-page="thinking"], body[data-page="goal-map"] { margin: 0; background: var(--void); color: var(--ink); font-family: var(--face); font-size: 15px; line-height: 1.5; overflow: hidden; -webkit-font-smoothing: antialiased; }
30
- button { font-family: inherit; }
31
- a { color: var(--accent); }
32
- .lbl { font: var(--label); letter-spacing: .14em; text-transform: uppercase; color: var(--ink-faint); }
33
- .mono { font: var(--mono); font-variant-numeric: tabular-nums; }
33
+ body[data-page="goal-map"], body[data-page="thinking"] .sky-stage { margin: 0; background: var(--void); color: var(--ink); font-family: var(--face); font-size: 15px; line-height: 1.5; overflow: hidden; -webkit-font-smoothing: antialiased; }
34
+ body[data-page="thinking"] { overflow: hidden; }
35
+ :where(body[data-page="goal-map"], .sky-stage) button { font-family: inherit; }
36
+ :where(body[data-page="goal-map"], .sky-stage) a { color: var(--accent); }
37
+ :where(body[data-page="goal-map"], .sky-stage) .lbl { font: var(--label); letter-spacing: .14em; text-transform: uppercase; color: var(--ink-faint); }
38
+ :where(body[data-page="goal-map"], .sky-stage) .mono { font: var(--mono); font-variant-numeric: tabular-nums; }
34
39
  [hidden] { display: none !important; }
40
+
41
+ /* THE STAGE (task 1004327). The box under the hall's top bar and beside its
42
+ 64px rail — the drawer's phone width has no rail — and, in full screen
43
+ (html[data-fullscreen="on"], shell.js), the whole window. `contain: layout
44
+ paint` makes it the containing block of every position: fixed layer inside
45
+ it, so the template's fixed layout below measures from the stage and needs
46
+ no second copy; sky-layout.js sizes the canvas to the same box. The
47
+ template's four viewport units (100vw, 46vh) became percentages for the
48
+ same reason: a fixed layer's % is its containing block — the stage here,
49
+ the window on /goal-map, where the two are the same number. */
50
+ .sky-stage { 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; }
51
+ html[data-fullscreen="on"] .sky-stage { top: 0; left: 0; }
52
+ @media (max-width: 768px) { .sky-stage { left: 0; } }
53
+ /* The full-screen switch floats top right over the stage, where the sky's
54
+ tools end; the strip steps in by the button's width so nothing sits under it. */
55
+ html[data-fullscreen="on"] .sky-stage .top { padding-right: 64px; }
56
+ /* Under 900 the brand takes a row of its own (the template's rule below), so
57
+ only that row steps in and the tools keep their width. */
58
+ @media (max-width: 900px) {
59
+ html[data-fullscreen="on"] .sky-stage .top { padding-right: 16px; }
60
+ html[data-fullscreen="on"] .sky-stage .brand { padding-right: 48px; }
61
+ }
62
+ /* style.css rules whose names the template also uses — each one reset to what
63
+ the template's page had, at the SAME specificity and earlier than the sky's
64
+ own rules below, so the sky's declarations still win. */
65
+ :where(.sky-stage) * { box-sizing: revert; }
66
+ :where(.sky-stage) a { text-decoration: revert; }
67
+ :where(.sky-stage) .skip { position: static; left: auto; top: auto; z-index: auto; }
68
+ :where(.sky-stage) .filters { align-items: normal; margin: 0; }
69
+ :where(.sky-stage) .foot { margin: 0; border: 0; font-size: inherit; }
70
+ :where(.sky-stage) :focus-visible { outline: revert; outline-offset: revert; box-shadow: revert; border-radius: revert; }
71
+ :where(.sky-stage) input[type="text"], :where(.sky-stage) input[type="search"] { min-height: 0; padding: 1px 2px; border-radius: 0; }
72
+ :where(.sky-stage) a:hover { text-decoration: revert; }
35
73
  #sky { position: fixed; inset: 0; display: block; width: 100%; height: 100%; cursor: grab; touch-action: none; }
36
74
  #sky.dragging { cursor: grabbing; }
37
75
  #sky:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
@@ -48,6 +86,8 @@ a { color: var(--accent); }
48
86
  .lens button[aria-pressed="true"] { background: var(--glass-strong); color: var(--ink); }
49
87
  .lens button:focus-visible, .pill:focus-visible, .row:focus-visible, .obj:focus-visible, input:focus-visible, .door:focus-visible, .menu button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
50
88
  .tools { display: inline-flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; max-width: 680px; }
89
+ /* /goal-map carries no switcher (task 1004328), so its tools take the switcher's push to the right. */
90
+ .brand + .tools { margin-left: auto; }
51
91
  .pill { background: var(--glass); }
52
92
  .pill[aria-pressed="true"] { background: var(--glass-strong); color: var(--ink); }
53
93
  .pill[aria-pressed="true"]::before { content: ""; display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); margin-right: 8px; vertical-align: 1px; }
@@ -55,7 +95,7 @@ a { color: var(--accent); }
55
95
  .pill.icon::before { display: none; }
56
96
  .spot { outline: 2px solid var(--accent) !important; outline-offset: 4px; box-shadow: 0 0 0 8px rgba(251,149,105,.12), 0 0 40px rgba(251,149,105,.35); }
57
97
 
58
- .composer { position: fixed; left: 50%; top: calc(76px + env(safe-area-inset-top, 0px)); transform: translateX(-50%); width: min(620px, calc(100vw - 32px)); z-index: 6; display: flex; flex-direction: column; gap: 8px; align-items: center; }
98
+ .composer { position: fixed; left: 50%; top: calc(76px + env(safe-area-inset-top, 0px)); transform: translateX(-50%); width: min(620px, calc(100% - 32px)); z-index: 6; display: flex; flex-direction: column; gap: 8px; align-items: center; }
59
99
  .composer form { display: flex; width: 100%; height: 48px; border-radius: 999px; background: rgba(0,0,0,.5); box-shadow: inset 0 0 0 1px var(--rule); backdrop-filter: blur(6px); align-items: center; padding: 0 6px 0 22px; gap: 8px; }
60
100
  .composer form:focus-within { box-shadow: inset 0 0 0 1px var(--ink-faint); }
61
101
  .composer input { flex: 1; min-width: 0; height: 100%; background: transparent; border: 0; color: var(--ink); font: 300 17px/1 var(--face); letter-spacing: -.01em; outline: none; }
@@ -135,7 +175,7 @@ a { color: var(--accent); }
135
175
  .jump li button:hover, .jump li button:focus-visible { background: var(--glass); color: var(--ink); }
136
176
  .jump li .lbl { flex-shrink: 0; width: 60px; }
137
177
 
138
- .tour, .start { position: fixed; left: 50%; bottom: calc(110px + env(safe-area-inset-bottom, 0px)); transform: translateX(-50%); width: min(560px, calc(100vw - 32px)); z-index: 8; padding: 18px 22px 16px; border-radius: 16px; display: flex; flex-direction: column; gap: 10px; }
178
+ .tour, .start { position: fixed; left: 50%; bottom: calc(110px + env(safe-area-inset-bottom, 0px)); transform: translateX(-50%); width: min(560px, calc(100% - 32px)); z-index: 8; padding: 18px 22px 16px; border-radius: 16px; display: flex; flex-direction: column; gap: 10px; }
139
179
  .tour .step { display: flex; align-items: center; gap: 10px; }
140
180
  .tour .dots { display: flex; gap: 5px; margin-left: auto; }
141
181
  .tour .dots i { width: 5px; height: 5px; border-radius: 50%; background: rgba(255,255,255,.2); }
@@ -203,12 +243,12 @@ a { color: var(--accent); }
203
243
  .lens button, .pill { height: 32px; padding: 0 12px; font-size: 12px; }
204
244
  .pill.icon { width: 32px; }
205
245
  .tools { margin-left: auto; }
206
- .composer { top: calc(186px + env(safe-area-inset-top, 0px)); width: calc(100vw - 32px); }
246
+ .composer { top: calc(186px + env(safe-area-inset-top, 0px)); width: calc(100% - 32px); }
207
247
  .composer form { height: 44px; padding-left: 16px; }
208
248
  .composer input { font-size: 16px; }
209
249
  .gravity { font-size: 11.5px; }
210
250
  .jump, .legend, .time { display: none; }
211
- .room, .panel { left: 10px; right: 10px; width: auto; top: auto; bottom: calc(70px + env(safe-area-inset-bottom, 0px)); max-height: 46vh; }
251
+ .room, .panel { left: 10px; right: 10px; width: auto; top: auto; bottom: calc(70px + env(safe-area-inset-bottom, 0px)); max-height: 46%; }
212
252
  .alt { margin-left: 0; text-align: left; }
213
253
  .tour, .start { bottom: calc(80px + env(safe-area-inset-bottom, 0px)); }
214
254
  .card { max-width: 240px; }
@@ -12,7 +12,8 @@
12
12
  links, injected into this head by the server like every hall page's
13
13
  (tests/hall_tokens.mjs: no hall page hardcodes a web-font stylesheet).
14
14
  The template carried its own Google Fonts link for the same two faces. -->
15
- <link rel="stylesheet" href="/builders/thinking.css?v=2026-09-25-sky4">
15
+ <link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
16
+ <link rel="stylesheet" href="/builders/thinking.css?v=2026-09-27-split">
16
17
  </head>
17
18
  <!-- THE SKY (task 1004231 / sky part 3 of 5, goal 1000095; ADR 0331, ADR 0227
18
19
  §11). This page is docs/design/mocks/sky/sky.template.html — the prototype
@@ -27,48 +28,59 @@
27
28
  The file is still `thinking` and the URL still /thinking: this is the
28
29
  ideator's home, the page that replaced THE ROOM (task 1004098) — and the
29
30
  room survives inside it as the list drawer (L). /sky, the old ambient
30
- page's URL, 301s here (src/bongos/serve-internal.js). It carries no hall
31
- chrome on purpose: the sky is a full-screen surface with its own header,
32
- and the hall's doors (the record, the goal, the task) open from the panel.
31
+ page's URL, 301s here (src/bongos/serve-internal.js).
33
32
 
34
- The same markup, sheet and scripts serve THE PROJECT SKY at /goal-map
35
- (part 4, task 1004232): that body says data-sky-lens="all", so the boot
36
- opens in "the whole project" and the tour hands back to it. The switcher
37
- is two doors — "the whole project" leaves for /goal-map from here, and
38
- "simple" / "your thinking" leave for here from there. -->
39
- <body data-page="thinking" data-page-title="Your sky">
33
+ IN THE HALL (task 1004327, owner decision 2026-09-27). The sky used to
34
+ carry no hall chrome; it now sits in the sidebar and top bar like every
35
+ hall page, and the shell's full-screen switch (data-fullscreen-switch)
36
+ hides both so the sky fills the window again, remembered for this page.
37
+ The whole sky lives inside <main class="sky-stage">, which is a
38
+ containing block for its position: fixed layers (thinking.css), so the
39
+ template's fixed layout is unchanged and simply measures from the stage
40
+ instead of the window. The sky's heading is a <div class="brand">, not a
41
+ <header>: the shell's top bar is the page's one <header>, and in full
42
+ screen it stays in the document as the way back.
43
+
44
+ ONLY YOUR THINKING (task 1004328, owner decision 2026-09-27). The sheet,
45
+ the seven scripts and the drawing are shared with THE PROJECT SKY at
46
+ /goal-map; the lenses and the tools are not. This page's switcher is
47
+ "simple" and "your thinking" and nothing else. "The whole project", the
48
+ "my ideas" overlay, explore and its start dialog are the project map's
49
+ and live only in goal-map.html, and nothing here leaves for that page as
50
+ a lens. -->
51
+ <body data-page="thinking" data-page-title="Your thinking" data-fullscreen-switch>
52
+
53
+ <div class="app">
54
+ <aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
55
+ <div class="app-scrim" id="app-scrim" hidden></div>
56
+ <div class="app-body">
57
+ <header class="app-topbar" id="app-topbar"></header>
40
58
 
41
59
  <!-- The one <main> landmark the kit's a11y floor asks of every hall page
42
- (one-header-main-h1). The template had none; the canvas is the page's
43
- content, and the landmark wraps it with no rule of its own, so nothing
44
- moves — the canvas is position: fixed. -->
45
- <main>
60
+ (one-header-main-h1). In the hall (task 1004327) it is the STAGE: the box
61
+ under the top bar and beside the rail that the whole sky is drawn in, and
62
+ the containing block every fixed layer below measures from. -->
63
+ <main id="hall-main" class="sky-stage">
46
64
  <canvas id="sky" tabindex="0" role="img" aria-label="The sky, drawn from the ledger. Every thought and goal here is also a row in the list; press L to open it, ? to take the tour."></canvas>
47
- </main>
48
65
 
49
- <!-- The template wrote the whole top strip as the <header>. The kit's phone
50
- floor (top-bar-80px) measures the page's <header>, and the strip wraps to
51
- 125px at 390 by the template's own rule — so the header is the BRAND (the
52
- h1 and the figures) and the lens and tools stay its siblings inside the
53
- same .top strip. No selector names the element, so the render is the
54
- template's, pixel for pixel. -->
66
+ <!-- The template wrote the whole top strip as the <header>; the sky's first
67
+ build made only the BRAND the header (the kit's top-bar-80px floor). In
68
+ the hall the shell's top bar is the page's header, so the brand is a
69
+ <div> — no selector names the element, so the render is unchanged. -->
55
70
  <div class="top">
56
- <header class="brand">
71
+ <div class="brand">
57
72
  <h1>your sky <em id="lensWord">— your thinking</em></h1>
58
73
  <div class="stats" id="stats"></div>
59
- </header>
74
+ </div>
60
75
  <div class="lens" role="group" aria-label="Filter" id="lensGroup">
61
76
  <button type="button" id="lensSimple" aria-pressed="false">simple</button>
62
77
  <button type="button" id="lensMine" aria-pressed="true">your thinking</button>
63
- <button type="button" id="lensAll" aria-pressed="false">the whole project</button>
64
78
  </div>
65
79
  <div class="tools" id="tools">
66
80
  <button type="button" class="pill" id="roomBtn" aria-pressed="false" title="The same sky, as a list (L)">list</button>
67
81
  <button type="button" class="pill" id="recentBtn" aria-pressed="false" hidden title="Only the thoughts of the last thirty days.">recent</button>
68
82
  <button type="button" class="pill" id="claudeBtn" aria-pressed="false" title="Show the thoughts a session filed under your name. Off by default: only what a person filed.">claude layer</button>
69
83
  <button type="button" class="pill" id="influenceBtn" aria-pressed="false" title="Only the goals your thoughts touched — everything else fades.">my influence</button>
70
- <button type="button" class="pill" id="exploreBtn" aria-pressed="true" hidden title="Explore: the sky reveals itself as you follow routes from where you begin.">explore</button>
71
- <button type="button" class="pill" id="ideasBtn" aria-pressed="false" hidden title="Lay your thoughts over the project map.">my ideas</button>
72
84
  <button type="button" class="pill" id="rehearse" aria-pressed="false" title="What if: invents removals, dimming and nods — none has happened in the ledger yet — so you can see how the dead would read. Sizes are never invented.">what if</button>
73
85
  <button type="button" class="pill icon" id="clockBtn" title="Show or hide the clock" aria-label="Show or hide the clock">◷</button>
74
86
  <button type="button" class="pill icon" id="tourBtn" title="How to read the sky (?)" aria-label="How to read the sky">?</button>
@@ -103,13 +115,6 @@
103
115
 
104
116
  <aside class="panel" id="panel" hidden aria-live="polite"></aside>
105
117
 
106
- <div class="start glass" id="start" hidden role="dialog" aria-label="Where do you begin?">
107
- <span class="lbl">explore</span>
108
- <h3>Where do you begin?</h3>
109
- <p>The sky reveals itself as you go. Pick a well to start in; every route you chart brings the goal at its far end into view.</p>
110
- <div class="opts" id="startOpts"></div>
111
- </div>
112
-
113
118
  <div class="tour glass" id="tour" hidden role="dialog" aria-label="How to read the sky">
114
119
  <div class="step"><span class="lbl" id="tourN"></span><span class="dots" id="tourDots"></span></div>
115
120
  <h3 id="tourH"></h3>
@@ -129,14 +134,20 @@
129
134
  <div class="alt" id="alt"></div>
130
135
  </div>
131
136
  </footer>
137
+ </main>
138
+ </div>
139
+ </div>
140
+
141
+ <script src="/builders/shell.js?v=2026-09-27-fullscreen"></script>
142
+ <script src="/builders/palette.js" defer></script>
132
143
 
133
- <script src="/builders/idea-objects.js?v=2026-09-25-sky4"></script>
134
- <script src="/builders/sky-data.js?v=2026-09-25-sky4"></script>
135
- <script src="/builders/sky-layout.js?v=2026-09-25-sky4"></script>
136
- <script src="/builders/sky-draw.js?v=2026-09-25-sky4"></script>
137
- <script src="/builders/sky-interaction.js?v=2026-09-25-sky4"></script>
138
- <script src="/builders/sky-panel.js?v=2026-09-25-sky4"></script>
139
- <script src="/builders/sky-composer.js?v=2026-09-25-sky4"></script>
140
- <script src="/builders/sky-tour.js?v=2026-09-25-sky4"></script>
144
+ <script src="/builders/idea-objects.js?v=2026-09-27-split"></script>
145
+ <script src="/builders/sky-data.js?v=2026-09-27-split"></script>
146
+ <script src="/builders/sky-layout.js?v=2026-09-27-split"></script>
147
+ <script src="/builders/sky-draw.js?v=2026-09-27-split"></script>
148
+ <script src="/builders/sky-interaction.js?v=2026-09-27-split"></script>
149
+ <script src="/builders/sky-panel.js?v=2026-09-27-split"></script>
150
+ <script src="/builders/sky-composer.js?v=2026-09-27-split"></script>
151
+ <script src="/builders/sky-tour.js?v=2026-09-27-split"></script>
141
152
  </body>
142
153
  </html>
@@ -1,9 +1,8 @@
1
1
  {
2
- "_": "THE SKY (task 1004231 / sky part 3 of 5, goal 1000095; ADR 0331) through modules/ui-design/kit/render.js. The page draws GET /sky + GET /sky/mine; against the kit's own stub neither exists, so every state below renders the EMPTY sky — a real state the floors must hold in (the composer, the tour, the list drawer and the lenses all exist with no thought filed). To look at the populated sky, run it against the hall-preview harness, whose sky.json + sky__mine.json fixtures carry the wells and one of each of the five objects:\n\n node scripts/hall-preview/server.js --port 4615 --fixture-me\n node modules/ui-design/kit/render.js --states modules/hall-ui/public/thinking.states.json --base-in http://localhost:4615 --base-out http://localhost:4615\n\nThe tour opens by itself on a first visit (sky-tour-seen), so states that want the bare sky skip it first.",
2
+ "_": "THE SKY (task 1004231 / sky part 3 of 5, goal 1000095; ADR 0331) through modules/ui-design/kit/render.js. The page draws GET /sky + GET /sky/mine; against the kit's own stub neither exists, so every state below renders the EMPTY sky — a real state the floors must hold in (the composer, the tour, the list drawer and the lenses all exist with no thought filed). To look at the populated sky, run it against the hall-preview harness, whose sky.json + sky__mine.json fixtures carry the wells and one of each of the five objects:\n\n node scripts/hall-preview/server.js --port 4615 --fixture-me\n node modules/ui-design/kit/render.js --states modules/hall-ui/public/thinking.states.json --base-in http://localhost:4615 --base-out http://localhost:4615\n\nThe tour opens by itself on a first visit (sky-tour-seen), so states that want the bare sky skip it first.\n\nIN THE HALL (task 1004327): the page sits in the hall chrome, so it renders in BOTH modes — the chrome follows the hall theme and the sky's stage stays the void in both. The full-* states press the shell's full-screen switch: the sidebar and the top bar leave, and the one button that brings them back floats top right.",
3
3
  "page": "/builders/thinking",
4
4
  "surface": "hall-ui",
5
5
  "stub": { "prefix": "/builders/" },
6
- "modes": ["dark"],
7
6
  "modeQuery": false,
8
7
  "ignoreRequests": ["/api/(gds|bongos)/", "fonts\\.g(oogleapis|static)\\.com"],
9
8
  "ignoreConsole": ["/api/(gds|bongos)/", "\\b401\\b", "\\b404\\b", "Unauthorized", "Not Found", "load failed", "fonts\\.g(oogleapis|static)\\.com"],
@@ -16,7 +15,7 @@
16
15
  },
17
16
  "sky": {
18
17
  "auth": true,
19
- "_": "The 'your thinking' lens, bare: composer, lenses, tools, legend and the clock.",
18
+ "_": "The 'your thinking' lens, bare: composer, the two lenses (simple, your thinking), tools, legend and the clock.",
20
19
  "actions": [["wait", 1200], ["clickIfVisible", "#tourSkip"], ["wait", 300]],
21
20
  "expect": { "visible": ["#sky", "#composerWrap"] }
22
21
  },
@@ -38,6 +37,24 @@
38
37
  "actions": [["wait", 1200], ["clickIfVisible", "#tourSkip"], ["wait", 200], ["fill", "#thought", "a thought about the sky"], ["wait", 600]],
39
38
  "expect": { "visible": ["#composerWrap", "#gravity"] }
40
39
  },
40
+ "full": {
41
+ "auth": true,
42
+ "_": "Full screen: the shell's switch pressed — the sky fills the window, and one button brings the chrome back.",
43
+ "actions": [["wait", 1200], ["clickIfVisible", "#tourSkip"], ["wait", 200], ["click", "#fullscreen-toggle"], ["wait", 400]],
44
+ "expect": { "visible": ["#sky", "#composerWrap", "#fullscreen-toggle"], "hidden": ["#app-sidebar", "#topbar-title"], "attr": [["#fullscreen-toggle", "aria-pressed", "true"]] }
45
+ },
46
+ "full-list": {
47
+ "auth": true,
48
+ "_": "Full screen with the list drawer open (L).",
49
+ "actions": [["wait", 1200], ["clickIfVisible", "#tourSkip"], ["wait", 200], ["click", "#fullscreen-toggle"], ["wait", 300], ["press", "l"], ["wait", 300]],
50
+ "expect": { "visible": ["#room", "#fullscreen-toggle"], "hidden": ["#app-sidebar"] }
51
+ },
52
+ "full-back": {
53
+ "auth": true,
54
+ "_": "Escape leaves full screen, and the chrome is back.",
55
+ "actions": [["wait", 1200], ["clickIfVisible", "#tourSkip"], ["wait", 200], ["click", "#fullscreen-toggle"], ["wait", 300], ["focus", "#sky"], ["press", "Escape"], ["wait", 400]],
56
+ "expect": { "visible": ["#app-sidebar", "#topbar-title"], "attr": [["#fullscreen-toggle", "aria-pressed", "false"]] }
57
+ },
41
58
  "out": { "auth": false, "actions": [["wait", 600]] }
42
59
  },
43
60
  "reducedMotion": { "auth": true, "actions": [["wait", 1200], ["clickIfVisible", "#tourSkip"], ["wait", 200]] }
@@ -162,7 +162,7 @@
162
162
 
163
163
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
164
164
  chrome exists before the page script runs. -->
165
- <script src="/builders/shell.js?v=2026-09-26-nopeople"></script>
165
+ <script src="/builders/shell.js?v=2026-09-27-fullscreen"></script>
166
166
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
167
167
  <script src="/builders/palette.js" defer></script>
168
168
  <!-- shared frontend toolkit (window.OTB) — loaded before watch.js, which pulls from it. -->
@@ -171,7 +171,7 @@
171
171
 
172
172
  <!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
173
173
  time so the chrome exists before the deferred page scripts execute. -->
174
- <script src="/builders/shell.js?v=2026-09-26-nopeople"></script>
174
+ <script src="/builders/shell.js?v=2026-09-27-fullscreen"></script>
175
175
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
176
176
  <script src="/builders/palette.js" defer></script>
177
177
  <!-- shared frontend toolkit (window.OTB) — loaded FIRST so work.js can pull from it. -->
@@ -21,3 +21,23 @@ Pinned by `tests/hall_sky.mjs` (31 cases: the reads, the five objects, the two c
21
21
  - **THE LEDGER DRAWER IS THE ACCESSIBLE PATH** — the old goal map's table, kept (ADR 0227 §11): `L` opens it; every goal is a row that counts both directions of its dependencies ("waits on N · waited on by N") and the panel names them as doors; the filters are alive / wells / settled / collapsed / with routes. Every drawn thing is a row, one key away — spec decision 11's surviving rule.
22
22
  - **ADR 0227 §1's refusal stands.** Tasks resolve onto their rings only inside ONE well past the zoom threshold (dust density on the rings below it), never for every goal at once — at the whole-sky fit no task is a body — and there is no "show every task" control anywhere on the page or in the shuttle menu. `tests/goal_map_page.mjs` pins both, plus the nav slot, the two doors, the wells, alive, the routes, the menu, explore, "my ideas", the ledger, the clock, the bands, the tour and a hostile title.
23
23
  - **Same tier, same gates as `/thinking`.** `requireBuilderPage` (the tier the map sat on); no hall chrome (`STANDALONE_EXEMPT` in `tests/hall_audit.mjs`); `goal-map.states.json` renders the EMPTY sky against the kit stub like `thinking.states.json`, and the populated sky against the harness (`sky.json` + `sky__mine.json`). `goal-forest-*` (the per-goal forest on the goal detail page) is untouched.
24
+
25
+ ## In the hall, with a full-screen switch (task 1004327, BV2.TW20)
26
+
27
+ **Owner decision 2026-09-27: `/thinking` and the studio sit in the hall chrome, and each has one switch that hides it.** `/goal-map` keeps its standalone form (TW21 reshapes it next). What this settled:
28
+ - **The switch is the shell's, not a page's.** A page opts in with `<body data-fullscreen-switch>`; `shell.js` renders `#fullscreen-toggle` as the LAST top-bar control and keeps the state on `html[data-fullscreen="on"]` (the `data-theme` / `data-social` pattern). It is remembered PER PAGE (`hall-fullscreen:<data-page>` in localStorage, try/catch), and Escape leaves — unless the page used that press (it calls `preventDefault`: the sky's tour, menu and pick do) or the focus is in a text field. `tests/hall_fullscreen.mjs` boots the real shell and drives click, reload and Escape.
29
+ - **Full screen collapses the top bar, it does not remove it.** The sidebar, scrim and foot go; the top bar shrinks to its one button, floating top right where it already sat. That keeps exactly one shown `<header>` in both states (the kit's one-header-main-h1 floor) — which is also why the sky's brand and the studio's head are `<div>`s now.
30
+ - **The sky is drawn on a STAGE.** Everything the sky owns lives in `main.sky-stage`, fixed under the top bar and beside the 64px rail (the whole window in full screen, no rail under 768). `contain: layout paint` makes the stage the containing block of every fixed layer, so the template's fixed layout is untouched and simply measures from the stage; its four viewport units became `%` of that box (the same number on `/goal-map`). `SKY.viewport()` gives the canvas its size and the pointer its offset (`S.OX` / `S.OY`), and a `ResizeObserver` re-measures when the switch changes the box without the window resizing.
31
+ - **The sky's palette lives on the stage, not the body, on `/thinking`** — so the chrome around it keeps the hall's own light and dark. `style.css` now loads under `thinking.css`, and the handful of hall rules whose names the template also uses (`.skip`, `.filters`, `.foot`, `:focus-visible`, text inputs, `* { box-sizing }`) are reset under `:where(.sky-stage)` at the same specificity, before the sky's own rules. The sky's global rules are `:where()`-scoped for the same reason, which leaves `/goal-map` byte-for-byte in its old cascade.
32
+ - **Both pages render in both modes now.** The chrome follows the hall theme; the sky's stage is the void in both. The `full` and `full-back` states in both states files (and `full-list` in the sky's) press the switch.
33
+
34
+ ## Two skies, one renderer (task 1004328, BV2.TW21)
35
+
36
+ **Owner decision 2026-09-27: "Your thinking is only your thinking."** Part 4's "one page, two doors" above is SUPERSEDED: the two pages share the renderer and nothing else. The owner confirmed that "the your projects tab" means the whole-project lens, its switch and the project-map features. What this settled:
37
+ - **The lens and the features separate; the drawing does not.** `thinking.css`, the seven `sky-*.js` files and every draw path stay shared. What each page may be in is `S.lenses` (set once in `sky-data.js` from `data-sky-lens`): `['simple', 'mine']` on `/thinking`, `['all']` on `/goal-map`. `setLens` clamps to it, so nothing — a button, the tour, a jump, the panel's "descend" — can take a page into a lens it lacks, and no button leaves for the other page (`location.assign` is gone from `sky-interaction.js`).
38
+ - **Absent, not hidden.** `/thinking` carries no "the whole project" button, no "my ideas", no "explore" and no "where do you begin" dialog; `/goal-map` carries no switcher at all and none of the ideator's pills (recent, claude layer, my influence). The scripts touch a one-page control through `SKY.hide` / `SKY.press` / `SKY.on`, which do nothing when the element is not there. `thinking.css` gained one rule, `.brand + .tools { margin-left: auto; }`, so `/goal-map`'s tools keep the switcher's push to the right and its top strip reads as before.
39
+ - **Two tours from one list.** Every step in `ALL_STEPS` says which sky it belongs to; `/thinking` walks nine of the template's thirteen (the ideator's steps, the wells and rings, the clock, the composer, simple), `/goal-map` walks eight — the wells, rings, life, routes, settled systems, the clock and explore — opened by one new step, "This is the whole project.", because the template's first step is "This is your sky." and belongs to `/thinking`. The project tour turns explore off for its length and gives it back. Each tour has its own seen key (S.tourKey: sky-tour-seen here, sky-map-tour-seen on the map), so one does not silence the other.
40
+ - **"Descend into the well" stays in its sky.** On `/goal-map` it only flies; on `/thinking` it steps "simple" up to "your thinking" (the one lens there that zooms). The legend drops "unexplored" on `/thinking`, which has no explore.
41
+ - **Kept on both:** the list drawer (the room on `/thinking`, the ledger on `/goal-map`), the clock, the composer, "what if", the panel. Filing a thought from the project map still works; the owner's decision names the lens and the overlay, not the composer.
42
+
43
+ Pinned by `tests/hall_sky.mjs` (absence of every project-map control, every path into a lens, the nine-step tour never entering "all") and `tests/goal_map_page.mjs` (no switcher, setLens clamped, the eight-step tour with explore off and given back).
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.1051",
3
+ "version": "1.19.1053",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.1051",
9
+ "version": "1.19.1053",
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.19.1051",
3
+ "version": "1.19.1053",
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",
@@ -7607,5 +7607,17 @@
7607
7607
  "id": "1004312",
7608
7608
  "text": "Tweak Mode is planned and queued. An artist will take a whole page of the site from one calm studio, rewrite every word of it as a document (or offline in Word, uploaded back), and approve the finished page before it goes live"
7609
7609
  }
7610
+ ],
7611
+ "1.19.1052": [
7612
+ {
7613
+ "id": "1004327",
7614
+ "text": "The studio and Your thinking now sit inside the builders hall like every other page, with the sidebar and the top bar around them. Each has one new button in the top-right corner that goes full screen: the sidebar and top bar"
7615
+ }
7616
+ ],
7617
+ "1.19.1053": [
7618
+ {
7619
+ "id": "1004328",
7620
+ "text": "Your thinking and the project map are now two separate pages that share only their drawing. Your thinking keeps just your own views, simple and your thinking, with your own tools: the list, recent, claude layer, my influence,"
7621
+ }
7610
7622
  ]
7611
7623
  }
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.1051'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.1053'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -20,7 +20,13 @@
20
20
  // The REAL goal-map.html markup and the REAL seven sky-*.js files boot in a vm
21
21
  // through bootSkyPage (tests/helpers.mjs) — the same helper tests/hall_sky.mjs
22
22
  // uses for /thinking, pointed at this page's own file, so a button lost from
23
- // either door fails its own suite.
23
+ // either page fails its own suite.
24
+ //
25
+ // TWO SKIES, ONE RENDERER (task 1004328, owner decision 2026-09-27). The
26
+ // project sky is the ONLY home of the whole-project lens and its features —
27
+ // explore, "my ideas", the ledger of goals — and it offers no other lens: no
28
+ // switcher, no "simple", no "your thinking", and the ideator's pills
29
+ // (recent, claude layer, my influence) are /thinking's alone.
24
30
  //
25
31
  // Run: node tests/goal_map_page.mjs
26
32
  import assert from 'node:assert/strict';
@@ -85,45 +91,45 @@ const goal = (p, id) => p.S().goalById.get(String(id));
85
91
 
86
92
  // ═══ the lens is home ════════════════════════════════════════════════════════
87
93
 
88
- await test('it opens in "the whole project": the brand line, the switcher, the tools and the figures are the project sky\'s', async () => {
94
+ await test('it opens in "the whole project": the brand line, the tools and the figures are the project sky\'s', async () => {
89
95
  const p = await boot({ sky: FOUR(), versions: versions(), mine: mine([star()]) });
90
96
  assert.ok(p.ready(), 'the page booted');
91
97
  const S = p.S();
92
98
  assert.equal(S.home, 'all', 'the body says which lens is home');
93
99
  assert.equal(S.lens, 'all');
94
- assert.equal(p.el('lensAll').getAttribute('aria-pressed'), 'true');
95
- assert.equal(p.el('lensMine').getAttribute('aria-pressed'), 'false');
96
100
  assert.equal(p.el('lensWord').textContent, '— the whole project');
97
101
  assert.equal(p.el('lensWord').parentNode.textContent, 'the sky — the whole project', 'the h1 is the project sky\'s');
98
102
  assert.equal(p.el('roomTitle').textContent, 'the ledger');
99
103
  assert.equal(p.el('exploreBtn').hidden, false, 'explore is a tool here');
100
104
  assert.equal(p.el('ideasBtn').hidden, false, 'so is "my ideas"');
101
- assert.equal(p.el('claudeBtn').hidden, true, 'the ideator\'s pills are not');
102
- assert.equal(p.el('influenceBtn').hidden, true);
105
+ for (const id of ['claudeBtn', 'influenceBtn', 'recentBtn']) assert.equal(p.el(id), null, `the ideator's #${id} is not on this page at all`);
103
106
  assert.match(p.stats(), /<b>4<\/b> goals · <b>1<\/b> alive · <b>2<\/b> wells · <b>1<\/b> settled · <b>1<\/b> collapsed · <b>1<\/b> routes · <b>13<\/b> tasks/, 'the figures line is the goals\' line');
104
107
  const urls = p.requests.map((r) => r.url);
105
108
  for (const want of ['/api/bongos/sky', '/api/bongos/sky/mine', '/api/bongos/me', '/api/bongos/inbox/template']) assert.ok(urls.includes(want), `reads ${want}`);
106
109
  assert.ok(!urls.some((u) => /goals\/graph/.test(u)), 'the goal map\'s aggregate read is gone — the sky\'s own reads carry the routes');
107
110
  });
108
111
 
109
- await test('the same seven files open /thinking in "your thinking" — one page, two doors', async () => {
112
+ await test('the same seven files open /thinking in "your thinking" — one renderer, two skies', async () => {
110
113
  const p = bootSkyPage({ page: 'thinking', sky: FOUR(), versions: versions() }); await drain();
111
114
  assert.equal(p.S().home, 'mine');
112
115
  assert.equal(p.S().lens, 'mine');
113
116
  assert.equal(p.el('lensWord').parentNode.textContent, 'your sky — your thinking');
114
117
  });
115
118
 
116
- await test('the switcher is two doors: "simple" and "your thinking" leave for /thinking, and "the whole project" stays', async () => {
117
- const a = await boot({ sky: FOUR() });
118
- a.click('lensSimple');
119
- assert.equal(a.navigated, '/thinking');
120
- const b = await boot({ sky: FOUR() });
121
- b.click('lensMine');
122
- assert.equal(b.navigated, '/thinking');
123
- const c = await boot({ sky: FOUR() });
124
- c.click('lensAll');
125
- assert.equal(c.navigated, null, 'already here');
126
- assert.equal(c.S().lens, 'all');
119
+ await test('THE WHOLE PROJECT ONLY: no switcher, no "simple", no "your thinking" — and no lens this page lacks is ever entered', async () => {
120
+ const p = await boot({ sky: FOUR(), versions: versions(), mine: mine([star()]) });
121
+ const S = p.S(); const SKY = p.SKY();
122
+ assert.deepEqual(JSON.parse(JSON.stringify(S.lenses)), ['all']);
123
+ for (const id of ['lensGroup', 'lensSimple', 'lensMine', 'lensAll']) assert.equal(p.el(id), null, `#${id} is not on this page`);
124
+ const html = read('goal-map.html').replace(/<!--[\s\S]*?-->/g, '');
125
+ assert.equal(/your thinking|>simple</.test(html), false, 'the page offers neither lens in its markup');
126
+ assert.equal(/href="[^"]*\/thinking"/.test(html), false, 'and never links to /thinking (the shared sheet is thinking.css, which is not a link to the page)');
127
+ for (const l of ['mine', 'simple']) { SKY.setLens(l, true); assert.equal(S.lens, 'all', `setLens('${l}') stays in the whole project`); }
128
+ assert.equal(p.el('lensWord').parentNode.textContent, 'the sky — the whole project');
129
+ SKY.pick({ kind: 'goal', ref: goal(p, '7') }, false); p.click('descendBtn');
130
+ assert.equal(S.lens, 'all', 'descending stays on the project map');
131
+ assert.equal(p.navigated, null, 'nothing leaves for /thinking');
132
+ assert.ok(/\.brand \+ \.tools \{ margin-left: auto; \}/.test(read('thinking.css')), 'the tools take the switcher\'s push to the right, so the top strip reads as before');
127
133
  });
128
134
 
129
135
  // ═══ the wells ═══════════════════════════════════════════════════════════════
@@ -339,19 +345,37 @@ await test('a band is a version, and clicking one fills the screen with it', asy
339
345
 
340
346
  // ═══ the tour ════════════════════════════════════════════════════════════════
341
347
 
342
- await test('the tour walks all three lenses here too, and hands the sky back to the whole project', async () => {
343
- const p = await boot({ sky: FOUR(), versions: versions() });
348
+ await test('the tour is the project map\'s own eight steps, all in the whole project, with explore off while it runs and given back after', async () => {
349
+ const p = await boot({ sky: FOUR(), versions: versions(), storage: {} });
350
+ const S = p.S();
351
+ assert.deepEqual(S.explore.on, true, 'explore is on before the tour');
352
+ assert.deepEqual(JSON.parse(JSON.stringify(p.SKY().STEPS.map((s) => s.h))), [
353
+ 'This is the whole project.', 'A goal is a gravity well.', 'A ring is a criterion.', 'An alive well glows.',
354
+ 'Routes run between goals that wait on each other.', 'A settled system is a goal that finished.', 'The sky has a clock.', 'By default, you explore.',
355
+ ]);
344
356
  p.key('?');
345
357
  assert.equal(p.el('tour').hidden, false);
346
- assert.equal(p.S().lens, 'mine', 'step one is "This is your sky"');
347
- for (let n = 0; n < 6; n++) p.click('tourNext');
348
- assert.equal(p.S().lens, 'all', 'step seven is the alive well, on the project map');
358
+ assert.equal(p.el('tourN').textContent, 'how to read the sky · 1 of 8');
359
+ assert.equal(S.lens, 'all', 'step one is the whole project');
360
+ assert.equal(S.explore.on, false, 'and the tour sees every well');
361
+ assert.equal(p.el('start').hidden, true, 'so "where do you begin" waits');
362
+ const lenses = new Set([S.lens]);
363
+ for (let n = 0; n < 7; n++) { p.click('tourNext'); lenses.add(S.lens); }
364
+ assert.deepEqual([...lenses], ['all'], 'no step of this tour leaves the whole project');
365
+ assert.equal(p.el('tourN').textContent, 'how to read the sky · 8 of 8');
349
366
  p.key('Escape');
350
367
  assert.equal(p.el('tour').hidden, true);
351
- assert.equal(p.S().lens, 'all', 'back to the whole project, not to your thinking');
352
- assert.equal(p.el('lensAll').getAttribute('aria-pressed'), 'true');
368
+ assert.equal(S.lens, 'all', 'back to the whole project');
369
+ assert.equal(S.explore.on, true, 'explore is given back');
370
+ assert.equal(p.el('exploreBtn').getAttribute('aria-pressed'), 'true');
353
371
  const first = await boot({ sky: FOUR(), tourSeen: false });
354
372
  assert.equal(first.timers.pending(), 1, 'a first visit schedules the tour here as well');
373
+ // The two tours differ, so each keeps its own seen key (task 1004328).
374
+ const afterThinking = await boot({ sky: FOUR(), tourSeen: false, storage: { 'sky-explore': 'off', 'sky-tour-seen': '4' } });
375
+ assert.equal(afterThinking.timers.pending(), 1, 'having seen /thinking\'s tour does not mark this one seen');
376
+ assert.equal(afterThinking.S().tourKey, 'sky-map-tour-seen');
377
+ const seenHere = await boot({ sky: FOUR(), tourSeen: false, storage: { 'sky-explore': 'off', 'sky-map-tour-seen': '4' } });
378
+ assert.equal(seenHere.timers.pending(), 0, 'and this one, once seen, stays seen');
355
379
  });
356
380
 
357
381
  // ═══ born of a thought (part 5) ══════════════════════════════════════════════
@@ -401,12 +425,14 @@ await test('the page is the sky\'s own surface — the same sheet and the same s
401
425
  assert.match(html, /<body data-page="goal-map" data-page-title="The sky" data-sky-lens="all">/);
402
426
  assert.match(html, /<title>The sky — \{\{worldName\}\}<\/title>/);
403
427
  assert.match(html, /<h1>the sky <em id="lensWord">— the whole project<\/em><\/h1>/, 'the h1 is in static markup, so every state has one');
404
- assert.equal(/shell\.js|style\.css/.test(html), false, 'no hall chrome, like /thinking');
428
+ assert.equal(/shell\.js|style\.css/.test(html), false, 'no hall chrome yet — /thinking joined it (task 1004327), the project sky did not');
405
429
  const scripts = (h) => [...h.matchAll(/<script src="\/builders\/([^"?]+)\?v=([^"]+)"/g)].map((m) => [m[1], m[2]]);
406
430
  assert.deepEqual(scripts(html).map((s) => s[0]), ['idea-objects.js', 'sky-data.js', 'sky-layout.js', 'sky-draw.js', 'sky-interaction.js', 'sky-panel.js', 'sky-composer.js', 'sky-tour.js']);
407
- assert.deepEqual(scripts(html), scripts(thinking), 'the two doors load the same files at the same stamp');
431
+ // /thinking also loads the shell (task 1004327); the SKY's files are the same.
432
+ const skyScripts = (h) => scripts(h).filter((s) => s[0] !== 'shell.js');
433
+ assert.deepEqual(skyScripts(html), skyScripts(thinking), 'the two doors load the same sky files at the same stamp');
408
434
  assert.equal(html.match(/thinking\.css\?v=([^"]+)/)[1], thinking.match(/thinking\.css\?v=([^"]+)/)[1]);
409
- assert.match(read('thinking.css'), /body\[data-page="thinking"\], body\[data-page="goal-map"\] \{/, 'the sheet names both bodies');
435
+ assert.match(read('thinking.css'), /body\[data-page="goal-map"\], body\[data-page="thinking"\] \.sky-stage \{/, 'the sheet names both doors: the project sky\'s body, and the stage /thinking draws in inside the hall');
410
436
  for (const f of ['goal-map.css', 'goal-map.js', 'goal-map-lib.js']) assert.equal(fs.existsSync(path.join(HALL, f)), false, `${f} is retired`);
411
437
  assert.equal(fs.existsSync(path.join(ROOT, 'tests/goal_map_lib.mjs')), false, 'and so is the lib\'s test');
412
438
  assert.equal(/goal-map-lib|goal-map\.js|goal-map\.css/.test(html), false);
@@ -362,8 +362,9 @@ test('the module doc still records where the 2179 density pass landed', () => {
362
362
  // private copy of the hall's, it is a different surface's own, pinned by
363
363
  // tests/hall_sky.mjs against the template. It still stamps the branding token
364
364
  // and the pack's font links like every hall page (the server injects them).
365
+ // /thinking left this list in task 1004327 (owner decision 2026-09-27): it sits
366
+ // in the hall chrome now, with the shell's full-screen switch to hide it.
365
367
  const STANDALONE_EXEMPT = {
366
- 'thinking.html': 'the sky (task 1004231, ADR 0331) — the design of record is its own surface, not the hall chrome',
367
368
  'goal-map.html': 'the project sky (task 1004232) — the same surface as thinking.html, opened in its whole-project lens',
368
369
  };
369
370