@bongos/core 1.24.55 → 1.24.57

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 (37) hide show
  1. package/.bongos-core.json +67 -37
  2. package/docs/copy-inventory.md +16 -16
  3. package/docs/copy-registry.json +17 -17
  4. package/docs/module-api-changelog.md +4 -0
  5. package/docs/page-readings.json +18 -18
  6. package/modules/hall-ui/public/approval-queue.css +9 -9
  7. package/modules/hall-ui/public/atlas.css +5 -1
  8. package/modules/hall-ui/public/atlas.js +14 -11
  9. package/modules/hall-ui/public/city-draw.js +14 -14
  10. package/modules/hall-ui/public/sky-draw.js +24 -6
  11. package/modules/hall-ui/public/sky-interaction.js +1 -1
  12. package/modules/hall-ui/public/sky-panel.js +1 -1
  13. package/modules/hall-ui/public/studio.css +22 -22
  14. package/modules/hall-ui/public/thinking.css +46 -48
  15. package/modules/hall-ui/public/tweak-editor.css +18 -18
  16. package/modules/hall-ui/public/welcome-boot.css +5 -8
  17. package/modules/hall-ui/public/welcome-boot.js +11 -15
  18. package/package-lock.json +2 -2
  19. package/package.json +1 -1
  20. package/release-notes.json +12 -0
  21. package/scripts/bongos/css-literal-scan.js +144 -0
  22. package/scripts/bongos/design-generators-fresh.js +32 -0
  23. package/scripts/bongos/fitness-ratchets.js +70 -3
  24. package/scripts/bongos/fitness.js +3 -2
  25. package/scripts/gds/css-literal-scan.js +7 -0
  26. package/scripts/gds/design-generators-fresh.js +6 -0
  27. package/src/bongos/module-scope-map.js +1 -1
  28. package/src/module-api.js +1 -1
  29. package/tests/design_counts_ratchet.mjs +134 -0
  30. package/tests/hall_catalog_world.mjs +7 -2
  31. package/tests/hall_city.mjs +5 -4
  32. package/tests/hall_private_palettes.mjs +148 -0
  33. package/tests/hall_sky.mjs +29 -32
  34. package/tests/hall_street.mjs +3 -2
  35. package/tests/helpers.mjs +25 -1
  36. package/tests/welcome_boot.mjs +1 -1
  37. package/tests/welcome_boot_browser.mjs +1 -1
@@ -22,39 +22,37 @@
22
22
  too). Same reason `body` below is qualified: a pack backdrop is written as
23
23
  `body{background:…}` and the void must win. */
24
24
  body[data-page] .sky-stage {
25
- color-scheme: dark;
26
- --void: #000000; --ink: #ffffff; --ink-soft: #c0c0c0; --ink-faint: #a8a8a8; --ink-dim: #65656a; --ink-ghost: #3a3a3f;
27
- --rule: rgba(var(--sky-ink-rgb),.16); --rule-soft: rgba(var(--sky-ink-rgb),.08); --glass: rgba(var(--sky-ink-rgb),.075); --glass-strong: rgba(var(--sky-ink-rgb),.14);
28
- --accent: #fb9569; --accent-on: #2a1305; --chart: #f2d27a;
29
- --sky-ink-rgb: 255,255,255; --sky-void-rgb: 0,0,0; --sky-accent-rgb: 251,149,105; --sky-chart-rgb: 242,210,122;
30
- --sky-cold: #141418; --sky-cold-rgb: 20,20,24; --sky-drop: rgba(0,0,0,.6); --sky-temp: 1;
31
- --label: 400 11px/1.2 "JetBrains Mono", ui-monospace, Consolas, monospace; --mono: 400 13px/1.4 "JetBrains Mono", ui-monospace, Consolas, monospace;
32
- --face: Manrope, "Segoe UI", system-ui, -apple-system, sans-serif;
25
+ /* The sky's palette is the HALL's (task 1005087): the void is the hall's
26
+ ground, the ink ladder is the hall's ink at the template's alphas, and the
27
+ accent is the pack's own, so a re-branded instance's sky wears its colours.
28
+ Dark is the template's look because the hall's dark tier IS the template's
29
+ values (#000 void, #fff ink, #c0c0c0 / #a8a8a8 soft and faint); light
30
+ follows the hall's paper. sky-draw.js reads --ink, --void, --sky-cold,
31
+ --accent and --chart off the stage as colours, so the canvas repaints with
32
+ the toggle and with the pack. */
33
+ --void: var(--bg);
34
+ --ink-dim: color-mix(in srgb, var(--ink-faint) 60%, var(--bg));
35
+ --ink-ghost: color-mix(in srgb, var(--ink) 22%, var(--bg));
36
+ --rule: color-mix(in srgb, var(--ink) 16%, transparent); --rule-soft: color-mix(in srgb, var(--ink) 8%, transparent);
37
+ --glass: color-mix(in srgb, var(--ink) 7.5%, transparent); --glass-strong: color-mix(in srgb, var(--ink) 14%, transparent);
38
+ /* on paper the accent darkens until it reads: the hall's accent-ink does exactly that, and is the accent itself by night */
39
+ --accent: var(--accent-ink); --chart: var(--warn);
40
+ --sky-cold: color-mix(in srgb, var(--ink) 8%, var(--bg)); --sky-drop: rgba(0, 0, 0, .6); --sky-temp: 1;
41
+ --label: 400 11px/1.2 var(--font-mono); --mono: 400 13px/1.4 var(--font-mono);
42
+ --face: var(--font-body);
33
43
  }
34
- /* LIGHT (task 1004500, owner ask 2026-10-01). The sky follows the hall's theme:
35
- the void becomes paper and the white ladder becomes an ink ladder at the SAME
36
- alphas, so every mark keeps its place in the hierarchy; the accent and the
37
- chart yellow darken until they read on paper. Dark above is the template's,
38
- untouched. Written twice for the same reason style.css writes dark twice:
39
- html[data-theme] once shell.js runs, the media query for first paint.
40
- sky-draw.js reads these channels (--sky-*-rgb) off the stage, so the canvas
41
- repaints with the toggle. */
44
+ /* LIGHT (task 1004500): the marks keep their place on paper; only the shadow
45
+ under a card and the canvas's warmth change. Written twice, as style.css
46
+ writes its dark tier: html[data-theme] once shell.js runs, the media query
47
+ for first paint. */
42
48
  html[data-theme="light"] body[data-page] .sky-stage {
43
49
  color-scheme: light;
44
- --void: #f4f3f5; --ink: #17181a; --ink-soft: #3f4044; --ink-faint: #55565a; --ink-dim: #6c6d72; --ink-ghost: #a3a4a8;
45
- --rule: rgba(23,24,26,.16); --rule-soft: rgba(23,24,26,.08); --glass: rgba(23,24,26,.06); --glass-strong: rgba(23,24,26,.11);
46
- --accent: #b8501f; --accent-on: #ffffff; --chart: #8f6a0c;
47
- --sky-ink-rgb: 23,24,26; --sky-void-rgb: 244,243,245; --sky-accent-rgb: 184,80,31; --sky-chart-rgb: 143,106,12;
48
- --sky-cold: #dcdbde; --sky-cold-rgb: 220,219,222; --sky-drop: rgba(23,24,26,.14); --sky-temp: .42;
50
+ --sky-drop: rgba(0, 0, 0, .14); --sky-temp: .42;
49
51
  }
50
52
  @media (prefers-color-scheme: light) {
51
53
  html:not([data-theme]) body[data-page] .sky-stage {
52
54
  color-scheme: light;
53
- --void: #f4f3f5; --ink: #17181a; --ink-soft: #3f4044; --ink-faint: #55565a; --ink-dim: #6c6d72; --ink-ghost: #a3a4a8;
54
- --rule: rgba(23,24,26,.16); --rule-soft: rgba(23,24,26,.08); --glass: rgba(23,24,26,.06); --glass-strong: rgba(23,24,26,.11);
55
- --accent: #b8501f; --accent-on: #ffffff; --chart: #8f6a0c;
56
- --sky-ink-rgb: 23,24,26; --sky-void-rgb: 244,243,245; --sky-accent-rgb: 184,80,31; --sky-chart-rgb: 143,106,12;
57
- --sky-cold: #dcdbde; --sky-cold-rgb: 220,219,222; --sky-drop: rgba(23,24,26,.14); --sky-temp: .42;
55
+ --sky-drop: rgba(0, 0, 0, .14); --sky-temp: .42;
58
56
  }
59
57
  }
60
58
  html, body { height: 100%; }
@@ -128,10 +126,10 @@ html[data-fullscreen="on"] .sky-stage .top { padding-right: 184px; }
128
126
  .pill[aria-pressed="true"]::before { content: ""; display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); margin-right: 8px; vertical-align: 1px; }
129
127
  .pill.icon { width: 36px; padding: 0; display: inline-grid; place-items: center; }
130
128
  .pill.icon::before { display: none; }
131
- .spot { outline: 2px solid var(--accent) !important; outline-offset: 4px; box-shadow: 0 0 0 8px rgba(var(--sky-accent-rgb),.12), 0 0 40px rgba(var(--sky-accent-rgb),.35); }
129
+ .spot { outline: 2px solid var(--accent) !important; outline-offset: 4px; box-shadow: 0 0 0 8px color-mix(in srgb, var(--accent) 12%, transparent), 0 0 40px color-mix(in srgb, var(--accent) 35%, transparent); }
132
130
 
133
131
  .composer { position: fixed; left: 50%; top: calc(76px + env(safe-area-inset-top, 0px)); transform: translateX(-50%); width: min(620px, calc(100% - 32px)); z-index: 6; display: flex; flex-direction: column; gap: 8px; align-items: center; }
134
- .composer form { display: flex; width: 100%; height: 48px; border-radius: 999px; background: rgba(var(--sky-void-rgb),.5); box-shadow: inset 0 0 0 1px var(--rule); backdrop-filter: blur(6px); align-items: center; padding: 0 6px 0 22px; gap: 8px; }
132
+ .composer form { display: flex; width: 100%; height: 48px; border-radius: 999px; background: color-mix(in srgb, var(--void) 50%, transparent); box-shadow: inset 0 0 0 1px var(--rule); backdrop-filter: blur(6px); align-items: center; padding: 0 6px 0 22px; gap: 8px; }
135
133
  .composer form:focus-within { box-shadow: inset 0 0 0 1px var(--ink-faint); }
136
134
  .composer input { flex: 1; min-width: 0; height: 100%; background: transparent; border: 0; color: var(--ink); font: 300 17px/1 var(--face); letter-spacing: -.01em; outline: none; }
137
135
  .composer input::placeholder { color: var(--ink-dim); }
@@ -140,12 +138,12 @@ html[data-fullscreen="on"] .sky-stage .top { padding-right: 184px; }
140
138
  .gravity { min-height: 18px; font: var(--mono); font-variant-numeric: tabular-nums; color: var(--ink-dim); text-align: center; }
141
139
  .gravity b { font-weight: 400; color: var(--ink-soft); }
142
140
  .gravity .hot { color: var(--accent); }
143
- .receipt { display: flex; align-items: center; gap: 12px; height: 34px; padding: 0 8px 0 16px; border-radius: 999px; box-shadow: inset 0 0 0 1px var(--rule-soft); background: rgba(var(--sky-void-rgb),.45); color: var(--ink-soft); font-size: 13px; }
144
- .receipt .dot { width: 7px; height: 7px; border-radius: 50%; background: rgba(var(--sky-ink-rgb),.5); box-shadow: 0 0 0 4px rgba(var(--sky-ink-rgb),.05); }
141
+ .receipt { display: flex; align-items: center; gap: 12px; height: 34px; padding: 0 8px 0 16px; border-radius: 999px; box-shadow: inset 0 0 0 1px var(--rule-soft); background: color-mix(in srgb, var(--void) 45%, transparent); color: var(--ink-soft); font-size: 13px; }
142
+ .receipt .dot { width: 7px; height: 7px; border-radius: 50%; background: color-mix(in srgb, var(--ink) 50%, var(--void)); box-shadow: 0 0 0 4px color-mix(in srgb, var(--ink) 5%, transparent); }
145
143
  .receipt button { border: 0; background: transparent; color: var(--ink-dim); cursor: pointer; height: 26px; padding: 0 8px; border-radius: 999px; font-size: 12px; white-space: nowrap; flex-shrink: 0; }
146
144
  .receipt button:hover { color: var(--ink); background: var(--glass); }
147
145
 
148
- .glass { background: rgba(var(--sky-ink-rgb),.07); box-shadow: inset 0 0 0 1px rgba(var(--sky-ink-rgb),.14), inset 0 1px 0 rgba(var(--sky-ink-rgb),.12), 0 18px 40px var(--sky-drop); backdrop-filter: blur(14px) saturate(140%); -webkit-backdrop-filter: blur(14px) saturate(140%); }
146
+ .glass { background: var(--glass); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ink) 14%, transparent), inset 0 1px 0 color-mix(in srgb, var(--ink) 12%, transparent), 0 18px 40px var(--sky-drop); backdrop-filter: blur(14px) saturate(140%); -webkit-backdrop-filter: blur(14px) saturate(140%); }
149
147
  .card { position: fixed; z-index: 7; max-width: 300px; padding: 12px 14px; border-radius: 14px; pointer-events: none; color: var(--ink); font-size: 13.5px; line-height: 1.4; display: flex; flex-direction: column; gap: 5px; opacity: 0; transform: translateY(4px); transition: opacity .12s ease, transform .12s ease; }
150
148
  .card.on { opacity: 1; transform: none; }
151
149
  .card .k { display: flex; align-items: center; gap: 8px; }
@@ -159,7 +157,7 @@ html[data-fullscreen="on"] .sky-stage .top { padding-right: 184px; }
159
157
  .menu button { border: 0; background: transparent; color: var(--ink-soft); text-align: left; padding: 8px 10px; border-radius: 8px; font-size: 13.5px; cursor: pointer; }
160
158
  .menu button:hover { background: var(--glass-strong); color: var(--ink); }
161
159
 
162
- .panel { position: fixed; right: 20px; top: calc(140px + env(safe-area-inset-top, 0px)); bottom: calc(96px + env(safe-area-inset-bottom, 0px)); width: 340px; z-index: 5; display: flex; flex-direction: column; gap: 14px; padding: 22px 24px; border-radius: 16px; background: rgba(var(--sky-void-rgb),.62); box-shadow: inset 0 0 0 1px var(--rule-soft); backdrop-filter: blur(8px); overflow: auto; }
160
+ .panel { position: fixed; right: 20px; top: calc(140px + env(safe-area-inset-top, 0px)); bottom: calc(96px + env(safe-area-inset-bottom, 0px)); width: 340px; z-index: 5; display: flex; flex-direction: column; gap: 14px; padding: 22px 24px; border-radius: 16px; background: color-mix(in srgb, var(--void) 62%, transparent); box-shadow: inset 0 0 0 1px var(--rule-soft); backdrop-filter: blur(8px); overflow: auto; }
163
161
  .panel .kind { display: flex; align-items: center; gap: 12px; min-height: 28px; }
164
162
  .panel .kind .glyph { width: 32px; display: grid; place-items: center; }
165
163
  .panel h2 { margin: 0; font-size: 21px; font-weight: 300; line-height: 1.25; letter-spacing: -.02em; text-wrap: balance; }
@@ -185,7 +183,7 @@ html[data-fullscreen="on"] .sky-stage .top { padding-right: 184px; }
185
183
  .orbit-list .st { font: var(--label); letter-spacing: .1em; text-transform: uppercase; color: var(--ink-dim); flex-shrink: 0; width: 72px; }
186
184
  .orbit-list .st.live { color: var(--accent); }
187
185
 
188
- .room { position: fixed; left: 20px; top: calc(140px + env(safe-area-inset-top, 0px)); bottom: calc(96px + env(safe-area-inset-bottom, 0px)); width: 440px; z-index: 5; display: flex; flex-direction: column; gap: 14px; padding: 22px 0 0; border-radius: 16px; background: rgba(var(--sky-void-rgb),.62); box-shadow: inset 0 0 0 1px var(--rule-soft); backdrop-filter: blur(8px); }
186
+ .room { position: fixed; left: 20px; top: calc(140px + env(safe-area-inset-top, 0px)); bottom: calc(96px + env(safe-area-inset-bottom, 0px)); width: 440px; z-index: 5; display: flex; flex-direction: column; gap: 14px; padding: 22px 0 0; border-radius: 16px; background: color-mix(in srgb, var(--void) 62%, transparent); box-shadow: inset 0 0 0 1px var(--rule-soft); backdrop-filter: blur(8px); }
189
187
  .room .head { display: flex; align-items: baseline; gap: 14px; padding: 0 24px; }
190
188
  .room .head h2 { margin: 0; font-size: 21px; font-weight: 300; letter-spacing: -.02em; }
191
189
  .room .head .lbl { margin-left: auto; }
@@ -203,9 +201,9 @@ html[data-fullscreen="on"] .sky-stage .top { padding-right: 184px; }
203
201
  .empty { padding: 20px 24px; color: var(--ink-dim); font-size: 14px; }
204
202
 
205
203
  .jump { position: fixed; left: 20px; top: calc(76px + env(safe-area-inset-top, 0px)); z-index: 6; }
206
- .jump input { width: 260px; height: 40px; border-radius: 999px; border: 0; box-shadow: inset 0 0 0 1px var(--rule-soft); background: rgba(var(--sky-void-rgb),.5); color: var(--ink); padding: 0 16px; font: 400 14px/1 var(--face); }
204
+ .jump input { width: 260px; height: 40px; border-radius: 999px; border: 0; box-shadow: inset 0 0 0 1px var(--rule-soft); background: color-mix(in srgb, var(--void) 50%, transparent); color: var(--ink); padding: 0 16px; font: 400 14px/1 var(--face); }
207
205
  .jump input::placeholder { color: var(--ink-dim); }
208
- .jump ul { list-style: none; margin: 6px 0 0; padding: 6px; border-radius: 12px; background: rgba(var(--sky-void-rgb),.8); box-shadow: inset 0 0 0 1px var(--rule-soft); max-height: 300px; overflow: auto; width: 360px; }
206
+ .jump ul { list-style: none; margin: 6px 0 0; padding: 6px; border-radius: 12px; background: color-mix(in srgb, var(--void) 80%, transparent); box-shadow: inset 0 0 0 1px var(--rule-soft); max-height: 300px; overflow: auto; width: 360px; }
209
207
  .jump li button { width: 100%; text-align: left; border: 0; background: transparent; color: var(--ink-soft); padding: 8px 10px; border-radius: 8px; font-size: 13.5px; cursor: pointer; display: flex; gap: 10px; align-items: baseline; }
210
208
  .jump li button:hover, .jump li button:focus-visible { background: var(--glass); color: var(--ink); }
211
209
  .jump li .lbl { flex-shrink: 0; width: 60px; }
@@ -213,7 +211,7 @@ html[data-fullscreen="on"] .sky-stage .top { padding-right: 184px; }
213
211
  .tour, .start { position: fixed; left: 50%; bottom: calc(110px + env(safe-area-inset-bottom, 0px)); transform: translateX(-50%); width: min(560px, calc(100% - 32px)); z-index: 8; padding: 18px 22px 16px; border-radius: 16px; display: flex; flex-direction: column; gap: 10px; }
214
212
  .tour .step { display: flex; align-items: center; gap: 10px; }
215
213
  .tour .dots { display: flex; gap: 5px; margin-left: auto; }
216
- .tour .dots i { width: 5px; height: 5px; border-radius: 50%; background: rgba(var(--sky-ink-rgb),.2); }
214
+ .tour .dots i { width: 5px; height: 5px; border-radius: 50%; background: color-mix(in srgb, var(--ink) 20%, var(--void)); }
217
215
  .tour .dots i.on { background: var(--accent); }
218
216
  .tour h3, .start h3 { margin: 0; font-size: 19px; font-weight: 300; letter-spacing: -.02em; line-height: 1.25; }
219
217
  .tour p, .start p { margin: 0; font-size: 14px; line-height: 1.5; color: var(--ink-soft); }
@@ -234,7 +232,7 @@ html[data-fullscreen="on"] .sky-stage .top { padding-right: 184px; }
234
232
  .alt { margin-left: auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; text-align: right; color: var(--ink-dim); font: var(--mono); font-variant-numeric: tabular-nums; white-space: nowrap; }
235
233
  .alt b { color: var(--ink-soft); font-weight: 400; }
236
234
  .hint { color: var(--ink-ghost); }
237
- .time { display: flex; align-items: center; gap: 8px; width: min(360px, 100%); align-self: center; padding: 3px 4px 3px 4px; border-radius: 999px; background: rgba(var(--sky-void-rgb),.45); box-shadow: inset 0 0 0 1px var(--rule-soft); opacity: .8; }
235
+ .time { display: flex; align-items: center; gap: 8px; width: min(360px, 100%); align-self: center; padding: 3px 4px 3px 4px; border-radius: 999px; background: color-mix(in srgb, var(--void) 45%, transparent); box-shadow: inset 0 0 0 1px var(--rule-soft); opacity: .8; }
238
236
  .time:hover { opacity: 1; }
239
237
  .time .play { width: 24px; height: 24px; border-radius: 50%; border: 0; background: var(--glass-strong); color: var(--ink); cursor: pointer; display: grid; place-items: center; font-size: 9px; flex-shrink: 0; }
240
238
  .time .play[aria-pressed="true"] { background: var(--accent); color: var(--accent-on); }
@@ -246,7 +244,7 @@ html[data-fullscreen="on"] .sky-stage .top { padding-right: 184px; }
246
244
  .composer .mode:hover { color: var(--ink); }
247
245
  .composer .mode[aria-pressed="true"] { background: var(--glass-strong); color: var(--ink); }
248
246
  .composer .mode[aria-pressed="true"]::before { content: ""; display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); margin-right: 7px; vertical-align: 1px; }
249
- .steps { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 10px; max-width: 100%; font-size: 13px; color: var(--ink-soft); padding: 5px 8px 5px 14px; border-radius: 999px; background: rgba(var(--sky-void-rgb),.5); box-shadow: inset 0 0 0 1px var(--rule-soft); }
247
+ .steps { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 10px; max-width: 100%; font-size: 13px; color: var(--ink-soft); padding: 5px 8px 5px 14px; border-radius: 999px; background: color-mix(in srgb, var(--void) 50%, transparent); box-shadow: inset 0 0 0 1px var(--rule-soft); }
250
248
  .steps b { font: var(--label); letter-spacing: .14em; text-transform: uppercase; color: var(--accent); font-weight: 400; }
251
249
  .steps .skip { border: 0; background: transparent; color: var(--ink-dim); cursor: pointer; font-size: 12px; height: 24px; padding: 0 8px; border-radius: 999px; }
252
250
  .steps .skip:hover { color: var(--ink); background: var(--glass); }
@@ -255,16 +253,16 @@ html[data-fullscreen="on"] .sky-stage .top { padding-right: 184px; }
255
253
  .mk { display: block; width: 9px; height: 9px; border-radius: 50%; box-sizing: border-box; }
256
254
  .mk.star { background: var(--accent); box-shadow: 0 0 10px color-mix(in srgb, var(--accent) 75%, transparent); }
257
255
  .mk.star.dim { opacity: .55; box-shadow: 0 0 4px color-mix(in srgb, var(--accent) 40%, transparent); }
258
- .mk.protostar { width: 7px; height: 7px; background: rgba(var(--sky-ink-rgb),.5); box-shadow: 0 0 0 4px rgba(var(--sky-ink-rgb),.05); }
259
- .mk.satellite { width: 6px; height: 6px; background: rgba(var(--sky-ink-rgb),.4); outline: 1px solid rgba(var(--sky-ink-rgb),.16); outline-offset: 3px; }
260
- .mk.remnant { width: 12px; height: 12px; background: transparent; border: 1px solid rgba(var(--sky-ink-rgb),.4); }
261
- .mk.cold { width: 6px; height: 6px; background: var(--sky-cold); border: 1px solid rgba(var(--sky-ink-rgb),.18); }
262
- .mk.well { width: 14px; height: 14px; background: transparent; border: 1px dashed rgba(var(--sky-ink-rgb),.35); }
263
- .mk.alive { width: 14px; height: 14px; background: transparent; border: 1px solid rgba(var(--sky-ink-rgb),.35); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent), 0 0 10px var(--accent); }
264
- .mk.settled { width: 12px; height: 12px; background: transparent; border: 1px solid rgba(var(--sky-ink-rgb),.5); box-shadow: inset 0 0 0 3px var(--void), inset 0 0 0 4px rgba(var(--sky-ink-rgb),.35), 0 0 8px rgba(var(--sky-ink-rgb),.25); }
265
- .mk.collapsed { width: 9px; height: 9px; background: var(--void); border: 1px solid rgba(var(--sky-ink-rgb),.3); box-shadow: 0 0 0 2px rgba(var(--sky-void-rgb),1), 0 0 0 3px rgba(var(--sky-ink-rgb),.08); }
266
- .mk.unseen { width: 12px; height: 12px; background: transparent; border: 1px dashed rgba(var(--sky-ink-rgb),.2); }
267
- .mk.task { width: 4px; height: 4px; background: rgba(var(--sky-ink-rgb),.6); }
256
+ .mk.protostar { width: 7px; height: 7px; background: color-mix(in srgb, var(--ink) 50%, var(--void)); box-shadow: 0 0 0 4px color-mix(in srgb, var(--ink) 5%, transparent); }
257
+ .mk.satellite { width: 6px; height: 6px; background: color-mix(in srgb, var(--ink) 40%, var(--void)); outline: 1px solid color-mix(in srgb, var(--ink) 16%, transparent); outline-offset: 3px; }
258
+ .mk.remnant { width: 12px; height: 12px; background: transparent; border: 1px solid color-mix(in srgb, var(--ink) 40%, transparent); }
259
+ .mk.cold { width: 6px; height: 6px; background: var(--sky-cold); border: 1px solid color-mix(in srgb, var(--ink) 18%, transparent); }
260
+ .mk.well { width: 14px; height: 14px; background: transparent; border: 1px dashed color-mix(in srgb, var(--ink) 35%, transparent); }
261
+ .mk.alive { width: 14px; height: 14px; background: transparent; border: 1px solid color-mix(in srgb, var(--ink) 35%, transparent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent), 0 0 10px var(--accent); }
262
+ .mk.settled { width: 12px; height: 12px; background: transparent; border: 1px solid color-mix(in srgb, var(--ink) 50%, transparent); box-shadow: inset 0 0 0 3px var(--void), inset 0 0 0 4px color-mix(in srgb, var(--ink) 35%, transparent), 0 0 8px color-mix(in srgb, var(--ink) 25%, transparent); }
263
+ .mk.collapsed { width: 9px; height: 9px; background: var(--void); border: 1px solid color-mix(in srgb, var(--ink) 30%, transparent); box-shadow: 0 0 0 2px var(--void), 0 0 0 3px color-mix(in srgb, var(--ink) 8%, transparent); }
264
+ .mk.unseen { width: 12px; height: 12px; background: transparent; border: 1px dashed color-mix(in srgb, var(--ink) 20%, transparent); }
265
+ .mk.task { width: 4px; height: 4px; background: color-mix(in srgb, var(--ink) 60%, var(--void)); }
268
266
  .mk.task.live { background: var(--accent); box-shadow: 0 0 6px var(--accent); }
269
267
  .mk.born { width: 10px; height: 10px; background: transparent; border: 1px solid var(--accent); box-shadow: inset 0 0 0 2px var(--void), inset 0 0 0 5px var(--accent); }
270
268
  .mk.route { width: 14px; height: 2px; border-radius: 1px; background: var(--chart); box-shadow: 0 0 6px var(--chart); }
@@ -24,11 +24,11 @@ body[data-page="tweak-editor"] .tw-stage {
24
24
  --tw-night: #0c1122;
25
25
  --tw-moon: #f1e6d2;
26
26
  --tw-floor: #0d0d12;
27
- --tw-lamp: rgba(251, 149, 105, .16);
28
- --tw-lamp-out: rgba(251, 149, 105, 0);
27
+ --tw-lamp: color-mix(in srgb, var(--accent) 16%, transparent);
28
+ --tw-lamp-out: color-mix(in srgb, var(--accent) 0%, transparent);
29
29
  --tw-ink: #ffffff;
30
- --tw-ink-soft: #c0c0c0;
31
- --tw-ink-faint: #a8a8a8;
30
+ --tw-ink-soft: rgba(255, 255, 255, .75);
31
+ --tw-ink-faint: rgba(255, 255, 255, .66);
32
32
  --tw-ink-dim: #8a8a90;
33
33
  --tw-glass: rgba(255, 255, 255, .07);
34
34
  --tw-bar-back: rgba(7, 8, 13, .84);
@@ -41,22 +41,22 @@ body[data-page="tweak-editor"] .tw-stage {
41
41
  --tw-panel-rule: rgba(255, 255, 255, .12);
42
42
  --tw-scrim: rgba(4, 5, 9, .88);
43
43
  --tw-drop: rgba(0, 0, 0, .55);
44
- --tw-accent: #fb9569;
44
+ --tw-accent: var(--accent);
45
45
  --tw-on-accent: #000000;
46
- --tw-link: #fcbfa4;
47
- --tw-paper: #fbfafc;
48
- --tw-paper-ink: #17181a;
49
- --tw-paper-soft: #55565a;
50
- --tw-paper-faint: #65656a;
46
+ --tw-link: color-mix(in srgb, var(--accent) 55%, #ffffff);
47
+ --tw-paper: var(--bg-card);
48
+ --tw-paper-ink: var(--ink);
49
+ --tw-paper-soft: var(--ink-soft);
50
+ --tw-paper-faint: var(--ink-faint);
51
51
  --tw-shared: #4a5bb8;
52
- --tw-engineer: #a3481a;
53
- --tw-ok: #1f7a44;
54
- --tw-ok-wash: rgba(31, 122, 68, .11);
52
+ --tw-engineer: var(--accent-deep);
53
+ --tw-ok: var(--ok);
54
+ --tw-ok-wash: var(--ok-weak);
55
55
  --tw-on-ok: #ffffff;
56
- --tw-line-hover: rgba(23, 24, 26, .035);
57
- --tw-line-focus: rgba(251, 149, 105, .08);
58
- --tw-face: var(--font-body, Manrope, "Segoe UI", system-ui, sans-serif);
59
- --tw-mono: var(--font-mono, "JetBrains Mono", ui-monospace, Consolas, monospace);
56
+ --tw-line-hover: color-mix(in srgb, var(--ink) 3.5%, transparent);
57
+ --tw-line-focus: color-mix(in srgb, var(--accent) 8%, transparent);
58
+ --tw-face: var(--font-body);
59
+ --tw-mono: var(--font-mono);
60
60
  }
61
61
 
62
62
  /* The stage fills the content box: the hall's content column is a reading
@@ -399,7 +399,7 @@ html[data-fullscreen="on"] .tw-frame { left: 0; }
399
399
  display: inline-block;
400
400
  padding: 0 6px;
401
401
  border-radius: 5px;
402
- border: 1px solid var(--tw-rule-soft, rgba(23, 24, 26, .14));
402
+ border: 1px solid var(--tw-rule-soft, color-mix(in srgb, var(--ink) 14%, transparent));
403
403
  background: var(--tw-line-hover);
404
404
  color: var(--tw-paper-soft);
405
405
  font-size: .92em;
@@ -36,10 +36,7 @@ html.wb-open { overflow: hidden; }
36
36
  10%, 88% { opacity: 1; }
37
37
  100% { transform: translate(var(--wb-dx), var(--wb-dy)); opacity: 0; }
38
38
  }
39
- .wb-beacon { animation: wb-beacon 1.8s steps(1) infinite; filter: drop-shadow(0 0 4px #ef4444); }
40
- @keyframes wb-beacon { 0%, 60% { opacity: 1; } 61%, 100% { opacity: .15; } }
41
- .wb-star { animation: wb-twinkle ease-in-out infinite alternate; }
42
- @keyframes wb-twinkle { from { opacity: .15; } to { opacity: .9; } }
39
+ .wb-star { opacity: .6; }
43
40
  .wb-moon { animation: wb-moon 6s ease-in-out infinite alternate; filter: blur(6px); }
44
41
  @keyframes wb-moon { from { opacity: .35; } to { opacity: .8; } }
45
42
  .wb__cloud { position: absolute; z-index: 2; border-radius: 50%; background: rgba(255, 255, 255, .06); filter: blur(30px); animation: wb-drift linear infinite; pointer-events: none; }
@@ -75,18 +72,18 @@ html.wb-open { overflow: hidden; }
75
72
  letter-spacing: -.01em; text-transform: lowercase; white-space: nowrap;
76
73
  }
77
74
  /* the mark's ink follows the painting: pale by night, dark by day; the moon stays salmon */
78
- .wb__mark { --mark-ink: #e2e1e4; }
75
+ .wb__mark { --mark-ink: rgba(255, 255, 255, .88); }
79
76
  .wb__planet { transform-box: fill-box; transform-origin: center; }
80
77
  .wb__moonlet { transform-box: view-box; transform-origin: 100px 100px; }
81
- .wb--day .wb__mark { --mark-ink: #17181a; }
78
+ .wb--day .wb__mark { --mark-ink: rgba(0, 0, 0, .9); }
82
79
  .wb--lit .wb__ring { stroke-dasharray: 100; animation: wb-ring 1.5s cubic-bezier(.6, 0, .2, 1) .5s both; }
83
- .wb--lit .wb__planet { animation: wb-planet 1s cubic-bezier(.3, 1.4, .5, 1) .3s both; }
80
+ .wb--lit .wb__planet { animation: wb-planet 1s var(--ds-ease-arrive) .3s both; }
84
81
  .wb--lit .wb__moonlet { animation: wb-orbit 1.6s cubic-bezier(.25, .8, .3, 1) 1.1s both, wb-glow 3.2s ease-in-out 2.7s infinite alternate; }
85
82
  .wb--lit .wb__name { animation: wb-name 1.2s ease-out 1.5s both; }
86
83
  @keyframes wb-ring { from { stroke-dashoffset: 100; } to { stroke-dashoffset: 0; } }
87
84
  @keyframes wb-planet { from { transform: scale(.35); opacity: 0; } to { transform: none; opacity: 1; } }
88
85
  @keyframes wb-orbit { from { transform: rotate(-200deg); opacity: 0; } 25% { opacity: 1; } to { transform: none; opacity: 1; } }
89
- @keyframes wb-glow { from { filter: drop-shadow(0 0 0 rgba(251, 149, 105, 0)); } to { filter: drop-shadow(0 0 7px rgba(251, 149, 105, .95)); } }
86
+ @keyframes wb-glow { from { filter: drop-shadow(0 0 0 color-mix(in srgb, var(--accent) 0%, transparent)); } to { filter: drop-shadow(0 0 7px color-mix(in srgb, var(--accent) 95%, transparent)); } }
90
87
  @keyframes wb-name { from { opacity: 0; letter-spacing: .18em; } to { opacity: 1; letter-spacing: -.01em; } }
91
88
  .wb__version { font-family: var(--font-mono); font-size: 12px; color: #a5b4cf; }
92
89
  .wb--day .wb__banner { background: rgba(255, 255, 255, .7); border-color: rgba(20, 22, 30, .14); }
@@ -74,8 +74,8 @@
74
74
  // ---------- the roles (the owner's mapping) ----------
75
75
  // `mode` is the craft mode the pick sets; `href` null means stay on Home.
76
76
  var ROLES = [
77
- { id: 'owner', label: 'Owner', room: 'City', href: '/city', mode: 'governing', dot: '#f59e0b' },
78
- { id: 'ideator', label: 'Ideator', room: 'Your thinking', href: '/thinking', mode: 'ideating', dot: '#facc15' },
77
+ { id: 'owner', label: 'Owner', room: 'City', href: '/city', mode: 'governing', dot: 'var(--accent)' },
78
+ { id: 'ideator', label: 'Ideator', room: 'Your thinking', href: '/thinking', mode: 'ideating', dot: 'var(--warn)' },
79
79
  { id: 'artist', label: 'Artist', room: 'Studio', href: '/studio', mode: 'artistry', dot: '#f472b6' },
80
80
  { id: 'engineer', label: 'Engineer', room: 'Home', href: null, mode: 'building', dot: '#60a5fa' },
81
81
  ];
@@ -92,14 +92,13 @@
92
92
  var TOWERS = [
93
93
  { x: 140, y: 168, w: 62, h: 118, glow: '#60a5fa' },
94
94
  { x: 280, y: 150, w: 58, h: 200, glow: '#22d3ee' },
95
- { x: 184, y: 296, w: 100, h: 196, glow: '#fbbf24' },
96
- { x: 636, y: 222, w: 42, h: 140, glow: '#4ade80' },
97
- { x: 702, y: 212, w: 84, h: 190, glow: '#4ade80' },
98
- { x: 2, y: 196, w: 52, h: 150, glow: '#4ade80' },
95
+ { x: 184, y: 296, w: 100, h: 196, glow: 'var(--accent)' },
96
+ { x: 636, y: 222, w: 42, h: 140, glow: 'color-mix(in srgb, var(--sea) 55%, #ffffff)' },
97
+ { x: 702, y: 212, w: 84, h: 190, glow: 'color-mix(in srgb, var(--sea) 55%, #ffffff)' },
98
+ { x: 2, y: 196, w: 52, h: 150, glow: 'color-mix(in srgb, var(--sea) 55%, #ffffff)' },
99
99
  { x: 910, y: 410, w: 110, h: 160, glow: '#22d3ee' },
100
100
  ];
101
- // Aircraft warning lights on the masts, and the hover-car lanes on the right.
102
- var BEACONS = [[10, 157], [322, 103], [393, 198], [941, 251]];
101
+ // The hover-car lanes on the right (the masts' blinking lights went in task 1005087).
103
102
  var LANES = [
104
103
  { x1: 770, y1: 350, x2: 1040, y2: 362, dur: 5.5 },
105
104
  { x1: 1040, y1: 340, x2: 760, y2: 352, dur: 7 },
@@ -129,17 +128,14 @@
129
128
  var rnd = seeded(11);
130
129
  var svg = el('svg', { viewBox: '0 0 ' + AW + ' ' + AH, preserveAspectRatio: 'none', 'class': 'wb-motion' }, host);
131
130
  if (night) {
132
- // the moon breathes; stars twinkle in the open sky
131
+ // the moon breathes; the stars stand still (task 1005087: no twinkle, no beacons)
133
132
  el('circle', { 'class': 'wb-moon', cx: MOON[0], cy: MOON[1], r: 26, fill: 'rgba(226,232,255,.25)' }, svg);
134
133
  for (var i = 0; i < 40; i++) {
135
134
  var sx = rnd() * AW, sy = rnd() * 140;
136
135
  if (Math.abs(sx - MOON[0]) < 50 && Math.abs(sy - MOON[1]) < 50) continue;
137
- el('circle', { 'class': 'wb-star', cx: sx.toFixed(1), cy: sy.toFixed(1), r: (0.6 + rnd() * 0.9).toFixed(2), fill: '#e0e7ff',
138
- style: 'animation-duration:' + (2 + rnd() * 3).toFixed(2) + 's;animation-delay:' + (-rnd() * 4).toFixed(2) + 's' }, svg);
136
+ el('circle', { 'class': 'wb-star', cx: sx.toFixed(1), cy: sy.toFixed(1), r: (0.6 + rnd() * 0.9).toFixed(2), fill: '#e0e7ff' }, svg);
137
+ rnd(); rnd(); // the two draws the twinkle timing took, so every star keeps its place
139
138
  }
140
- BEACONS.forEach(function (b, i) {
141
- el('circle', { 'class': 'wb-beacon', cx: b[0], cy: b[1], r: 3.2, fill: '#ef4444', style: 'animation-delay:' + (-i * 0.45) + 's' }, svg);
142
- });
143
139
  }
144
140
  // data pulses climbing the circuit towers (the whole set brightens as the city powers up)
145
141
  var pulses = el('g', { 'class': 'wb-pulses' }, svg);
@@ -392,7 +388,7 @@
392
388
  }
393
389
 
394
390
  var api = {
395
- DAY_KEY: DAY_KEY, SIGNIN_KEY: SIGNIN_KEY, ROLES: ROLES, ART: ART, TOWERS: TOWERS, BEACONS: BEACONS, LANES: LANES, AW: AW, AH: AH,
391
+ DAY_KEY: DAY_KEY, SIGNIN_KEY: SIGNIN_KEY, ROLES: ROLES, ART: ART, TOWERS: TOWERS, LANES: LANES, AW: AW, AH: AH,
396
392
  localDay: localDay, firstVisitToday: firstVisitToday, greetingFor: greetingFor, nameOf: nameOf, timeOfDay: timeOfDay,
397
393
  artFor: artFor, projectName: projectName, logoHtml: logoHtml, BUILT_ON: BUILT_ON, rolesFor: rolesFor, pulseRuns: pulseRuns,
398
394
  onAccess: onAccess, finish: finish, close: close,
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.24.55",
3
+ "version": "1.24.57",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.24.55",
9
+ "version": "1.24.57",
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.24.55",
3
+ "version": "1.24.57",
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",
@@ -11301,5 +11301,17 @@
11301
11301
  "id": "1005083",
11302
11302
  "text": "There is now a public design-system page at /brand on cloudbongos.com showing every colour, size, corner and shared building block, with light and dark values. It is built automatically from the same files the site uses, so it"
11303
11303
  }
11304
+ ],
11305
+ "1.24.56": [
11306
+ {
11307
+ "id": "1005087",
11308
+ "text": "The Studio, the thinking sky, the tweak editor, the city, approvals, the atlas and the welcome screen now take their colours and fonts from the project's own branding, so a re-branded project sees them in its own colours; the"
11309
+ }
11310
+ ],
11311
+ "1.24.57": [
11312
+ {
11313
+ "id": "1005093",
11314
+ "text": "Stylesheets can no longer gain new typed-in colours, text sizes, corners, shadows or breakpoints without the build failing, and the public design reference must be regenerated whenever its sources change."
11315
+ }
11304
11316
  ]
11305
11317
  }
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // css-literal-scan.js — the detector behind the per-stylesheet DESIGN COUNTS
4
+ // ratchet in fitness-ratchets.js (task 1005093, goal 1000138 "one Cloud Bongos
5
+ // design system"; DESIGN.md, the Fifteen Rule and the --ds-* scales).
6
+ //
7
+ // WHAT IT COUNTS, per tracked stylesheet, in five kinds:
8
+ // colours — every typed-in colour: a hex (#abc, #aabbcc, #aabbccdd) or a
9
+ // colour function (rgb/rgba/hsl/hsla/hwb/lab/lch/oklab/oklch).
10
+ // Counted per OCCURRENCE. The one literal the Fifteen Rule allows
11
+ // — black or white with alpha as a scrim/shadow/halo
12
+ // (rgba(0,0,0,.4), rgb(255 255 255 / .2)) — is not counted.
13
+ // Named colours (red, white…) are not detected.
14
+ // font_sizes — font-size / font declarations whose value carries a typed-in
15
+ // non-zero length (px, rem, em, pt, %, vw…) instead of a token.
16
+ // radii — *-radius declarations with a typed-in non-zero length.
17
+ // shadows — box-shadow / text-shadow declarations that are not none/inherit
18
+ // and carry a typed-in length or colour.
19
+ // breakpoints — @media / @container rules with a typed-in length (a custom
20
+ // property cannot be used in a media query, so these never become
21
+ // tokens; the count stops the set of breakpoints sprawling).
22
+ // Sizes, radii, shadows and breakpoints are counted per DECLARATION / RULE.
23
+ //
24
+ // WHAT IT DOES NOT COUNT. A custom-property declaration (`--x: #123`) is a token
25
+ // DEFINITION — tokens.css and the hall's token block are exactly where literals
26
+ // belong — so its value is skipped. Page-private palettes are guarded separately
27
+ // (tests/hall_private_palettes.mjs). Custom-property NAMES are removed from a
28
+ // value before it is read (so --ds-space-12 is not a "12"), and url(...) bodies
29
+ // are emptied (a data URI is not a colour). A literal inside a var() fallback
30
+ // DOES count: a fallback is a typed-in value.
31
+ //
32
+ // Scope: every tracked *.css outside docs/ (the design mocks there are not
33
+ // served), node_modules and vendor/.
34
+ //
35
+ // Print the counts: node scripts/bongos/css-literal-scan.js [--all]
36
+
37
+ const path = require('path');
38
+ const fs = require('fs');
39
+ const { execFileSync } = require('child_process');
40
+
41
+ const ROOT = path.resolve(__dirname, '..', '..');
42
+ const KINDS = ['colours', 'font_sizes', 'radii', 'shadows', 'breakpoints'];
43
+
44
+ const EXCLUDE_RE = /^docs\/|(^|\/)(node_modules|vendor)\//;
45
+ const COMMENT_RE = /\/\*[\s\S]*?\*\//g;
46
+ const URL_RE = /url\(\s*(?:"[^"]*"|'[^']*'|[^)]*)\)/gi;
47
+ const VAR_NAME_RE = /--[\w-]+/g;
48
+ // A declaration: a property, a colon, and a value that ends at ; or } (a selector
49
+ // such as `a:hover {` ends at `{`, so it never matches).
50
+ const DECL_RE = /(?:^|[{;\s])(-?-?[a-zA-Z][\w-]*)\s*:\s*([^;{}]+)(?=[;}])/g;
51
+ const AT_RULE_RE = /@(media|container)\b([^{;]*)\{/g;
52
+ const HEX_RE = /#[0-9a-fA-F]{3,8}(?![\w-])/g;
53
+ const COLOUR_FN_RE = /(?<![\w-])(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch)\(([^()]*)\)/gi;
54
+ const LENGTH_RE = /(?<![\w.#-])(\d*\.?\d+)(px|rem|em|pt|vw|vh|vmin|vmax|ch|ex|%)(?![\w%])/g;
55
+ const MEDIA_LENGTH_RE = /(?<![\w.-])\d*\.?\d+(px|em|rem)(?![\w])/;
56
+ const NO_SHADOW_RE = /^\s*(none|inherit|initial|unset|revert)\s*(!important)?\s*$/i;
57
+
58
+ /** True for black or white with alpha — the Fifteen Rule's one allowed literal. */
59
+ function isScrim(args) {
60
+ const parts = args.split(/[\s,/]+/).filter(Boolean);
61
+ if (parts.length !== 4) return false;
62
+ const rgb = parts.slice(0, 3).map((p) => p.toLowerCase());
63
+ const black = rgb.every((p) => p === '0' || p === '0%');
64
+ const white = rgb.every((p) => p === '255' || p === '100%');
65
+ return black || white;
66
+ }
67
+
68
+ function colourCount(value) {
69
+ let n = (value.match(HEX_RE) || []).length;
70
+ COLOUR_FN_RE.lastIndex = 0;
71
+ let m;
72
+ while ((m = COLOUR_FN_RE.exec(value))) {
73
+ const fn = m[0].slice(0, m[0].indexOf('(')).toLowerCase();
74
+ if ((fn === 'rgb' || fn === 'rgba') && isScrim(m[1])) continue;
75
+ n += 1;
76
+ }
77
+ return n;
78
+ }
79
+
80
+ function hasLength(value) {
81
+ LENGTH_RE.lastIndex = 0;
82
+ let m;
83
+ while ((m = LENGTH_RE.exec(value))) if (Number(m[1]) !== 0) return true;
84
+ return false;
85
+ }
86
+
87
+ /** The five counts for one stylesheet's text. */
88
+ function countSheet(text) {
89
+ const counts = Object.fromEntries(KINDS.map((k) => [k, 0]));
90
+ const css = String(text).replace(COMMENT_RE, ' ').replace(URL_RE, 'url()');
91
+ AT_RULE_RE.lastIndex = 0;
92
+ let m;
93
+ while ((m = AT_RULE_RE.exec(css))) if (MEDIA_LENGTH_RE.test(m[2])) counts.breakpoints += 1;
94
+ DECL_RE.lastIndex = 0;
95
+ while ((m = DECL_RE.exec(css))) {
96
+ const prop = m[1].toLowerCase();
97
+ if (prop.startsWith('--')) continue; // a token definition
98
+ const value = m[2].replace(VAR_NAME_RE, ' ');
99
+ counts.colours += colourCount(value);
100
+ if ((prop === 'font-size' || prop === 'font') && hasLength(value)) counts.font_sizes += 1;
101
+ else if (prop.endsWith('radius') && hasLength(value)) counts.radii += 1;
102
+ else if ((prop === 'box-shadow' || prop === 'text-shadow') && !NO_SHADOW_RE.test(value)
103
+ && (hasLength(value) || colourCount(value) > 0)) counts.shadows += 1;
104
+ }
105
+ return counts;
106
+ }
107
+
108
+ function trackedStylesheets(root = ROOT) {
109
+ const out = execFileSync('git', ['ls-files', '-z', '--', '*.css'], { cwd: root, encoding: 'utf8' });
110
+ return out.split('\0').filter((f) => f && !EXCLUDE_RE.test(f)).sort();
111
+ }
112
+
113
+ /**
114
+ * Counts for every tracked stylesheet: { sheets: { [path]: counts }, scanned }.
115
+ * Sheets whose five counts are all zero are omitted (a missing sheet reads as zero).
116
+ */
117
+ function scanCssLiterals({ root = ROOT, files = null, read = null } = {}) {
118
+ const list = files || trackedStylesheets(root);
119
+ const reader = read || ((f) => fs.readFileSync(path.join(root, f), 'utf8'));
120
+ const sheets = {};
121
+ let scanned = 0;
122
+ for (const f of list) {
123
+ let text;
124
+ try { text = reader(f); } catch { continue; }
125
+ scanned += 1;
126
+ const c = countSheet(text);
127
+ if (KINDS.some((k) => c[k] > 0)) sheets[f] = c;
128
+ }
129
+ return { sheets, scanned };
130
+ }
131
+
132
+ if (require.main === module) {
133
+ const { sheets, scanned } = scanCssLiterals();
134
+ const totals = Object.fromEntries(KINDS.map((k) => [k, 0]));
135
+ for (const [f, c] of Object.entries(sheets)) {
136
+ for (const k of KINDS) totals[k] += c[k];
137
+ if (process.argv.includes('--all') || KINDS.some((k) => c[k] > 0)) {
138
+ process.stdout.write(`${f} ${KINDS.map((k) => `${k}=${c[k]}`).join(' ')}\n`);
139
+ }
140
+ }
141
+ process.stdout.write(`\n${scanned} stylesheet(s) scanned · totals ${KINDS.map((k) => `${k}=${totals[k]}`).join(' ')}\n`);
142
+ }
143
+
144
+ module.exports = { scanCssLiterals, countSheet, KINDS };
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+ // design-generators-fresh.js — the design system's generated files are fresh
3
+ // (task 1005093, goal 1000138). Part of fitness.js's "generated artifacts fresh"
4
+ // check; kept in its own file because fitness.js sits at the 1,500-line budget.
5
+ //
6
+ // Both generated files read the shared token sheet (modules/ui-design/public/tokens.css) and the brand pack; the public
7
+ // reference also reads the hall's sheets and DESIGN.md. A stylesheet edit without a
8
+ // regeneration used to surface only as a failing unit test; this names it in fitness.
9
+ // Skipped when the ui-design module is not in the checkout. `root` is injectable for
10
+ // tests/design_counts_ratchet.mjs, which runs it against a copied tree.
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { execFileSync } = require('child_process');
15
+
16
+ const { REPO_ROOT } = require('./fitness-lib.js');
17
+
18
+ function checkDesignGeneratorsFresh(root = REPO_ROOT) {
19
+ const violations = [];
20
+ for (const [script, out, task] of [
21
+ ['gen-design-tokens.js', 'config/design-tokens.neutral.json', '1005082'],
22
+ ['gen-design-reference.js', 'modules/ui-design/public/brand.html (the public /brand page)', '1005083'],
23
+ ]) {
24
+ const gen = path.join(root, 'modules', 'ui-design', 'scripts', script);
25
+ if (!fs.existsSync(gen)) continue;
26
+ try { execFileSync(process.execPath, [gen, '--check'], { cwd: root, stdio: 'pipe' }); }
27
+ catch { violations.push(`${out} is STALE — a design source (the token sheet, the brand pack, a hall sheet or DESIGN.md) changed without a regeneration. Run \`node modules/ui-design/scripts/${script}\` and commit (task ${task}).`); }
28
+ }
29
+ return violations;
30
+ }
31
+
32
+ module.exports = { checkDesignGeneratorsFresh };