@bongos/core 1.19.125 → 1.19.126

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.
package/.bongos-core.json CHANGED
@@ -2,11 +2,11 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.125",
6
- "core_contract": "1.19.125",
7
- "source_commit": "669cdd43ff95a6612999a3456ff5b68cdf8ccfd0",
5
+ "core_version": "1.19.126",
6
+ "core_contract": "1.19.126",
7
+ "source_commit": "b56cedc2da8ef6691bd7990c6e3219896694e540",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-14T23:11:49.244Z",
9
+ "built_at": "2026-08-15T00:16:09.571Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 303,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 1569,
20
- "tree_sha256": "5310661ece545602d2be2b0c77f859959a02816dded3aeb8c6da211aaef421e9",
20
+ "tree_sha256": "5703791569f4c26556ad21f0bbcf605faf1b42cfd3631a1ce5f25a05269fd4b4",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -4167,12 +4167,12 @@
4167
4167
  {
4168
4168
  "path": "package-lock.json",
4169
4169
  "mode": "0000644",
4170
- "sha256": "9d8c84867fb0a2470c1597ff6932dd8099f69ecbdaeddf1a8a2761d7e7e14cbd"
4170
+ "sha256": "64edc372bb49cd5c30eb5a1d46f54361293de787d1717d2e116f52568ff93f2c"
4171
4171
  },
4172
4172
  {
4173
4173
  "path": "package.json",
4174
4174
  "mode": "0000644",
4175
- "sha256": "d9d1ace99af356b2d7c958b93d94d14bb144eafdf8c75a0b7dd42ca50727a5a1"
4175
+ "sha256": "c652ffaf3901e4ccc8f64ff9340f7f4f6c418ab738fc52f2b71450083d2a4e97"
4176
4176
  },
4177
4177
  {
4178
4178
  "path": "public-docs/index.html",
@@ -4412,7 +4412,7 @@
4412
4412
  {
4413
4413
  "path": "public-landing/assets/cosmos.css",
4414
4414
  "mode": "0000644",
4415
- "sha256": "368015cca5170dffd39e3fae55c8feca512df12fbb0e8f4daf3758e2aa5cf5f4"
4415
+ "sha256": "a55b4dd516485060edb4f1cd0be5164bd2026d77186d9b19c1f728a64eda9c7b"
4416
4416
  },
4417
4417
  {
4418
4418
  "path": "public-landing/buddha.png",
@@ -4427,7 +4427,7 @@
4427
4427
  {
4428
4428
  "path": "public-landing/projects.html",
4429
4429
  "mode": "0000644",
4430
- "sha256": "925e7964f66d59f2df260a611e602bdeafe3f8f3b73d3d16db7bdfa073b4163c"
4430
+ "sha256": "6dcb73054fa58d3b0254f1d82a7f639ed465db2c2fd1e83e26f29b80532047e5"
4431
4431
  },
4432
4432
  {
4433
4433
  "path": "public-landing/world/assets/clouds/cloud-island-large.png",
@@ -5757,7 +5757,7 @@
5757
5757
  {
5758
5758
  "path": "src/module-api.js",
5759
5759
  "mode": "0000644",
5760
- "sha256": "7319962aeeada78ddc037f8b3eb24c6ecf42e1edd295ee4edb6cd4e0b3247e66"
5760
+ "sha256": "c64972eb393e195b4f24de3459036423077430a5cf643fd2e3a8da525ab3af56"
5761
5761
  },
5762
5762
  {
5763
5763
  "path": "src/module-loader/catalog.js",
@@ -6267,7 +6267,7 @@
6267
6267
  {
6268
6268
  "path": "tests/cosmos_token_contrast.mjs",
6269
6269
  "mode": "0000644",
6270
- "sha256": "3a727fd6bf78aa8e61643fea0a9418cd67f97dee8ffc3f620673d715da2fcc38"
6270
+ "sha256": "3689ce2b23d0baedb5a277676e939af712cea8d0cb3c07857e33f36eb12e1233"
6271
6271
  },
6272
6272
  {
6273
6273
  "path": "tests/cost.mjs",
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.125",
3
+ "version": "1.19.126",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.125",
9
+ "version": "1.19.126",
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.125",
3
+ "version": "1.19.126",
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",
@@ -98,6 +98,7 @@
98
98
  --ink:#fdeedd; --ink-soft:#dcb693; --ink-faint:#b89272;
99
99
  --rule:#5e3521; --rule-soft:#3f2317;
100
100
  --accent-deep:color-mix(in srgb, var(--accent) 45%, #ffffff);
101
+ --sea-ink:color-mix(in srgb, var(--sea) 65%, #ffffff);
101
102
  --ok:#4ade80; --warn:#fbbf24; --err:#f87171;
102
103
  --on-ok:#0b2e18;
103
104
  --page-bg:#150c06;
@@ -124,6 +125,7 @@
124
125
  --ink:#fdeedd; --ink-soft:#dcb693; --ink-faint:#b89272;
125
126
  --rule:#5e3521; --rule-soft:#3f2317;
126
127
  --accent-deep:color-mix(in srgb, var(--accent) 45%, #ffffff);
128
+ --sea-ink:color-mix(in srgb, var(--sea) 65%, #ffffff);
127
129
  --ok:#4ade80; --warn:#fbbf24; --err:#f87171;
128
130
  --on-ok:#0b2e18;
129
131
  --page-bg:#150c06;
@@ -154,6 +156,10 @@
154
156
  --err-soft:color-mix(in srgb, var(--err) 12%, var(--bg-card));
155
157
  --accent-soft:color-mix(in srgb, var(--accent) 15%, var(--bg-card));
156
158
  --sea-soft:color-mix(in srgb, var(--sea) 16%, var(--bg-card));
159
+ /* the strip's STATE-carrying marks (done fill, now ring/pulse) — light uses
160
+ --sea as-is; dark derives brighter (the --accent-deep pattern) so a pack's
161
+ light-tuned sea clears WCAG 1.4.11's 3:1 on the near-black card too */
162
+ --sea-ink:var(--sea);
157
163
  --neutral-soft:color-mix(in srgb, var(--ink) 8%, var(--bg-card));
158
164
  /* ink-derived so the shadow is a WARM brown on the marigold ground instead of
159
165
  the cold navy it used to cast. The dark blocks override it with a plain black
@@ -225,7 +231,7 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
225
231
  (.rec) — "live" always says when it was last verified. ── */
226
232
  .chip{display:inline-flex;align-items:center;gap:7px;font-size:12px;font-weight:700;letter-spacing:0.3px;border-radius:999px;padding:4px 12px;background:var(--neutral-soft);color:var(--ink-soft);}
227
233
  .chip .cdot{width:7px;height:7px;border-radius:50%;background:var(--ink-faint);flex:0 0 auto;}
228
- .chip .rec{font-weight:600;opacity:0.72;font-size:11.5px;}
234
+ .chip .rec{font-weight:600;font-size:11.5px;}
229
235
  .chip.busy{background:var(--accent-soft);color:var(--accent-deep);}
230
236
  .chip.busy .cdot{background:var(--accent);animation:chipPulse 1.4s ease-in-out infinite;}
231
237
  .chip.live{background:var(--ok-soft);color:var(--ok);}
@@ -297,13 +303,15 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
297
303
  .strip .stages{display:flex;list-style:none;margin-bottom:6px;}
298
304
  .strip .stages li{flex:1;display:flex;flex-direction:column;align-items:center;gap:7px;position:relative;}
299
305
  .strip .stages li:not(:first-child)::before{content:"";position:absolute;top:11px;right:50%;width:100%;height:3px;border-radius:2px;background:var(--sea-soft);z-index:0;margin-right:12px;}
300
- .strip .stages li.done:not(:first-child)::before{background:var(--sea);}
306
+ .strip .stages li.done:not(:first-child)::before{background:var(--sea-ink);}
301
307
  .strip .sdot{position:relative;z-index:1;width:24px;height:24px;border-radius:50%;border:3px solid var(--sea-soft);background:var(--bg-card);display:flex;align-items:center;justify-content:center;}
302
- .strip .stages li.done .sdot{border-color:var(--sea);background:var(--sea);}
303
- .strip .stages li.done .sdot::after{content:"";width:8px;height:5px;border-left:2.5px solid #fff;border-bottom:2.5px solid #fff;transform:rotate(-45deg) translate(1px,-1px);}
304
- .strip .stages li.now .sdot{border-color:var(--sea);}
305
- .strip .stages li.now .sdot::after{content:"";width:8px;height:8px;border-radius:50%;background:var(--sea);animation:beatPulse 1.2s ease-in-out infinite;}
306
- @keyframes beatPulse{0%,100%{transform:scale(.7);opacity:.6}50%{transform:scale(1.15);opacity:1}}
308
+ .strip .stages li.done .sdot{border-color:var(--sea-ink);background:var(--sea-ink);}
309
+ /* tick ink is --on-ok, not #fff — dark's bright fills took a white tick to 1.7:1 */
310
+ .strip .stages li.done .sdot::after{content:"";width:8px;height:5px;border-left:2.5px solid var(--on-ok);border-bottom:2.5px solid var(--on-ok);transform:rotate(-45deg) translate(1px,-1px);}
311
+ .strip .stages li.now .sdot{border-color:var(--sea-ink);}
312
+ .strip .stages li.now .sdot::after{content:"";width:8px;height:8px;border-radius:50%;background:var(--sea-ink);animation:beatPulse 1.2s ease-in-out infinite;}
313
+ /* scale-only: an opacity trough took the pulse below the 3:1 its token clears */
314
+ @keyframes beatPulse{0%,100%{transform:scale(.7)}50%{transform:scale(1.15)}}
307
315
  .strip .slbl{font-size:12px;font-weight:700;color:var(--ink-faint);}
308
316
  .strip .stages li.now .slbl,.strip .stages li.done .slbl{color:color-mix(in srgb,var(--sea) 40%,var(--ink));}
309
317
  .strip .pnote{text-align:center;font-size:13px;font-weight:600;color:var(--ink-soft);min-height:18px;line-height:1.5;}
@@ -414,8 +422,8 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
414
422
  /* Home LEAVES the hub, so it never takes the current-view pill and a hairline
415
423
  sets it apart. Without it the header offered no way back to the landing page
416
424
  except the wordmark, which nothing announced as a link. */
417
- .navrow .navHome{color:var(--glass-ink);opacity:.9;}
418
- .navrow .navHome:hover{opacity:1;color:var(--chrome-accent);}
425
+ .navrow .navHome{color:var(--glass-ink);}
426
+ .navrow .navHome:hover{color:var(--chrome-accent);}
419
427
  .navSep{width:1px;align-self:stretch;margin:4px 3px;background:var(--glass-border);flex:0 0 auto;}
420
428
  /* below the mid breakpoint the three groups stack and centre rather than
421
429
  fighting for one row — equal flanks only buy a centred nav while it fits */
@@ -440,7 +448,9 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
440
448
  .search svg{width:15px;height:15px;fill:none;stroke:var(--glass-ink);stroke-width:2;flex:0 0 auto;}
441
449
  .search input{flex:1;min-width:0;background:none;border:none;outline:none;font-family:var(--font-mono);
442
450
  font-size:13px;color:var(--chrome-ink);}
443
- .search input::placeholder{color:var(--glass-ink);opacity:.75;}
451
+ /* no opacity dim — .75 washed this to 3.87:1, and the .mineBar override
452
+ inherited the multiply so its own token could never fix it (task 1002751) */
453
+ .search input::placeholder{color:var(--glass-ink);}
444
454
  .search .count{font-family:var(--font-mono);font-size:11.5px;color:var(--glass-ink);white-space:nowrap;}
445
455
 
446
456
  /* the only-mine filter — a real two-state control, not a link: it filters the
@@ -163,7 +163,12 @@ h1[tabindex="-1"]:focus{outline:none;}
163
163
  step that can't be performed yet (its prerequisite — usually the domain — is missing) */
164
164
  .humanSteps>li.sdone{border-color:color-mix(in srgb,var(--ok) 30%,var(--bg-card));background:linear-gradient(180deg,color-mix(in srgb,var(--ok) 8%,var(--bg-card)) 0%,var(--bg-card) 100%);}
165
165
  .humanSteps>li.sdone::before{content:"✓";background:var(--ok);color:var(--on-ok);}
166
- .humanSteps>li.slocked{opacity:.6;}
166
+ /* locked = muted TOKENS, not subtree opacity — opacity multiplies after the
167
+ token resolves and took this copy below AA (task 1002751) */
168
+ .humanSteps>li.slocked{border-color:color-mix(in srgb,var(--line) 55%,var(--bg-card));}
169
+ .humanSteps>li.slocked b{color:var(--ink-soft);}
170
+ .humanSteps>li.slocked p{color:var(--ink-faint);}
171
+ .humanSteps>li.slocked::before{color:var(--ink-faint);}
167
172
  .humanSteps details{margin-top:10px;}
168
173
  .humanSteps summary{cursor:pointer;font-weight:700;font-size:12.5px;color:var(--ink-faint);}
169
174
  .humanSteps details p{margin-top:8px;}
@@ -194,16 +199,19 @@ h1[tabindex="-1"]:focus{outline:none;}
194
199
  content:"";position:absolute;top:11px;right:50%;width:100%;height:3px;border-radius:2px;
195
200
  background:var(--sea-soft);z-index:0;margin-right:12px;
196
201
  }
197
- .prog .stages li.done:not(:first-child)::before{background:var(--sea);}
202
+ /* state marks paint --sea-ink / --on-ok, mirroring cosmos's .strip (task
203
+ 1002751) — raw --sea sat at 3.03:1 in dark and the #fff tick at 1.74:1 on
204
+ dark's bright --ok */
205
+ .prog .stages li.done:not(:first-child)::before{background:var(--sea-ink);}
198
206
  .prog .sdot{
199
207
  position:relative;z-index:1;width:24px;height:24px;border-radius:50%;
200
208
  border:3px solid var(--sea-soft);background:var(--bg-card);display:flex;align-items:center;justify-content:center;
201
209
  }
202
- .prog .stages li.done .sdot{border-color:var(--sea);background:var(--sea);}
203
- .prog .stages li.done .sdot::after{content:"";width:8px;height:5px;border-left:2.5px solid #fff;border-bottom:2.5px solid #fff;transform:rotate(-45deg) translate(1px,-1px);}
204
- .prog .stages li.now .sdot{border-color:var(--sea);}
210
+ .prog .stages li.done .sdot{border-color:var(--sea-ink);background:var(--sea-ink);}
211
+ .prog .stages li.done .sdot::after{content:"";width:8px;height:5px;border-left:2.5px solid var(--on-ok);border-bottom:2.5px solid var(--on-ok);transform:rotate(-45deg) translate(1px,-1px);}
212
+ .prog .stages li.now .sdot{border-color:var(--sea-ink);}
205
213
  .prog .stages li.now .sdot::after{
206
- content:"";width:8px;height:8px;border-radius:50%;background:var(--sea);
214
+ content:"";width:8px;height:8px;border-radius:50%;background:var(--sea-ink);
207
215
  animation:beatPulse 1.2s ease-in-out infinite;
208
216
  }
209
217
  /* idle labels match cosmos's .strip .slbl — the 55% sea mix washed to 4.15:1 in dark */
@@ -229,7 +237,9 @@ h1[tabindex="-1"]:focus{outline:none;}
229
237
 
230
238
  /* the collapsed address step — a confirmation, not a form (task 1002704) */
231
239
  .addrConfirm{border:2px solid color-mix(in srgb,var(--ok) 30%,var(--bg-card));border-radius:18px;padding:18px 20px;background:linear-gradient(180deg,color-mix(in srgb,var(--ok) 8%,var(--bg-card)) 0%,var(--bg-card) 100%);margin-bottom:16px;}
232
- .addrConfirm.optedOut{border-color:var(--line);background:var(--bg);opacity:.75;}
240
+ .addrConfirm.optedOut{border-color:var(--line);background:var(--bg);}
241
+ .addrConfirm.optedOut .addrBig{color:var(--ink-soft);}
242
+ .addrConfirm.optedOut>p{color:var(--ink-faint);}
233
243
  .addrBig{font-family:var(--font-display);font-weight:700;font-size:19px;letter-spacing:-0.3px;line-height:1.4;}
234
244
  .addrBig code{font-family:var(--font-mono);font-size:15px;background:var(--bg-deep);border-radius:8px;padding:2px 9px;color:var(--accent-deep);word-break:break-all;}
235
245
  .addrConfirm>p{font-size:13.5px;font-weight:500;color:var(--ink-soft);line-height:1.5;margin-top:6px;}
package/src/module-api.js CHANGED
@@ -677,8 +677,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
677
677
  // landed since 1.19.123 with no explicit bump. run 31844281033. (task 1002620)
678
678
  // 1.19.125 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
679
679
  // landed since 1.19.124 with no explicit bump. run 31849445746. (task 1002620)
680
+ // 1.19.126 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
681
+ // landed since 1.19.125 with no explicit bump. run 31853055269. (task 1002620)
680
682
  // ---------------------------------------------------------------------------
681
- const CORE_VERSION = '1.19.125'; // CI auto-patch carrier (ADR 0161); see changelog above
683
+ const CORE_VERSION = '1.19.126'; // CI auto-patch carrier (ADR 0161); see changelog above
682
684
 
683
685
  // A namespaced logger so a module's log lines are attributable + consistent.
684
686
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -165,6 +165,21 @@ const PAIRS = [
165
165
  // .chip.busy + the .tbl .pill / .cardRel / builder-badge family: accent-deep
166
166
  // ink on the accent-soft tint, both modes
167
167
  ['.chip.busy label', 'var(--accent-deep)', 'var(--accent-soft)', 4.5],
168
+ // lifecycle strip STATE INDICATORS (task 1002751): non-text at the WCAG
169
+ // 1.4.11 bar of 3.0. --sea-ink derives brighter in dark exactly the way
170
+ // --accent-deep does, so a pack's light-tuned sea can't vanish on the dark
171
+ // card; the done tick takes --on-ok (the ink-on-a-filled-state-dot token).
172
+ ['strip done/now indicator on card', 'var(--sea-ink)', CARD, 3.0],
173
+ ['strip done fill vs its idle rail', 'var(--sea-ink)', 'var(--sea-soft)', 3.0],
174
+ ['strip indicator on the strip tint', 'var(--sea-ink)', 'color-mix(in srgb,var(--sea) 8%,var(--bg-card))', 3.0],
175
+ ['strip done tick on its fill', 'var(--on-ok)', 'var(--sea-ink)', 3.0],
176
+ ['flowDown done tick on its fill', 'var(--on-ok)', 'var(--ink-faint)', 3.0],
177
+ ['.steps done/now ring on card', 'var(--accent)', CARD, 3.0],
178
+ ['.steps done vs idle connector', 'var(--accent)', 'var(--line)', 3.0],
179
+ // the opted-out address card swaps to the page ground (--bg); its muted copy
180
+ // must still measure (19px/700 headline = large text, 3.0)
181
+ ['optedOut .addrBig (large) on --bg', 'var(--ink-soft)', 'var(--bg)', 3.0],
182
+ ['addrOpt warning on --bg', 'var(--warn)', 'var(--bg)', 4.5],
168
183
  // lifecycle strip: 12px/700
169
184
  ['.strip .slbl idle', 'var(--ink-faint)', CARD, 4.5],
170
185
  ['.strip .slbl now/done', 'color-mix(in srgb,var(--sea) 40%,var(--ink))', CARD, 4.5],
@@ -254,6 +269,81 @@ test('dark blocks never re-point a token at the locked space palette', () => {
254
269
  }
255
270
  });
256
271
 
272
+ // opacity multiplies AFTER the token resolves, so a dimmed subtree's contrast
273
+ // is invisible to every token assertion above (task 1002751: .slocked at .6
274
+ // took 13.5px copy to 3.02:1; .chip .rec at .72 took recency to 3.48:1;
275
+ // .addrConfirm.optedOut at .75 took its note to 3.43:1; a placeholder rule's
276
+ // .75 survived into an override that set only the color, so no token value
277
+ // could ever fix it). Dim with a muted TOKEN so the result stays measured.
278
+ // Known limits, accepted: @keyframes bodies, animation-fill-mode resting
279
+ // values, and JS-set inline opacity are out of static reach — never make an
280
+ // animation or script the resting presenter of text or a state mark.
281
+ const pageSrc = fs
282
+ .readFileSync(path.join(path.dirname(CSS), '..', 'projects.html'), 'utf8')
283
+ .replace(/<!--[\s\S]*?-->/g, '')
284
+ .replace(/\/\*[\s\S]*?\*\//g, '');
285
+ const cssSrc = src.replace(/\/\*[\s\S]*?\*\//g, '');
286
+ const dimsBelowFull = (body) => {
287
+ const m = body.match(/opacity\s*:\s*([\d.]+%?)/);
288
+ if (!m) return false;
289
+ const v = m[1].endsWith('%') ? parseFloat(m[1]) / 100 : parseFloat(m[1]);
290
+ return v < 1;
291
+ };
292
+
293
+ test('text is never dimmed by subtree opacity', () => {
294
+ // Two nets. (1) Anywhere in either sheet: a rule that declares BOTH a color
295
+ // and a sub-1 opacity is dimmed text by construction. (2) The create flow's
296
+ // known text containers may not carry opacity at all — this also catches
297
+ // ancestor dims (.createSplit) that net 1 can't see.
298
+ const offenders = [];
299
+ for (const [where, text] of [['cosmos.css', cssSrc], ['projects.html', pageSrc]]) {
300
+ for (const m of text.matchAll(/([^{}@]+)\{([^{}]*)\}/g)) {
301
+ if (/(?:^|[;{\s])color\s*:/.test(m[2]) && dimsBelowFull(m[2])) {
302
+ offenders.push(`${where}: ${m[1].trim().slice(0, 60)} declares color + opacity<1`);
303
+ }
304
+ }
305
+ }
306
+ for (const sel of ['\\.humanSteps', '\\.addrConfirm', '\\.prog\\b', '\\.choice', '\\.review', '\\.createSplit', '\\.chip \\.rec']) {
307
+ for (const text of [cssSrc, pageSrc]) {
308
+ for (const m of text.matchAll(new RegExp(`${sel}[^{}]*\\{([^}]*)\\}`, 'g'))) {
309
+ if (dimsBelowFull(m[1])) offenders.push(`text container dimmed: ${m[0].slice(0, 90)}`);
310
+ }
311
+ }
312
+ }
313
+ assert.deepEqual(offenders, [], `\n ${offenders.join('\n ')}\n`);
314
+ });
315
+
316
+ test('the .prog strip mirrors .strip: state marks are --sea-ink, ticks are --on-ok', () => {
317
+ // /create's done panel renders the page-local .prog, NOT cosmos's .strip —
318
+ // a re-point of one and not the other is exactly how the 1.74:1 dark tick
319
+ // shipped the first time. State-mark rules (done fill/connector, now
320
+ // ring/pulse, the tick) must use the derived tokens; raw --sea is only legal
321
+ // as a tint ingredient (color-mix) or the idle -soft rail.
322
+ const progRules = [...pageSrc.matchAll(/\.prog[^{}]*\{([^}]*)\}/g)];
323
+ assert.ok(progRules.length >= 5, 'the .prog rules moved out of projects.html — update this test');
324
+ const bad = [];
325
+ for (const m of progRules) {
326
+ const body = m[1];
327
+ if (/(?:border(?:-\w+)?|background)\s*:\s*var\(\s*--sea\s*\)/.test(body)) {
328
+ bad.push(`raw --sea state mark: ${m[0].slice(0, 80)}`);
329
+ }
330
+ if (/border-(?:left|bottom)\s*:[^;]*#fff\b/i.test(body)) {
331
+ bad.push(`hardcoded #fff tick: ${m[0].slice(0, 80)}`);
332
+ }
333
+ }
334
+ assert.deepEqual(bad, [], `\n ${bad.join('\n ')}\n`);
335
+ });
336
+
337
+ test('the pulse animation never dims its mark', () => {
338
+ // beatPulse drives the now-dot in BOTH .strip and .prog. An opacity trough
339
+ // (the old 0.6) put the pulsing mark below the 3:1 its token clears at rest
340
+ // — the PAIRS row above would assert a color the pixel never sustains.
341
+ const kf = cssSrc.match(/@keyframes beatPulse\{([\s\S]*?)\}\s*\}/) ||
342
+ cssSrc.match(/@keyframes beatPulse\{([^@]*)\}/);
343
+ assert.ok(kf, '@keyframes beatPulse not found in cosmos.css — update this test');
344
+ assert.ok(!/opacity\s*:/.test(kf[1]), 'beatPulse dims the now-mark by opacity again');
345
+ });
346
+
257
347
  test('the two dark blocks stay in lockstep', () => {
258
348
  // cosmos.css carries dark twice — an explicit data-mode hook and a
259
349
  // prefers-color-scheme twin. A change to one and not the other is the