@bongos/core 1.19.440 → 1.19.442

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 (33) hide show
  1. package/.bongos-core.json +71 -31
  2. package/clients/bongos-client/index.d.ts +1 -2
  3. package/docs/api/openapi.json +3 -20
  4. package/docs/api-reference.md +1 -1
  5. package/docs/copy-inventory.md +180 -173
  6. package/docs/copy-registry.json +300 -226
  7. package/docs/design/mocks/hub/mine.html +89 -1
  8. package/docs/design/projects-hub-direction-v2.md +59 -0
  9. package/docs/module-api-changelog.md +4 -0
  10. package/modules/platform-identity/routes/projects.js +2 -1
  11. package/modules/provisioning/provisioning.js +11 -2
  12. package/modules/provisioning/routes/provisioning.js +10 -5
  13. package/modules/ui-design/kit/fixtures/featured-hub.json +1 -1
  14. package/modules/ui-design/kit/serve.js +29 -4
  15. package/package-lock.json +2 -2
  16. package/package.json +1 -1
  17. package/public-landing/assets/cosmos.css +89 -65
  18. package/public-landing/assets/world/r4-card-chrome.jpg +0 -0
  19. package/public-landing/assets/world/r4-card-chrome.jpg.json +14 -0
  20. package/public-landing/assets/world/r4-card-deepfield.jpg +0 -0
  21. package/public-landing/assets/world/r4-card-deepfield.jpg.json +14 -0
  22. package/public-landing/assets/world/r4-card-dust.jpg +0 -0
  23. package/public-landing/assets/world/r4-card-dust.jpg.json +14 -0
  24. package/public-landing/assets/world/r4-card-nebula.jpg +0 -0
  25. package/public-landing/assets/world/r4-card-nebula.jpg.json +14 -0
  26. package/public-landing/projects-floor.states.json +28 -1
  27. package/public-landing/projects.html +201 -69
  28. package/public-landing/projects.probes.json +16 -2
  29. package/public-landing/projects.states.json +3 -1
  30. package/src/module-api.js +1 -1
  31. package/tests/projects_hub.mjs +173 -65
  32. package/tests/projects_hub_pre_uat.mjs +11 -9
  33. package/tests/provisioning_settings.mjs +49 -4
@@ -84,6 +84,36 @@ body{
84
84
  .door.danger{border-color:color-mix(in srgb,var(--err) 45%,transparent);color:var(--err);}
85
85
  .door.danger:hover{border-color:var(--err);}
86
86
 
87
+ /* ── the every-project table, in the ledger row's grammar (task 1003356): the
88
+ SAME tracks the mine row lays (72px planet, then 1.1fr 1.3fr 1fr 232px), the
89
+ facts as label-face lines, the module list where the mine row draws its rail,
90
+ and at most two doors. No header row, no cell borders: the sort left the
91
+ column head and became a hairline pill group, the mine view's own control. ── */
92
+ .tblSort{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:16px 0 8px;}
93
+ .tblSort .sLbl{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-faint);margin-right:6px;}
94
+ .tblSort .dir{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);}
95
+ .trows{list-style:none;margin:8px 0 0;padding:0;}
96
+ .trow{display:grid;grid-template-columns:72px minmax(0,1.1fr) minmax(0,1.3fr) minmax(0,1fr) 232px;grid-template-areas:"orb who facts runs doors" "orb who tag runs doors";gap:0 22px;align-items:center;padding:22px 0;border-top:1px solid var(--rule-soft);}
97
+ .trow>.worldOrb{grid-area:orb;}
98
+ .trow .tWho{grid-area:who;}
99
+ .trow .facts{grid-area:facts;align-self:end;}
100
+ .trow .ptag{grid-area:tag;align-self:start;margin:5px 0 0;font-size:13.5px;font-weight:400;line-height:1.45;color:var(--ink-soft);}
101
+ .trow .runs{grid-area:runs;}
102
+ .trow .doors{grid-area:doors;}
103
+ .trows .trow:last-child{border-bottom:1px solid var(--rule-soft);}
104
+ .tWho{display:grid;gap:8px;justify-items:start;align-content:center;min-width:0;}
105
+ .tWho .mname{font-family:var(--font-body);font-weight:500;font-size:17px;letter-spacing:-.015em;color:var(--ink);}
106
+ .tWho .chip{white-space:normal;line-height:1.3;}
107
+ .trow .runs{display:grid;gap:6px;min-width:0;}
108
+ .trow .runs .rLbl{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-faint);}
109
+ .trow .tags{display:flex;gap:6px;flex-wrap:wrap;}
110
+ .trow .tags span{font-family:var(--font-mono);font-size:10.5px;padding:2px 8px;border-radius:999px;border:1px solid var(--rule);color:var(--ink-faint);white-space:nowrap;}
111
+ .trow .pill{display:inline-block;font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;padding:3px 9px;border-radius:999px;border:1px solid var(--rule);color:var(--ink-faint);}
112
+ .trow .typeTag{font-family:var(--font-mono);font-size:10.5px;font-weight:500;color:var(--ink-soft);border:1px solid var(--rule);border-radius:999px;padding:2px 9px;}
113
+ .rowHall{display:inline-flex;align-items:center;justify-content:center;width:40px;height:40px;border:1px solid var(--rule);border-radius:var(--radius-pill);color:var(--ink-soft);}
114
+ .rowHall svg{width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:1.4;}
115
+ .rowHall:hover{border-color:var(--ink-faint);color:var(--ink);}
116
+
87
117
  /* ── v2 · the poster row: the planet large, the name in the display voice, the rail full width ── */
88
118
  .prow{display:grid;grid-template-columns:132px 1fr auto;gap:0 28px;align-items:start;padding:30px 0;border-top:1px solid var(--rule-soft);}
89
119
  .prow .pname{font-family:var(--font-display);font-weight:300;font-size:30px;letter-spacing:-.035em;line-height:1.04;text-transform:lowercase;color:var(--ink);margin:0 0 8px;}
@@ -146,6 +176,7 @@ body[data-state="list"] section[data-state="list"],
146
176
  body[data-state="cards"] section[data-state="cards"],
147
177
  body[data-state="manage"] section[data-state="manage"],
148
178
  body[data-state="empty"] section[data-state="empty"]{display:block;}
179
+ body[data-state="table"] section[data-state="table"]{display:block;}
149
180
  .vh{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;}
150
181
  .skip{position:absolute;left:12px;top:-60px;background:var(--bg-card);color:var(--ink);padding:11px 18px;border-radius:999px;}
151
182
  .skip:focus{top:12px;}
@@ -171,6 +202,9 @@ a:focus-visible,button:focus-visible,label:focus-within{outline:2px solid var(--
171
202
  .choice label{border-left:0;padding-left:0;border-top:1px solid var(--rule-soft);}
172
203
  .choice label:first-child{border-top:0;}
173
204
  .planetRow{grid-template-columns:72px 1fr;}
205
+ .trow{grid-template-columns:56px 1fr;gap:14px 16px;padding:20px 0;}
206
+ .trow .worldOrb{--d:56px;}
207
+ .trow{grid-template-areas:"orb who" "tag tag" "facts facts" "runs runs" "doors doors";}
174
208
  }
175
209
  </style>
176
210
  </head>
@@ -181,6 +215,8 @@ a:focus-visible,button:focus-visible,label:focus-within{outline:2px solid var(--
181
215
  ?state=cards the three row treatments side by side: v1 the ledger, v2 the poster row, v3 the shelf
182
216
  ?state=manage the manage page: the ledger of facts, the rail, the doors, hairline sections, the planet slot
183
217
  ?state=empty the mine view with no projects: a band of the Projects band's grammar
218
+ ?state=table the every-project table (task 1003356) in the SAME ledger grammar: the masthead,
219
+ the sort as hairline pills, one row per project on the mine row's tracks
184
220
  -->
185
221
  <a class="skip" href="#main">Skip to content</a>
186
222
  <header class="bar">Cloud Bongos</header>
@@ -352,6 +388,58 @@ a:focus-visible,button:focus-visible,label:focus-within{outline:2px solid var(--
352
388
  </div>
353
389
  </section>
354
390
 
391
+ <section data-state="table">
392
+ <div class="view">
393
+ <div class="tblMast">
394
+ <h2 class="tblTitle">every project</h2>
395
+ <span class="mastObj" aria-hidden="true"></span>
396
+ </div>
397
+ <div class="tblSort" role="group" aria-label="Sort the projects">
398
+ <span class="sLbl">sort</span>
399
+ <button type="button" class="hpill" aria-pressed="true">name <span class="dir">a to z</span></button>
400
+ <button type="button" class="hpill" aria-pressed="false">builders</button>
401
+ </div>
402
+ <ul class="trows">
403
+ <li class="trow">
404
+ <span class="worldOrb" data-shape="pair" data-material="chrome" data-style="plain" style="--acc:var(--accent);--d:64px"><span class="disc"></span></span>
405
+ <div class="tWho"><span class="mname">Cloud Bongos</span><span class="pill">this platform</span><span class="chip calm"><span class="cdot"></span>not checked</span></div>
406
+ <span class="ptag">the AI-first build platform this map is drawn on.</span><div class="facts"><span class="fact">9 builders</span><span class="fact">30+ shipped in 30 days</span><span class="fact">listed 12 jul 2026</span></div>
407
+ <div class="runs"><span class="rLbl">runs</span><span class="tags"><span>core</span></span></div>
408
+ <div class="doors"><a class="door" href="#">view</a><a class="rowHall" href="#" aria-label="Builders' hall" title="Builders' hall"><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M2.8 7.4 10 3.5l7.2 3.9"/><path d="M5.2 8.8v5.6M10 8.8v5.6M14.8 8.8v5.6"/><path d="M3.4 16.3h13.2"/></svg></a></div>
409
+ </li>
410
+ <li class="trow">
411
+ <span class="worldOrb" data-shape="bead" data-material="chrome" data-style="wash" style="--acc:var(--terracotta);--d:64px"><span class="disc"></span></span>
412
+ <div class="tWho"><span class="mname">Off the Boats</span><span class="pill">you build here</span><span class="chip calm"><span class="cdot"></span>not checked</span></div>
413
+ <span class="ptag">A singular shop, and a broader videogame in a persistent ancient Greek world.</span><div class="facts"><span class="fact"><span class="typeTag">Game</span></span><span class="fact">11 builders</span><span class="fact">0 shipped in 30 days</span><span class="fact">listed 15 jul 2026</span></div>
414
+ <div class="runs"><span class="rLbl">runs</span><span class="tags"><span>art-pipeline</span><span>character-anim</span><span>demo</span><span>+1</span></span></div>
415
+ <div class="doors"><a class="door" href="#">view</a><a class="rowHall" href="#" aria-label="Builders' hall" title="Builders' hall"><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M2.8 7.4 10 3.5l7.2 3.9"/><path d="M5.2 8.8v5.6M10 8.8v5.6M14.8 8.8v5.6"/><path d="M3.4 16.3h13.2"/></svg></a></div>
416
+ </li>
417
+ <li class="trow">
418
+ <span class="worldOrb" data-shape="pair" data-material="obsidian" data-style="tint" style="--acc:var(--sea);--d:64px"><span class="disc"></span></span>
419
+ <div class="tWho"><span class="mname">Demo</span><span class="pill">yours</span><span class="chip live"><span class="cdot"></span>live <span class="rec">checked 3m ago</span></span></div>
420
+ <span class="ptag">A live demo instance on Cloud Bongos.</span><div class="facts"><span class="fact">type not declared</span><span class="fact">builders not known</span><span class="fact">listed 14 jul 2026</span></div>
421
+ <div class="runs"><span class="rLbl">runs</span><span class="fact">not known</span></div>
422
+ <div class="doors"><a class="door" href="#">view</a></div>
423
+ </li>
424
+ <li class="trow">
425
+ <span class="worldOrb" data-shape="blob" data-material="frost" data-style="wash" style="--acc:var(--gold-soft);--d:64px"><span class="disc"></span></span>
426
+ <div class="tWho"><span class="mname">Emersonian Circles</span><span class="chip calm"><span class="cdot"></span>not checked</span></div>
427
+ <span class="ptag">An independent project built on Cloud Bongos.</span><div class="facts"><span class="fact">type not declared</span><span class="fact">1 builder</span><span class="fact">30+ shipped in 30 days</span><span class="fact">listed 25 jul 2026</span></div>
428
+ <div class="runs"><span class="rLbl">runs</span><span class="tags"><span>core</span></span></div>
429
+ <div class="doors"><a class="door" href="#">view</a></div>
430
+ </li>
431
+ <li class="trow">
432
+ <span class="worldOrb" data-shape="blob" data-material="chrome" data-style="tint" style="--acc:var(--sun-lit);--d:64px"><span class="disc"></span></span>
433
+ <div class="tWho"><span class="mname">Tidewater</span><span class="chip calm"><span class="cdot"></span>not checked</span></div>
434
+ <span class="ptag">A project that has declared nothing yet.</span><div class="facts"><span class="fact">type not declared</span><span class="fact">builders not known</span><span class="fact">listed 20 jul 2026</span></div>
435
+ <div class="runs"><span class="rLbl">runs</span><span class="fact">not known</span></div>
436
+ <div class="doors"><a class="door" href="#">view</a></div>
437
+ </li>
438
+ </ul>
439
+ <p class="vLede" role="status" style="margin-top:22px">Sorted by name, A to Z.</p>
440
+ </div>
441
+ </section>
442
+
355
443
  <section data-state="empty">
356
444
  <div class="view">
357
445
  <div class="vHead"><h2 class="vTitle">your projects</h2></div>
@@ -369,7 +457,7 @@ a:focus-visible,button:focus-visible,label:focus-within{outline:2px solid var(--
369
457
  No motion in script; nothing is built here. */
370
458
  (function () {
371
459
  var state = new URLSearchParams(location.search || location.hash.slice(1) || (window.__Q || '')).get('state') || 'list';
372
- if (['list', 'cards', 'manage', 'empty'].indexOf(state) === -1) state = 'list';
460
+ if (['list', 'cards', 'manage', 'empty', 'table'].indexOf(state) === -1) state = 'list';
373
461
  document.body.setAttribute('data-state', state);
374
462
  })();
375
463
  </script>
@@ -276,3 +276,62 @@ Three mastheads, rendered dark and light at 1440 / 390 / 320 (ALL CLEAN) and mea
276
276
  **Retargeted.** `tests/projects_hub.mjs` (`renderManage` reads `addressRow(inst, true)`; the delete gate reads off `renderDelete`) and `tests/projects_hub_pre_uat.mjs` (the restart door's one home, the restart sender, the one remaining confirm, the field's move and its section, the F19 and correction doors, the minute tick, the manifest snag's link, the announcements, the no-address door). The copy registry regenerated.
277
277
 
278
278
  **Flagged to the copy desk, not changed here.** The manage page's own site door still reads "Open site →" while the row now reads "View" — each reads naturally in place (the row pairs two doors, the page sits among verbs), so neither was churned. `stripMarkup`'s queued-flow notes still say "this card updates when it starts" on a page that is not a card. And a prose pointer inside a row's address line inherits the mono face meant for the address itself — true before this build too, and left alone rather than churned.
279
+
280
+ ## 18. The every-project table takes the Your-projects row: the table is gone, the ledger stands (task 1003356)
281
+
282
+ **Date:** 2026-08-30 · the owner, after the Your-projects makeover (task 1003345) and the table's fixed tracks (task 1003348): *"also the your projects page has a better design than the every project table."* This is the every-project half. The masthead the owner picked in §9.4 (T1, the ledger, "every project") is untouched; what changed is everything under it.
283
+
284
+ **The round.** One reading, so no pick page (the round's own rule: a pick page only where two readings compete). A `?state=table` variant on `docs/design/mocks/hub/mine.html` — the sheet the Your-projects round already used, so the two views could be read side by side in one file — `check-mock` **ALL RULES PASS**, shot through the kit's stub at 1440 / 390 / 320 dark, **ALL CLEAN**. Two things the render caught before any page file was touched:
285
+
286
+ - The row's name and its pill were being clipped at 14ch and then hidden outright under 900px. `cosmos.css:593` and `:672` own a `.who` for the landing's face rows, and the mock's row had reached for the same name. Renamed `.tWho`. (The mine mock's `?state=list` has the same latent collision from its own round; the *shipped* mine view does not — it renders `.projHead` — so the mock is left as the record it is.)
287
+ - The tagline had no home in the first reading, and dropping it would have taken the only sentence saying what a project *is* out of the browse list. It went to the mine row's second line — the area the mine row gives its address — which is what made the two rows the same grammar rather than a family resemblance.
288
+
289
+ **The markup call, and why.** The task left the `<table>` to the builder: *"the `<table>` markup, its header row and cell borders go, or stay as the accessible structure under the row's look if the sort's a11y needs it."* **The table is gone.** Three reasons, in order of weight:
290
+
291
+ 1. Once every fact is a self-describing label-face line stacked inside one track, the rows share no columns — so there is nothing for a header row to head, and `aria-sort` (valid only on a `columnheader`) has no host. A row is a record now, not a set of cells on shared axes, and the honest markup for a list of records is a list.
292
+ 2. A `<table>` whose `<tr>` is `display:grid` loses its table role in every engine. Keeping the element would have meant re-declaring `role="table" / "row" / "cell"` by hand — markup wearing a role the layout had already taken off it.
293
+ 3. The sort's accessibility is *better* off the header, not worse: two `aria-pressed` toggles in a `role="group"` — the mine view's own filter-pill pattern, already shipped and already keyboard-reachable — plus a `role="status"` sentence under the list (`#mapSorted`, "Sorted by name, a to z.") that states the live order for a reader who never saw the press. Pressing the pressed pill reverses, exactly as the column header did.
294
+
295
+ **What was built.** `#mapRows` is a `<ul class="tbl">` of `<li class="tblRow">`, one grid, five tracks — `72px minmax(0,1.1fr) minmax(0,1.3fr) minmax(0,1fr) 232px` — and two area rows, `"orb who facts runs doors" / "orb who tag runs doors"`: the same five tracks `#view-mine .proj` + `.projBody` compose, with **runs where the mine row draws its rail** and **the tagline where it puts its address**. The planet is the project's own (`planetAttrs(templateOf(p))` + `accentOf(p)`, the owner's choice first — task 1003347), decorative, since the name beside it is the row's door. The facts are `factLines()`: the declared type through `typeHtml`, builders, shipped in 30 days, the Listed date. At most two doors: `view` as the mine row's hairline pill, and the hall — member-gated and address-gated as before, now standing at the door's 40px rather than riding a text link's cell as a 28px glyph. Hairlines above each row, the last one closing; a row's hover is its hairline taking ink. The platform's own row keeps its static apex-only markup and its ids, and now paints its own shipped-in-30-days figure from the feed it was already reading — the callout §9.4's T3 flagged and never wired.
296
+
297
+ **The truths that had to survive the move, and how.** The columns drew a distinction the ledger had to keep in words:
298
+
299
+ | Fact | Was | Is |
300
+ |---|---|---|
301
+ | builders, null | `—` in the figure voice | `builders not known` |
302
+ | builders, 0 | `0` | `0 builders` — still a real answer, not an absence |
303
+ | type, not declared | `—` | `type not declared` (through the same `typeHtml`) |
304
+ | runs, null vs `[]` | `—` vs `core` | unchanged — under an explicit `runs` label, where a dash is the value of a labelled field rather than a sentence standing on its own |
305
+ | liveness, unprobed | `• —` | `• not checked` beside the name (v1's §5 proposal, taken now that the chip sits where a reader looks) |
306
+ | shipped / listed | never a column | rendered only where there is an answer — the poster card's own rule, so nothing new is claimed |
307
+
308
+ **Kept.** The masthead and its object; the sort keys and the comparator (unknown builders still sort to the bottom in both directions); the search in the bar; the platform's row pinned first, apex-only; an owned row's name as a manage anchor and its plain click routing to manage; the row-click-opens-the-card contract; `#sub-table-body`'s scroll as the floor under a very long row.
309
+
310
+ **The kit.** `render.js --page /projects` **ALL CLEAN** (13 states + a new `in-table-by-builders`), `--states projects-empty` and `--states projects-floor` ALL CLEAN, `probe.js --page /projects` **45/45 ALL PASS** (43 + two new: the pills carry the order and the sentence states it; pressing the pressed one reverses). `tests/projects_hub.mjs` 134/134, `projects_hub_pre_uat.mjs` 56/56, `<redacted>.mjs` 34/34, `fitness.js` all checks passed, the copy registry regenerated.
311
+
312
+ **Retargeted.** Every table pin: the self row's extraction (`<li class="tblRow" id="selfRow">`), the generated-row template, the six-cell parity test (now "every row lays the same five parts", which reads the planet expression too, so dropping the planet cannot pass), the column-source test (now fact-source), the fixed-tracks test (now asserts the two views' tracks line for line and that **no table rule outlived the table**), the `—`/0 pin, the Runs pin, `.rowHall`'s box, the placeholder-href guard, and `projects_hub_pre_uat.mjs`'s keyboard-sort test. One test is new: the sort control's own contract.
313
+
314
+ **Flagged to the copy desk, not changed here.** `typeHtml`'s "not declared" now speaks in the row's voice but is still the formatter the create wizard's review reads through `typeLabel`; the two are not in conflict, but a copy pass over the row's four fact lines as a set would be worth one sitting.
315
+
316
+ ## 19. The card's backdrop: a plate set the owner picks, beside the planet (task 1003352)
317
+
318
+ **Date:** 2026-08-30 · deferred out of the planet round (§16) when the owner said, on that review, *"this could also house project card backdrop customization."* It waited because a card plate set is an art round of its own and the free image coverage had ended; the owner confirmed the spend for this session.
319
+
320
+ **The art round.** Four **5:2** bands — the shape a poster card's banner actually is. Round 3's four space plates are 16:9 field grounds; a 5:2 crop of one puts its density wherever it happens to fall, so the round generated for the card's own composition instead: the interest centre-right, the left third and both lower corners near-black, because the card puts its disc and name lower-left and its two figure callouts upper-left and lower-right.
321
+
322
+ | Key | What it is |
323
+ |---|---|
324
+ | `nebula` | a violet-and-bronze nebula wash, sparse stars — the field's own language in the card's shape |
325
+ | `dust` | warm bronze dust lanes, very low contrast; a haze rather than a shape |
326
+ | `deepfield` | a deep field of very distant galaxies on near-black; the quietest of the four |
327
+ | `chrome` | a sweeping ribbon of liquid chrome — the world's own material with no object in it |
328
+
329
+ **Two things worth recording about the round.** `nano_banana_pro` was asked first (the model round 3 used); after twenty-five minutes not one of its four jobs had left `waiting`, while the same four prompts on `soul_location` returned in fifteen seconds — a stuck provider queue, not a bad request. And the nebula came back too dark to read at the card's 560px: a **gamma** lift (1.75) raises the wash without crushing the black the disc and the name sit on, which is what a contrast lift did instead when it was tried first. Both are in the sidecars, so the plates can be remade.
330
+
331
+ **The key is the planet's kind, not a new kind.** `card_backdrop` joins `PLANET_VOCAB` rather than getting a vocabulary of its own, because everything true of the planet is true of it: a closed list, no default, the platform's to draw, pushed to no instance, restarting nothing. That one line is the whole server change — `planetChoice()` loops the vocab, the settings route builds both its validator and its `settingKeys` from it, and `publicInstance()` spreads it — so the key reaches the row, the own-scoped read and the public wire with no new branch. Two edits beyond it: `planetChoicesForHosts` now admits a host that chose **only** a backdrop, and `/projects/featured` carries the third key.
332
+
333
+ **On the page.** `openCard` reads `backdropPlate(p.card_backdrop) || safeHttpUrl(p.art_url)` — the owner's plate stands in front of the project's own picture, and a key this platform does not draw resolves to `''` rather than a broken `src`. The picker is **its own section beside the planet**, not a control inside it: the owner's words were "beside the planet", and one heading is how a project is drawn *everywhere* while the other is one surface's backdrop, so folding them together would have made both headings lie. It is the planet section's twin otherwise — one band of hairline cells (five: "its own picture" and the four plates), the preview at the planet preview's width as a 5:2 band, one save door, the settings PATCH, the receipt the page repaints from. The preview's caption speaks for the ROW, so a pick that is ahead of the row reads **"not saved yet"** rather than "nothing chosen" under a plate the owner can already see.
334
+
335
+ **What the grade caught, and the class it closed.** The first round shipped `card_backdrop` into `PLANET_VOCAB` and stopped there — and the settings route's `validateOrRespond` schema was a **hand-written field list**, not the vocabulary. Strict mode rejects any field a schema does not declare, so `settingKeys` would have copied the key that the validator had already 400'd: every real save from the manage page would have failed. Nothing caught it, because the kit's stub accepted any body key, so 47 probes and a clean render all passed against a request the server refuses. Both halves are fixed rather than patched: the route's schema is now **derived from the vocabularies** (`vocabSchema(SETTINGS_VOCAB)` + `vocabSchema(PLANET_VOCAB)`), so a key can no longer exist in one and not the other; the stub validates against the same closed lists (copied — a kit module may not import core — and pinned to their source by a test); and `provisioning_settings.mjs` now PATCHes the **real** router with `card_backdrop`, refuses an off-vocabulary one, and walks every declared key of both vocabularies asserting a 200, which is the guard that would have failed on the first round.
336
+
337
+ **The kit.** The read path renders where the hub's own catalog is served — `projects-floor.states.json` is the sibling allowed to alias a fixture, so `featured-hub.json` gives Example a `card_backdrop` and the new `card-backdrop` state pins the plate's `src` on the card. `render.js --page /projects` **ALL CLEAN** (with `in-manage-card`), the two sibling states files ALL CLEAN, `probe.js` **47/47** (two new: the pick saves with the route's no-restart receipt; handing it back restores the project's own picture). `projects_hub` 138/138, `projects_hub_pre_uat` 56/56, `<redacted>` 34/34, `provisioning_settings` 26/26, `provisioning_settings_env` 12/12, `validate-design --strict` valid, `fitness` all checks passed, the copy registry regenerated. DESIGN.md carries the round-4 table.
@@ -1317,5 +1317,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
1317
1317
  landed since 1.19.438 with no explicit bump. run 33290633434. (task 1002620)
1318
1318
  1.19.440 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1319
1319
  landed since 1.19.439 with no explicit bump. run 33321131006. (task 1002620)
1320
+ 1.19.441 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1321
+ landed since 1.19.440 with no explicit bump. run 33323478836. (task 1002620)
1322
+ 1.19.442 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1323
+ landed since 1.19.441 with no explicit bump. run 33326781131. (task 1002620)
1320
1324
  ---------------------------------------------------------------------------
1321
1325
  ```
@@ -148,7 +148,8 @@ module.exports = function projectsRoutes() {
148
148
  // the wire always carries both keys: null = the platform's own pick
149
149
  const planetOf = (p) => {
150
150
  const c = (planets && planets[hostOfOrigin(p.origin)]) || {};
151
- return { planet_template: c.planet_template || null, planet_accent: c.planet_accent || null };
151
+ return { planet_template: c.planet_template || null, planet_accent: c.planet_accent || null,
152
+ card_backdrop: c.card_backdrop || null };
152
153
  };
153
154
  // The enrichment's keys (builders, tags, owner, owner_avatar, faces,
154
155
  // shipped_30d, shipped_30d_capped, hall_origin) never collide with the
@@ -142,7 +142,16 @@ const PLANET_TEMPLATES = Object.freeze([
142
142
  ...PLANET_SHAPES.flatMap((s) => PLANET_MATERIALS.flatMap((m) => ['tint', 'wash', 'plain'].map((y) => `${s}-${m}-${y}`))),
143
143
  ]);
144
144
  const PLANET_ACCENTS = Object.freeze(['sea', 'terracotta', 'gold-soft', 'sun-lit', 'accent']);
145
- const PLANET_VOCAB = Object.freeze({ planet_template: PLANET_TEMPLATES, planet_accent: PLANET_ACCENTS });
145
+ // The card's backdrop (task 1003352) is the same KIND of key as the planet, so it
146
+ // rides the same vocab: a closed list of platform-drawn plates, null = the
147
+ // project's own art (or none). It is not a policy either — nothing is pushed to
148
+ // the instance and nothing restarts; the card simply reads the row.
149
+ const CARD_BACKDROPS = Object.freeze(['nebula', 'dust', 'deepfield', 'chrome']);
150
+ // Named PLANET_VOCAB for the keys it began with; what it actually holds is every
151
+ // key the PLATFORM draws a project with — no default, no env transport, no restart.
152
+ const PLANET_VOCAB = Object.freeze({
153
+ planet_template: PLANET_TEMPLATES, planet_accent: PLANET_ACCENTS, card_backdrop: CARD_BACKDROPS,
154
+ });
146
155
 
147
156
  // The optional project-detail interview (BV1.R15, task 1002334) — the "tell us
148
157
  // about your project" step every creation form offers and every owner may skip.
@@ -1088,7 +1097,7 @@ async function planetChoicesForHosts(db, hosts) {
1088
1097
  for (const r of rows) {
1089
1098
  if (out[r.host]) continue;
1090
1099
  const choice = planetChoice(r);
1091
- if (choice.planet_template || choice.planet_accent) out[r.host] = choice;
1100
+ if (Object.values(choice).some(Boolean)) out[r.host] = choice;
1092
1101
  }
1093
1102
  return out;
1094
1103
  }
@@ -796,12 +796,17 @@ module.exports = function provisioningRoutes() {
796
796
  router.patch('/provisioning/instances/:id/settings', auth.requireBuilder, async (req, res) => {
797
797
  const id = parseId(req, res); // (req, res) — parseId sends 400 bad_id + returns null on failure
798
798
  if (!id) return;
799
+ // The schema IS the vocabularies, not a copy of them (task 1003352): strict
800
+ // mode rejects any field the schema does not declare, so a key added to a
801
+ // vocabulary but forgotten here reaches settingKeys and then 400s on every
802
+ // real save — which is exactly what card_backdrop did until the grader caught
803
+ // it. Deriving both from the same source makes that drift unrepresentable.
804
+ // The platform-drawn keys are nullable on purpose: null hands the pick back.
805
+ const vocabSchema = (vocab) => Object.fromEntries(
806
+ Object.entries(vocab).map(([key, values]) => [key, { type: 'string', enum: values }]));
799
807
  if (validateOrRespond(req, res, {
800
- platform_visibility: { type: 'string', enum: provisioning.SETTINGS_VOCAB.platform_visibility },
801
- joinability: { type: 'string', enum: provisioning.SETTINGS_VOCAB.joinability },
802
- // the planet (task 1003347): nullable on purpose — null hands the pick back to the platform
803
- planet_template: { type: 'string', enum: provisioning.PLANET_VOCAB.planet_template },
804
- planet_accent: { type: 'string', enum: provisioning.PLANET_VOCAB.planet_accent },
808
+ ...vocabSchema(provisioning.SETTINGS_VOCAB),
809
+ ...vocabSchema(provisioning.PLANET_VOCAB),
805
810
  })) return;
806
811
  const settingKeys = [...Object.keys(provisioning.SETTINGS_VOCAB), ...Object.keys(provisioning.PLANET_VOCAB)];
807
812
  const patch = {};
@@ -1 +1 @@
1
- {"_":"The hub's catalog (task 1003345): the landing's featured.json plus Tidewater, an all-null row nobody owns — the card-floor state opens ITS card, because Demo is owned by the signed-in stub (provisioning-instances.json) and an owned orb's plain click opens the manage view, never the card. Served under the name \"featured\" by projects.states.json's stub.fixtures alias; the landing keeps featured.json.","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","planet_template":null,"planet_accent":null,"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","planet_template":"pair-obsidian-tint","planet_accent":"sea","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,"planet_template":null,"planet_accent":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},{"origin":"https://tidewater.cloudbongos.com","name":"Tidewater","tagline":"A project that has declared nothing yet.","art_url":null,"type":null,"template":null,"planet_template":null,"planet_accent":null,"created_at":"2026-07-20T08:00:00.000Z","builders":null,"tags":null,"owner":null,"owner_avatar":null,"faces":null,"shipped_30d":null,"shipped_30d_capped":null,"hall_origin":null}]}
1
+ {"_":"The hub's catalog (task 1003345): the landing's featured.json plus Tidewater, an all-null row nobody owns — the card-floor state opens ITS card, because Demo is owned by the signed-in stub (provisioning-instances.json) and an owned orb's plain click opens the manage view, never the card. Served under the name \"featured\" by projects.states.json's stub.fixtures alias; the landing keeps featured.json.","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","planet_template":null,"planet_accent":null,"card_backdrop":"nebula","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","planet_template":"pair-obsidian-tint","planet_accent":"sea","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,"planet_template":null,"planet_accent":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},{"origin":"https://tidewater.cloudbongos.com","name":"Tidewater","tagline":"A project that has declared nothing yet.","art_url":null,"type":null,"template":null,"planet_template":null,"planet_accent":null,"created_at":"2026-07-20T08:00:00.000Z","builders":null,"tags":null,"owner":null,"owner_avatar":null,"faces":null,"shipped_30d":null,"shipped_30d_capped":null,"hall_origin":null}]}
@@ -137,15 +137,40 @@ const ASKED = new Map();
137
137
  // the run — the page repaints from the row it re-reads, so the row must carry the choice; a
138
138
  // PATCH with only planet keys gets the route's no-restart receipt, a policy key the push
139
139
  const PLANETS = new Map();
140
- const PLANET_KEYS = ['planet_template', 'planet_accent'];
141
- const NO_PLANET = { planet_template: null, planet_accent: null };
140
+ const PLANET_KEYS = ['planet_template', 'planet_accent', 'card_backdrop'];
141
+ const NO_PLANET = { planet_template: null, planet_accent: null, card_backdrop: null };
142
142
  function readBody(req, cb) {
143
143
  let d = '';
144
144
  req.on('data', (c) => { d += c; });
145
145
  req.on('end', () => { let b = {}; try { b = JSON.parse(d || '{}'); } catch { b = {}; } cb(b); });
146
146
  }
147
+ // The real route validates STRICTLY against its vocabularies (an undeclared key
148
+ // or an off-vocabulary value is a 400). The stub mirrored none of that, so the
149
+ // probes stayed green on a body the server would have refused — which is how a
150
+ // missing card_backdrop entry in the route's schema shipped past a full probe
151
+ // run (task 1003352, grade round 2). The lists are copied on purpose: a kit
152
+ // module may not import core. They are small, closed, and pinned by a test.
153
+ const STUB_VOCAB = {
154
+ platform_visibility: ['public', 'gated'],
155
+ joinability: ['open', 'apply', 'invite_only'],
156
+ planet_template: null, // the template set is long; shape is checked, not membership
157
+ planet_accent: ['sea', 'terracotta', 'gold-soft', 'sun-lit', 'accent'],
158
+ card_backdrop: ['nebula', 'dust', 'deepfield', 'chrome'],
159
+ };
160
+ function settingsRejection(body) {
161
+ for (const [key, value] of Object.entries(body)) {
162
+ if (!(key in STUB_VOCAB)) return `unknown field: ${key}`;
163
+ if (value === null) continue; // null hands a pick back
164
+ if (typeof value !== 'string') return `${key} must be a string`;
165
+ const allowed = STUB_VOCAB[key];
166
+ if (allowed && !allowed.includes(value)) return `${key} is not one of: ${allowed.join(', ')}`;
167
+ }
168
+ return null;
169
+ }
147
170
  function settingsPatch(res, id, body) {
148
- if (PLANET_KEYS.some((k) => k in body)) PLANETS.set(String(id), { planet_template: body.planet_template || null, planet_accent: body.planet_accent || null });
171
+ const bad = settingsRejection(body);
172
+ if (bad) return json(res, 400, { error: { code: 'validation_failed', message: bad } });
173
+ if (PLANET_KEYS.some((k) => k in body)) PLANETS.set(String(id), Object.fromEntries(PLANET_KEYS.map((k) => [k, body[k] || null])));
149
174
  const policy = ['platform_visibility', 'joinability'].some((k) => k in body);
150
175
  const push = policy ? FLOW_RECEIPTS.settings.push
151
176
  : { queued: false, restart: false, message: 'Saved. The sky, your list and the card draw it this way from here; nothing on your project restarts for it.' };
@@ -154,7 +179,7 @@ function settingsPatch(res, id, body) {
154
179
  // the row carries its planet: a saved pick for the run, else the fixture's own (or null)
155
180
  function withPlanet(inst) {
156
181
  const saved = PLANETS.get(String(inst.id));
157
- return { ...inst, planet_template: inst.planet_template || null, planet_accent: inst.planet_accent || null, ...(saved || {}) };
182
+ return { ...inst, ...Object.fromEntries(PLANET_KEYS.map((k) => [k, inst[k] || null])), ...(saved || {}) };
158
183
  }
159
184
  function flowReceipt(rest, method) {
160
185
  if (method === 'POST' && rest === '/provisioning/instances') return FLOW_RECEIPTS.provision;
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.440",
3
+ "version": "1.19.442",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.440",
9
+ "version": "1.19.442",
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.440",
3
+ "version": "1.19.442",
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",
@@ -862,73 +862,88 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
862
862
  background-repeat:no-repeat;background-size:calc(var(--dia) * 6.89655) auto;
863
863
  background-position:calc(var(--dia) * -5.58621) calc(var(--dia) * -2.14014);filter:var(--mast-shadow);}
864
864
 
865
- .tbl{width:100%;border-collapse:collapse;background:none;}
866
- .tbl th{text-align:left;font-family:var(--label-face);font-size:var(--label-size);font-weight:400;
867
- letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-faint);padding:0 14px 12px;
868
- border-bottom:1px solid var(--rule);white-space:nowrap;}
869
- .tbl th:first-child,.tbl td:first-child{padding-left:0;}
870
- .tbl th:last-child,.tbl td:last-child{padding-right:0;}
871
- /* the sort control is a real button so the keyboard can reach it; it must look
872
- like the header it replaced, so it inherits everything the th declares. */
873
- .tbl th button{all:unset;cursor:pointer;}
874
- .tbl th button:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}
875
- .tbl td{padding:17px 14px;border-bottom:1px solid var(--rule-soft);font-size:15px;font-weight:400;
876
- color:var(--ink-soft);vertical-align:middle;transition:border-color .2s cubic-bezier(.2,.7,.3,1);}
865
+ /* ── THE LEDGER ROW (task 1003356; the owner: "the your projects page has a
866
+ better design than the every project table"). The table is gone — not
867
+ restyled: once every fact is a self-describing label-face line inside the
868
+ row, the rows share no columns to head, and a <table> whose <tr> is a grid
869
+ loses its table role in every engine anyway. So the rows are a LIST, the
870
+ sort left the column head for a pill group (.tblSort), and the whole view
871
+ takes the Your-projects row's grammar (#view-mine .proj, direction §14.1):
872
+ the SAME five tracks, the planet as the row's object, the facts stacked,
873
+ the tagline where the mine row puts its address, the module list where the
874
+ mine row draws its rail, and at most two doors. ── */
875
+ .tbl{list-style:none;margin:0;padding:0;}
876
+ .tbl .tblRow{display:grid;
877
+ grid-template-columns:72px minmax(0,1.1fr) minmax(0,1.3fr) minmax(0,1fr) 232px;
878
+ grid-template-areas:"orb who facts runs doors" "orb who tag runs doors";
879
+ gap:0 22px;align-items:center;padding:22px 0;
880
+ border-top:1px solid var(--rule-soft);transition:border-color .2s cubic-bezier(.2,.7,.3,1);}
881
+ .tbl .tblRow:last-child{border-bottom:1px solid var(--rule-soft);}
877
882
  /* the whole row opens the project card, so it must look like it does: the
878
- hairline under it takes ink, as a band cell's does */
879
- .tbl tbody tr{cursor:pointer;}
880
- .tbl tbody tr:hover td{border-bottom-color:var(--ink-faint);}
881
- .tbl .pname{display:flex;align-items:center;gap:12px;}
882
- .tbl .pdot{width:10px;height:10px;border-radius:50%;background:var(--acc);flex:0 0 auto;}
883
- .tbl .ptitle{font-family:var(--font-display);font-weight:500;font-size:17px;letter-spacing:-.015em;color:var(--ink);text-decoration:none;}
883
+ hairline above it takes ink, as a band cell's does */
884
+ .tbl .tblRow{cursor:pointer;}
885
+ .tbl .tblRow:hover{border-top-color:var(--ink-faint);}
886
+ /* the planet, docked: the sky's object, relative instead of absolute, and the
887
+ row's decoration — the name beside it is the accessible door (#view-mine's rule) */
888
+ .tbl .worldOrb{grid-area:orb;position:relative;left:auto;top:auto;transform:none;display:block;
889
+ cursor:inherit;opacity:1;pointer-events:none;}
890
+ .tbl .worldOrb .disc{display:block;margin:0;}
891
+ .tbl .tWho{grid-area:who;display:grid;gap:8px;justify-items:start;align-content:center;min-width:0;}
892
+ .tbl .ptitle{font-family:var(--font-body);font-weight:500;font-size:17px;letter-spacing:-.015em;color:var(--ink);text-decoration:none;}
893
+ .tbl a.ptitle{display:inline-flex;align-items:center;min-height:24px;}
884
894
  .tbl a.ptitle:hover{text-decoration:underline;}
885
- .tbl .ptag{font-size:13.5px;color:var(--ink-soft);font-weight:400;}
886
- .tbl .num{font-family:var(--font-display);font-weight:300;font-size:22px;letter-spacing:-.02em;color:var(--ink);
887
- font-variant-numeric:tabular-nums;}
895
+ .tbl .tWho .chip{white-space:normal;line-height:1.3;}
896
+ /* the facts: label-face lines, one fact each, the mine row's .projMeta */
897
+ .tbl .facts{grid-area:facts;align-self:end;display:grid;gap:5px;min-width:0;
898
+ font-family:var(--label-face);font-size:var(--label-size);font-weight:400;
899
+ letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-faint);}
900
+ /* the tagline, where the mine row puts its address: the honest clause in the body face */
901
+ .tbl .ptag{grid-area:tag;align-self:start;margin:5px 0 0;font-size:13.5px;font-weight:400;
902
+ line-height:1.45;color:var(--ink-soft);}
903
+ /* Runs, where the mine row draws its rail: the module list under its own label */
904
+ .tbl .runs{grid-area:runs;display:grid;gap:6px;min-width:0;font-size:13.5px;color:var(--ink-soft);}
905
+ .tbl .rLbl{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);
906
+ text-transform:uppercase;color:var(--ink-faint);}
888
907
  .tbl .tags{display:flex;gap:6px;flex-wrap:wrap;}
889
908
  .tbl .tags span{font-family:var(--font-mono);font-size:10.5px;padding:2px 8px;border-radius:999px;
890
909
  border:1px solid var(--rule);color:var(--ink-faint);white-space:nowrap;}
891
910
  .tbl .pill{display:inline-block;font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);
892
911
  text-transform:uppercase;padding:3px 9px;border-radius:999px;border:1px solid var(--rule);color:var(--ink-faint);}
893
- /* the row's door: the accent on the one pressable thing in the row, as text
894
- (the footer-link grammar), lowercase like every control in the world */
895
- .rowGo{display:inline-flex;align-items:center;min-height:24px;padding:3px 0;font-family:var(--font-body);font-size:15px;
896
- text-transform:lowercase;color:var(--accent-deep);text-decoration:underline;text-underline-offset:4px;
897
- text-decoration-color:color-mix(in srgb,var(--accent) 55%,transparent);white-space:nowrap;
898
- transition:text-decoration-color .2s cubic-bezier(.2,.7,.3,1);}
899
- .rowGo:hover{text-decoration-color:var(--accent-deep);}
900
- .tbl td.right{text-align:right;}
901
- /* Nothing in an action or status cell may wrap: "Manage →" breaking after the
902
- space and the chip's recency dropping to a second line both read as a broken
903
- control rather than a tight column. */
904
- .tbl .btn,.tbl .chip{white-space:nowrap;}
905
- .tbl .colStatus,.tbl .colGo,.tbl .colType{white-space:nowrap;}
906
- .tbl .colGo{padding-left:8px;}
907
- /* ── FIXED tracks (task 1003348; the owner: "the columns don't look aligned").
908
- An auto table sized every column by its content, so the status column hugged
909
- its widest chip and the headers drifted off their cells. table-layout:fixed
910
- takes the widths from the header row — one set of tracks for every row, each
911
- column edge one vertical line — and the name column absorbs the slack. The
912
- Project header sits over the NAME (past the 10px dot and its 12px gap), and a
913
- right-aligned column's header is right-aligned too. Below the table's minimum
914
- the wrap scrolls (#sub-table-body), never the page. ── */
915
- .tbl{table-layout:fixed;min-width:860px;}
916
- .tbl th.colName{padding-left:22px;}
917
- .tbl th.right{text-align:right;}
918
- .tbl th.colType{width:88px;}
919
- .tbl th.colBuilders{width:96px;}
920
- .tbl th.colTags{width:168px;}
921
- .tbl th.colStatus{width:224px;}
922
- .tbl th.colGo{width:112px;}
912
+ /* the doors: hairline pills, lowercase, right of the row — the mine row's
913
+ .btn.small.ghost, with the hall glyph as the second (task 1003356) */
914
+ .tbl .rowDoors{grid-area:doors;display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end;align-items:center;}
915
+ .rowGo{display:inline-flex;align-items:center;justify-content:center;min-height:40px;padding:0 18px;
916
+ font-family:var(--font-body);font-weight:400;font-size:14.5px;text-transform:lowercase;
917
+ color:var(--ink);background:none;border:1px solid var(--rule);border-radius:var(--radius-pill);
918
+ text-decoration:none;white-space:nowrap;transition:border-color .15s ease;}
919
+ .rowGo:hover{border-color:var(--ink-faint);color:var(--ink);}
920
+ /* ── the sort, off the column head (task 1003356): the mine view's filter
921
+ pills — hairline, lowercase, the pressed one inverted to ink ── */
922
+ .tblSort{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:0 0 8px;}
923
+ .tblSort .sortLbl{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);
924
+ text-transform:uppercase;color:var(--ink-faint);margin-right:6px;}
925
+ .tblSort button{display:inline-flex;align-items:center;gap:8px;min-height:36px;padding:0 15px;
926
+ font-family:var(--font-body);font-weight:400;font-size:14.5px;text-transform:lowercase;
927
+ color:var(--ink-soft);background:none;border:1px solid var(--rule);border-radius:var(--radius-pill);cursor:pointer;}
928
+ .tblSort button:hover{border-color:var(--ink-faint);color:var(--ink);}
929
+ .tblSort button[aria-pressed="true"]{background:var(--ink);border-color:var(--ink);color:var(--bg);}
930
+ .tblSort button:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}
931
+ .tblSort .dir{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);}
923
932
  /* the declared-type tag (task 1002772) — its own vocabulary: not the relationship
924
933
  .pill, not the liveness .chip. Two different facts never wear the same chrome. */
925
934
  .typeTag{font-family:var(--font-mono);font-size:10.5px;font-weight:500;color:var(--ink-soft);border:1px solid var(--rule);border-radius:999px;padding:2px 9px;white-space:nowrap;}
926
- @media (max-width:780px){.tbl .colTags,.tbl .colType{display:none;}.tbl{min-width:600px;}}
927
935
  @media (max-width:900px){
928
936
  .tblMast{gap:16px;padding:22px 0 20px;}
929
937
  .tblTitle{font-size:clamp(28px,7.4vw,34px);}
930
938
  .mastObj{--dia:clamp(72px,18vw,96px);}
931
- .tbl th,.tbl td{padding-left:10px;padding-right:10px;}
939
+ /* a phone stacks the row the way the Your-projects row stacks: the planet
940
+ and the name together, then everything else full width */
941
+ .tbl .tblRow{grid-template-columns:56px minmax(0,1fr);
942
+ grid-template-areas:"orb who" "tag tag" "facts facts" "runs runs" "doors doors";
943
+ gap:14px 16px;padding:20px 0;}
944
+ .tbl .tblRow .worldOrb{--d:56px;}
945
+ .tbl .facts,.tbl .ptag,.tbl .runs{align-self:auto;}
946
+ .tbl .rowDoors{justify-content:flex-start;}
932
947
  }
933
948
 
934
949
  /* ── the living sky (task 1002762): camera, spotlight, hover card ──
@@ -1071,10 +1086,13 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
1071
1086
  .projCard .cardHall:hover{text-decoration-color:var(--accent-deep);}
1072
1087
  .cardHall svg,.rowHall svg{width:17px;height:17px;fill:none;stroke:currentColor;stroke-width:1.5;
1073
1088
  stroke-linecap:round;stroke-linejoin:round;}
1074
- /* the table row's hall glyph: beside Open, a member's door, only where the hall is elsewhere */
1075
- .rowHall{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;margin-left:10px;
1076
- vertical-align:middle;color:var(--accent-deep);border-radius:var(--radius-pill);}
1077
- .rowHall:hover{background:var(--glass);}
1089
+ /* the row's second door (task 1003356): a member's hall, only where the hall is
1090
+ elsewhere. It stands beside "view" in the row's door slot, so it takes the
1091
+ door's hairline pill at the same 40px height rather than a glyph's 28px. */
1092
+ .rowHall{display:inline-flex;align-items:center;justify-content:center;width:44px;height:40px;
1093
+ color:var(--ink-soft);border:1px solid var(--rule);border-radius:var(--radius-pill);
1094
+ transition:border-color .15s ease,color .15s ease;}
1095
+ .rowHall:hover{border-color:var(--ink-faint);color:var(--ink);}
1078
1096
  @media (max-width:640px){
1079
1097
  .projCard h3{font-size:26px;padding:0 20px;}
1080
1098
  .projCard .cardTag,.projCard .cardRel,.projCard .cardFaces,.projCard .cardFigs,.projCard .cardMeta,
@@ -1148,12 +1166,9 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
1148
1166
  .mineBar .search .count{display:none;} /* the "All 16" pill already carries the number */
1149
1167
  }
1150
1168
  /* the carried surfaces: only the hit-area floors the world asks for (direction
1151
- record §5, travelling with the pick) — the footer links, the table's sort
1152
- buttons (after their all:unset) — and the table scrolls inside its own wrap
1153
- on a phone instead of pushing the page sideways */
1169
+ record §5, travelling with the pick) — the footer links; the sort pills carry
1170
+ their own 36px. The wrap keeps its scroll as the floor under a very long row. */
1154
1171
  #sub-table-body{overflow-x:auto;}
1155
- .tbl th button{min-height:24px;display:inline-flex;align-items:center;}
1156
- .tbl a.ptitle{display:inline-flex;align-items:center;min-height:24px;}
1157
1172
  .foot a,.finePrint a{display:inline-block;padding:3px 0;min-height:24px;}
1158
1173
  /* ═══════════════════════════════════════════════════════════════════════
1159
1174
  YOUR PROJECTS + THE MANAGE PAGE, IN THE WORLD (task 1003345; direction
@@ -1163,9 +1178,10 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
1163
1178
  structure, the project's own planet as the row's object, the strip drawn
1164
1179
  as the lifecycle rail, the doors as hairline pills with the accent only on
1165
1180
  the one that acts. One set of FIXED tracks across every row, so the list
1166
- reads as one ledger — the same column grammar the every-project table takes
1167
- in its `.tbl` rules (table-layout:fixed, the header row's widths; task
1168
- 1003348, both halves pinned together in tests/projects_hub.mjs). Dark only
1181
+ reads as one ledger — and since task 1003356 the every-project table lays
1182
+ the SAME five tracks in its `.tbl .tblRow` rule, so the two views are one
1183
+ grammar (task 1003348 aligned them; both halves are pinned together in
1184
+ tests/projects_hub.mjs). Dark only
1169
1185
  (ADR 0204); every colour a token; halos are outlines, never box-shadow.
1170
1186
  ═══════════════════════════════════════════════════════════════════════ */
1171
1187
  #view-mine .wrap,#view-manage .wrap{max-width:1180px;}
@@ -1361,6 +1377,14 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
1361
1377
  #view-manage .pickPreview{display:grid;gap:12px;justify-items:center;padding-top:6px;}
1362
1378
  #view-manage .pickWords{margin:0;font-size:14px;font-weight:400;line-height:1.45;color:var(--ink);text-align:center;max-width:168px;}
1363
1379
  #view-manage .pickWords small{display:block;font-size:12.5px;color:var(--ink-soft);}
1380
+ /* the card's backdrop preview (task 1003352): the poster's own 5:2 band at the
1381
+ width the planet preview stands at, so the two sections read as one column.
1382
+ No ground of its own — "its own picture" is an empty hairline frame, which is
1383
+ the honest drawing of a project that may have no art at all. */
1384
+ #view-manage .cardBand{display:block;width:168px;aspect-ratio:5/2;background-size:cover;
1385
+ background-position:center;background-repeat:no-repeat;background-color:var(--bg);
1386
+ border:1px solid var(--rule-soft);}
1387
+ #view-manage .cardBand[data-own]{border-style:dashed;}
1364
1388
  #view-manage .pickBands{display:grid;border-bottom:1px solid var(--rule);}
1365
1389
  #view-manage .pickBand{display:grid;grid-template-columns:88px 1fr;gap:0 18px;align-items:center;border-top:1px solid var(--rule);}
1366
1390
  #view-manage .pickLabel{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-faint);padding:14px 0;}