@bongos/core 1.19.407 → 1.19.409

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.
@@ -226,6 +226,35 @@ await ta('upsertProject carries the declared type, COALESCE-preserved like tagli
226
226
  assert.ok(!pool2.calls[0].params.includes('spacefaring'), 'unknown types never reach the SQL');
227
227
  });
228
228
 
229
+ await ta('upsertProject carries the planet template, COALESCE-preserved; an unknown name is not a choice (task 1003319)', async () => {
230
+ const pool = mockPool([{ rows: [{ id: '1', origin: 'https://p', template: 'bead-wash' }] }]);
231
+ await P.upsertProject({ origin: 'https://p', name: 'Proj', template: 'bead-wash' }, { pool });
232
+ const c = pool.calls[0];
233
+ assert.match(c.text, /template\s*=\s*COALESCE/, 'a later templateless upsert must not erase a curated template');
234
+ assert.ok(c.params.includes('bead-wash'), 'the template value is parameterized');
235
+ const pool2 = mockPool([{ rows: [{ id: '2' }] }]);
236
+ await P.upsertProject({ origin: 'https://q', name: 'Q', template: 'saucer-neon' }, { pool: pool2 });
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`);
247
+ }
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}`);
251
+ // and the public projection carries it, so the sky can read a curated choice
252
+ const pool3 = mockPool([{ rows: [] }]);
253
+ await P.listFeaturedProjects({ pool: pool3 });
254
+ const sel = (pool3.calls[0].text.match(/SELECT([\s\S]*?)FROM/i) || [, ''])[1];
255
+ assert.match(sel, /\btemplate\b/, 'the featured projection carries the template');
256
+ });
257
+
229
258
  await ta('listFeaturedProjects returns a PUBLIC projection (featured+active only, no owner/status)', async () => {
230
259
  const pool = mockPool([{ rows: [{ origin: 'https://p', name: 'Proj', tagline: 't', art_url: null, created_at: '2026-08-01T00:00:00Z' }] }]);
231
260
  const out = await P.listFeaturedProjects({ pool });
@@ -465,7 +494,10 @@ await ta('upsertActivity upserts per (client_id, github_id) with the talent snap
465
494
  const c = pool.calls[0];
466
495
  assert.match(c.text, /INSERT INTO platform_identity_builder_project_activity/);
467
496
  assert.match(c.text, /ON CONFLICT \(client_id, github_id\) DO UPDATE/);
468
- assert.deepEqual(c.params, ['c1', 42, 100, 5, 3, 'metic', ['engineer']]);
497
+ // recent_ships (task 1002803) rides the same row as a jsonb STRING param —
498
+ // an empty list when the sender reported none, never NULL. Its own cap,
499
+ // shape and link bound are covered by tests/hub_recent_ships.mjs.
500
+ assert.deepEqual(c.params, ['c1', 42, 100, 5, 3, 'metic', ['engineer'], '[]']);
469
501
  });
470
502
 
471
503
  await ta('upsertActivity sanitizes third-party self-report (ADR 0171 D7): off-ladder rank → null, disciplines validated + capped', async () => {
@@ -568,7 +600,9 @@ await ta('getRollupForBuilder aggregates un-fanned + names only publicly-catalog
568
600
  assert.deepEqual(out.totals, { projects: 2, credits: 150, works_shipped: 7, karma: 4 });
569
601
  assert.deepEqual(out.disciplines, ['engineer', 'ui']);
570
602
  assert.equal(out.projects.length, 1, 'only the public catalog row is named');
571
- assert.deepEqual(out.projects[0], { name: 'Public Proj', origin: 'https://pub.example', rank_held: 'metic' });
603
+ // recent_ships rides the project row it belongs to (task 1002803), so a work
604
+ // title can only surface where the project name already may.
605
+ assert.deepEqual(out.projects[0], { name: 'Public Proj', origin: 'https://pub.example', rank_held: 'metic', recent_ships: [] });
572
606
  assert.equal(out.more_projects, 1, 'the non-public project is counted, never named');
573
607
  // The whitelist holds structurally: client_id never appears in the shape.
574
608
  assert.ok(!JSON.stringify(out).includes('client_id'), 'client_id must not cross the port');
@@ -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","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,"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,"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":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}]}
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.407",
3
+ "version": "1.19.409",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.407",
9
+ "version": "1.19.409",
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.407",
3
+ "version": "1.19.409",
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",
@@ -69,7 +69,6 @@
69
69
  /* flat, not a gradient to white: one ground from the top bar to the footer.
70
70
  A fade-out at the footer would reintroduce the fog. */
71
71
  --sky-bg:var(--bg);
72
- --star-display:none;
73
72
  --chrome-ink:var(--ink);
74
73
  --chrome-ink-dim:var(--ink-soft);
75
74
  --chrome-accent:var(--accent-deep);
@@ -109,7 +108,6 @@
109
108
  --on-ok:#0b2e18;
110
109
  --page-bg:var(--bg);
111
110
  --sky-bg:var(--bg);
112
- --star-display:block;
113
111
  --chrome-ink:var(--ink);
114
112
  --chrome-ink-dim:var(--ink-soft);
115
113
  --chrome-accent:var(--accent-deep);
@@ -132,6 +130,7 @@
132
130
  --label-space:.1em;
133
131
  /* the view masthead's object (task 1003332): dark drops the contact shadow */
134
132
  --mast-shadow:none;
133
+ --orb-shadow:opacity(1);
135
134
  color-scheme:dark;
136
135
  }
137
136
  @media (prefers-color-scheme: dark){
@@ -146,7 +145,6 @@
146
145
  --on-ok:#0b2e18;
147
146
  --page-bg:var(--bg);
148
147
  --sky-bg:var(--bg);
149
- --star-display:block;
150
148
  --chrome-ink:var(--ink);
151
149
  --chrome-ink-dim:var(--ink-soft);
152
150
  --chrome-accent:var(--accent-deep);
@@ -169,6 +167,7 @@
169
167
  --label-space:.1em;
170
168
  /* the view masthead's object (task 1003332): dark drops the contact shadow */
171
169
  --mast-shadow:none;
170
+ --orb-shadow:opacity(1);
172
171
  color-scheme:dark;
173
172
  }
174
173
  }
@@ -259,7 +258,16 @@
259
258
  Root-absolute path: a url() in a custom property resolves against the
260
259
  DOCUMENT. Light gives the object the world's one contact shadow. */
261
260
  --plate-set-cut:url(/assets/world/r2-chrome-blobs-set-cut.webp);
261
+ /* the sky's planets take the world's other two whole objects (task 1003319):
262
+ the round blob and the bongo pair, alpha cutouts in both modes for the
263
+ masthead's reason. */
264
+ --plate-bead-cut:url(/assets/world/r2-chrome-blob-one-cut.webp);
265
+ --plate-bongos-cut:url(/assets/world/r2-bongos-chrome-a-cut.webp);
262
266
  --mast-shadow:drop-shadow(0 26px 30px color-mix(in srgb,var(--ink) 20%,transparent));
267
+ /* the planet's contact shadow, scaled to a 60-130px object. Dark's twin is
268
+ a NO-OP filter (opacity(1), not none) so the search spotlight's
269
+ drop-shadow can compose after it in one filter list. */
270
+ --orb-shadow:drop-shadow(0 12px 14px color-mix(in srgb,var(--ink) 22%,transparent));
263
271
  /* the footer's measured height per width — the sky is 100vh minus the bar and
264
272
  this, so the map fits the viewport with no scrollbar (T2's rule); the
265
273
  toolbar row that used to be in that sum is gone (task 1003300) */
@@ -287,12 +295,11 @@
287
295
  html{scroll-behavior:smooth;}
288
296
  body{font-family:var(--font-body);color:var(--ink);background:var(--page-bg);min-height:100vh;-webkit-font-smoothing:antialiased;overflow-x:hidden;}
289
297
 
290
- /* ── the sky: mode-swapped — sunrise white/orange in light, the gate's deep
291
- space (verbatim space.css recipe) in dark. Fixed layer, never
298
+ /* ── the sky: the page's fixed ground layer, the page ground in both modes.
299
+ Nothing is drawn on it: the twinkling stars left with task 1003319 (the world
300
+ draws no atmosphere, and a 3.2s loop read as a pulse). Fixed layer, never
292
301
  fixed-attachment on body. ── */
293
302
  .sky{position:fixed;inset:0;z-index:0;pointer-events:none;overflow:hidden;background:var(--sky-bg);}
294
- .star{position:absolute;display:var(--star-display);border-radius:50%;background:#fff;box-shadow:0 0 3px rgba(255,255,255,.5);animation:twinkle 3.2s ease-in-out infinite;}
295
- @keyframes twinkle{0%,100%{opacity:.22}50%{opacity:.95}}
296
303
 
297
304
  /* ── header: mode-aware chrome (ink on sunrise, white + glow in space) ── */
298
305
  .top{position:relative;z-index:2;display:flex;align-items:center;justify-content:space-between;padding:20px clamp(18px,4vw,40px);}
@@ -721,55 +728,88 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
721
728
  .qWrap.is-open .qCount{display:none;}
722
729
  }
723
730
 
724
- /* ── the orb field. Ground B (owner, 2026-08-13): dual-mode full-bleed — the
725
- sky IS the page and follows light/dark, which is what --sky-bg/--star-display
726
- already encode. Deliberately STATIC: where things sit is this pass, how the
727
- sky moves is not. Component-scoped custom properties only — a page-local or
728
- component `:root` would outrank the branding pack spliced after this file. ── */
731
+ /* ── THE FIELD: the void (task 1003319; the owner's ask at the hub pick,
732
+ docs/design/projects-hub-direction-v2.md §7, the round in §11). Nothing is
733
+ drawn on the ground — no grid, no stars: DESIGN.md's world draws no
734
+ atmosphere, and its depth comes from the rendered objects themselves. The
735
+ ground is the page ground in both modes (true black / paper), closed by the
736
+ bar's hairline above and the footer's below. Where things sit is still the
737
+ slot layout; how the sky moves is task 1002762's camera. Component-scoped
738
+ custom properties only — a page-local or component `:root` would outrank
739
+ the branding pack spliced after this file. ── */
729
740
  .fieldWrap{padding:0;max-width:none;}
730
- .field{position:relative;height:calc(100vh - var(--top-h) - var(--foot-h));min-height:340px;overflow:hidden;
731
- --grid-ink:color-mix(in srgb,var(--rule) 92%,transparent);}
741
+ .field{position:relative;height:calc(100vh - var(--top-h) - var(--foot-h));min-height:340px;overflow:hidden;}
732
742
  .field .ground{position:absolute;inset:0;background:var(--sky-bg);}
733
- /* the gravity sheet, flattened: two gradients under a radial mask */
734
- .field .ground::after{content:"";position:absolute;inset:-10%;
735
- background-image:linear-gradient(var(--grid-ink) 1px,transparent 1px),
736
- linear-gradient(90deg,var(--grid-ink) 1px,transparent 1px);
737
- background-size:74px 74px;
738
- -webkit-mask-image:radial-gradient(ellipse 62% 58% at 50% 50%,#000 12%,transparent 78%);
739
- mask-image:radial-gradient(ellipse 62% 58% at 50% 50%,#000 12%,transparent 78%);}
740
- .field .stars{position:absolute;inset:0;display:var(--star-display);}
741
- .field .stars i{position:absolute;width:2px;height:2px;border-radius:50%;background:#fff;opacity:.55;}
742
743
 
743
- /* a project rendered as an orb. Colour comes from tokens, never a literal, so
744
- every accent follows a re-theme. */
744
+ /* ── THE PLANET: a project rendered as one of the world's three whole chrome
745
+ objects in the project's accent (task 1003319; DESIGN.md "Project orbs" and
746
+ "Planet templates"). The orb's box is --d wide (the physics' diameter) by
747
+ the object's own aspect; the object is the disc's ::before — a colour layer
748
+ over the plate, both clipped by the plate's alpha as a MASK, so the accent
749
+ never paints the box. SHAPE (data-shape) is a plate + one rectangle on the
750
+ landing's crop maths; STYLE (data-style) is how the accent lands: tint = a
751
+ `color` blend (the accent's hue and chroma, the chrome's light), wash =
752
+ `overlay` (the chrome lit by the accent, its iridescence kept), chrome =
753
+ the plate as rendered. Colour comes from tokens, never a literal, so every
754
+ accent follows a re-theme. The relationship ring is a circle BEHIND the
755
+ object (the rail dot's grammar), never a box-shadow: a mask clips a shadow
756
+ away, and the world has no shadow vocabulary for UI. ── */
745
757
  .worldOrb{position:absolute;transform:translate(-50%,-50%);display:flex;flex-direction:column;
746
- align-items:center;gap:9px;cursor:pointer;background:none;border:none;padding:0;text-decoration:none;}
747
- .worldOrb .disc{width:var(--d,96px);height:var(--d,96px);border-radius:50%;position:relative;
748
- background:radial-gradient(circle at 34% 28%,color-mix(in srgb,var(--acc) 22%,#fff) 0%,
749
- var(--acc) 42%,color-mix(in srgb,var(--acc) 72%,#000) 100%);
750
- box-shadow:0 0 34px -4px color-mix(in srgb,var(--acc) 55%,transparent);}
751
- .worldOrb .disc::after{content:"";position:absolute;inset:0;border-radius:50%;
752
- border:1.5px solid color-mix(in srgb,#fff 55%,transparent);}
753
- .worldOrb .nm{font-family:var(--font-display);font-weight:700;font-size:14.5px;color:var(--chrome-ink);
754
- text-shadow:var(--chrome-glow);white-space:nowrap;}
755
- .worldOrb .sub{font-family:var(--font-mono);font-size:11.5px;color:var(--chrome-accent);
756
- text-shadow:var(--chrome-glow);white-space:nowrap;}
757
- /* z-index 3: the disc's ::after rim is positioned and later in DOM order, so
758
- without this the badge is painted over by the orb it labels */
759
- .worldOrb .badge{position:absolute;top:-11px;left:50%;transform:translateX(-50%);z-index:3;
760
- font-family:var(--font-mono);font-size:10.5px;font-weight:500;padding:2px 9px;border-radius:999px;
761
- white-space:nowrap;background:var(--gold-soft);color:var(--accent-on);
762
- box-shadow:0 2px 8px color-mix(in srgb,var(--ink) 30%,transparent);}
763
- /* the owned ring is ONE ring, not two — a second cream halo read as a defect */
764
- .worldOrb[data-rel="owner"] .disc{box-shadow:0 0 34px -4px color-mix(in srgb,var(--acc) 55%,transparent),
765
- 0 0 0 3px var(--accent);}
766
- .worldOrb[data-rel="owner"] .badge{background:var(--accent);color:var(--accent-on);}
767
- /* building, not owning — a lighter claim, said differently on purpose */
768
- .worldOrb[data-rel="builder"] .disc{box-shadow:0 0 34px -4px color-mix(in srgb,var(--acc) 55%,transparent),
769
- 0 0 0 2px color-mix(in srgb,var(--accent) 55%,transparent);}
770
- .worldOrb[data-rel="builder"] .badge{background:var(--accent-soft);color:var(--accent-deep);
771
- border:1px solid color-mix(in srgb,var(--accent) 45%,transparent);}
758
+ align-items:center;gap:10px;cursor:pointer;background:none;border:none;padding:0;text-decoration:none;}
759
+ .worldOrb .disc{--dd:calc(var(--d,96px) * var(--orb-scale,1));width:var(--dd);aspect-ratio:var(--tpl-ar,1);position:relative;isolation:isolate;
760
+ filter:var(--orb-shadow);transition:translate .4s cubic-bezier(.16,1,.3,1);}
761
+ /* the landing's hover: the object rises, 400ms on the world's exponential ease-out */
762
+ .worldOrb:hover .disc{translate:0 -6px;}
763
+ .worldOrb .disc::before{content:"";position:absolute;inset:0;
764
+ background-image:linear-gradient(var(--tpl-ink),var(--tpl-ink)),var(--tpl-src);
765
+ background-repeat:no-repeat;
766
+ background-size:100% 100%,calc(var(--dd) * var(--tpl-zoom)) auto;
767
+ background-position:0 0,calc(var(--dd) * var(--tpl-px)) calc(var(--dd) * var(--tpl-py));
768
+ background-blend-mode:var(--tpl-blend),normal;
769
+ -webkit-mask-image:var(--tpl-src);mask-image:var(--tpl-src);
770
+ -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
771
+ -webkit-mask-size:calc(var(--dd) * var(--tpl-zoom)) auto;mask-size:calc(var(--dd) * var(--tpl-zoom)) auto;
772
+ -webkit-mask-position:calc(var(--dd) * var(--tpl-px)) calc(var(--dd) * var(--tpl-py));
773
+ mask-position:calc(var(--dd) * var(--tpl-px)) calc(var(--dd) * var(--tpl-py));}
774
+ /* the shapes — the same rectangles the landing's .o1/.o2/.o3 declare
775
+ (tests/projects_hub.mjs pins the numbers equal):
776
+ blob r2-chrome-blob-one u[.14,.865] v[.172,.808] the round blob, whole
777
+ pair r2-bongos-chrome-a the whole plate the chrome pair
778
+ bead r2-chrome-blobs-set u[.810,.955] v[.556,.840] the set's small bead */
779
+ .worldOrb[data-shape="blob"]{--tpl-src:var(--plate-bead-cut);--tpl-ar:1.13994;--tpl-zoom:1.37931;--tpl-px:-.19310;--tpl-py:-.23724;}
780
+ .worldOrb[data-shape="pair"]{--tpl-src:var(--plate-bongos-cut);--tpl-ar:1;--tpl-zoom:1;--tpl-px:0;--tpl-py:0;}
781
+ .worldOrb[data-shape="bead"]{--tpl-src:var(--plate-set-cut);--tpl-ar:.91476;--tpl-zoom:6.89655;--tpl-px:-5.58621;--tpl-py:-2.14014;}
782
+ /* the styles */
783
+ .worldOrb[data-style="tint"]{--tpl-ink:var(--acc);--tpl-blend:color;}
784
+ .worldOrb[data-style="wash"]{--tpl-ink:var(--acc);--tpl-blend:overlay;}
785
+ .worldOrb[data-style="chrome"]{--tpl-ink:transparent;--tpl-blend:normal;}
786
+ /* the relationship ring: yours takes the accent (the one live thing about the
787
+ orb); building there is a hairline of it */
788
+ .worldOrb[data-rel="owner"] .disc::after,.worldOrb[data-rel="builder"] .disc::after{content:"";position:absolute;
789
+ left:50%;top:50%;width:calc(100% + 18px);aspect-ratio:1;translate:-50% -50%;border-radius:50%;z-index:-1;}
790
+ .worldOrb[data-rel="owner"] .disc::after{border:1.5px solid var(--accent);}
791
+ .worldOrb[data-rel="builder"] .disc::after{border:1px solid color-mix(in srgb,var(--accent) 55%,transparent);}
792
+ /* the labels, in the world's type: the name in the ledger's title voice, the
793
+ sub-line as a label; the halo is the world's black one, dark only */
794
+ .worldOrb .nm{font-family:var(--font-body);font-weight:500;font-size:15px;letter-spacing:-.015em;color:var(--ink);
795
+ text-shadow:var(--type-halo);white-space:nowrap;}
796
+ .worldOrb .sub{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);
797
+ text-transform:uppercase;color:var(--ink-faint);white-space:nowrap;}
798
+ /* the badge states a fact (yours / you build here), so it is a hairline pill
799
+ in the label face, not an accent fill (the One Warm Thing Rule); z-index 3
800
+ keeps it over the object it labels */
801
+ .worldOrb .badge{position:absolute;top:-14px;left:50%;transform:translateX(-50%);z-index:3;
802
+ font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;
803
+ font-weight:400;padding:3px 10px;border-radius:999px;white-space:nowrap;
804
+ background:var(--bg);color:var(--ink-faint);border:1px solid var(--rule);}
805
+ .worldOrb[data-rel="owner"] .badge{color:var(--ink);border-color:var(--ink-faint);}
772
806
  .worldOrb .chip{transform:scale(.88);white-space:nowrap;}
807
+ /* narrow: the drawn object is four fifths of its box while the physics keeps
808
+ the whole box for spacing — at 390 the labels and the badges need the air
809
+ more than the object needs the size */
810
+ @media (max-width:900px){
811
+ .field{--orb-scale:.8;}
812
+ }
773
813
 
774
814
  /* what the only-mine filter shows when you are connected to nothing. Not a
775
815
  hypothetical — GET /my-projects returns { owned: [], building: [] } for the
@@ -777,16 +817,15 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
777
817
  .fieldEmpty{position:absolute;inset:0;display:none;flex-direction:column;align-items:center;
778
818
  justify-content:center;gap:14px;text-align:center;padding:26px;z-index:4;}
779
819
  .field.isEmpty .fieldEmpty{display:flex;}
780
- .fieldEmpty p{font-size:15px;font-weight:600;color:var(--chrome-ink);max-width:44ch;line-height:1.55;
781
- text-shadow:var(--chrome-glow);}
782
- .fieldEmpty .sub{font-size:13.5px;font-weight:600;color:var(--glass-ink);}
820
+ .fieldEmpty p{font-size:15.5px;font-weight:400;color:var(--ink);max-width:44ch;line-height:1.5;}
821
+ .fieldEmpty .sub{font-size:14px;font-weight:400;color:var(--ink-soft);}
783
822
 
784
- .hintbar{position:absolute;left:50%;bottom:14px;transform:translateX(-50%);display:flex;gap:8px;
785
- flex-wrap:wrap;justify-content:center;font-family:var(--font-mono);font-size:11.5px;
786
- color:var(--glass-ink);background:var(--glass-bg);border:1.5px solid var(--glass-border);
787
- border-radius:999px;padding:7px 16px;backdrop-filter:blur(4px);max-width:92%;}
788
- .hintbar kbd{font-family:var(--font-mono);background:color-mix(in srgb,var(--accent) 24%,transparent);
789
- border-radius:5px;padding:1px 6px;font-size:11px;color:var(--chrome-accent);}
823
+ /* the hint is one label line on the ground — no pill, no accent: it is
824
+ neither pressable nor live (task 1003319) */
825
+ .hintbar{position:absolute;left:50%;bottom:16px;transform:translateX(-50%);display:flex;gap:18px;
826
+ flex-wrap:wrap;justify-content:center;font-family:var(--label-face);font-size:var(--label-size);
827
+ letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-faint);max-width:92%;}
828
+ .hintbar kbd{font-family:inherit;font-size:inherit;color:var(--ink-soft);}
790
829
 
791
830
  /* ── THE TABLE IN THE WORLD (task 1003332; owner's review of the shipped chrome).
792
831
  The table is a VIEW of the same world as the sky, not a panel beside it: a
@@ -877,16 +916,16 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
877
916
  /* a dimmed orb is backdrop — no pointer, no focus (the JS drops it from the
878
917
  tab order); at .25 opacity its label could not carry interaction honestly */
879
918
  .worldOrb.dim{opacity:.25;filter:saturate(.35);pointer-events:none;}
880
- /* the spotlight glow is an OUTLINE + drop-shadow, never box-shadow: the owned
881
- ring already lives on the disc's box-shadow, and a same-specificity
882
- box-shadow here would silently delete it by source order */
883
- .worldOrb.match .disc{outline:3px solid color-mix(in srgb,var(--acc,var(--accent)) 55%,transparent);
884
- outline-offset:3px;filter:drop-shadow(0 0 16px color-mix(in srgb,var(--acc,var(--accent)) 60%,transparent));}
919
+ /* the spotlight is a drop-shadow in the accent on the disc's own filter — a
920
+ glow that follows the silhouette (the object is a mask, so an outline or a
921
+ box-shadow would be clipped away). Composed after the contact shadow, which
922
+ dark states as a no-op filter for exactly this. */
923
+ .worldOrb.match .disc{filter:var(--orb-shadow) drop-shadow(0 0 16px color-mix(in srgb,var(--acc,var(--accent)) 60%,transparent));}
885
924
  .field .hoverCard{position:absolute;z-index:6;pointer-events:none;display:flex;flex-direction:column;gap:2px;
886
- background:var(--bg-card);border:1.5px solid var(--line);border-radius:12px;box-shadow:var(--shadow-card);
925
+ background:var(--bg-card);border:1px solid var(--rule);border-radius:var(--radius-sm);
887
926
  padding:9px 13px;max-width:240px;}
888
- .field .hoverCard .hn{font-family:var(--font-display);font-weight:700;font-size:13.5px;color:var(--ink);white-space:nowrap;}
889
- .field .hoverCard .ht{font-size:12px;color:var(--ink-faint);font-weight:600;}
927
+ .field .hoverCard .hn{font-family:var(--font-body);font-weight:500;font-size:14px;letter-spacing:-.015em;color:var(--ink);white-space:nowrap;}
928
+ .field .hoverCard .ht{font-size:12.5px;color:var(--ink-soft);font-weight:400;}
890
929
 
891
930
  /* ── the project card ──
892
931
  An overlay INSIDE the map, not a destination: opening a project must not cost
@@ -327,23 +327,7 @@ summary{min-height:24px;padding:3px 0;}
327
327
  </style>
328
328
  </head>
329
329
  <body>
330
- <div class="sky" aria-hidden="true">
331
- <span class="star" style="top:2%;left:12%;width:2px;height:2px;"></span>
332
- <span class="star" style="top:5%;left:28%;width:1.5px;height:1.5px;animation-delay:.7s;"></span>
333
- <span class="star" style="top:3%;left:47%;width:2px;height:2px;animation-delay:1.4s;"></span>
334
- <span class="star" style="top:8%;left:63%;width:1.5px;height:1.5px;animation-delay:.3s;"></span>
335
- <span class="star" style="top:4%;left:78%;width:2.5px;height:2.5px;animation-delay:2s;"></span>
336
- <span class="star" style="top:10%;left:88%;width:1.5px;height:1.5px;animation-delay:1s;"></span>
337
- <span class="star" style="top:14%;left:20%;width:1.5px;height:1.5px;animation-delay:1.8s;"></span>
338
- <span class="star" style="top:22%;left:55%;width:2px;height:2px;animation-delay:.5s;"></span>
339
- <span class="star" style="top:34%;left:36%;width:1.5px;height:1.5px;animation-delay:2.4s;"></span>
340
- <span class="star" style="top:30%;left:78%;width:1.5px;height:1.5px;animation-delay:1.2s;"></span>
341
- <span class="star" style="top:48%;left:8%;width:2px;height:2px;animation-delay:.9s;"></span>
342
- <span class="star" style="top:55%;left:90%;width:1.5px;height:1.5px;animation-delay:1.6s;"></span>
343
- <span class="star" style="top:68%;left:24%;width:1.5px;height:1.5px;animation-delay:2.1s;"></span>
344
- <span class="star" style="top:76%;left:64%;width:2px;height:2px;animation-delay:.4s;"></span>
345
- <span class="star" style="top:88%;left:42%;width:1.5px;height:1.5px;animation-delay:1.9s;"></span>
346
- </div>
330
+ <div class="sky" aria-hidden="true"></div>
347
331
 
348
332
  <!-- ══ THE BAR ═══════════════════════════════════════════════════════════════
349
333
  A header of the PAGE, not of the orb field, so it never drifts, scales or
@@ -471,7 +455,6 @@ summary{min-height:24px;padding:3px 0;}
471
455
  <div class="fieldWrap" id="sub-sky-body">
472
456
  <div class="field" id="orbField">
473
457
  <div class="ground"></div>
474
- <div class="stars" id="mapStars" aria-hidden="true"></div>
475
458
  <!-- The camera (task 1002762). Pan and zoom are ONE transform on this
476
459
  layer; orbs write only their own physics delta, so the two compose
477
460
  for free and the ground, stars, empty state and hintbar stay put.
@@ -479,12 +462,13 @@ summary{min-height:24px;padding:3px 0;}
479
462
  sentinel spans, and a tag opened inside one would leave the child
480
463
  instance an unbalanced tree. -->
481
464
  <div class="skyCam" id="skyCam">
482
- <!-- The platform's own orb. Static markup, not JS-built, precisely so the
465
+ <!-- The platform's own orb: the bongo pair in chrome, the platform's own
466
+ mark (task 1003319). Static markup, not JS-built, precisely so the
483
467
  apex-only sentinels can remove it: a child instance has no "this
484
468
  platform" to point at, and stripApexOnly() is a string transform that
485
469
  cannot reach anything a script renders later. -->
486
470
  <!--apex-only:start-->
487
- <a class="worldOrb" id="selfOrb" href="/" style="--acc:var(--accent);--d:122px;left:50%;top:44%">
471
+ <a class="worldOrb" id="selfOrb" href="/" data-shape="pair" data-style="chrome" data-template="pair-chrome" style="--acc:var(--accent);--d:132px;left:50%;top:44%">
488
472
  <span class="disc"></span>
489
473
  <span class="nm" data-brand-product-inline>Cloud Bongos</span>
490
474
  <span class="sub">this platform</span>
@@ -1282,9 +1266,12 @@ summary{min-height:24px;padding:3px 0;}
1282
1266
  The orbs are placed on fixed slots: where things sit is this pass; how the
1283
1267
  sky moves is goal 1000031. ══ */
1284
1268
 
1269
+ /* the third number is the orb's width (--d, the physics' diameter). A
1270
+ cutout's silhouette reads a quarter smaller than a filled disc of the
1271
+ same box, so the slots grew by that with the planets (task 1003319). */
1285
1272
  var ORB_SLOTS = [
1286
- [22, 30, 74], [78, 66, 60], [33, 74, 56], [70, 26, 64],
1287
- [13, 58, 52], [88, 42, 50], [50, 80, 54], [62, 48, 58]
1273
+ [22, 30, 92], [78, 66, 75], [33, 74, 70], [70, 26, 80],
1274
+ [13, 58, 65], [88, 42, 62], [50, 80, 68], [62, 48, 72]
1288
1275
  ];
1289
1276
  /* ONE accent palette for every orb on the page — the map's spheres and the
1290
1277
  manage cards' — so a project reads the same colour in both views.
@@ -1294,6 +1281,16 @@ summary{min-height:24px;padding:3px 0;}
1294
1281
  deriving it from contract tokens is what makes it survive a re-brand. */
1295
1282
  var ORB_ACCENTS = ['var(--sea)', 'var(--terracotta)', 'var(--gold-soft)', 'var(--sun-lit)', 'var(--accent)'];
1296
1283
  var ORB_GLOWS = ORB_ACCENTS.map(function (t) { return 'color-mix(in srgb,' + t + ' 45%,transparent)'; });
1284
+ /* THE PLANET TEMPLATES (task 1003319). A project's orb is one of the world's
1285
+ three whole chrome objects (blob / pair / bead, on the landing's own crop
1286
+ maths) in the project's accent, styled tint (the accent replaces the
1287
+ chrome's colour), wash (the accent lights it) or chrome (as rendered). A
1288
+ curated `template` on the catalog row wins; otherwise the same canonical
1289
+ walk that assigns the accent assigns the template, and the order here IS
1290
+ the walk: consecutive positions change both the shape and the style, and
1291
+ nine templates against five accents repeat only every forty-five projects.
1292
+ Mirrors platform-identity's PROJECT_TEMPLATES (the DB CHECK). */
1293
+ var ORB_TEMPLATES = ['blob-tint', 'pair-wash', 'bead-chrome', 'blob-wash', 'pair-chrome', 'bead-tint', 'blob-chrome', 'pair-tint', 'bead-wash'];
1297
1294
  var catalog = [];
1298
1295
  var mineOrigins = {}; /* normalized host → 'owner' | 'builder' */
1299
1296
  var ownedInstances = []; /* the viewer's OWN provisioning rows (never torn_down) */
@@ -1329,16 +1326,26 @@ summary{min-height:24px;padding:3px 0;}
1329
1326
  function assignAccents() {
1330
1327
  catalog.slice()
1331
1328
  .sort(function (a, b) { return String(a.origin).localeCompare(String(b.origin)); })
1332
- .forEach(function (p, i) { p._acc = ORB_ACCENTS[i % ORB_ACCENTS.length]; });
1329
+ .forEach(function (p, i) {
1330
+ p._acc = ORB_ACCENTS[i % ORB_ACCENTS.length];
1331
+ p._tpl = ORB_TEMPLATES[i % ORB_TEMPLATES.length];
1332
+ });
1333
1333
  }
1334
1334
  function accentOf(p) { return p._acc || ORB_ACCENTS[0]; }
1335
+ /* a curated template wins over the walk; an unknown name is not a choice */
1336
+ function templateOf(p) {
1337
+ return ORB_TEMPLATES.indexOf(p.template) !== -1 ? p.template : (p._tpl || ORB_TEMPLATES[0]);
1338
+ }
1335
1339
  /* the manage cards' accent rule — one project, one colour, in the sky AND
1336
1340
  the list. Shared with orbFor(), which used to carry this hash inline. */
1337
- function slugAccent(slug) {
1341
+ function slugHash(slug) {
1338
1342
  var h = 0, s = String(slug || '');
1339
1343
  for (var i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0;
1340
- return h % ORB_ACCENTS.length;
1344
+ return h;
1341
1345
  }
1346
+ function slugAccent(slug) { return slugHash(slug) % ORB_ACCENTS.length; }
1347
+ /* an owned-only entry has no catalog row to carry a template: its slug walks */
1348
+ function slugTemplate(slug) { return ORB_TEMPLATES[slugHash(slug) % ORB_TEMPLATES.length]; }
1342
1349
  function relOf(p) {
1343
1350
  /* an instance the viewer provisioned is a server-resolved authority too —
1344
1351
  own-scoped by owner_builder_id, no client inference (direction §2
@@ -1431,6 +1438,11 @@ summary{min-height:24px;padding:3px 0;}
1431
1438
  a.setAttribute('data-generated', '');
1432
1439
  var rel = relOf(p);
1433
1440
  a.setAttribute('data-rel', rel);
1441
+ /* the planet: shape and style as data attributes, cosmos.css does the rest */
1442
+ var tpl = templateOf(p), tplParts = tpl.split('-');
1443
+ a.setAttribute('data-template', tpl);
1444
+ a.setAttribute('data-shape', tplParts[0]);
1445
+ a.setAttribute('data-style', tplParts[1]);
1434
1446
  var inst = p._inst;
1435
1447
  var href = safeHttpUrl(p.origin);
1436
1448
  if (inst) {
@@ -1533,7 +1545,7 @@ summary{min-height:24px;padding:3px 0;}
1533
1545
  name: inst.slug, tagline: inst.domain || '',
1534
1546
  origin: inst.domain ? 'https://' + inst.domain : '',
1535
1547
  builders: null, tags: null, type: inst.type || null,
1536
- _inst: inst, _acc: ORB_ACCENTS[slugAccent(inst.slug)]
1548
+ _inst: inst, _acc: ORB_ACCENTS[slugAccent(inst.slug)], _tpl: slugTemplate(inst.slug)
1537
1549
  });
1538
1550
  });
1539
1551
  var list = browse.filter(function (p) {
@@ -2213,7 +2225,8 @@ summary{min-height:24px;padding:3px 0;}
2213
2225
  that must not interpenetrate — sits above it; collisions measure from
2214
2226
  the disc's own center */
2215
2227
  var disc = el.querySelector('.disc');
2216
- var discOff = disc ? (disc.offsetTop + r) - el.offsetHeight / 2 : 0;
2228
+ /* a planet's box is not square (task 1003319): its own height, not r */
2229
+ var discOff = disc ? (disc.offsetTop + disc.offsetHeight / 2) - el.offsetHeight / 2 : 0;
2217
2230
  /* reduced motion IS the mock: no carry-over, every orb sits exactly on
2218
2231
  its slot (C9) — carrying frozen positions across a re-sort would
2219
2232
  leave zero-motion orbs parked off their own anchors forever */
@@ -2491,19 +2504,6 @@ summary{min-height:24px;padding:3px 0;}
2491
2504
  } else { stopSky(); hideHover(); }
2492
2505
  }
2493
2506
 
2494
- /* the starfield only paints where --star-display is block (dark mode) */
2495
- (function stars() {
2496
- var frag = document.createDocumentFragment();
2497
- for (var i = 0; i < 90; i++) {
2498
- var s = document.createElement('i');
2499
- s.style.left = ((i * 37 + 11) % 100) + '%';
2500
- s.style.top = ((i * 61 + 7) % 100) + '%';
2501
- s.style.opacity = (0.25 + ((i * 13) % 50) / 100).toFixed(2);
2502
- frag.appendChild(s);
2503
- }
2504
- $('mapStars').appendChild(frag);
2505
- })();
2506
-
2507
2507
  /* ══ THE CREATE WIZARD (task 1002763) — absorbed whole from /create, which
2508
2508
  now 302s here. An inner scope so its ~40 helpers stay out of the hub's
2509
2509
  namespace; it shares the hub's $, esc, API and `me`, so there is one
@@ -8,7 +8,7 @@
8
8
  "ignoreControls": [".skip", ".vh"],
9
9
  "states": {
10
10
  "out-map": { "auth": false, "expect": { "expanded": false } },
11
- "in-map": { "auth": true, "expect": { "expanded": false } },
11
+ "in-map": { "auth": true, "expect": { "expanded": false, "count": [["#orbField .worldOrb[data-shape][data-style]", 4]], "attr": [["#selfOrb", "data-template", "pair-chrome"], ["#orbField a.worldOrb[data-origin=\"https://amazonprimea.com\"]", "data-template", "bead-wash"], ["#orbField a.worldOrb[data-origin=\"https://demo.cloudbongos.com\"]", "data-shape", "pair"], ["#orbField a.worldOrb[data-origin=\"https://emersonian-circles.cloudbongos.com\"]", "data-template", "bead-chrome"]] } },
12
12
  "in-map-projects": { "auth": true, "actions": [["click", "#projTrig"], ["wait", 350]], "expect": { "expanded": true, "open": "#menuProjects" } },
13
13
  "in-map-profile": { "auth": true, "actions": [["click", "#who"], ["wait", 350]], "expect": { "expanded": true, "open": "#menuProfile" } },
14
14
  "in-map-kbd": { "auth": true, "actions": [["focus", "#projTrig"], ["press", "Enter"], ["wait", 350], ["press", "ArrowDown"], ["wait", 120]], "expect": { "expanded": true, "activeText": "Table" } },