@bongos/core 1.19.406 → 1.19.408

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.
@@ -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" } },
package/src/module-api.js CHANGED
@@ -1,22 +1,13 @@
1
1
  // src/module-api.js — THE PUBLISHED, SEMVER'D DOORWAY (ADR 0083 / design 01§3).
2
- //
3
- // This is the ONLY core file a module may `require`. It re-exports the kernel
4
- // capabilities a module legitimately needs; everything else in the core is
5
- // off-limits to modules. The one-way rule (enforced by the fitness checks in
6
- // BV1.R40 once modules exist):
7
- //
8
- // module ──require──▶ src/module-api.js ──▶ kernel
9
- // core ──✗──────────────────────────────▶ module (forbidden)
10
- // module ──✗──────────────────────────────▶ module (forbidden; use a seam)
11
- //
12
- // Adding to this surface is a MINOR semver bump (safe — existing modules keep
13
- // working). Changing or removing an export is a MAJOR bump (breaking). Each
14
- // module declares the `coreVersion` it supports; `bongos upgrade` (BV1.R50)
15
- // pre-checks every enabled module against CORE_VERSION below.
16
- //
17
- // Introduced by the doorway task (BV1.R37 / task 1406). The seam registry is in
18
- // src/module-seams.js (hardened in BV1.R39 / task 1408); the loader that mounts
19
- // modules behind kernel-composed auth is BV1.R41 / task 1410.
2
+ // The ONLY core file a module may `require`: it re-exports the kernel capabilities a
3
+ // module legitimately needs; everything else in the core is off-limits. The one-way
4
+ // rule (fitness-enforced, BV1.R40): module ──require──▶ src/module-api.js ──▶ kernel;
5
+ // core ──✗──▶ module and module ──✗──▶ module are forbidden (use a seam).
6
+ // Adding an export is a MINOR semver bump (safe); changing or removing one is MAJOR.
7
+ // Each module declares the `coreVersion` it supports; `bongos upgrade` (BV1.R50)
8
+ // pre-checks every enabled module against CORE_VERSION below. Introduced by BV1.R37
9
+ // (task 1406); seams in src/module-seams.js (BV1.R39 / task 1408); the loader that
10
+ // mounts modules behind kernel-composed auth is BV1.R41 / task 1410.
20
11
 
21
12
  const auth = require('./bongos/auth');
22
13
  const util = require('node:util');
@@ -1252,8 +1243,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
1252
1243
  // landed since 1.19.404 with no explicit bump. run 33212035244. (task 1002620)
1253
1244
  // 1.19.406 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1254
1245
  // landed since 1.19.405 with no explicit bump. run 33213981964. (task 1002620)
1246
+ // 1.19.407 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1247
+ // landed since 1.19.406 with no explicit bump. run 33214451873. (task 1002620)
1248
+ // 1.19.408 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1249
+ // landed since 1.19.407 with no explicit bump. run 33216756796. (task 1002620)
1255
1250
  // ---------------------------------------------------------------------------
1256
- const CORE_VERSION = '1.19.406'; // CI auto-patch carrier (ADR 0161); see changelog above
1251
+ const CORE_VERSION = '1.19.408'; // CI auto-patch carrier (ADR 0161); see changelog above
1257
1252
 
1258
1253
  // A namespaced logger so a module's log lines are attributable + consistent.
1259
1254
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -1399,3 +1399,112 @@ test('the joinability in-flight line shares the visibility section’s push pred
1399
1399
  assert.match(SCRIPT, /\$\('mJoin'\)\.addEventListener\('click'[\s\S]{0,200}data-join-save[\s\S]{0,40}doJoinSave\(\)/,
1400
1400
  'the Save control is wired to ITS save — a copy that left the listener on #mVis ships a dead button');
1401
1401
  });
1402
+
1403
+ /* ── the planets and the field (task 1003319) — the map's orbs as the landing's
1404
+ chrome objects in the project's accent, a per-project template with a
1405
+ deterministic default walk, and the field brought to the world. ── */
1406
+ test('the planet templates: nine names, the walk changes shape and style each step, the catalog and the migration agree', () => {
1407
+ const list = (SCRIPT.match(/var ORB_TEMPLATES = \[([^\]]*)\]/) || [, ''])[1];
1408
+ const names = [...list.matchAll(/'([a-z-]+)'/g)].map((m) => m[1]);
1409
+ assert.equal(names.length, 9);
1410
+ assert.equal(new Set(names).size, 9, 'nine distinct templates');
1411
+ for (const t of names) assert.match(t, /^(blob|pair|bead)-(tint|wash|chrome)$/);
1412
+ for (let i = 1; i < names.length; i++) {
1413
+ const [s0, y0] = names[i - 1].split('-');
1414
+ const [s1, y1] = names[i].split('-');
1415
+ assert.ok(s0 !== s1 && y0 !== y1, `walk step ${i} changes both shape and style`);
1416
+ }
1417
+ const pi = fs.readFileSync(path.join(ROOT, 'modules', 'platform-identity', 'platform-identity.js'), 'utf8');
1418
+ const server = [...((pi.match(/const PROJECT_TEMPLATES = \[([\s\S]*?)\];/) || [, ''])[1]).matchAll(/'([a-z-]+)'/g)].map((m) => m[1]);
1419
+ assert.deepEqual(server, names, 'the hub and the catalog walk the same list in the same order');
1420
+ const mig = fs.readFileSync(path.join(ROOT, 'modules', 'platform-identity', 'migrations', 'platform_identity_012_project_template.sql'), 'utf8');
1421
+ for (const t of names) assert.ok(mig.includes(`'${t}'`), `migration 012 names ${t}`);
1422
+ });
1423
+
1424
+ test('the walk assigns the template by canonical position beside the accent; a curated template wins; the platform is the pair in chrome', () => {
1425
+ assert.match(SCRIPT, /function assignAccents[\s\S]*?p\._acc = ORB_ACCENTS\[i % ORB_ACCENTS\.length\];\s*p\._tpl = ORB_TEMPLATES\[i % ORB_TEMPLATES\.length\];/,
1426
+ 'one walk, two assignments');
1427
+ assert.match(SCRIPT, /function templateOf\(p\) \{\s*return ORB_TEMPLATES\.indexOf\(p\.template\) !== -1 \? p\.template : \(p\._tpl \|\| ORB_TEMPLATES\[0\]\);/,
1428
+ 'a curated name wins; an unknown name is not a choice');
1429
+ assert.match(SCRIPT, /_acc: ORB_ACCENTS\[slugAccent\(inst\.slug\)\], _tpl: slugTemplate\(inst\.slug\)/, 'an owned-only entry walks by its slug');
1430
+ assert.match(SCRIPT, /var tpl = templateOf\(p\), tplParts = tpl\.split\('-'\);\s*a\.setAttribute\('data-template', tpl\);\s*a\.setAttribute\('data-shape', tplParts\[0\]\);\s*a\.setAttribute\('data-style', tplParts\[1\]\);/,
1431
+ 'the orb carries its template as data attributes; the sheet does the rest');
1432
+ assert.match(HUB, /<a class="worldOrb" id="selfOrb" href="\/" data-shape="pair" data-style="chrome" data-template="pair-chrome"/, 'the platform is its own mark');
1433
+ });
1434
+
1435
+ test("the planet is the landing's object on the landing's crop maths: the three rectangles are byte-equal across the two sheets", () => {
1436
+ const LANDING = fs.readFileSync(path.join(ROOT, 'public-landing', 'index.html'), 'utf8');
1437
+ const landingCrop = (cls) => {
1438
+ const m = LANDING.match(new RegExp(`\\.${cls}\\{[^}]*--bead-ar:([^;]+);--bead-zoom:([^;]+);--bead-px:([^;]+);--bead-py:([^;}]+)`));
1439
+ assert.ok(m, `the landing declares .${cls}`);
1440
+ return m.slice(1, 5).map((s) => s.trim());
1441
+ };
1442
+ const hubCrop = (shape) => {
1443
+ const m = COSMOS.match(new RegExp(`\\.worldOrb\\[data-shape="${shape}"\\]\\{--tpl-src:var\\((--plate-[a-z-]+)\\);--tpl-ar:([^;]+);--tpl-zoom:([^;]+);--tpl-px:([^;]+);--tpl-py:([^;}]+)`));
1444
+ assert.ok(m, `the hub declares the ${shape}`);
1445
+ return { src: m[1], crop: m.slice(2, 6).map((s) => s.trim()) };
1446
+ };
1447
+ assert.deepEqual(hubCrop('blob').crop, landingCrop('o1'));
1448
+ assert.equal(hubCrop('blob').src, '--plate-bead-cut');
1449
+ assert.deepEqual(hubCrop('pair').crop, landingCrop('o2'));
1450
+ assert.equal(hubCrop('pair').src, '--plate-bongos-cut');
1451
+ assert.deepEqual(hubCrop('bead').crop, landingCrop('o3'));
1452
+ assert.equal(hubCrop('bead').src, '--plate-set-cut');
1453
+ // the plates by root-absolute token in the hub's own sheet (a url() in a custom property resolves against the document)
1454
+ assert.match(COSMOS, /--plate-bead-cut:url\(\/assets\/world\/r2-chrome-blob-one-cut\.webp\);/);
1455
+ assert.match(COSMOS, /--plate-bongos-cut:url\(\/assets\/world\/r2-bongos-chrome-a-cut\.webp\);/);
1456
+ });
1457
+
1458
+ test('the style is a blend on a masked colour layer; the accent never paints the box; the ring and the spotlight follow the silhouette', () => {
1459
+ assert.match(COSMOS, /\.worldOrb \.disc::before\{content:"";position:absolute;inset:0;\s*background-image:linear-gradient\(var\(--tpl-ink\),var\(--tpl-ink\)\),var\(--tpl-src\);/);
1460
+ assert.match(COSMOS, /background-blend-mode:var\(--tpl-blend\),normal;\s*-webkit-mask-image:var\(--tpl-src\);mask-image:var\(--tpl-src\);/, 'the mask is the plate itself');
1461
+ assert.match(COSMOS, /\.worldOrb\[data-style="tint"\]\{--tpl-ink:var\(--acc\);--tpl-blend:color;\}/);
1462
+ assert.match(COSMOS, /\.worldOrb\[data-style="wash"\]\{--tpl-ink:var\(--acc\);--tpl-blend:overlay;\}/);
1463
+ assert.match(COSMOS, /\.worldOrb\[data-style="chrome"\]\{--tpl-ink:transparent;--tpl-blend:normal;\}/);
1464
+ assert.match(COSMOS, /\.worldOrb \.disc\{--dd:calc\(var\(--d,96px\) \* var\(--orb-scale,1\)\);width:var\(--dd\);aspect-ratio:var\(--tpl-ar,1\);position:relative;isolation:isolate;\s*filter:var\(--orb-shadow\);/,
1465
+ "the box is --d (scaled) by the object's own aspect");
1466
+ assert.doesNotMatch(COSMOS, /var\(--d,96px\) \* var\(--tpl-/, 'every crop calc reads the scaled width');
1467
+ assert.match(COSMOS, /@media \(max-width:900px\)\{\s*\.field\{--orb-scale:\.8;\}/, 'narrow: the object is four fifths of its box; the physics keeps the box');
1468
+ assert.doesNotMatch(COSMOS, /\.worldOrb \.disc\{[^}]*radial-gradient/, 'no painted ball');
1469
+ assert.doesNotMatch(COSMOS, /\.worldOrb\[data-rel="owner"\] \.disc\{box-shadow/, 'a mask clips a box-shadow away');
1470
+ assert.match(COSMOS, /\.worldOrb\[data-rel="owner"\] \.disc::after\{border:1\.5px solid var\(--accent\);\}/, 'the ring is a circle behind the object');
1471
+ assert.match(COSMOS, /\.worldOrb\.match \.disc\{filter:var\(--orb-shadow\) drop-shadow\(/, 'the spotlight composes after the contact shadow');
1472
+ assert.doesNotMatch(COSMOS, /\.worldOrb\.match \.disc\{outline/);
1473
+ assert.match(COSMOS, /--orb-shadow:drop-shadow\(0 12px 14px color-mix\(in srgb,var\(--ink\) 22%,transparent\)\);/);
1474
+ assert.equal((COSMOS.match(/--orb-shadow:opacity\(1\);/g) || []).length, 2, 'dark states the no-op filter in both twins');
1475
+ assert.match(COSMOS, /\.worldOrb:hover \.disc\{translate:0 -6px;\}/, "the landing's hover rise");
1476
+ assert.match(SCRIPT, /var discOff = disc \? \(disc\.offsetTop \+ disc\.offsetHeight \/ 2\) - el\.offsetHeight \/ 2 : 0;/,
1477
+ "the physics measures the disc's own height: a planet's box is not square");
1478
+ });
1479
+
1480
+ test("the field is the void: no drawn grid, no stars, no twinkle; the labels, the badge and the hint in the world's type", () => {
1481
+ assert.doesNotMatch(COSMOS, /--grid-ink/);
1482
+ assert.doesNotMatch(COSMOS, /\.field \.ground::after/);
1483
+ assert.doesNotMatch(COSMOS, /--star-display/);
1484
+ assert.doesNotMatch(COSMOS, /@keyframes twinkle/);
1485
+ assert.doesNotMatch(HUB, /class="star"/);
1486
+ assert.doesNotMatch(HUB, /id="mapStars"/);
1487
+ assert.doesNotMatch(SCRIPT, /function stars\(\)/);
1488
+ assert.match(HUB, /<div class="sky" aria-hidden="true"><\/div>/, 'the ground layer stays, empty');
1489
+ assert.match(COSMOS, /\.worldOrb \.nm\{font-family:var\(--font-body\);font-weight:500;font-size:15px;letter-spacing:-\.015em;color:var\(--ink\);\s*text-shadow:var\(--type-halo\);/,
1490
+ "the name in the ledger's title voice, the world's black halo");
1491
+ assert.match(COSMOS, /\.worldOrb \.sub\{font-family:var\(--label-face\);/);
1492
+ assert.match(COSMOS, /\.worldOrb \.badge\{[^}]*background:var\(--bg\);color:var\(--ink-faint\);border:1px solid var\(--rule\);\}/, 'a hairline pill');
1493
+ assert.doesNotMatch(COSMOS, /\.worldOrb \.badge\{[^}]*--gold-soft/);
1494
+ assert.doesNotMatch(COSMOS, /\.worldOrb\[data-rel="owner"\] \.badge\{background:var\(--accent\)/, 'a fact is not an accent fill (the One Warm Thing Rule)');
1495
+ assert.match(COSMOS, /\.hintbar\{[^}]*font-family:var\(--label-face\);/);
1496
+ assert.doesNotMatch(COSMOS, /\.hintbar\{[^}]*border-radius:999px/, 'no pill');
1497
+ assert.doesNotMatch(COSMOS, /\.hintbar kbd\{[^}]*--accent/, 'no accent on a hint');
1498
+ assert.doesNotMatch(COSMOS, /\.field \.hoverCard\{[^}]*box-shadow/, 'no shadow vocabulary for UI');
1499
+ });
1500
+
1501
+ test('the kit proves the field path: the fixture carries a curated template and the states file reads it back', () => {
1502
+ const fixture = JSON.parse(fs.readFileSync(path.join(ROOT, 'modules', 'ui-design', 'kit', 'fixtures', 'featured.json'), 'utf8'));
1503
+ const otb = fixture.projects.find((p) => p.origin === 'https://amazonprimea.com');
1504
+ assert.equal(otb.template, 'bead-wash');
1505
+ assert.ok(fixture.projects.every((p) => 'template' in p), 'the wire never drops the key');
1506
+ const states = JSON.parse(fs.readFileSync(path.join(ROOT, 'public-landing', 'projects.states.json'), 'utf8'));
1507
+ const attrs = states.states['in-map'].expect.attr.map((a) => a.join(' '));
1508
+ assert.ok(attrs.includes('#selfOrb data-template pair-chrome'));
1509
+ assert.ok(attrs.some((a) => a.includes('amazonprimea.com') && a.endsWith('data-template bead-wash')), 'the curated template reaches the orb');
1510
+ });