@bongos/core 1.19.1012 → 1.19.1013

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 (34) hide show
  1. package/.bongos-core.json +51 -41
  2. package/docs/adr/0331-the-sky-is-the-ideators-home.md +6 -1
  3. package/docs/copy-inventory.md +99 -56
  4. package/docs/copy-registry.json +830 -465
  5. package/docs/file-map.md +1 -1
  6. package/docs/module-api-changelog.md +2 -0
  7. package/modules/hall-ui/public/idea-objects.js +4 -1
  8. package/modules/hall-ui/public/sky-composer.js +155 -0
  9. package/modules/hall-ui/public/sky-data.js +287 -0
  10. package/modules/hall-ui/public/sky-draw.js +244 -0
  11. package/modules/hall-ui/public/sky-interaction.js +137 -0
  12. package/modules/hall-ui/public/sky-layout.js +81 -0
  13. package/modules/hall-ui/public/sky-panel.js +141 -0
  14. package/modules/hall-ui/public/sky-tour.js +80 -0
  15. package/modules/hall-ui/public/thinking.css +203 -267
  16. package/modules/hall-ui/public/thinking.html +119 -58
  17. package/modules/hall-ui/public/thinking.states.json +33 -9
  18. package/package-lock.json +2 -2
  19. package/package.json +1 -1
  20. package/src/bongos/routes/me.js +4 -1
  21. package/src/bongos/serve-internal.js +20 -6
  22. package/src/module-api.js +1 -1
  23. package/tests/hall_audit.mjs +27 -0
  24. package/tests/hall_page_gate_map.mjs +11 -5
  25. package/tests/hall_sky.mjs +393 -405
  26. package/tests/hall_thinking_room.mjs +274 -296
  27. package/tests/helpers.mjs +185 -217
  28. package/tests/ideator_large_dead_thought_proof.mjs +33 -25
  29. package/tests/ideator_traceable_proof.mjs +61 -39
  30. package/modules/hall-ui/public/sky.css +0 -365
  31. package/modules/hall-ui/public/sky.html +0 -79
  32. package/modules/hall-ui/public/sky.js +0 -580
  33. package/modules/hall-ui/public/sky.states.json +0 -20
  34. package/modules/hall-ui/public/thinking.js +0 -418
@@ -43,7 +43,7 @@
43
43
 
44
44
  import { strict as assert } from 'node:assert';
45
45
  import { createRequire } from 'node:module';
46
- import { makeRunner, makeSqlAwareClient, bootThinkingPage, settle } from './helpers.mjs';
46
+ import { makeRunner, makeSqlAwareClient, bootSkyPage, settle } from './helpers.mjs';
47
47
 
48
48
  process.env.NODE_ENV = 'test';
49
49
 
@@ -182,13 +182,22 @@ async function anHourLater(filed) {
182
182
  return { ideas, total: ideas.length, limit: 50, offset: 0 };
183
183
  }
184
184
 
185
- // Boots the REAL room over a REAL trail response and hands back the HTML a
186
- // reader is actually shown. No receipt is reachable from here: bootThinkingPage's
187
- // localStorage answers null, which is the "another device, an hour later"
188
- // condition this proof is about.
185
+ // Boots the REAL room — since task 1004231 the sky's list drawer, one key away
186
+ // — over a REAL trail response (the rows GET /sky/mine hands back whole) and
187
+ // hands back the page with the drawer open, so `.rows()` is the HTML a reader
188
+ // is actually shown. No receipt is reachable from here: bootSkyPage's
189
+ // localStorage holds nothing of the filing, which is the "another device, an
190
+ // hour later" condition this proof is about. The live goal is a well in the
191
+ // sky because the routed filing minted a task in it — GET /sky's wells are the
192
+ // goals carrying at least one task — which is what lets the row NAME it.
189
193
  async function room(page) {
190
- const p = bootThinkingPage({ pages: [{ ...page, offset: 0 }], viewer: { id: FILER } });
191
- for (let i = 0; i < 10; i += 1) await settle();
194
+ const p = bootSkyPage({
195
+ mine: { builder_id: FILER, ideas: page.ideas, total: page.total, cap: 1000, truncated: false },
196
+ sky: { goals: [{ goal_id: String(LIVE_GOAL.id), title: 'The live goal', subtitle: null, status: 'open', version_id: LIVE_GOAL.version_id, category: null, task_total: 1, task_done: 0, created_at: '2026-08-01T00:00:00.000Z', criteria: [], tasks: [{ id: '9001', title: THOUGHT, status: 'backlog', credits_reward: 0, created_on: '2026-09-01', idea_id: null }] }], edges: [], counts: {}, cache: {} },
197
+ viewer: { id: FILER, permissions: {} },
198
+ });
199
+ for (let i = 0; i < 30; i += 1) await settle();
200
+ p.key('l');
192
201
  return p;
193
202
  }
194
203
 
@@ -213,15 +222,23 @@ await test('aimed: filing one line lands it as work, and the hall is told so at
213
222
  await test('aimed: AN HOUR LATER the room still names what it became, with no receipt in reach', async () => {
214
223
  const filed = await fileIt({ goalId: LIVE_GOAL.id });
215
224
  const page = await anHourLater(filed);
216
- const rows = (await room(page)).rows();
225
+ const p = await room(page);
226
+ const rows = p.rows();
217
227
 
218
228
  assert.match(rows, /the filing receipt does not survive a reload/, 'the thought is on the page');
219
- // "What became of it" — the task, by NAME and as a link. This is the answer
220
- // the 2026-09-19 filing could not get.
221
- assert.match(rows, new RegExp(`/task/${filed.captured.routed_task.id}\\b`),
222
- 'the row links the task the thought became');
229
+ // "What became of it" — the task, counted in the row and NAMED in the panel
230
+ // (the sky's drawer says "1 task"; its panel lists the descent by title, and
231
+ // the record is one door away). This is the answer the 2026-09-19 filing
232
+ // could not get.
233
+ assert.match(rows, /1 task/, 'the row says a task fell out of it');
223
234
  assert.doesNotMatch(rows, /nothing yet/,
224
235
  'a thought that became work is never drawn as one that caused none');
236
+ const i = p.S().ideas.find((x) => x.id === String(page.ideas[0].id));
237
+ p.SKY().pick({ kind: 'idea', ref: i }, false);
238
+ assert.match(p.panel(), /the descent — everything that fell out of it/);
239
+ assert.match(p.panel(), new RegExp(`<span class="st">backlog</span><span>${THOUGHT}`),
240
+ 'the panel names the task the thought became, and its state');
241
+ assert.match(p.panel(), new RegExp(`href="/idea/${page.ideas[0].id}"`), 'and the record is a door away');
225
242
  });
226
243
 
227
244
  await test('aimed: the goal that caught it survives the hour — the room attributes it to the SAME well', async () => {
@@ -256,10 +273,15 @@ await test('floated: an hour later the room says nothing has claimed it — in t
256
273
  assert.equal(st.state, 'protostar', 'filed, not yet ignited');
257
274
  assert.equal(st.position, 'open-space', 'and nothing caught it');
258
275
 
259
- const rows = (await room(page)).rows();
276
+ const p = await room(page);
277
+ const rows = p.rows();
260
278
  assert.match(rows, /the filing receipt does not survive a reload/, 'the thought is still on the page');
261
- assert.match(rows, /nothing has claimed it/,
262
- 'the room says so in the reader\'s words, not by leaving the cell blank');
279
+ assert.match(rows, /· open space/, 'the row says it floats');
280
+ const i = p.S().ideas.find((x) => x.id === String(page.ideas[0].id));
281
+ assert.equal(p.SKY().positionWord(i), 'nothing has claimed it');
282
+ p.SKY().pick({ kind: 'idea', ref: i }, false);
283
+ assert.match(p.panel(), /nothing has claimed it/,
284
+ 'the panel says so in the reader\'s words, not by leaving the fact blank');
263
285
  });
264
286
 
265
287
  // =========================================================================
@@ -275,41 +297,41 @@ await test('the two outcomes do not render alike — a caught thought and a floa
275
297
  const floatRows = (await room(await anHourLater(await fileIt({ goalId: null })))).rows();
276
298
 
277
299
  assert.notEqual(aimedRows, floatRows, 'the two rows are not the same HTML');
278
- assert.match(aimedRows, /data-object="protostar"/, 'both are protostars — neither has been paid yet');
279
- assert.match(floatRows, /data-object="protostar"/);
280
- // …and the object column is what separates them, which is exactly the
281
- // position axis idea-objects.js says is load-bearing for a protostar alone.
282
- assert.doesNotMatch(aimedRows, /nothing has claimed it/,
300
+ assert.match(aimedRows, /class="mk protostar"/, 'both are protostars — neither has been paid yet');
301
+ assert.match(floatRows, /class="mk protostar"/);
302
+ // …and WHERE IT SITS is what separates them, which is exactly the position
303
+ // axis idea-objects.js says is load-bearing for a protostar alone: the caught
304
+ // one is in its well, the floating one in open space.
305
+ assert.match(aimedRows, /· in The live goal/, 'the caught thought names the well that caught it');
306
+ assert.doesNotMatch(aimedRows, /· open space/,
283
307
  'the caught thought must never be described as unclaimed');
284
- assert.match(floatRows, /nothing has claimed it/);
308
+ assert.match(floatRows, /· open space/);
309
+ assert.doesNotMatch(floatRows, /· in The live goal/);
285
310
  });
286
311
 
287
312
  // =========================================================================
288
313
  // 4. HOW FAR THE CHAIN GETS BEFORE A GOAL IS NAMED — recorded, not assumed
289
314
  // =========================================================================
290
315
 
291
- await test('the room reaches the goal by LINK, not by name — pinned so the follow-up is visible', async () => {
292
- // HONEST LIMIT OF THIS PROOF. The criterion asks that a person can answer
293
- // "what happened to the thing I filed" unaided, and they can: the row names
294
- // the task and links it, and the task record names its goal. What the room
295
- // does NOT do is print the goal's own name, even though `state.wellGoalId`
296
- // is resolved server-side and sits on the row it draws (idea-objects.js
297
- // positionWord deliberately returns "has a home" / "waiting for a nod" and
298
- // never the well's identity).
299
- //
300
- // That is a rendering gap, not a traceability one, so it is recorded here
301
- // rather than fixed under a verify task (it is task 1004225) — and pinned, so that when the room
302
- // does start naming the well this assertion fails and is deleted along with
303
- // the follow-up it points at.
316
+ await test('the room NAMES the well that caught it — the gap task 1004225 recorded is closed by the sky', async () => {
317
+ // The previous version of this case pinned the OPPOSITE: the old room printed
318
+ // the position ("has a home") and never the well's identity, and said so
319
+ // here so that the day the room started naming the well, the assertion would
320
+ // fail and be replaced. That day is task 1004231: the sky's drawer says
321
+ // "· in <the well>" on the row, and its panel's gravity fact links the goal.
304
322
  const filed = await fileIt({ goalId: LIVE_GOAL.id });
305
323
  const page = await anHourLater(filed);
306
- const rows = (await room(page)).rows();
324
+ const p = await room(page);
325
+ const rows = p.rows();
307
326
 
308
327
  assert.ok(page.ideas[0].state.wellGoalId, 'the well IS resolved and delivered to the page');
309
- assert.doesNotMatch(rows, new RegExp(`goal ${LIVE_GOAL.id}\\b`),
310
- 'and today the page does not print it — the goal is one link away, on the task');
311
- assert.match(rows, /has a home|waiting for a nod/,
312
- 'what it prints instead is the position, which says a goal caught it without saying which');
328
+ assert.equal(String(page.ideas[0].state.wellGoalId), String(LIVE_GOAL.id));
329
+ assert.match(rows, /· in The live goal/, 'the row names the well by its title');
330
+ const i = p.S().ideas.find((x) => x.id === String(page.ideas[0].id));
331
+ p.SKY().pick({ kind: 'idea', ref: i }, false);
332
+ assert.match(p.panel(), new RegExp(`gravity</span><span class="v"><a href="#" data-goal="${LIVE_GOAL.id}">The live goal</a>`),
333
+ 'and the panel makes the well a door');
334
+ assert.match(p.panel(), /waiting for a nod/, 'while still saying what its position means');
313
335
  });
314
336
 
315
337
  summary();
@@ -1,365 +0,0 @@
1
- /* sky.css — what THE SKY has that no other hall page has
2
- (task 1004105 / BV2.R26, goal 1000095).
3
-
4
- The composition is the shell's: `.page-head--board`, `.scroll` and
5
- `.btn-ghost` come from style.css and are not restated here. What is left is
6
- the four shapes only this page has — the well, its criterion rings, the
7
- thought glyph in five objects, and the panel beside the field.
8
-
9
- THE FIVE MARKS ARE THE ROOM'S, ONE SIZE UP. thinking.css draws the same five
10
- objects at a fixed 9px because a table row has no room to say size in the
11
- mark and says it in a bar instead. Here the mark IS the size channel, so the
12
- geometry is parameterised by `--d` and everything else about it — filled /
13
- haloed / orbited / hollow / hairline-edged — is deliberately the same
14
- vocabulary. A reader who learned the five in the room must not have to learn
15
- them again here, which is the same reason `idea-objects.js` is shared.
16
-
17
- NO COLOUR THAT IS NOT A TOKEN. Every value below is one of the world's
18
- fifteen or a color-mix of them, so the page inherits dark and light from the
19
- pack rather than declaring either: there is not one mode-specific rule in
20
- this sheet, which is why it needs no dark twin. `--line` is a WIDTH; a
21
- hairline's colour is `--rule` / `--rule-soft`.
22
-
23
- NO MOTION. Nothing here transitions or animates, so the sheet sits entirely
24
- inside the Kill Switch rather than being excused by it. A field of a
25
- person's own thinking that twinkles is a toy; this one is a measurement. */
26
-
27
- /* ---- the layout ----------------------------------------------------------
28
- Field and panel side by side on a desk, stacked on a phone. The panel is
29
- fixed-width because its job is to hold a sentence at a readable measure
30
- whatever the field is doing beside it. */
31
- .sky-wrap {
32
- display: grid;
33
- grid-template-columns: minmax(0, 1fr) 320px;
34
- gap: var(--sp-lg);
35
- align-items: start;
36
- }
37
-
38
- /* ---- the wells -----------------------------------------------------------
39
- One goal, one cell. The mock draws a single continuous starfield of absolute
40
- coordinates; that reading survives 1280px and dies at 320, where four wells
41
- overlap into one smear. `auto-fit` keeps one well per row on a phone and
42
- four across on a desk with no second set of numbers. */
43
- .sky-field-grid {
44
- display: grid;
45
- grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
46
- gap: var(--sp-lg) var(--sp-md);
47
- margin: 0 0 var(--sp-md);
48
- }
49
- .sky-well { min-width: 0; }
50
- .sky-well__h {
51
- margin: 0;
52
- font-family: var(--label-face);
53
- font-size: var(--label-size);
54
- letter-spacing: var(--label-space);
55
- text-transform: var(--label-case);
56
- font-weight: 400;
57
- color: var(--ink-soft);
58
- }
59
- .sky-well__h a {
60
- display: inline-block;
61
- min-height: 24px;
62
- padding: 3px 0;
63
- line-height: 18px;
64
- color: var(--ink-soft);
65
- border-bottom: var(--line) solid var(--rule-soft);
66
- }
67
- .sky-well__h a:hover { color: var(--ink); border-bottom-color: var(--ink); text-decoration: none; }
68
- .sky-well__note {
69
- margin: 2px 0 var(--sp-hair);
70
- font-size: 12px;
71
- color: var(--ink-faint);
72
- }
73
-
74
- /* The well itself. `--surface-2` and not a literal black: the direction's void
75
- is the DARK mode of this pack, and a page that hard-coded it would be a hole
76
- in a light instance. The recessed surface is the same shape in both. */
77
- .sky-field {
78
- position: relative;
79
- aspect-ratio: 1 / 1;
80
- border-radius: 50%;
81
- background: var(--surface-2);
82
- /* The well's own edge. Faint, because the rings inside it are the thing
83
- carrying meaning and an outer border competing with them would read as a
84
- sixth criterion. */
85
- box-shadow: inset 0 0 0 var(--line) var(--rule-soft);
86
- }
87
-
88
- /* ---- the criterion rings -------------------------------------------------
89
- "A goal's criteria are the rings inside its well, closing as each is
90
- satisfied." Closed is a solid hairline in `--ink-faint`; open is the softer
91
- rule. `--ink-faint` and not `--rule` for the closed one is the finding the
92
- room's remnant glyph recorded: at the hairline's weight `--rule` measures
93
- under the 3:1 non-text floor on the dark ground, and a satisfied criterion is
94
- exactly the thing that must read. */
95
- .sky-ring {
96
- position: absolute;
97
- left: 50%;
98
- top: 50%;
99
- width: var(--r);
100
- height: var(--r);
101
- transform: translate(-50%, -50%);
102
- border-radius: 50%;
103
- border: var(--line) dashed var(--rule-soft);
104
- }
105
- .sky-ring[data-closed="1"] { border-style: solid; border-color: var(--ink-faint); }
106
-
107
- /* ---- a thought -----------------------------------------------------------
108
- The BUTTON is the 24px floor and the GLYPH is the drawing. They are two
109
- elements on purpose: a 9px cold body is the right mark and a 9px hit target
110
- is not a control, so the target is sized for a hand and the mark for the
111
- truth. Centred on its point, so `left`/`top` mean the thought's position and
112
- not the corner of its box. */
113
- .sky-body {
114
- position: absolute;
115
- transform: translate(-50%, -50%);
116
- display: flex;
117
- align-items: center;
118
- justify-content: center;
119
- width: 34px;
120
- height: 34px;
121
- padding: 0;
122
- margin: 0;
123
- border: 0;
124
- border-radius: 50%;
125
- background: transparent;
126
- cursor: pointer;
127
- }
128
- /* The hover halo is mixed from the ACCENT and not from `--ink`: `--ink` is
129
- #17181a in light and #ffffff in dark, so a flat wash of it over whatever is
130
- behind turns white and LIFTS the field in dark instead of marking the target
131
- (tests/hall_audit.mjs, "no rule washes a whole surface with a ground token" —
132
- the same inversion .palette__scrim and .ov-modal-bg both had). The accent
133
- does not flip, and a pressable thing warming under the pointer is the world's
134
- own rule for what the accent is for. */
135
- .sky-body:hover { background: color-mix(in srgb, var(--accent) 14%, transparent); }
136
- .sky-body:focus-visible { outline: 2px solid var(--focus); outline-offset: 0; }
137
- .sky-body[data-picked="1"] { box-shadow: inset 0 0 0 var(--line) var(--accent); }
138
-
139
- /* ---- the five objects ----------------------------------------------------
140
- THE MARK RESTATES THE WORD IN SHAPE. Every object differs from the others by
141
- geometry — filled, haloed, orbited, hollow, hairline-edged — and not only by
142
- tone, because a mark distinguished by colour alone tells a colour-blind
143
- reader nothing. Same rule, same five shapes, as thinking.css.
144
-
145
- `--lit` is the server's brightness. It rides the star's opacity and the
146
- star's ALONE: the other four have no live share to show and dimming them
147
- would invent one. A star whose brightness could not be READ arrives with
148
- `--lit: 1` and says so in the panel's words. The 0.35 floor is a contrast
149
- floor — below it the mark stops clearing 3:1 and a dim star becomes an absent
150
- one, which is the wrong reading entirely. A star is never nothing. */
151
- .sky-glyph {
152
- display: block;
153
- flex: none;
154
- box-sizing: border-box;
155
- width: var(--d, 10px);
156
- height: var(--d, 10px);
157
- border-radius: 50%;
158
- background: var(--ink-faint);
159
- }
160
-
161
- /* star — filled, accent, dimmed by its live share, sized by what it caused. */
162
- .sky-glyph[data-object="star"] {
163
- background: var(--accent);
164
- opacity: max(0.35, var(--lit, 1));
165
- }
166
- /* protostar — the halo: matter gathering, not yet burning. */
167
- .sky-glyph[data-object="protostar"] {
168
- background: var(--ink-soft);
169
- box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 18%, transparent);
170
- }
171
- /* satellite — a small body with an orbit detached from it, so it reads as
172
- something going round rather than as a thick edge. It is drawn beside the
173
- thought it was folded into (sky.js `group`), because "it orbits that one now"
174
- is a claim about position that the mark alone cannot make. */
175
- .sky-glyph[data-object="satellite"] {
176
- background: var(--ink-soft);
177
- /* `--ink-faint` and not the room's `--rule`: there the orbit sits against a
178
- card at 6px across, here it is the thing that tells a reader this body
179
- belongs to the one beside it, and at the hairline's weight on `--surface-2`
180
- `--rule` measured under the 3:1 non-text floor. */
181
- outline: var(--line) solid var(--ink-faint);
182
- outline-offset: 4px;
183
- }
184
- /* remnant — the hollow ring, and the whole reason size and brightness are two
185
- channels: a thought that caused a great deal of work the project then tore
186
- out keeps every pixel of its diameter and loses all of its fill. LARGE AND
187
- DEAD, never small. `--ink-faint` and not `--rule` for the 3:1 floor. */
188
- .sky-glyph[data-object="remnant"] {
189
- background: transparent;
190
- box-shadow: inset 0 0 0 1.5px var(--ink-faint);
191
- }
192
- /* cold body — a dark point with a hairline edge. It never ignited, so it has no
193
- size at all, and the mark says that by being the one object with nothing
194
- inside it and nothing around it. */
195
- .sky-glyph[data-object="cold-body"] {
196
- background: var(--surface);
197
- box-shadow: inset 0 0 0 var(--line) var(--ink-faint);
198
- }
199
- /* A size that could not be READ is drawn at the floor diameter and marked, so
200
- the floor is never read as a measured zero. Dotted, the sheet's existing
201
- "this is a footnote, not a warning" weight. */
202
- .sky-glyph[data-size="unknown"] { outline: var(--line) dotted var(--rule); outline-offset: 3px; }
203
-
204
- /* ---- the panel ----------------------------------------------------------- */
205
- .sky-panel {
206
- border-left: var(--line) solid var(--rule-soft);
207
- padding-left: var(--sp-md);
208
- min-width: 0;
209
- }
210
- .sky-panel__head { display: flex; align-items: center; gap: 12px; min-height: 30px; }
211
- .sky-panel__object {
212
- font-family: var(--label-face);
213
- font-size: var(--label-size);
214
- letter-spacing: var(--label-space);
215
- text-transform: var(--label-case);
216
- color: var(--ink-soft);
217
- }
218
- .sky-panel__title { margin: var(--sp-hair) 0 var(--sp-sm); font-size: 18px; line-height: 1.3; }
219
- .sky-panel__title a {
220
- display: inline-block;
221
- min-height: 24px;
222
- padding: 2px 0;
223
- color: var(--ink);
224
- border-bottom: var(--line) solid var(--rule);
225
- }
226
- .sky-panel__title a:hover { border-bottom-color: var(--ink); text-decoration: none; }
227
- .sky-panel__pos { margin: var(--sp-sm) 0 0; font-size: 13px; color: var(--accent-ink); }
228
- .sky-panel__blurb { margin: var(--sp-hair) 0 var(--sp-md); font-size: 13px; color: var(--ink-faint); }
229
- .sky-panel__out { margin: 0; }
230
- .sky-pick { margin: 0; font-size: 13.5px; color: var(--ink-faint); max-width: var(--measure); }
231
- /* The shell's prose-link recipe, restated for this page's two prose voices
232
- rather than added to `style.css`'s selector list: that sheet is linked with a
233
- hand-written `?v=` on all 29 hall pages, so a one-selector addition there is a
234
- 29-file cache bump for a rule only this page needs. Measured at 81x18 by the
235
- kit before this existed — a link in a line with no vertical rhythm of its own
236
- renders at its line box and fails the 24px floor on the height; the padding
237
- buys it back and the equal negative margin hands the space to the layout so
238
- the sentence does not re-space. */
239
- .sky-pick a,
240
- .sky-empty a {
241
- display: inline-block;
242
- min-width: 24px;
243
- min-height: 24px;
244
- padding: 3px 4px;
245
- margin: 0 -4px;
246
- line-height: 18px;
247
- text-align: center;
248
- }
249
-
250
- .sky-facts { display: grid; }
251
- .sky-fact {
252
- display: flex;
253
- align-items: baseline;
254
- gap: 14px;
255
- padding: 10px 0;
256
- border-top: var(--line) solid var(--rule-soft);
257
- }
258
- .sky-fact:last-child { border-bottom: var(--line) solid var(--rule-soft); }
259
- .sky-fact__k {
260
- flex: none;
261
- font-family: var(--label-face);
262
- font-size: var(--label-size);
263
- letter-spacing: var(--label-space);
264
- text-transform: var(--label-case);
265
- color: var(--ink-faint);
266
- }
267
- .sky-fact__v {
268
- margin-left: auto;
269
- text-align: right;
270
- font-size: 13.5px;
271
- color: var(--ink-soft);
272
- min-width: 0;
273
- display: flex;
274
- align-items: center;
275
- justify-content: flex-end;
276
- flex-wrap: wrap;
277
- gap: 8px;
278
- }
279
- .sky-fact__v a {
280
- display: inline-block;
281
- min-height: 24px;
282
- padding: 2px 0;
283
- color: var(--ink-soft);
284
- border-bottom: var(--line) solid var(--rule-soft);
285
- }
286
- .sky-fact__v a:hover { color: var(--ink); border-bottom-color: var(--ink); text-decoration: none; }
287
- .sky-quiet { color: var(--ink-faint); }
288
- .sky-brightword { font-size: 13px; color: var(--ink-soft); }
289
- /* Bar and word together, never the bar alone — and no bar at all when the share
290
- could not be read (sky.js brightHtml), because a bar at zero and a bar nobody
291
- could measure look identical and one of them is a claim. */
292
- .sky-bar {
293
- display: block;
294
- flex: none;
295
- width: 56px;
296
- height: 3px;
297
- border-radius: 999px;
298
- background:
299
- linear-gradient(to right, var(--fill, var(--accent)) 0 var(--w, 0%), transparent var(--w, 0%) 100%),
300
- var(--rule-soft);
301
- }
302
- .sky-bar[data-lit="out"] { --fill: var(--ink-faint); }
303
- .sky-hist {
304
- font-size: 11.5px;
305
- color: var(--ink-faint);
306
- text-decoration: none;
307
- border-bottom: var(--line) dotted var(--rule);
308
- cursor: help;
309
- }
310
-
311
- /* ---- the legend ---------------------------------------------------------- */
312
- .sky-legend {
313
- display: grid;
314
- grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
315
- gap: var(--sp-sm) var(--sp-md);
316
- margin: var(--sp-md) 0 0;
317
- padding-top: var(--sp-md);
318
- border-top: var(--line) solid var(--rule-soft);
319
- }
320
- .sky-leg { display: flex; align-items: flex-start; gap: 11px; }
321
- .sky-leg .sky-glyph { margin-top: 4px; }
322
- .sky-leg__body { display: grid; gap: 4px; min-width: 0; }
323
- .sky-leg__name {
324
- font-family: var(--label-face);
325
- font-size: var(--label-size);
326
- letter-spacing: var(--label-space);
327
- text-transform: var(--label-case);
328
- color: var(--ink-soft);
329
- }
330
- .sky-leg__what { font-size: 12.5px; line-height: 1.4; color: var(--ink-faint); }
331
-
332
- /* ---- notes + the empty state --------------------------------------------- */
333
- .sky-empty {
334
- margin: 0;
335
- padding: var(--sp-md) 0;
336
- font-size: 14px;
337
- color: var(--ink-soft);
338
- max-width: var(--measure);
339
- }
340
- .sky-foot {
341
- margin: var(--sp-md) 0 0;
342
- font-size: 13px;
343
- color: var(--ink-faint);
344
- max-width: var(--measure);
345
- }
346
-
347
- /* ---- the phone -----------------------------------------------------------
348
- The panel goes under the field and loses its rule-and-indent, because a
349
- 320px column has no width to give to a 22px gutter. Nothing below widens the
350
- document: the page never scrolls sideways. */
351
- @media (max-width: 860px) {
352
- .sky-wrap { grid-template-columns: minmax(0, 1fr); }
353
- .sky-panel {
354
- border-left: 0;
355
- padding-left: 0;
356
- border-top: var(--line) solid var(--rule-soft);
357
- padding-top: var(--sp-md);
358
- }
359
- }
360
- @media (max-width: 640px) {
361
- /* One well to a row, and capped so a single well does not become a
362
- full-width circle taller than the viewport. */
363
- .sky-field-grid { grid-template-columns: minmax(0, 1fr); }
364
- .sky-field { max-width: 300px; }
365
- }
@@ -1,79 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>Your sky — {{worldName}}</title>
7
- <meta name="description" content="The ambient view of your own thinking: goals as gravity wells, criteria as the rings inside them, and every thought you filed drawn at the size of the work it caused.">
8
- <link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
9
- <link rel="preconnect" href="https://fonts.googleapis.com">
10
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
- <link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
12
- <link rel="stylesheet" href="/builders/sky.css">
13
- </head>
14
- <!-- THE SKY (task 1004105 / BV2.R26). It is the ambient half of the ideator
15
- surface and, by spec decision 11, NEVER the only path to anything it shows:
16
- it takes no nav slot of its own and is reached from the room, which is the
17
- page it is ambient ABOUT. That is deliberate rather than a saving — a third
18
- ideator entry in the nav beside Ideas and Your thinking would make the
19
- picture look load-bearing, and the direction's first refusal is that it is
20
- not. The room links here; every thought here links back to its record. -->
21
- <body data-page="sky" data-page-title="Your sky">
22
-
23
- <div class="app">
24
- <aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
25
- <div class="app-scrim" id="app-scrim" hidden></div>
26
- <div class="app-body">
27
- <header class="app-topbar" id="app-topbar"></header>
28
- <div class="app-content">
29
- <!-- The LABEL voice: a working surface takes the board's head, not the
30
- reading room's (modules/hall-ui/CLAUDE.md, the two page-head voices).
31
- The sky is a working surface even though it is the ambient one. -->
32
- <div class="page-head page-head--board">
33
- <h1 class="page-head__h">Your sky</h1>
34
- <p class="page-head__sub" id="sky-sub">Drawing what you filed…</p>
35
- </div>
36
-
37
- <main id="hall-main">
38
- <section class="scroll" id="sky-scroll" aria-labelledby="sky-h">
39
- <h2 id="sky-h" class="scroll__h">Your thinking, as a map</h2>
40
- <p class="scroll__lede">
41
- Open goals are gravity wells and their criteria are the rings inside them,
42
- closing as each is met. A thought sits in the well that caught it, drawn at
43
- the size of the work it caused, lit by how much of that work is still alive.
44
- Nothing here is only here — the same trail is a list in
45
- <a href="/builders/thinking">your thinking</a>.
46
- </p>
47
-
48
- <div class="sky-wrap">
49
- <!-- The field and the panel are both written by sky.js from ONE
50
- read, so nothing on this page can be a second copy of a number
51
- the server already computed. -->
52
- <div id="sky-map" class="sky-field-grid"></div>
53
- <aside id="sky-panel" class="sky-panel" role="status" aria-live="polite" aria-label="The picked thought"></aside>
54
- </div>
55
-
56
- <div id="sky-legend" class="sky-legend"></div>
57
- <p class="sky-foot" id="sky-foot" hidden></p>
58
- </section>
59
- </main>
60
-
61
- <footer class="app-foot" id="app-foot"></footer>
62
- </div>
63
- </div>
64
- </div>
65
-
66
- <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
67
-
68
- <script src="/builders/shell.js?v=2026-09-22-movefeedback1"></script>
69
- <script src="/builders/palette.js" defer></script>
70
- <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
71
- <!-- The SAME shared vocabulary the room loads, at the same version: the server
72
- says which of the five objects a thought is, this file says what the object
73
- is called and what sentence goes with it, and two surfaces reading two
74
- copies is exactly how the sky ends up calling a thought "burned out" while
75
- the room beside it says "declined". -->
76
- <script src="/builders/idea-objects.js?v=2026-09-20-r03"></script>
77
- <script src="/builders/sky.js?v=2026-09-20-r26"></script>
78
- </body>
79
- </html>