@bongos/core 1.19.89 → 1.19.90

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.
@@ -0,0 +1,103 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Palette + hero options — cloudbongos.com/ (task 1002747)</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Baloo+2:wght@500;600;700&family=Quicksand:wght@400;500;600;700&family=Fira+Code:wght@400;500&display=swap">
10
+ <style>
11
+ :root{--ink:#12131c;--soft:#5d6070;--faint:#8b8fa3;--line:#dcdfe8;--bg:#f4f5f9;--card:#fff;}
12
+ @media(prefers-color-scheme:dark){:root{--ink:#f2f3f8;--soft:#a8adc0;--faint:#767b90;--line:#2b2f3d;--bg:#0d0f16;--card:#161a24;}}
13
+ *{box-sizing:border-box;margin:0;padding:0;}
14
+ body{background:var(--bg);color:var(--ink);font-family:"Quicksand",system-ui,sans-serif;
15
+ padding:clamp(20px,4vw,46px);-webkit-font-smoothing:antialiased;}
16
+ h1{font-family:"Baloo 2",sans-serif;font-weight:700;font-size:clamp(24px,3.4vw,34px);letter-spacing:-.7px;margin-bottom:8px;}
17
+ .sub{font-size:15px;font-weight:500;color:var(--soft);max-width:74ch;line-height:1.6;margin-bottom:10px;}
18
+ .sub b{color:var(--ink);}
19
+ .warn{border-left:3px solid #dc2626;background:color-mix(in srgb,#dc2626 8%,transparent);
20
+ border-radius:0 10px 10px 0;padding:12px 16px;font-size:14px;font-weight:600;line-height:1.6;
21
+ color:var(--ink);max-width:82ch;margin:16px 0 26px;}
22
+ .warn code{font-family:"Fira Code",monospace;font-size:12.5px;}
23
+ .bar{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:24px;}
24
+ .bar button{cursor:pointer;font-family:"Quicksand",sans-serif;font-weight:700;font-size:13.5px;
25
+ padding:8px 16px;border-radius:999px;border:2px solid var(--line);background:var(--card);color:var(--ink);}
26
+ .bar button[aria-pressed="true"]{border-color:var(--ink);background:var(--ink);color:var(--card);}
27
+ .bar span{font-family:"Fira Code",monospace;font-size:11px;color:var(--faint);letter-spacing:.5px;text-transform:uppercase;}
28
+ .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(430px,1fr));gap:clamp(16px,2.4vw,26px);}
29
+ .cell{background:var(--card);border:1.5px solid var(--line);border-radius:18px;overflow:hidden;}
30
+ .cellHead{display:flex;align-items:center;gap:10px;padding:12px 16px;border-bottom:1.5px solid var(--line);}
31
+ .dot{width:16px;height:16px;border-radius:50%;flex:0 0 auto;box-shadow:0 0 0 1.5px rgba(128,128,128,.28) inset;}
32
+ .nm{font-family:"Baloo 2",sans-serif;font-weight:700;font-size:16px;letter-spacing:-.2px;}
33
+ .tag{font-size:12px;font-weight:600;color:var(--faint);margin-left:auto;font-family:"Fira Code",monospace;}
34
+ .frame{position:relative;width:100%;aspect-ratio:16/10;overflow:hidden;background:var(--bg);}
35
+ .frame iframe{position:absolute;top:0;left:0;width:1280px;height:800px;border:0;
36
+ transform:scale(.336);transform-origin:top left;}
37
+ .note{padding:11px 16px;font-size:13px;font-weight:500;color:var(--soft);line-height:1.55;border-top:1.5px solid var(--line);}
38
+ .note b{color:var(--ink);font-weight:700;}
39
+ a.open{display:inline-block;margin-top:6px;font-size:12.5px;font-weight:700;color:inherit;}
40
+ </style>
41
+ </head>
42
+ <body>
43
+
44
+ <h1>Seven palettes, two heroes</h1>
45
+ <p class="sub">The same landing page, re-themed. Each palette is exactly what a branding pack does — it
46
+ overrides the <b>cosmos.css</b> contract tier. On a real instance only one exists, injected by
47
+ <b>themeMarkup()</b>. Click any tile's <b>open full</b> to walk it at full size with the switcher.</p>
48
+
49
+ <div class="warn">
50
+ <b>Found while checking these:</b> the palette shipping <b>today</b> fails WCAG AA. White on
51
+ <code>--accent #f97316</code> is <b>2.80:1</b> where 4.5 is required, and <code>--accent-deep #ea580c</code>
52
+ as link text on a card is <b>3.56:1</b>. That affects every primary button and link on <code>/create</code>
53
+ and <code>/projects</code> right now — not just this page. All six candidates below were checked with a
54
+ contrast calculator across twelve pairs each and pass cleanly in both modes.
55
+ </div>
56
+
57
+ <div class="bar">
58
+ <span>hero</span>
59
+ <button data-m="bongos" aria-pressed="true">bongos</button>
60
+ <button data-m="buddha" aria-pressed="false">buddha</button>
61
+ <span style="margin-left:14px">mode</span>
62
+ <button data-md="light" aria-pressed="true">light</button>
63
+ <button data-md="dark" aria-pressed="false">dark</button>
64
+ </div>
65
+
66
+ <div class="grid" id="grid"></div>
67
+
68
+ <script>
69
+ var SCHEMES=[
70
+ {k:'', n:'shipped today', d:'#f97316', note:'The current tokens. <b>Fails contrast</b> on buttons and links — see above.'},
71
+ {k:'adobe-clay', n:'adobe clay', d:'#b8492c', note:'Fired terracotta, sand, a whisper of sage. The only warm option with a real <b>second hue</b>, so the dark band has somewhere to go. <b>Risk:</b> too much beige reads as tired.'},
72
+ {k:'ember-marigold', n:'ember marigold',d:'#ce4806', note:'Today’s sunrise with the milk taken out — a genuinely pigmented backdrop so cards lift off it. <b>Risk:</b> one hue plays every part.'},
73
+ {k:'letterpress', n:'letterpress', d:'#C22A12', note:'Warm paper, near-black type, one vermilion struck on the downbeat. Most trustworthy. <b>Risk:</b> austerity fights the rounded type.'},
74
+ {k:'deep-water', n:'deep water', d:'#0d7f97', note:'Promotes the unused <code>--sea</code> token to the lead. Calm and coherent. <b>Risk:</b> reads as generic cloud-vendor SaaS.'},
75
+ {k:'sapwood', n:'sapwood', d:'#3d7a33', note:'Oat paper and pigment greens, dodging both eco-brand and terminal-green. <b>Risk:</b> green says gardening, not build platform.'},
76
+ {k:'plumfield', n:'plumfield', d:'#932bcf', note:'Leans hardest into the space metaphor; best dark mode of the set. <b>Risk:</b> violet+magenta is the uniform of crypto — bad for a capture-resistance pitch.'}
77
+ ];
78
+ var mascot='bongos', mode='light';
79
+ function render(){
80
+ document.getElementById('grid').innerHTML = SCHEMES.map(function(s){
81
+ var src='landing-page-mock.html?rail=0&mascot='+mascot+'&mode='+mode+'&scheme='+encodeURIComponent(s.k);
82
+ return '<div class="cell">'+
83
+ '<div class="cellHead"><span class="dot" style="background:'+s.d+'"></span>'+
84
+ '<span class="nm">'+s.n+'</span><span class="tag">'+(s.k||'current')+'</span></div>'+
85
+ '<div class="frame"><iframe loading="lazy" src="'+src+'" title="'+s.n+'"></iframe></div>'+
86
+ '<div class="note">'+s.note+'<br><a class="open" href="'+src.replace('rail=0','rail=1')+'" target="_blank">open full →</a></div>'+
87
+ '</div>';
88
+ }).join('');
89
+ }
90
+ document.querySelectorAll('[data-m]').forEach(function(b){
91
+ b.onclick=function(){ mascot=b.dataset.m;
92
+ document.querySelectorAll('[data-m]').forEach(function(x){x.setAttribute('aria-pressed',String(x===b));});
93
+ render(); };
94
+ });
95
+ document.querySelectorAll('[data-md]').forEach(function(b){
96
+ b.onclick=function(){ mode=b.dataset.md;
97
+ document.querySelectorAll('[data-md]').forEach(function(x){x.setAttribute('aria-pressed',String(x===b));});
98
+ render(); };
99
+ });
100
+ render();
101
+ </script>
102
+ </body>
103
+ </html>
@@ -5,11 +5,11 @@
5
5
  --bg-deep: #ffedd5;
6
6
  --ink: #2a1f17;
7
7
  --ink-soft: #6b5d52;
8
- --ink-faint: #857567;
8
+ --ink-faint: #756a60;
9
9
  --rule: #fdba74;
10
10
  --rule-soft: #fed7aa;
11
- --accent: #f97316;
12
- --accent-deep: #ea580c;
11
+ --accent: #ce4806;
12
+ --accent-deep: #9e3406;
13
13
  --terracotta: #fb7185;
14
14
  --font-display: "Baloo 2", "Quicksand", system-ui, sans-serif;
15
15
  --font-body: "Quicksand", system-ui, -apple-system, "Segoe UI", sans-serif;
@@ -90,11 +90,11 @@ function buildThemeCss(branding) {
90
90
  --bg-deep: ${v(c.bgDeep, '#ffedd5')};
91
91
  --ink: ${v(c.ink, '#2a1f17')};
92
92
  --ink-soft: ${v(c.inkSoft, '#6b5d52')};
93
- --ink-faint: ${v(c.inkFaint, '#857567')};
93
+ --ink-faint: ${v(c.inkFaint, '#756a60')};
94
94
  --rule: ${v(c.rule, '#fdba74')};
95
95
  --rule-soft: ${v(c.ruleSoft, '#fed7aa')};
96
- --accent: ${v(c.accent, '#f97316')};
97
- --accent-deep: ${v(c.accentDeep, '#ea580c')};
96
+ --accent: ${v(c.accent, '#ce4806')};
97
+ --accent-deep: ${v(c.accentDeep, '#9e3406')};
98
98
  --terracotta: ${v(c.terracotta, '#fb7185')};
99
99
  --font-display: ${v(fonts.display, '"Baloo 2", "Quicksand", system-ui, sans-serif')};
100
100
  --font-body: ${v(fonts.body, '"Quicksand", system-ui, -apple-system, "Segoe UI", sans-serif')};
@@ -70,7 +70,7 @@ ICON = 100 # dmg.iconSize
70
70
  COLOR_ROLES = {
71
71
  "bg": "#fff7ed", "bgDeep": "#ffedd5", "bgCard": "#ffffff",
72
72
  "ink": "#2a1f17", "inkSoft": "#6b5d52", "ruleSoft": "#fed7aa",
73
- "accent": "#f97316", "accentDeep": "#ea580c", "terracotta": "#fb7185",
73
+ "accent": "#ce4806", "accentDeep": "#9e3406", "terracotta": "#fb7185",
74
74
  }
75
75
 
76
76
  FONT_DIRS = (
@@ -64,7 +64,7 @@ const OUT_BUILD = path.join(APP_DIR, 'build-resources');
64
64
  const COLOR_ROLES = {
65
65
  bgCard: '#ffffff', bgDeep: '#ffedd5', ink: '#2a1f17',
66
66
  rule: '#fdba74', ruleSoft: '#fed7aa',
67
- accent: '#f97316', accentDeep: '#ea580c', terracotta: '#fb7185',
67
+ accent: '#ce4806', accentDeep: '#9e3406', terracotta: '#fb7185',
68
68
  };
69
69
 
70
70
  // The pack, resolved exactly as gen-brand.js resolves it (env → instance → neutral).
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.89",
3
+ "version": "1.19.90",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.89",
9
+ "version": "1.19.90",
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.89",
3
+ "version": "1.19.90",
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",
@@ -27,9 +27,9 @@
27
27
  :root{
28
28
  /* contract tier (light mode) */
29
29
  --bg:#fff7ed; --bg-card:#ffffff; --bg-deep:#ffedd5;
30
- --ink:#2a1f17; --ink-soft:#6b5d52; --ink-faint:#9c8d80;
30
+ --ink:#2a1f17; --ink-soft:#6b5d52; --ink-faint:#756a60;
31
31
  --rule:#fdba74; --rule-soft:#fed7aa;
32
- --accent:#f97316; --accent-deep:#ea580c; --sea:#38bdf8; --terracotta:#fb7185;
32
+ --accent:#ce4806; --accent-deep:#9e3406; --sea:#38bdf8; --terracotta:#fb7185;
33
33
  --font-display:"Baloo 2","Quicksand",system-ui,"Arial Rounded MT Bold",sans-serif;
34
34
  --font-body:"Quicksand",system-ui,-apple-system,"Segoe UI",sans-serif;
35
35
  --font-mono:"Fira Code",ui-monospace,SFMono-Regular,Menlo,monospace;
@@ -38,8 +38,9 @@
38
38
  --space-deep:#05060f; --space-mid:#0a0c20; --space-high:#0d0f28; --nebula:#2b2456;
39
39
  --sun-lit:#fb923c; --gold:#ffd98a; --gold-soft:#f6c976;
40
40
 
41
- /* semantic state tier */
42
- --ok:#16a34a; --warn:#d97706; --err:#dc2626;
41
+ /* semantic state tier — each is painted as 12px/700 chip TEXT on its own
42
+ -soft tint, so each must clear 4.5:1 against that tint AND the plain card. */
43
+ --ok:#107937; --warn:#9c5604; --err:#ca2323;
43
44
 
44
45
  /* chrome tier — the WHOLE WORLD flips with the mode (owner, 2026-08-12,
45
46
  Loop 2: light = the sunrise white/orange page, dark = the gate's space).
@@ -49,24 +50,33 @@
49
50
  --star-display:none;
50
51
  --chrome-ink:#2a1f17;
51
52
  --chrome-ink-dim:#6b5d52;
52
- --chrome-accent:#f97316;
53
+ --chrome-accent:var(--accent-deep);
53
54
  --chrome-glow:none;
54
55
  --glass-ink:#6b5d52;
55
56
  --glass-bg:rgba(255,255,255,0.65);
56
- --glass-border:rgba(249,115,22,0.4);
57
+ --glass-border:color-mix(in srgb, var(--accent) 42%, transparent);
57
58
  --ava-shadow:0 2px 8px rgba(42,31,23,0.12);
58
59
  --hero-lede:#6b5d52;
59
- --hero-b:#ea580c;
60
- --foot-ink:#9c8d80;
61
- --foot-code:#ea580c;
60
+ --hero-b:var(--accent-deep);
61
+ --foot-ink:var(--ink-faint);
62
+ --foot-code:var(--accent-deep);
62
63
  color-scheme:light;
63
64
  }
64
65
  /* dark tier — space sky + dark cards. Two hooks so an explicit choice beats
65
- the OS preference. */
66
+ the OS preference; keep the two blocks in lockstep (tests/cosmos_token_contrast.mjs).
67
+
68
+ A declaration here is PERMANENT for every instance: these selectors are (0,2,0)
69
+ and the branding pack injects a plain :root at (0,1,0), so it can never override
70
+ them. --accent and --sea are therefore deliberately absent — that omission is the
71
+ only reason a re-themed instance's accent reaches dark mode at all.
72
+ --accent-deep is the exception: it is text, and a hex tuned to 4.5:1 on a near-white
73
+ card cannot also clear it on a #12142e one. It is DERIVED from --accent (a different
74
+ property, so no cycle) and so still follows the pack. */
66
75
  :root[data-mode="dark"]{
67
76
  --bg:#0d0f28; --bg-card:#12142e; --bg-deep:#1d2046;
68
- --ink:#f4eee7; --ink-soft:#c9bfb2; --ink-faint:#93897d;
77
+ --ink:#f4eee7; --ink-soft:#c9bfb2; --ink-faint:#998e82;
69
78
  --rule:#5a4a7a; --rule-soft:#3a3560;
79
+ --accent-deep:color-mix(in srgb, var(--accent) 45%, #ffffff);
70
80
  --ok:#4ade80; --warn:#fbbf24; --err:#f87171;
71
81
  --page-bg:#05060f;
72
82
  --sky-bg:radial-gradient(120% 85% at 50% -12%, #2b2456 0%, rgba(43,36,86,0) 55%),
@@ -76,7 +86,7 @@
76
86
  --star-display:block;
77
87
  --chrome-ink:#ffffff;
78
88
  --chrome-ink-dim:rgba(255,255,255,0.85);
79
- --chrome-accent:#fb923c;
89
+ --chrome-accent:var(--accent-deep);
80
90
  --chrome-glow:0 2px 12px rgba(20,12,30,.7);
81
91
  --glass-ink:#ffd98a;
82
92
  --glass-bg:rgba(255,255,255,0.06);
@@ -91,8 +101,9 @@
91
101
  @media (prefers-color-scheme: dark){
92
102
  :root:not([data-mode="light"]){
93
103
  --bg:#0d0f28; --bg-card:#12142e; --bg-deep:#1d2046;
94
- --ink:#f4eee7; --ink-soft:#c9bfb2; --ink-faint:#93897d;
104
+ --ink:#f4eee7; --ink-soft:#c9bfb2; --ink-faint:#998e82;
95
105
  --rule:#5a4a7a; --rule-soft:#3a3560;
106
+ --accent-deep:color-mix(in srgb, var(--accent) 45%, #ffffff);
96
107
  --ok:#4ade80; --warn:#fbbf24; --err:#f87171;
97
108
  --page-bg:#05060f;
98
109
  --sky-bg:radial-gradient(120% 85% at 50% -12%, #2b2456 0%, rgba(43,36,86,0) 55%),
@@ -102,7 +113,7 @@
102
113
  --star-display:block;
103
114
  --chrome-ink:#ffffff;
104
115
  --chrome-ink-dim:rgba(255,255,255,0.85);
105
- --chrome-accent:#fb923c;
116
+ --chrome-accent:var(--accent-deep);
106
117
  --chrome-glow:0 2px 12px rgba(20,12,30,.7);
107
118
  --glass-ink:#ffd98a;
108
119
  --glass-bg:rgba(255,255,255,0.06);
@@ -241,7 +252,7 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
241
252
  @media (prefers-color-scheme: dark){:root:not([data-mode="light"]) .addr code{color:var(--sun-lit);}}
242
253
  .attach{display:flex;gap:10px;margin-top:10px;flex-wrap:wrap;}
243
254
  .attach input[type=text]{flex:1;min-width:220px;font-family:var(--font-body);font-size:15px;font-weight:600;color:var(--ink);background:var(--bg-card);border:2px solid var(--line);border-radius:12px;padding:9px 14px;outline:none;transition:border-color .15s ease, box-shadow .15s ease;}
244
- .attach input[type=text]::placeholder{color:color-mix(in srgb,var(--ink-faint) 70%,var(--bg-card));font-weight:500;}
255
+ .attach input[type=text]::placeholder{color:var(--ink-faint);font-weight:500;}
245
256
  .attach input[type=text]:focus{border-color:var(--accent);box-shadow:0 0 0 4px color-mix(in srgb,var(--accent) 13%,transparent);}
246
257
  .projMsg{font-size:13.5px;font-weight:600;margin-top:10px;min-height:18px;color:var(--ink-soft);line-height:1.5;}
247
258
  .projMsg.ok{color:var(--ok);}
@@ -272,7 +283,7 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
272
283
  .strip .stages li.now .sdot{border-color:var(--sea);}
273
284
  .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;}
274
285
  @keyframes beatPulse{0%,100%{transform:scale(.7);opacity:.6}50%{transform:scale(1.15);opacity:1}}
275
- .strip .slbl{font-size:12px;font-weight:700;color:color-mix(in srgb,var(--sea) 55%,var(--ink-faint));}
286
+ .strip .slbl{font-size:12px;font-weight:700;color:var(--ink-faint);}
276
287
  .strip .stages li.now .slbl,.strip .stages li.done .slbl{color:color-mix(in srgb,var(--sea) 40%,var(--ink));}
277
288
  .strip .pnote{text-align:center;font-size:13px;font-weight:600;color:var(--ink-soft);min-height:18px;line-height:1.5;}
278
289
  .strip .pnote a{color:var(--accent-deep);font-weight:700;text-decoration:none;}
@@ -131,7 +131,8 @@ input[type=text]{
131
131
  background:var(--bg-card);border:2px solid var(--line);border-radius:14px;padding:13px 16px;outline:none;
132
132
  transition:border-color .15s ease, box-shadow .15s ease;
133
133
  }
134
- input[type=text]::placeholder{color:color-mix(in srgb,var(--ink-faint) 70%,var(--bg-card));font-weight:500;}
134
+ /* no fade toward the card — a 70% mix washed placeholders to 2.89:1 (task 1002749) */
135
+ input[type=text]::placeholder{color:var(--ink-faint);font-weight:500;}
135
136
  input[type=text]:focus{border-color:var(--accent);box-shadow:0 0 0 4px color-mix(in srgb,var(--accent) 13%,transparent);}
136
137
  input[type=text].bad{border-color:var(--err);}
137
138
  .hint{font-size:13px;font-weight:600;color:var(--ink-faint);margin-top:7px;min-height:18px;}
@@ -328,7 +329,9 @@ input[type=text].bad{border-color:var(--err);}
328
329
  .ghrow:last-child{border-bottom:0;}
329
330
  .ghrow:hover{background:color-mix(in srgb,var(--accent) 6%,var(--bg-deep));}
330
331
  .ghrow[aria-selected=true]{background:color-mix(in srgb,var(--accent) 12%,var(--bg-deep));box-shadow:inset 3px 0 0 var(--accent);font-weight:600;}
331
- .ghrow .priv{margin-left:auto;font-size:11px;font-weight:600;color:var(--ink-faint);background:color-mix(in srgb,var(--ink) 8%,transparent);border-radius:5px;padding:1px 6px;}
332
+ /* --ink-soft, not --ink-faint: the badge tint is the darkest ground on the page,
333
+ and faint ink only reaches 3.96:1 on it (task 1002749). */
334
+ .ghrow .priv{margin-left:auto;font-size:11px;font-weight:600;color:var(--ink-soft);background:color-mix(in srgb,var(--ink) 8%,transparent);border-radius:5px;padding:1px 6px;}
332
335
  </style>
333
336
  </head>
334
337
  <body>
package/src/module-api.js CHANGED
@@ -605,8 +605,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
605
605
  // landed since 1.19.87 with no explicit bump. run 31656577595. (task 1002620)
606
606
  // 1.19.89 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
607
607
  // landed since 1.19.88 with no explicit bump. run 31657467485. (task 1002620)
608
+ // 1.19.90 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
609
+ // landed since 1.19.89 with no explicit bump. run 31717551308. (task 1002620)
608
610
  // ---------------------------------------------------------------------------
609
- const CORE_VERSION = '1.19.89'; // CI auto-patch carrier (ADR 0161); see changelog above
611
+ const CORE_VERSION = '1.19.90'; // CI auto-patch carrier (ADR 0161); see changelog above
610
612
 
611
613
  // A namespaced logger so a module's log lines are attributable + consistent.
612
614
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -32,7 +32,9 @@ test('R74: a vanilla boot resolves to the Cloud Bongos identity', () => {
32
32
 
33
33
  test('R74: the vanilla pack carries the Cloud Bongos LOOK (sunrise palette, Baloo 2/Quicksand, bongo favicon)', () => {
34
34
  const c = clientBranding(vanilla());
35
- assert.equal(c.theme.colors.accent, '#f97316', 'sunrise orange accent');
35
+ // fired down from the original #f97316 so white-on-accent clears WCAG AA
36
+ // (2.80:1 -> 4.61:1, task 1002749) — same ember hue, readable
37
+ assert.equal(c.theme.colors.accent, '#ce4806', 'sunrise orange accent');
36
38
  assert.equal(c.theme.colors.bg, '#fff7ed', 'warm sunrise base');
37
39
  assert.match(c.theme.fonts.display, /Baloo 2/);
38
40
  assert.match(c.theme.fonts.body, /Quicksand/);
@@ -49,12 +51,12 @@ test('R74: serving the hall on a vanilla boot injects the Cloud Bongos sunrise t
49
51
  const html = '<head><link rel="stylesheet" href="/builders/style.css"><title>x</title></head><body></body>';
50
52
  const out = injectBrandingGlobal(html, c);
51
53
  // palette override + sunrise body backdrop + bongo favicon + the type links
52
- assert.match(out, /:root\{[^}]*--accent:#f97316;/);
54
+ assert.match(out, /:root\{[^}]*--accent:#ce4806;/);
53
55
  assert.match(out, /body\{background:linear-gradient\(180deg/);
54
56
  assert.match(out, /<link rel="icon" href="data:image\/svg\+xml[^"]*circle/);
55
57
  assert.match(out, /fonts\.googleapis\.com\/css2\?family=Baloo\+2/);
56
58
  // and it lands AFTER the stylesheet so the Cloud Bongos look wins
57
- assert.ok(out.indexOf('/builders/style.css') < out.indexOf('--accent:#f97316'));
59
+ assert.ok(out.indexOf('/builders/style.css') < out.indexOf('--accent:#ce4806'));
58
60
  });
59
61
 
60
62
  test('R74: themeMarkup paints the backdrop only when the pack supplies one', () => {
@@ -0,0 +1,268 @@
1
+ // Guards the WCAG AA contrast of the cosmos.css token layer (task 1002749).
2
+ //
3
+ // The tokens are the ONLY input here: this test PARSES public-landing/assets/cosmos.css
4
+ // and resolves var()/color-mix() itself, so it measures what the surfaces actually paint.
5
+ // Duplicating the hex values would prove nothing.
6
+ //
7
+ // AA thresholds: 4.5:1 normal text, 3.0:1 large text (>=24px, or >=18.66px bold) and
8
+ // meaningful non-text boundaries. Every pair below is a real selector in cosmos.css.
9
+
10
+ import { test } from 'node:test';
11
+ import assert from 'node:assert/strict';
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+
16
+ const CSS = path.join(
17
+ path.dirname(fileURLToPath(import.meta.url)),
18
+ '..', 'public-landing', 'assets', 'cosmos.css'
19
+ );
20
+ const src = fs.readFileSync(CSS, 'utf8');
21
+
22
+ // ── parse ────────────────────────────────────────────────────────────────────
23
+ const declarations = (body) => {
24
+ const out = {};
25
+ // strip comments, then take --name:value up to the ; that ends the declaration
26
+ for (const m of body.replace(/\/\*[\s\S]*?\*\//g, '').matchAll(/(--[\w-]+)\s*:\s*([^;]+);/g)) {
27
+ // collapse the whitespace of multi-line values so the lockstep check compares
28
+ // meaning, not indentation
29
+ out[m[1]] = m[2].replace(/\s+/g, ' ').trim();
30
+ }
31
+ return out;
32
+ };
33
+
34
+ const bodyAt = (open) => {
35
+ let depth = 0;
36
+ for (let i = open; i < src.length; i++) {
37
+ if (src[i] === '{') depth++;
38
+ else if (src[i] === '}' && --depth === 0) return src.slice(open + 1, i);
39
+ }
40
+ throw new Error(`unbalanced braces at offset ${open}`);
41
+ };
42
+
43
+ const blockAfter = (marker) => {
44
+ const at = src.indexOf(marker);
45
+ assert.notEqual(at, -1, `cosmos.css no longer contains ${marker} — update this test`);
46
+ return bodyAt(src.indexOf('{', at));
47
+ };
48
+
49
+ // A bare :root appears twice: the contract/chrome tier first, the derived tier last.
50
+ // Both feed both modes, so the two merge into one raw map per mode.
51
+ const bareRoots = [...src.matchAll(/\n:root\{/g)].map((m) => bodyAt(src.indexOf('{', m.index)));
52
+ assert.equal(bareRoots.length, 2, 'expected exactly two bare :root blocks in cosmos.css');
53
+ const [contractTier, derivedTier] = bareRoots.map(declarations);
54
+ const firstRoot = contractTier;
55
+ const derived = derivedTier;
56
+
57
+ const darkExplicit = declarations(blockAfter(':root[data-mode="dark"]{'));
58
+ const darkMedia = declarations(blockAfter(':root:not([data-mode="light"]){'));
59
+
60
+ // THE CASCADE THAT ACTUALLY SHIPS. injectBrandingGlobal splices the branding pack's
61
+ // theme <style> at the END of <head> — after the cosmos.css <link> — as a plain
62
+ // :root. Equal specificity, later in document order, so for the 13 tokens the pack
63
+ // emits the PACK wins and cosmos.css's own :root value never renders. Measuring
64
+ // cosmos.css alone would measure a page nobody sees.
65
+ // The dark blocks are :root[data-mode="dark"] and :root:not([data-mode="light"]) —
66
+ // both (0,2,0) — so they outrank the pack's (0,1,0) and win back.
67
+ const packColors = JSON.parse(
68
+ fs.readFileSync(path.join(path.dirname(CSS), '..', '..', 'config', 'branding.neutral.json'), 'utf8')
69
+ ).theme.ui.colors;
70
+
71
+ // mirrors COLOR_VARS in src/bongos/serve-internal.js
72
+ const camelToVar = (k) => '--' + k.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase());
73
+ const PACK = Object.fromEntries(
74
+ Object.entries(packColors).map(([k, v]) => [camelToVar(k), v])
75
+ );
76
+
77
+ const RAW = {
78
+ light: { ...firstRoot, ...derived, ...PACK },
79
+ dark: { ...firstRoot, ...derived, ...PACK, ...darkExplicit },
80
+ };
81
+
82
+ // ── color resolution ─────────────────────────────────────────────────────────
83
+ const clamp = (v) => Math.min(255, Math.max(0, v));
84
+
85
+ function resolve(expr, mode, seen = new Set()) {
86
+ const e = String(expr).trim();
87
+
88
+ if (e === 'transparent') return [0, 0, 0, 0];
89
+
90
+ let m = e.match(/^#([0-9a-f]{6})$/i);
91
+ if (m) { const n = parseInt(m[1], 16); return [(n >> 16) & 255, (n >> 8) & 255, n & 255, 1]; }
92
+
93
+ m = e.match(/^#([0-9a-f]{3})$/i);
94
+ if (m) return [...m[1]].map((c) => parseInt(c + c, 16)).concat(1);
95
+
96
+ m = e.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/);
97
+ if (m) return [+m[1], +m[2], +m[3], m[4] === undefined ? 1 : +m[4]];
98
+
99
+ m = e.match(/^var\(\s*(--[\w-]+)\s*(?:,([\s\S]+))?\)$/);
100
+ if (m) {
101
+ const name = m[1];
102
+ assert.ok(!seen.has(name), `cyclic var reference at ${name}`);
103
+ const next = RAW[mode][name] ?? m[2];
104
+ assert.ok(next !== undefined, `${name} is not defined for ${mode} mode`);
105
+ return resolve(next, mode, new Set([...seen, name]));
106
+ }
107
+
108
+ m = e.match(/^color-mix\(\s*in\s+srgb\s*,\s*([\s\S]+?)\s+([\d.]+)%\s*,\s*([\s\S]+?)\s*\)$/);
109
+ if (m) {
110
+ const p = +m[2] / 100;
111
+ const a = resolve(m[1], mode, seen);
112
+ const b = resolve(m[3], mode, seen);
113
+ // color-mix premultiplies by alpha; every pair we assert on resolves opaque,
114
+ // and `transparent` correctly drags the result toward the other operand.
115
+ const alpha = a[3] * p + b[3] * (1 - p);
116
+ if (alpha === 0) return [0, 0, 0, 0];
117
+ return [0, 1, 2]
118
+ .map((i) => (a[i] * a[3] * p + b[i] * b[3] * (1 - p)) / alpha)
119
+ .concat(alpha);
120
+ }
121
+
122
+ throw new Error(`cannot resolve color expression: ${e}`);
123
+ }
124
+
125
+ // flatten a possibly-translucent color onto an opaque backdrop
126
+ const over = (fg, bg) =>
127
+ fg[3] === 1 ? fg : [0, 1, 2].map((i) => clamp(fg[i] * fg[3] + bg[i] * (1 - fg[3]))).concat(1);
128
+
129
+ const lin = (c) => { c /= 255; return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4; };
130
+ const lum = ([r, g, b]) => 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
131
+ const contrast = (fg, bg) => {
132
+ const [hi, lo] = [lum(fg), lum(bg)].sort((a, b) => b - a);
133
+ return (hi + 0.05) / (lo + 0.05);
134
+ };
135
+
136
+ const hex = (c) => '#' + c.slice(0, 3).map((v) => Math.round(v).toString(16).padStart(2, '0')).join('');
137
+
138
+ // ── the pairs cosmos.css actually paints ─────────────────────────────────────
139
+ // need: 4.5 normal text · 3.0 large text and meaningful non-text
140
+ const CARD = 'var(--bg-card)';
141
+ const PAIRS = [
142
+ // primary + secondary action
143
+ ['.btn label on its fill', '#ffffff', 'var(--accent)', 4.5],
144
+ ['.btn.ghost:hover label', 'var(--accent-deep)', CARD, 4.5],
145
+ ['.btn.danger label', 'var(--err)', CARD, 4.5],
146
+ // Inline links: cosmos.css recolors its own to --sun-lit in dark (:179-262), but
147
+ // create.html has ELEVEN --accent-deep consumers and zero dark CSS rules (its only
148
+ // data-mode hits are the JS toggle), so there the token itself must carry dark.
149
+ ['inline link on a card', 'var(--accent-deep)', CARD, 4.5],
150
+ ['inline link on the page ground', 'var(--accent-deep)', 'var(--bg)', 4.5],
151
+ ['code chip on --bg-deep', 'var(--accent-deep)', 'var(--bg-deep)', 4.5],
152
+ ['cosmos.css dark link (--sun-lit)', 'var(--sun-lit)', CARD, 4.5, 'dark'],
153
+ // body copy
154
+ ['--ink body copy', 'var(--ink)', CARD, 4.5],
155
+ ['--ink-soft secondary copy', 'var(--ink-soft)', CARD, 4.5],
156
+ ['--ink-faint .projMeta 13px', 'var(--ink-faint)', CARD, 4.5],
157
+ ['--ink-faint .finePrint on ground', 'var(--ink-faint)', 'var(--bg)', 4.5],
158
+ // chips: 12px/700 label on its own tint
159
+ ['.chip base label', 'var(--ink-soft)', 'var(--neutral-soft)', 4.5],
160
+ ['.chip.calm label', 'var(--ink-faint)', 'var(--neutral-soft)', 4.5],
161
+ ['.chip.live label', 'var(--ok)', 'var(--ok-soft)', 4.5],
162
+ ['.chip.maybe label', 'var(--warn)', 'var(--warn-soft)', 4.5],
163
+ ['.chip.down/.bad label', 'var(--err)', 'var(--err-soft)', 4.5],
164
+ // .chip.busy recolors in dark (cosmos.css:194-195) — assert what each mode paints
165
+ ['.chip.busy label', 'var(--accent-deep)', 'var(--accent-soft)', 4.5, 'light'],
166
+ ['.chip.busy label', 'var(--sun-lit)', 'var(--accent-soft)', 4.5, 'dark'],
167
+ // lifecycle strip: 12px/700
168
+ ['.strip .slbl idle', 'var(--ink-faint)', CARD, 4.5],
169
+ ['.strip .slbl now/done', 'color-mix(in srgb,var(--sea) 40%,var(--ink))', CARD, 4.5],
170
+ ['.strip.isLive .slbl', 'var(--ok)', CARD, 4.5],
171
+ // status messages
172
+ ['.projMsg.ok', 'var(--ok)', CARD, 4.5],
173
+ ['.projMsg.err / .confirmRow', 'var(--err)', CARD, 4.5],
174
+ // chrome: wordmark + footer sit on the page ground, not on a card
175
+ // create.html page-local grounds — it carries its own <style> and its own
176
+ // darkest tints, so cosmos.css's pairs alone do not cover /create
177
+ ['create.html .ghrow .priv 11px', 'var(--ink-soft)', 'color-mix(in srgb,var(--ink) 8%,var(--bg-deep))', 4.5],
178
+ ['create.html ::placeholder', 'var(--ink-faint)', CARD, 4.5],
179
+ ['create.html .ghlist .hint on deep', 'var(--ink-faint)', 'var(--bg-deep)', 4.5],
180
+ ['--chrome-accent wordmark', 'var(--chrome-accent)', 'var(--page-bg)', 4.5],
181
+ ['--chrome-ink', 'var(--chrome-ink)', 'var(--page-bg)', 4.5],
182
+ ['--foot-ink footer copy', 'var(--foot-ink)', 'var(--page-bg)', 4.5],
183
+ ['--foot-code footer code', 'var(--foot-code)', 'var(--page-bg)', 4.5],
184
+ ['--hero-b hero emphasis', 'var(--hero-b)', 'var(--bg)', 4.5],
185
+ ['--hero-lede hero lede', 'var(--hero-lede)', 'var(--bg)', 4.5],
186
+ ];
187
+
188
+ for (const mode of ['light', 'dark']) {
189
+ test(`cosmos.css tokens clear WCAG AA in ${mode} mode`, () => {
190
+ const ground = resolve('var(--page-bg)', mode);
191
+ const failures = [];
192
+ for (const [what, fgExpr, bgExpr, need, onlyMode] of PAIRS) {
193
+ if (onlyMode && onlyMode !== mode) continue;
194
+ const bg = over(resolve(bgExpr, mode), ground);
195
+ const fg = over(resolve(fgExpr, mode), bg);
196
+ const got = contrast(fg, bg);
197
+ if (got < need) {
198
+ failures.push(`${what}: ${got.toFixed(2)}:1 (needs ${need}) — ${hex(fg)} on ${hex(bg)}`);
199
+ }
200
+ }
201
+ assert.deepEqual(failures, [], `\n ${failures.join('\n ')}\n`);
202
+ });
203
+ }
204
+
205
+ test('the two dark blocks stay in lockstep', () => {
206
+ // cosmos.css carries dark twice — an explicit data-mode hook and a
207
+ // prefers-color-scheme twin. A change to one and not the other is the
208
+ // classic regression here, and it only shows up for half of users.
209
+ assert.deepEqual(
210
+ darkMedia,
211
+ darkExplicit,
212
+ 'the @media (prefers-color-scheme: dark) twin drifted from :root[data-mode="dark"]'
213
+ );
214
+ });
215
+
216
+ test('cosmos.css defaults do not drift from the branding pack', () => {
217
+ // For the 13 pack-owned tokens, cosmos.css's :root is a dead default — the pack
218
+ // always overrides it. That makes silent drift easy and dangerous: it was already
219
+ // real (cosmos.css said --ink-faint:#9c8d80, the pack shipped #857567), and it
220
+ // means anyone auditing cosmos.css alone measures colors no user ever sees.
221
+ const drift = [];
222
+ for (const [name, packValue] of Object.entries(PACK)) {
223
+ const cssValue = firstRoot[name];
224
+ if (cssValue === undefined) continue;
225
+ if (cssValue.toLowerCase() !== packValue.toLowerCase()) {
226
+ drift.push(`${name}: cosmos.css ${cssValue} vs pack ${packValue}`);
227
+ }
228
+ }
229
+ assert.deepEqual(drift, [], `\n ${drift.join('\n ')}\n`);
230
+ });
231
+
232
+ test('the dark blocks never pin a token the branding pack owns', () => {
233
+ // The pack injects a plain :root — specificity (0,1,0). Both dark selectors are
234
+ // (0,2,0), so ANY literal declared there outranks the pack forever and that
235
+ // instance gets Cloud Bongos' color in dark mode. A dark override of a
236
+ // pack-settable token is only safe if it DERIVES from one (var(--…)).
237
+ const PACK_OWNED = ['--accent', '--accent-deep', '--sea', '--terracotta'];
238
+ for (const block of [darkExplicit, darkMedia]) {
239
+ for (const name of PACK_OWNED) {
240
+ const v = block[name];
241
+ if (v === undefined) continue; // absent = the pack reaches dark. Ideal.
242
+ assert.match(
243
+ v,
244
+ /var\(--/,
245
+ `${name} is pinned to a literal in a dark block — a re-themed instance can never override it`
246
+ );
247
+ }
248
+ }
249
+ });
250
+
251
+ test('every accent-derived chrome token follows its contract token', () => {
252
+ // A branding pack re-themes the contract tier. Any chrome token that hardcodes
253
+ // the platform orange instead of referencing --accent leaks Cloud Bongos orange
254
+ // into a re-themed instance (ADR 0062 §7).
255
+ for (const [name, expected] of [
256
+ ['--chrome-accent', /var\(--accent(-deep)?\)/],
257
+ ['--glass-border', /var\(--accent(-deep)?\)/],
258
+ ['--hero-b', /var\(--accent(-deep)?\)/],
259
+ ['--foot-code', /var\(--accent(-deep)?\)/],
260
+ ['--foot-ink', /var\(--ink[\w-]*\)/],
261
+ ]) {
262
+ assert.match(
263
+ firstRoot[name] ?? '',
264
+ expected,
265
+ `${name} hardcodes a color instead of following the contract tier`
266
+ );
267
+ }
268
+ });