@bongos/core 1.19.413 → 1.19.415

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 (63) hide show
  1. package/.bongos-core.json +190 -40
  2. package/docs/adr/0204-the-apex-is-dark-only.md +36 -0
  3. package/docs/adr/README.md +1 -0
  4. package/docs/copy-inventory.md +310 -312
  5. package/docs/copy-registry.json +394 -412
  6. package/docs/design/landing-page-direction-v2.md +2 -0
  7. package/docs/design/mocks/hub/assets/r3-space-deepfield-half.jpg +0 -0
  8. package/docs/design/mocks/hub/assets/r3-space-deepfield-half.jpg.json +13 -0
  9. package/docs/design/mocks/hub/assets/r3-space-dust-half.jpg +0 -0
  10. package/docs/design/mocks/hub/assets/r3-space-dust-half.jpg.json +13 -0
  11. package/docs/design/mocks/hub/assets/r3-space-nebula-half.jpg +0 -0
  12. package/docs/design/mocks/hub/assets/r3-space-nebula-half.jpg.json +13 -0
  13. package/docs/design/mocks/hub/assets/r3-space-starfield-half.jpg +0 -0
  14. package/docs/design/mocks/hub/assets/r3-space-starfield-half.jpg.json +13 -0
  15. package/docs/design/mocks/hub/planets.html +127 -3
  16. package/docs/design/projects-hub-direction-v2.md +14 -0
  17. package/docs/module-api-changelog.md +4 -0
  18. package/docs/recipes/ui-look-before-you-ship.md +4 -1
  19. package/modules/platform-identity/migrations/platform_identity_013_project_template_materials.sql +31 -0
  20. package/modules/platform-identity/platform-identity.js +18 -5
  21. package/modules/platform-identity/routes/projects.js +3 -2
  22. package/modules/platform-identity/tests/platform-identity.mjs +23 -12
  23. package/modules/ui-design/kit/fixtures/featured.json +1 -1
  24. package/modules/ui-design/kit/lib.js +3 -1
  25. package/modules/ui-design/kit/render.js +5 -4
  26. package/package-lock.json +2 -2
  27. package/package.json +1 -1
  28. package/public-landing/assets/cosmos.css +50 -18
  29. package/public-landing/assets/world/r3-frost-bead-cut.webp +0 -0
  30. package/public-landing/assets/world/r3-frost-bead-cut.webp.json +15 -0
  31. package/public-landing/assets/world/r3-frost-blob-cut.webp +0 -0
  32. package/public-landing/assets/world/r3-frost-blob-cut.webp.json +15 -0
  33. package/public-landing/assets/world/r3-frost-pair-cut.webp +0 -0
  34. package/public-landing/assets/world/r3-frost-pair-cut.webp.json +15 -0
  35. package/public-landing/assets/world/r3-gold-bead-cut.webp +0 -0
  36. package/public-landing/assets/world/r3-gold-bead-cut.webp.json +15 -0
  37. package/public-landing/assets/world/r3-gold-blob-cut.webp +0 -0
  38. package/public-landing/assets/world/r3-gold-blob-cut.webp.json +15 -0
  39. package/public-landing/assets/world/r3-gold-pair-cut.webp +0 -0
  40. package/public-landing/assets/world/r3-gold-pair-cut.webp.json +15 -0
  41. package/public-landing/assets/world/r3-obsidian-bead-cut.webp +0 -0
  42. package/public-landing/assets/world/r3-obsidian-bead-cut.webp.json +15 -0
  43. package/public-landing/assets/world/r3-obsidian-blob-cut.webp +0 -0
  44. package/public-landing/assets/world/r3-obsidian-blob-cut.webp.json +15 -0
  45. package/public-landing/assets/world/r3-obsidian-pair-cut.webp +0 -0
  46. package/public-landing/assets/world/r3-obsidian-pair-cut.webp.json +15 -0
  47. package/public-landing/assets/world/r3-space-nebula.jpg +0 -0
  48. package/public-landing/assets/world/r3-space-nebula.jpg.json +13 -0
  49. package/public-landing/contact.html +2 -6
  50. package/public-landing/index.html +5 -44
  51. package/public-landing/index.states.json +3 -2
  52. package/public-landing/privacy.html +2 -6
  53. package/public-landing/projects.html +27 -55
  54. package/public-landing/projects.probes.json +0 -3
  55. package/public-landing/projects.states.json +4 -3
  56. package/public-landing/terms.html +2 -6
  57. package/scripts/gds/worktree.js +11 -0
  58. package/src/module-api.js +1 -1
  59. package/tests/landing_legal_pages.mjs +10 -0
  60. package/tests/landing_page.mjs +16 -19
  61. package/tests/projects_hub.mjs +69 -19
  62. package/tests/projects_hub_pre_uat.mjs +4 -3
  63. package/tests/ui_design_kit.mjs +10 -0
@@ -60,6 +60,8 @@ What the pick settles: the settled constraint (dark-first with a real, tested li
60
60
 
61
61
  **2026-08-28, the second note (task 1003334).** Seeing the before/after of task 1003333, the owner found the chrome bongo pair sitting on the top-right sweeping form did not fit ("the random bongos on the blobs background"). Offered three treatments (remove it from the hero; keep it off the blob on plain black or paper; phones only), the owner picked **remove it from the hero**. The pair is no longer a hero object: `.mark`, its arrival and its tokens leave; it stays as the closing figure, the second project orb and the bar's mark. The sweeping form stays as a bare backdrop. Under 900 the masthead is the big blob alone, with the words keeping the 56vw the pair used to hold above them.
62
62
 
63
+ **2026-08-28, the third note — dark only (task 1003336, [ADR 0204](../adr/<redacted>.md)).** Reviewing the planets (task 1003319) with both grounds in front of them, the owner took option 4 of §5 after all, widened to the whole apex: "only black or only white is a bit bland, frankly I am leaning towards only dark mode for this page and keep the dark/light mode to be solely for the builder hall … we could invest into a cool space themed backdrop for the blobs." Built: `data-mode="dark"` fixed on `<html>` of every apex page, the pre-paint script and the persisted `cb-color-mode` gone, the moon/sun glyph and its wiring out of both bars, the kit rendering dark only. What the 2026-08-27 pick settled about light — the cutouts on paper under a contact shadow, the toggle staying on the landing — no longer describes the apex; the cutouts remain the closing figure and the planets' masks. The light tier of `world.css` stays until its own removal. The space backdrop is the next task (1003337).
64
+
63
65
  ## 7. Shot list: real imagery still owed
64
66
 
65
67
  No version left a placeholder slot: every image on the three pages is a real plate (the round-2 chrome set, the single blob, the two bongo pairs, the two new blob cutouts) or the real Verro Labs medallion, and the proof is the ledger rendered as page content, so no hall or hub capture is used. What the build will still owe:
@@ -0,0 +1,13 @@
1
+ {
2
+ "kind": "backdrop",
3
+ "model": "nano_banana_2 (requested as nano_banana_pro; the catalog served nano_banana_2)",
4
+ "aspect_ratio": "16:9",
5
+ "resolution": "2k",
6
+ "generated": "2026-08-29T15:37Z",
7
+ "round": 3,
8
+ "task": 1003337,
9
+ "prompt": "Deep-space photograph on a pure black ground: almost entirely black with a barely-there violet-to-blue gradient in the upper left corner and a handful of pin-sharp stars, maximal emptiness, no nebula, no planets, no ground plane, no horizon, no lens flare, no text, no watermark. Pure black everywhere but the faint corner.",
10
+ "source": ".impeccable/mocks/world-assets/r3-space-deepfield.jpg",
11
+ "encoded": "ffmpeg JPEG -q:v 5 at 1376px (the sheet's candidate; docs/ is served raw by the kit's stub)",
12
+ "use": "docs/design/mocks/hub/planets.html ?state=backdrops"
13
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "kind": "backdrop",
3
+ "model": "nano_banana_2 (requested as nano_banana_pro; the catalog served nano_banana_2)",
4
+ "aspect_ratio": "16:9",
5
+ "resolution": "2k",
6
+ "generated": "2026-08-29T15:37Z",
7
+ "round": 3,
8
+ "task": 1003337,
9
+ "prompt": "Deep-space photograph on a pure black ground: thin drifting cosmic dust filaments in bronze and cool blue crossing the frame diagonally, sparse pin-sharp stars, one soft distant glow low right, generous empty black, no planets, no ground plane, no horizon, no lens flare, no text, no watermark. Pure black dominates.",
10
+ "source": ".impeccable/mocks/world-assets/r3-space-dust.jpg",
11
+ "encoded": "ffmpeg JPEG -q:v 5 at 1376px (the sheet's candidate; docs/ is served raw by the kit's stub)",
12
+ "use": "docs/design/mocks/hub/planets.html ?state=backdrops"
13
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "kind": "backdrop",
3
+ "model": "nano_banana_2 (requested as nano_banana_pro; the catalog served nano_banana_2)",
4
+ "aspect_ratio": "16:9",
5
+ "resolution": "2k",
6
+ "generated": "2026-08-29T15:37Z",
7
+ "round": 3,
8
+ "task": 1003337,
9
+ "prompt": "Deep-space photograph on a pure black ground: a faint wide nebula wash in muted violet and deep blue with a soft bronze warmth along one edge, very sparse pin-sharp stars of varied brightness and size, gentle depth, most of the frame near-black, nothing else, no planets, no ground plane, no horizon, no lens flare, no text, no watermark. Pure black dominates.",
10
+ "source": ".impeccable/mocks/world-assets/r3-space-nebula.jpg",
11
+ "encoded": "ffmpeg JPEG -q:v 5 at 1376px (the sheet's candidate; docs/ is served raw by the kit's stub)",
12
+ "use": "docs/design/mocks/hub/planets.html ?state=backdrops"
13
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "kind": "backdrop",
3
+ "model": "nano_banana_2 (requested as nano_banana_pro; the catalog served nano_banana_2)",
4
+ "aspect_ratio": "16:9",
5
+ "resolution": "2k",
6
+ "generated": "2026-08-29T15:37Z",
7
+ "round": 3,
8
+ "task": 1003337,
9
+ "prompt": "Deep-space photograph on a pure black ground: a fine field of pin-sharp stars in three depths (a few bright, many mid, a faint dust of tiny ones), a barely visible cool haze band drifting across the lower third, no nebula colour, no planets, no ground plane, no horizon, no lens flare, no text, no watermark. Pure black dominates.",
10
+ "source": ".impeccable/mocks/world-assets/r3-space-starfield.jpg",
11
+ "encoded": "ffmpeg JPEG -q:v 5 at 1376px (the sheet's candidate; docs/ is served raw by the kit's stub)",
12
+ "use": "docs/design/mocks/hub/planets.html ?state=backdrops"
13
+ }
@@ -126,7 +126,32 @@ body{
126
126
  section[data-state]{display:none;}
127
127
  body[data-state="sheet"] section[data-state="sheet"],
128
128
  body[data-state="void"] section[data-state="void"],
129
- body[data-state="grid"] section[data-state="grid"]{display:block;}
129
+ body[data-state="grid"] section[data-state="grid"],
130
+ body[data-state="backdrops"] section[data-state="backdrops"],
131
+ body[data-state="materials"] section[data-state="materials"]{display:block;}
132
+ /* round 3 (task 1003337): the materials' crops, byte-equal to cosmos.css (patch-checked), on
133
+ the generic attribute pair so the sheet's .planet and the field's .worldOrb share them;
134
+ the candidate backdrops as the field's ground (half-size copies, served raw from docs/) */
135
+ [data-style="plain"]{--tpl-ink:transparent;--tpl-blend:normal;}
136
+ [data-material="frost"][data-shape="blob"]{--tpl-src:var(--plate-frost-blob-cut);--tpl-ar:1.25629;--tpl-zoom:1.46135;--tpl-px:-.22359;--tpl-py:-.34269;}
137
+ [data-material="frost"][data-shape="pair"]{--tpl-src:var(--plate-frost-pair-cut);--tpl-ar:1.91273;--tpl-zoom:1.09673;--tpl-px:-.04837;--tpl-py:-.28669;}
138
+ [data-material="frost"][data-shape="bead"]{--tpl-src:var(--plate-frost-bead-cut);--tpl-ar:.98831;--tpl-zoom:1.49678;--tpl-px:-.22976;--tpl-py:-.23769;}
139
+ [data-material="gold"][data-shape="blob"]{--tpl-src:var(--plate-gold-blob-cut);--tpl-ar:.91864;--tpl-zoom:1.31406;--tpl-px:-.1477;--tpl-py:-.11629;}
140
+ [data-material="gold"][data-shape="pair"]{--tpl-src:var(--plate-gold-pair-cut);--tpl-ar:1.86206;--tpl-zoom:1.08625;--tpl-px:-.04367;--tpl-py:-.28123;}
141
+ [data-material="gold"][data-shape="bead"]{--tpl-src:var(--plate-gold-bead-cut);--tpl-ar:1.00479;--tpl-zoom:1.08272;--tpl-px:-.04244;--tpl-py:-.04244;}
142
+ [data-material="obsidian"][data-shape="blob"]{--tpl-src:var(--plate-obsidian-blob-cut);--tpl-ar:.794;--tpl-zoom:1.55448;--tpl-px:-.2697;--tpl-py:-.15965;}
143
+ [data-material="obsidian"][data-shape="pair"]{--tpl-src:var(--plate-obsidian-pair-cut);--tpl-ar:2.23705;--tpl-zoom:1.0248;--tpl-px:-.01015;--tpl-py:-.30191;}
144
+ [data-material="obsidian"][data-shape="bead"]{--tpl-src:var(--plate-obsidian-bead-cut);--tpl-ar:1.0027;--tpl-zoom:1.34698;--tpl-px:-.17053;--tpl-py:-.17255;}
145
+ .mock .field[data-backdrop] .ground{background:var(--sky-bg) var(--cand) center/cover no-repeat;}
146
+ .mock .field[data-backdrop="nebula"]{--cand:url(assets/r3-space-nebula-half.jpg);}
147
+ .mock .field[data-backdrop="starfield"]{--cand:url(assets/r3-space-starfield-half.jpg);}
148
+ .mock .field[data-backdrop="dust"]{--cand:url(assets/r3-space-dust-half.jpg);}
149
+ .mock .field[data-backdrop="deepfield"]{--cand:url(assets/r3-space-deepfield-half.jpg);}
150
+ .mock section[data-state="backdrops"] .field{height:min(62vh,640px);}
151
+ .mock section[data-state="backdrops"] .rowLabel{padding:22px var(--gutter) 10px;}
152
+ .matrix.walk{grid-template-columns:repeat(12,1fr);}
153
+ .sheet .cell .nm{white-space:normal;text-align:center;line-height:1.35;max-width:100%;}
154
+ @media (max-width:900px){.matrix.walk{grid-template-columns:repeat(3,1fr);}}
130
155
  .vh{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;}
131
156
  .skip{position:absolute;left:12px;top:-60px;background:var(--bg-card);color:var(--ink);padding:11px 18px;border-radius:999px;}
132
157
  .skip:focus{top:12px;}
@@ -139,6 +164,8 @@ body[data-state="grid"] section[data-state="grid"]{display:block;}
139
164
  ?state=sheet the nine templates at the five accents, and the blend candidates
140
165
  ?state=void field treatment A: the void — nothing drawn, the objects carry the depth
141
166
  ?state=grid field treatment B: the sheet — one hairline grid inside the camera
167
+ ?state=backdrops round 3 (task 1003337): the four rendered space candidates as the field's ground
168
+ ?state=materials round 3: the twelve-step walk, then frost / gold / obsidian at the five accents
142
169
  ?mode=dark|light
143
170
  -->
144
171
  <a class="skip" href="#main">Skip to content</a>
@@ -189,14 +216,73 @@ body[data-state="grid"] section[data-state="grid"]{display:block;}
189
216
  <div class="hintbar"><span><kbd>drag</kbd> pan</span><span><kbd>scroll</kbd> zoom</span><span><kbd>click a project</kbd> open it</span></div>
190
217
  </div></div>
191
218
  </section>
219
+
220
+ <section data-state="backdrops">
221
+ <div class="rowLabel">the nebula wash: a faint violet-and-bronze wash, sparse stars (the builder's recommendation; shipped as --field-plate)</div>
222
+ <div class="fieldWrap"><div class="field" data-backdrop="nebula">
223
+ <div class="ground"></div>
224
+ <div class="skyCam">
225
+ <a class="worldOrb" href="#" data-rel="owner" data-shape="pair" data-material="chrome" data-style="chrome" style="--acc:var(--accent);--d:140px;left:50%;top:44%"><span class="badge">yours</span><span class="disc"></span><span class="nm">Cloud Bongos</span><span class="sub">this platform</span></a>
226
+ <a class="worldOrb" href="#" data-rel="none" data-shape="blob" data-material="frost" data-style="wash" style="--acc:var(--terracotta);--d:92px;left:22%;top:30%"><span class="disc"></span><span class="nm">Demo</span></a>
227
+ <a class="worldOrb" href="#" data-rel="builder" data-shape="bead" data-material="gold" data-style="tint" style="--acc:var(--gold-soft);--d:75px;left:78%;top:66%"><span class="badge">you build here</span><span class="disc"></span><span class="nm">Off the Boats</span></a>
228
+ <a class="worldOrb" href="#" data-rel="builder" data-shape="pair" data-material="obsidian" data-style="tint" style="--acc:var(--sea);--d:70px;left:33%;top:74%"><span class="badge">you build here</span><span class="disc"></span><span class="nm">Emersonian Circles</span></a>
229
+ <a class="worldOrb" href="#" data-rel="none" data-shape="bead" data-material="chrome" data-style="chrome" style="--acc:var(--sun-lit);--d:80px;left:70%;top:26%"><span class="disc"></span><span class="nm">Mercury</span></a>
230
+ </div>
231
+ <div class="hintbar"><span><kbd>drag</kbd> pan</span><span><kbd>scroll</kbd> zoom</span><span><kbd>click a project</kbd> open it</span></div>
232
+ </div></div>
233
+ <div class="rowLabel">the starfield: three depths of pin-sharp stars, a cool haze band</div>
234
+ <div class="fieldWrap"><div class="field" data-backdrop="starfield">
235
+ <div class="ground"></div>
236
+ <div class="skyCam">
237
+ <a class="worldOrb" href="#" data-rel="owner" data-shape="pair" data-material="chrome" data-style="chrome" style="--acc:var(--accent);--d:140px;left:50%;top:44%"><span class="badge">yours</span><span class="disc"></span><span class="nm">Cloud Bongos</span><span class="sub">this platform</span></a>
238
+ <a class="worldOrb" href="#" data-rel="none" data-shape="blob" data-material="frost" data-style="wash" style="--acc:var(--terracotta);--d:92px;left:22%;top:30%"><span class="disc"></span><span class="nm">Demo</span></a>
239
+ <a class="worldOrb" href="#" data-rel="builder" data-shape="bead" data-material="gold" data-style="tint" style="--acc:var(--gold-soft);--d:75px;left:78%;top:66%"><span class="badge">you build here</span><span class="disc"></span><span class="nm">Off the Boats</span></a>
240
+ <a class="worldOrb" href="#" data-rel="builder" data-shape="pair" data-material="obsidian" data-style="tint" style="--acc:var(--sea);--d:70px;left:33%;top:74%"><span class="badge">you build here</span><span class="disc"></span><span class="nm">Emersonian Circles</span></a>
241
+ <a class="worldOrb" href="#" data-rel="none" data-shape="bead" data-material="chrome" data-style="chrome" style="--acc:var(--sun-lit);--d:80px;left:70%;top:26%"><span class="disc"></span><span class="nm">Mercury</span></a>
242
+ </div>
243
+ <div class="hintbar"><span><kbd>drag</kbd> pan</span><span><kbd>scroll</kbd> zoom</span><span><kbd>click a project</kbd> open it</span></div>
244
+ </div></div>
245
+ <div class="rowLabel">the dust: bronze-and-blue filaments, one distant glow</div>
246
+ <div class="fieldWrap"><div class="field" data-backdrop="dust">
247
+ <div class="ground"></div>
248
+ <div class="skyCam">
249
+ <a class="worldOrb" href="#" data-rel="owner" data-shape="pair" data-material="chrome" data-style="chrome" style="--acc:var(--accent);--d:140px;left:50%;top:44%"><span class="badge">yours</span><span class="disc"></span><span class="nm">Cloud Bongos</span><span class="sub">this platform</span></a>
250
+ <a class="worldOrb" href="#" data-rel="none" data-shape="blob" data-material="frost" data-style="wash" style="--acc:var(--terracotta);--d:92px;left:22%;top:30%"><span class="disc"></span><span class="nm">Demo</span></a>
251
+ <a class="worldOrb" href="#" data-rel="builder" data-shape="bead" data-material="gold" data-style="tint" style="--acc:var(--gold-soft);--d:75px;left:78%;top:66%"><span class="badge">you build here</span><span class="disc"></span><span class="nm">Off the Boats</span></a>
252
+ <a class="worldOrb" href="#" data-rel="builder" data-shape="pair" data-material="obsidian" data-style="tint" style="--acc:var(--sea);--d:70px;left:33%;top:74%"><span class="badge">you build here</span><span class="disc"></span><span class="nm">Emersonian Circles</span></a>
253
+ <a class="worldOrb" href="#" data-rel="none" data-shape="bead" data-material="chrome" data-style="chrome" style="--acc:var(--sun-lit);--d:80px;left:70%;top:26%"><span class="disc"></span><span class="nm">Mercury</span></a>
254
+ </div>
255
+ <div class="hintbar"><span><kbd>drag</kbd> pan</span><span><kbd>scroll</kbd> zoom</span><span><kbd>click a project</kbd> open it</span></div>
256
+ </div></div>
257
+ <div class="rowLabel">the deep field: near-empty, a violet corner</div>
258
+ <div class="fieldWrap"><div class="field" data-backdrop="deepfield">
259
+ <div class="ground"></div>
260
+ <div class="skyCam">
261
+ <a class="worldOrb" href="#" data-rel="owner" data-shape="pair" data-material="chrome" data-style="chrome" style="--acc:var(--accent);--d:140px;left:50%;top:44%"><span class="badge">yours</span><span class="disc"></span><span class="nm">Cloud Bongos</span><span class="sub">this platform</span></a>
262
+ <a class="worldOrb" href="#" data-rel="none" data-shape="blob" data-material="frost" data-style="wash" style="--acc:var(--terracotta);--d:92px;left:22%;top:30%"><span class="disc"></span><span class="nm">Demo</span></a>
263
+ <a class="worldOrb" href="#" data-rel="builder" data-shape="bead" data-material="gold" data-style="tint" style="--acc:var(--gold-soft);--d:75px;left:78%;top:66%"><span class="badge">you build here</span><span class="disc"></span><span class="nm">Off the Boats</span></a>
264
+ <a class="worldOrb" href="#" data-rel="builder" data-shape="pair" data-material="obsidian" data-style="tint" style="--acc:var(--sea);--d:70px;left:33%;top:74%"><span class="badge">you build here</span><span class="disc"></span><span class="nm">Emersonian Circles</span></a>
265
+ <a class="worldOrb" href="#" data-rel="none" data-shape="bead" data-material="chrome" data-style="chrome" style="--acc:var(--sun-lit);--d:80px;left:70%;top:26%"><span class="disc"></span><span class="nm">Mercury</span></a>
266
+ </div>
267
+ <div class="hintbar"><span><kbd>drag</kbd> pan</span><span><kbd>scroll</kbd> zoom</span><span><kbd>click a project</kbd> open it</span></div>
268
+ </div></div>
269
+ </section>
270
+
271
+ <section data-state="materials" class="sheet">
272
+ <h2>Three materials, five accents</h2>
273
+ <p>Round 3: the same three whole objects rendered in frosted glass, brushed gold and polished obsidian, cut out and cropped from their own alpha, on the same masked colour layer. A template is now shape, material and style; the walk changes all three each step.</p>
274
+ <div class="rowLabel">the walk, twelve steps (the hub's default order)</div>
275
+ <div class="matrix walk" id="walk"></div>
276
+ <div id="matrix2"></div>
277
+ </section>
192
278
  </main>
193
- <footer class="foot">A composition sheet for task 1003319. Nothing here is served.</footer>
279
+ <footer class="foot">A composition sheet for tasks 1003319 and 1003337. Nothing here is served.</footer>
194
280
  <script>
195
281
  /* The mock's state contract: read ?state= once, apply one state (sheet | void | grid). The
196
282
  matrix is built with createElement + textContent. No motion in script. */
197
283
  (function () {
198
284
  var state = new URLSearchParams(location.search || location.hash.slice(1) || (window.__Q || '')).get('state') || 'sheet';
199
- if (state !== 'sheet' && state !== 'void' && state !== 'grid') state = 'sheet';
285
+ if (['sheet', 'void', 'grid', 'backdrops', 'materials'].indexOf(state) === -1) state = 'sheet';
200
286
  document.body.setAttribute('data-state', state);
201
287
  var ACCENTS = [['sea', 'var(--sea)'], ['terracotta', 'var(--terracotta)'], ['gold-soft', 'var(--gold-soft)'], ['sun-lit', 'var(--sun-lit)'], ['accent', 'var(--accent)']];
202
288
  var TEMPLATES = ['blob-tint', 'pair-wash', 'bead-chrome', 'blob-wash', 'pair-chrome', 'bead-tint', 'blob-chrome', 'pair-tint', 'bead-wash'];
@@ -226,6 +312,44 @@ body[data-state="grid"] section[data-state="grid"]{display:block;}
226
312
  });
227
313
  host.appendChild(row);
228
314
  });
315
+ /* round 3: the walk and the materials matrix, the same cell grammar */
316
+ var WALK = ['blob-tint', 'pair-gold-wash', 'bead-obsidian-plain', 'blob-frost-wash', 'pair-chrome', 'bead-gold-tint', 'blob-obsidian-wash', 'pair-frost-tint', 'bead-wash', 'blob-gold-plain', 'pair-obsidian-tint', 'bead-frost-wash'];
317
+ function cellFor(t, acc) {
318
+ var parts = t.split('-'), shape = parts[0], material = parts.length === 3 ? parts[1] : 'chrome', style = parts[parts.length - 1];
319
+ var cell = document.createElement('div');
320
+ cell.className = 'cell';
321
+ var p = document.createElement('span');
322
+ p.className = 'planet';
323
+ p.setAttribute('data-shape', shape);
324
+ p.setAttribute('data-material', material);
325
+ p.setAttribute('data-style', style);
326
+ p.style.setProperty('--acc', acc);
327
+ var n = document.createElement('span');
328
+ n.className = 'nm';
329
+ n.textContent = t;
330
+ cell.appendChild(p);
331
+ cell.appendChild(n);
332
+ return cell;
333
+ }
334
+ var walkHost = document.getElementById('walk');
335
+ if (walkHost) WALK.forEach(function (t, i) { walkHost.appendChild(cellFor(t, ACCENTS[i % ACCENTS.length][1])); });
336
+ var host2 = document.getElementById('matrix2');
337
+ if (host2) {
338
+ ['frost', 'gold', 'obsidian'].forEach(function (m) {
339
+ ACCENTS.forEach(function (a) {
340
+ var label = document.createElement('div');
341
+ label.className = 'rowLabel';
342
+ label.textContent = m + ', the ' + a[0] + ' accent';
343
+ host2.appendChild(label);
344
+ var row = document.createElement('div');
345
+ row.className = 'matrix';
346
+ ['blob', 'pair', 'bead'].forEach(function (s) {
347
+ ['tint', 'wash', 'plain'].forEach(function (y) { row.appendChild(cellFor(s + '-' + m + '-' + y, a[1])); });
348
+ });
349
+ host2.appendChild(row);
350
+ });
351
+ });
352
+ }
229
353
  })();
230
354
  </script>
231
355
  </body>
@@ -170,3 +170,17 @@ Three mastheads, rendered dark and light at 1440 / 390 / 320 (ALL CLEAN) and mea
170
170
  **What was built.** In `cosmos.css` the orb's disc is a masked colour layer over a cutout (`.disc::before`: the plate as both background and mask, `background-blend-mode` by style), sized `--d` by the object's own aspect; `[data-shape]` carries the three crops, `[data-style]` the three blends; the relationship ring is a circle behind the object (a mask clips a box-shadow away), the search spotlight an accent drop-shadow on the disc's filter after the contact shadow (dark's `--orb-shadow` is `opacity(1)`, a no-op, so the two compose). The starfield, its 3.2s twinkle, the 74px gravity grid and `--star-display` are gone; the labels, the badge, the hint and the hover card moved onto the world's type (hairline pill, label face, no accent on a fact, no shadow). In `projects.html` `ORB_TEMPLATES` walks beside `ORB_ACCENTS` in `assignAccents` (consecutive positions change shape and style; nine against five repeat every forty-five projects), `templateOf()` prefers a curated name, an owned-only entry walks by its slug, the orb carries `data-template` / `data-shape` / `data-style`, the physics measures the disc's own height, and the slot widths grew a quarter (a cutout's silhouette reads smaller than a filled disc of the same box). The platform's own orb is the pair in chrome. On the catalog side migration `<redacted>` adds the nullable `template` column with its nine-value CHECK, `upsertProject` carries it COALESCE-preserved, `POST /projects` validates it against `PROJECT_TEMPLATES`, and the public `/projects/featured` projection returns it. The kit's fixture gives Example a curated `bead-wash` and the `in-map` state reads it back off the orb.
171
171
 
172
172
  **Not done, on purpose:** no owner-facing picker — a template is set through the curation route today, the way `featured` is; whether a project's owner chooses their own planet from the manage view is the owner's call. The manage view's small `.orb` spheres keep their old look (the scope named the sky's `a.worldOrb` only).
173
+
174
+ ## 12. Dark only (task 1003336)
175
+
176
+ **Date:** 2026-08-28 (decided), 2026-08-29 (built) · the owner's call on the planets review (§11): the apex renders dark only and the light/dark switch is the builders' hall's alone — [ADR 0204](../adr/<redacted>.md). On `/projects`: `data-mode="dark"` is fixed on `<html>`, the pre-paint script is gone, the `#modeToggle` glyph and its `resolvedMode` / `paintModeToggle` wiring leave the bar (Projects, then the chip), the `.modeBtn` rules and the icon-swap twins leave `cosmos.css`, the mode-toggle probe leaves `projects.probes.json`, and `projects.states.json` declares `"modes": ["dark"]` so the kit renders the hub's twelve states at three widths in one mode (48 shots, ALL CLEAN; probes 31/31). The light tier of `cosmos.css` stays until its own removal. Why first: the space backdrop for the field (task 1003337) is a black-ground plate; one mode is what lets the field commit to it.
177
+
178
+ ## 13. The space and the materials (task 1003337)
179
+
180
+ **Date:** 2026-08-29 · the owner's ask on the planets review (§11): "use higgsfield for some further ideas colors, styles, and inspiration … we could invest into a cool space themed backdrop for the blobs", and the pick of the FULL material round the same day.
181
+
182
+ **The round.** Higgsfield (`nano_banana_pro`, served as `nano_banana_2`, 2k; 15 generations + 9 cutouts, ~37 credits): four text-free deep-space plates on pure black (`r3-space-nebula` — a faint violet-and-bronze wash with sparse stars; `-starfield` — three depths of pin-sharp stars and a cool haze band; `-dust` — bronze-and-blue filaments with one distant glow; `-deepfield` — near-empty with a violet corner), and the world's three whole objects in three new materials — **frosted glass**, **brushed gold**, **polished obsidian** — one whole object per 1:1 plate (the first frost blob ran off the frame and was retaken), each cut out with `remove_background` and its alpha verified with ffmpeg (min 0, max 255). The sheet: `docs/design/mocks/hub/planets.html` gains `?state=backdrops` (the four candidates as the field with the real orbs over them) and `?state=materials` (the twelve-step walk, then every material at the five accents).
183
+
184
+ **What was built.** The template grammar grows from shape × style to shape × **material** × style: `<shape>-<style>` stays the chrome material's name (nine rows keep their meaning), `<shape>-<material>-<style>` names a new material with style `tint` / `wash` / `plain` — thirty-six names in migration `<redacted>`, `PROJECT_TEMPLATES` (every valid name, the `POST /projects` enum) and `PROJECT_TEMPLATE_WALK` (the hub's twelve-step default, each step changing shape, material and style — `ORB_WALK` in `projects.html`, `ORB_TEMPLATES` the full set). `cosmos.css` carries a plate token per material × shape, a two-attribute crop rule per plate measured from the cutout's alpha box (pinned by `tests/projects_hub.mjs`, which recomputes every rule from the boxes), and `[data-style="plain"]`. The orb carries `data-material` beside `data-shape` and `data-style`. The field's ground is `--field-plate` at `cover` under the page ground, drifting 2–3% over 90 s by transform (dead under reduced motion) — one token, so the pick is one line.
185
+
186
+ **The pick, open.** The owner was not in the session, so the backdrop shipped on the builder's recommendation — the nebula wash (its violet and bronze are the chrome's own colours; the dust plate's glow competes with the orbs; the starfield is plain; the deepfield's corner is heavier than asked) — with the other three beside it in `.impeccable/mocks/world-assets/` and on the sheet. Swapping is one token (`--field-plate`) plus a copy into `assets/world/`. The materials are not a pick: they ship as templates, and a curated `template` chooses one per project.
@@ -1263,5 +1263,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
1263
1263
  landed since 1.19.411 with no explicit bump. run 33259357678. (task 1002620)
1264
1264
  1.19.413 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1265
1265
  landed since 1.19.412 with no explicit bump. run 33259477562. (task 1002620)
1266
+ 1.19.414 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1267
+ landed since 1.19.413 with no explicit bump. run 33260596712. (task 1002620)
1268
+ 1.19.415 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1269
+ landed since 1.19.414 with no explicit bump. run 33262322596. (task 1002620)
1266
1270
  ---------------------------------------------------------------------------
1267
1271
  ```
@@ -56,7 +56,8 @@ Then a state's own `expect` (a band open, a dialog present, a field's value, a c
56
56
  {
57
57
  "page": "/projects",
58
58
  "surface": "public-landing",
59
- "modeQuery": "mode",
59
+ "modes": ["dark"],
60
+ "modeQuery": false,
60
61
  "ignoreRequests": ["/api/(gds|bongos)/me$", "/my-projects"],
61
62
  "ignoreConsole": ["\\b401\\b"],
62
63
  "ignoreControls": [".skip", ".vh"],
@@ -69,6 +70,8 @@ Then a state's own `expect` (a band open, a dialog present, a field's value, a c
69
70
  }
70
71
  ```
71
72
 
73
+ `modes` names the modes the page renders (default both). A page that pins one mode — the apex pins `data-mode="dark"` on `<html>` since task 1003336 — declares it here so a plain `--page` render never walks a mode the page cannot show; `--modes` on the command line still overrides. `modeQuery: false` says no query walks a mode.
74
+
72
75
  Actions: `click`, `focus`, `press`, `fill`, `wait`, `clickAt x y`, `clickIfVisible`, `dispatchClick`, `check`, `back`, `forward`. Expectations: `expanded`, `dialog`, `visible[]`, `hidden[]`, `open`, `closed`, `activeId`, `activeText`, `expandedIds[]`, `param{}`, `attr[[sel,name,value]]`, `text[[sel,value]]`, `href[[sel,value]]`, `count[[sel,n]]`, `value[[sel,v]]`, `hasClass[[sel,class,bool]]`, `checked`, `noOverflow`, `top[[sel,px]]` (an element's viewport top — assert it closed, open a band, assert it again: the proof an overlay did not push the page). The kit validates the file before it opens a browser and names every problem.
73
76
 
74
77
  The worked examples: `public-landing/projects.states.json` (the hub's thirteen), `public-landing/index.states.json` (the landing's four), `modules/hall-ui/public/ranks.states.json` (a second surface under its own prefix — it renders **red**, deliberately: the hall predates the 24px floor and Ranks has no `<h1>`, and the states file declares the shell's un-fixtured feeds rather than hiding the findings; idea 1000863 carries them).
@@ -0,0 +1,31 @@
1
+ -- platform_identity_013_project_template_materials.sql — the planet templates grow from
2
+ -- shape × style to shape × MATERIAL × style (task 1003337).
3
+ --
4
+ -- Round 3 rendered the world's three whole objects (blob / pair / bead) in three new
5
+ -- materials — frosted glass, brushed gold, polished obsidian — beside the first round's
6
+ -- chrome. A template name is `<shape>-<style>` for chrome (the first round's names, kept
7
+ -- verbatim so every stored row stays valid) or `<shape>-<material>-<style>` for a new
8
+ -- material, where style is tint / wash / plain (plain = the material as rendered; the
9
+ -- chrome material's plain is spelled `chrome`). Thirty-six names; the hub's default walk
10
+ -- (PROJECT_TEMPLATE_WALK) uses twelve of them, each step changing shape, material and style.
11
+ --
12
+ -- Additive + platform_identity_-namespaced (ADR 0083 §5): the CHECK from migration 012 is
13
+ -- re-stated with the wider set; the column, its nullability and the curation route are
14
+ -- unchanged. Idempotent: safe to re-run.
15
+
16
+ BEGIN;
17
+
18
+ ALTER TABLE platform_identity_projects DROP CONSTRAINT IF EXISTS platform_identity_projects_template_check;
19
+ ALTER TABLE platform_identity_projects ADD CONSTRAINT platform_identity_projects_template_check
20
+ CHECK (template IS NULL OR template IN (
21
+ -- chrome, the first round (migration 012), names kept verbatim
22
+ 'blob-tint', 'blob-wash', 'blob-chrome',
23
+ 'pair-tint', 'pair-wash', 'pair-chrome',
24
+ 'bead-tint', 'bead-wash', 'bead-chrome',
25
+ -- the materials round
26
+ 'blob-frost-tint', 'blob-frost-wash', 'blob-frost-plain', 'blob-gold-tint', 'blob-gold-wash', 'blob-gold-plain', 'blob-obsidian-tint', 'blob-obsidian-wash', 'blob-obsidian-plain',
27
+ 'pair-frost-tint', 'pair-frost-wash', 'pair-frost-plain', 'pair-gold-tint', 'pair-gold-wash', 'pair-gold-plain', 'pair-obsidian-tint', 'pair-obsidian-wash', 'pair-obsidian-plain',
28
+ 'bead-frost-tint', 'bead-frost-wash', 'bead-frost-plain', 'bead-gold-tint', 'bead-gold-wash', 'bead-gold-plain', 'bead-obsidian-tint', 'bead-obsidian-wash', 'bead-obsidian-plain'
29
+ ));
30
+
31
+ COMMIT;
@@ -234,11 +234,21 @@ async function getClientByOrigin(origin, { pool = defaultPool } = {}) {
234
234
  // provisioning's copy on its own instances table).
235
235
  const PROJECT_TYPES = ['game', 'research', 'business', 'non-profit', 'not-sure'];
236
236
 
237
- // The sky's planet templates (task 1003319): shape (blob / pair / bead, the world's three
238
- // whole chrome objects) × style (how the accent lands: tint / wash / chrome). A CATALOG field
239
- // curated through POST /projects; null = the hub's deterministic walk (the accent's), whose
240
- // order this list IS. Pinned by the DB CHECK (migration 012); mirrored by ORB_TEMPLATES.
241
- const PROJECT_TEMPLATES = ['blob-tint', 'pair-wash', 'bead-chrome', 'blob-wash', 'pair-chrome', 'bead-tint', 'blob-chrome', 'pair-tint', 'bead-wash'];
237
+ // The sky's planet templates (task 1003319; the materials, task 1003337): SHAPE (blob / pair /
238
+ // bead, the world's three whole objects) × MATERIAL (chrome — the first round, named by omission
239
+ // — frost / gold / obsidian) × STYLE (how the accent lands: tint / wash / plain; the chrome
240
+ // material's plain is spelled `chrome`, the first round's name, so stored rows stay valid).
241
+ // PROJECT_TEMPLATES is EVERY valid name (the DB CHECK, migrations 012 + 013; the POST /projects
242
+ // enum); PROJECT_TEMPLATE_WALK is the hub's deterministic default order — consecutive positions
243
+ // change shape, material AND style. A CATALOG field curated through POST /projects; null = the
244
+ // walk. Mirrored by ORB_TEMPLATES / ORB_WALK in projects.html.
245
+ const PROJECT_SHAPES = ['blob', 'pair', 'bead'];
246
+ const PROJECT_MATERIALS = ['frost', 'gold', 'obsidian'];
247
+ const PROJECT_TEMPLATES = [
248
+ ...PROJECT_SHAPES.flatMap((s) => ['tint', 'wash', 'chrome'].map((y) => `${s}-${y}`)),
249
+ ...PROJECT_SHAPES.flatMap((s) => PROJECT_MATERIALS.flatMap((m) => ['tint', 'wash', 'plain'].map((y) => `${s}-${m}-${y}`))),
250
+ ];
251
+ const PROJECT_TEMPLATE_WALK = ['blob-tint', 'pair-gold-wash', 'bead-obsidian-plain', 'blob-frost-wash', 'pair-chrome', 'bead-gold-tint', 'blob-obsidian-wash', 'pair-frost-tint', 'bead-wash', 'blob-gold-plain', 'pair-obsidian-tint', 'bead-frost-wash'];
242
252
 
243
253
  // The stored JOIN DOOR (ADR 0182 D1, task 1002323). Three values, and ALL THREE ARE
244
254
  // VISIBLE: `private` is visible-but-closed (the `goals.visibility` meaning, ADR 0112),
@@ -1357,6 +1367,9 @@ module.exports = {
1357
1367
  upsertProject,
1358
1368
  PROJECT_TYPES,
1359
1369
  PROJECT_TEMPLATES,
1370
+ PROJECT_TEMPLATE_WALK,
1371
+ PROJECT_MATERIALS,
1372
+ PROJECT_SHAPES,
1360
1373
  getProject,
1361
1374
  listFeaturedProjects,
1362
1375
  listAllProjects,
@@ -48,8 +48,9 @@ module.exports = function projectsRoutes() {
48
48
  description: { type: 'string', maxLength: 2000 },
49
49
  art_url: { type: 'string', maxLength: 2048 },
50
50
  type: { type: 'string', enum: [...pi.PROJECT_TYPES] },
51
- // The planet template (task 1003319): the shape and style the sky cuts this
52
- // project's orb in. Curated here, never self-reported; absent means the hub's walk.
51
+ // The planet template (task 1003319; materials, task 1003337): the shape, material and
52
+ // style the sky cuts this project's orb in. Curated here, never self-reported; absent
53
+ // means the hub's walk.
53
54
  template: { type: 'string', enum: [...pi.PROJECT_TEMPLATES] },
54
55
  })) return;
55
56
  try {
@@ -235,19 +235,30 @@ await ta('upsertProject carries the planet template, COALESCE-preserved; an unkn
235
235
  const pool2 = mockPool([{ rows: [{ id: '2' }] }]);
236
236
  await P.upsertProject({ origin: 'https://q', name: 'Q', template: 'saucer-neon' }, { pool: pool2 });
237
237
  assert.ok(!pool2.calls[0].params.includes('saucer-neon'), 'unknown templates never reach the SQL');
238
- // the vocabulary: the world's three whole objects × the three ways the accent lands,
239
- // in the walk's order (consecutive positions change shape AND style)
240
- assert.equal(P.PROJECT_TEMPLATES.length, 9);
241
- assert.equal(new Set(P.PROJECT_TEMPLATES).size, 9, 'nine distinct templates');
242
- for (const t of P.PROJECT_TEMPLATES) assert.match(t, /^(blob|pair|bead)-(tint|wash|chrome)$/);
243
- for (let i = 1; i < P.PROJECT_TEMPLATES.length; i++) {
244
- const [s0, y0] = P.PROJECT_TEMPLATES[i - 1].split('-');
245
- const [s1, y1] = P.PROJECT_TEMPLATES[i].split('-');
246
- assert.ok(s0 !== s1 && y0 !== y1, `walk step ${i} changes both shape and style`);
238
+ // the vocabulary: the world's three whole objects × four materials (chrome named by
239
+ // omission) × the three ways the accent lands — every valid name (the CHECK's set)
240
+ const RE = /^(blob|pair|bead)-(tint|wash|chrome|(frost|gold|obsidian)-(tint|wash|plain))$/;
241
+ assert.deepEqual(P.PROJECT_SHAPES, ['blob', 'pair', 'bead']);
242
+ assert.deepEqual(P.PROJECT_MATERIALS, ['frost', 'gold', 'obsidian']);
243
+ assert.equal(P.PROJECT_TEMPLATES.length, 36);
244
+ assert.equal(new Set(P.PROJECT_TEMPLATES).size, 36, 'thirty-six distinct templates');
245
+ for (const t of P.PROJECT_TEMPLATES) assert.match(t, RE);
246
+ for (const t of ['blob-tint', 'pair-wash', 'bead-chrome', 'blob-wash', 'pair-chrome', 'bead-tint', 'blob-chrome', 'pair-tint', 'bead-wash']) assert.ok(P.PROJECT_TEMPLATES.includes(t), `the first round's ${t} stays valid`);
247
+ // the walk: twelve of them, consecutive positions (wrapping) change shape, material AND style
248
+ const parts = (t) => { const p = t.split('-'); return p.length === 3 ? p : [p[0], 'chrome', p[1]]; };
249
+ assert.equal(P.PROJECT_TEMPLATE_WALK.length, 12);
250
+ assert.equal(new Set(P.PROJECT_TEMPLATE_WALK).size, 12, 'twelve distinct walk positions');
251
+ for (let i = 0; i < P.PROJECT_TEMPLATE_WALK.length; i++) {
252
+ const [s0, m0, y0] = parts(P.PROJECT_TEMPLATE_WALK[(i + 11) % 12]);
253
+ const [s1, m1, y1] = parts(P.PROJECT_TEMPLATE_WALK[i]);
254
+ assert.ok(s0 !== s1 && m0 !== m1 && y0 !== y1, `walk step ${i} changes shape, material and style`);
255
+ assert.ok(P.PROJECT_TEMPLATES.includes(P.PROJECT_TEMPLATE_WALK[i]), 'every walk position is a valid name');
247
256
  }
248
- // the migration pins the same nine
249
- const mig = readFileSync(new URL('../migrations/platform_identity_012_project_template.sql', import.meta.url), 'utf8');
250
- for (const t of P.PROJECT_TEMPLATES) assert.ok(mig.includes(`'${t}'`), `migration 012 names ${t}`);
257
+ // migration 013 pins the same thirty-six (012 named the first nine)
258
+ const mig = readFileSync(new URL('../migrations/platform_identity_013_project_template_materials.sql', import.meta.url), 'utf8');
259
+ for (const t of P.PROJECT_TEMPLATES) assert.ok(mig.includes(`'${t}'`), `migration 013 names ${t}`);
260
+ const mig12 = readFileSync(new URL('../migrations/platform_identity_012_project_template.sql', import.meta.url), 'utf8');
261
+ for (const t of P.PROJECT_TEMPLATES.slice(0, 9)) assert.ok(mig12.includes(`'${t}'`), `migration 012 named ${t}`);
251
262
  // and the public projection carries it, so the sky can read a curated choice
252
263
  const pool3 = mockPool([{ rows: [] }]);
253
264
  await P.listFeaturedProjects({ pool: pool3 });
@@ -1 +1 @@
1
- {"projects":[{"origin":"https://amazonprimea.com","name":"Off the Boats","tagline":"A singular shop, and a broader videogame in a persistent ancient Greek world that collides with modern day commerce & products.","art_url":"/assets/world/r2-chrome-blob-one.jpg","type":"game","template":"bead-wash","created_at":"2026-07-15T22:12:57.732Z","builders":11,"tags":["art-pipeline","character-anim","demo","game"],"owner":"lars589","owner_avatar":"https://avatars.githubusercontent.com/u/283020391?v=4","faces":["https://avatars.githubusercontent.com/u/283020391?v=4","https://avatars.githubusercontent.com/u/64553469?v=4","https://avatars.githubusercontent.com/u/290184380?v=4","https://avatars.githubusercontent.com/u/295990394?v=4","https://avatars.githubusercontent.com/u/35337514?v=4"],"shipped_30d":0,"shipped_30d_capped":false,"hall_origin":"https://builders.amazonprimea.com"},{"origin":"https://demo.cloudbongos.com","name":"Demo","tagline":"A live demo instance on Cloud Bongos.","art_url":null,"type":null,"template":null,"created_at":"2026-07-14T19:16:38.160Z","builders":null,"tags":null,"owner":null,"owner_avatar":null,"faces":null,"shipped_30d":null,"shipped_30d_capped":null,"hall_origin":null},{"origin":"https://emersonian-circles.cloudbongos.com","name":"Emersonian Circles","tagline":"An independent project built on Cloud Bongos.","art_url":null,"type":null,"template":null,"created_at":"2026-07-25T02:21:55.641Z","builders":1,"tags":[],"owner":"lars589","owner_avatar":"https://avatars.githubusercontent.com/u/283020391?v=4","faces":[],"shipped_30d":30,"shipped_30d_capped":true,"hall_origin":null}]}
1
+ {"projects":[{"origin":"https://amazonprimea.com","name":"Off the Boats","tagline":"A singular shop, and a broader videogame in a persistent ancient Greek world that collides with modern day commerce & products.","art_url":"/assets/world/r2-chrome-blob-one.jpg","type":"game","template":"bead-wash","created_at":"2026-07-15T22:12:57.732Z","builders":11,"tags":["art-pipeline","character-anim","demo","game"],"owner":"lars589","owner_avatar":"https://avatars.githubusercontent.com/u/283020391?v=4","faces":["https://avatars.githubusercontent.com/u/283020391?v=4","https://avatars.githubusercontent.com/u/64553469?v=4","https://avatars.githubusercontent.com/u/290184380?v=4","https://avatars.githubusercontent.com/u/295990394?v=4","https://avatars.githubusercontent.com/u/35337514?v=4"],"shipped_30d":0,"shipped_30d_capped":false,"hall_origin":"https://builders.amazonprimea.com"},{"origin":"https://demo.cloudbongos.com","name":"Demo","tagline":"A live demo instance on Cloud Bongos.","art_url":null,"type":null,"template":"pair-gold-wash","created_at":"2026-07-14T19:16:38.160Z","builders":null,"tags":null,"owner":null,"owner_avatar":null,"faces":null,"shipped_30d":null,"shipped_30d_capped":null,"hall_origin":null},{"origin":"https://emersonian-circles.cloudbongos.com","name":"Emersonian Circles","tagline":"An independent project built on Cloud Bongos.","art_url":null,"type":null,"template":null,"created_at":"2026-07-25T02:21:55.641Z","builders":1,"tags":[],"owner":"lars589","owner_avatar":"https://avatars.githubusercontent.com/u/283020391?v=4","faces":[],"shipped_30d":30,"shipped_30d_capped":true,"hall_origin":null}]}
@@ -235,7 +235,8 @@ function reducedMotionProblems({ running = [], frames = 0 } = {}) {
235
235
 
236
236
  // ── the states file ───────────────────────────────────────────────────────────
237
237
  // { "page": "/projects", "surface"?: "public-landing", "stub"?: { prefix?, child? },
238
- // "modeQuery"?: "mode" | false, "ignoreRequests"?: [regex strings], "ignoreConsole"?: [...],
238
+ // "modes"?: ["dark" | "light", …] (the modes the page renders; default both — a page that pins
239
+ // one mode declares it here), "modeQuery"?: "mode" | false, "ignoreRequests"?: [regex strings], "ignoreConsole"?: [...],
239
240
  // "ignoreControls"?: [selectors], "states": { name: { url?, auth?, actions?, expect? } },
240
241
  // "reducedMotion"?: { auth?, url?, actions? } }
241
242
  function validateStatesFile(json) {
@@ -245,6 +246,7 @@ function validateStatesFile(json) {
245
246
  if (json.surface !== undefined && typeof json.surface !== 'string') errors.push('surface: a string (a checkout dir or a module key)');
246
247
  errors.push(...validateStub(json.stub, 'stub'));
247
248
  if (json.modeQuery !== undefined && json.modeQuery !== false && typeof json.modeQuery !== 'string') errors.push('modeQuery: the query param the page reads for its colour mode (default "mode"), or false');
249
+ if (json.modes !== undefined && !(Array.isArray(json.modes) && json.modes.length && json.modes.every((m) => MODES.includes(m)))) errors.push('modes: a non-empty array of "dark" | "light" — the modes the page renders (default both); a page that pins one mode declares it here');
248
250
  for (const k of ['ignoreRequests', 'ignoreConsole', 'ignoreControls']) if (json[k] !== undefined && !(Array.isArray(json[k]) && json[k].every((s) => typeof s === 'string'))) errors.push(`${k}: an array of strings`);
249
251
  const states = json.states;
250
252
  if (!states || typeof states !== 'object' || Array.isArray(states) || !Object.keys(states).length) errors.push('states: required — a non-empty object of name → { url?, auth?, actions?, expect? }');
@@ -3,9 +3,10 @@
3
3
  // render.js — render a page's states and audit every render against the platform
4
4
  // floors (task 1003320, ADR 0197). The states live in a JSON file BESIDE the
5
5
  // surface (<dir>/<page>.states.json: name → url + actions + expectations); each
6
- // is rendered in dark + light at 1440x900 / 390x844 / 320x700 through two stubs
7
- // this file starts itself (signed out, signed in), then a reduced-motion pass per
8
- // mode. Writes the shots + a JSON report; exits non-zero on any problem.
6
+ // is rendered at 1440x900 / 390x844 / 320x700 in the modes the states file names
7
+ // (`"modes"`; both by default — the apex pins dark, task 1003336; `--modes` overrides)
8
+ // through two stubs this file starts itself (signed out, signed in), then a
9
+ // reduced-motion pass per mode. Writes the shots + a JSON report; exits non-zero on any problem.
9
10
  //
10
11
  // node modules/ui-design/kit/render.js --page /projects (public-landing/projects.states.json)
11
12
  // node modules/ui-design/kit/render.js --page /builders/ranks --surface hall-ui
@@ -65,7 +66,7 @@ async function main() {
65
66
  const args = process.argv.slice(2);
66
67
  if (lib.hasFlag(args, '--help')) usage('');
67
68
  const { file, json, surface } = locate(args);
68
- const modes = lib.listArg(args, '--modes', [...lib.MODES]).filter((m) => lib.MODES.includes(m));
69
+ const modes = lib.listArg(args, '--modes', json.modes || [...lib.MODES]).filter((m) => lib.MODES.includes(m));
69
70
  const sizes = lib.listArg(args, '--sizes', Object.keys(lib.SIZES)).map(Number).filter((w) => lib.SIZES[w]);
70
71
  const only = lib.listArg(args, '--only', null);
71
72
  const names = Object.keys(json.states).filter((n) => !only || only.includes(n));
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.413",
3
+ "version": "1.19.415",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.413",
9
+ "version": "1.19.415",
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.413",
3
+ "version": "1.19.415",
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",