@bongos/core 1.24.54 → 1.24.55

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,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/bongos/package-core.js",
5
- "core_version": "1.24.54",
6
- "core_contract": "1.24.54",
7
- "source_commit": "c1487272513fac29c1563998fc31e24bb13f757b",
5
+ "core_version": "1.24.55",
6
+ "core_contract": "1.24.55",
7
+ "source_commit": "5a654cca6b5584dadcb4cf5a0a604b4dbe9d2423",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-11-07T12:33:45.032Z",
9
+ "built_at": "2026-11-07T13:11:45.343Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 637,
13
13
  "agent_docs_stubbed": 32,
14
- "functional_verbatim": 3888,
14
+ "functional_verbatim": 3893,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 4558,
20
- "tree_sha256": "e78d2d21e0a1487420a245e16e47f567621987eb0e689ee656e28bfe3fa2c426",
19
+ "file_count": 4563,
20
+ "tree_sha256": "2d8156970dd1580c0720307f19bac2dbfaa764eb9b8fb743cc2dabc6b0fb2b03",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -3002,7 +3002,7 @@
3002
3002
  {
3003
3003
  "path": "docs/module-api-changelog.md",
3004
3004
  "mode": "0000644",
3005
- "sha256": "75015c4dd158ca7b02815ea925bedd1904233bd355e8a2a634239518664aa4b5"
3005
+ "sha256": "27e53e829bdb2a25f3e0a669b7d91456162f7c48df2ebacce7dfb81be08eccb9"
3006
3006
  },
3007
3007
  {
3008
3008
  "path": "docs/modules-contract.md",
@@ -11694,6 +11694,21 @@
11694
11694
  "mode": "0000644",
11695
11695
  "sha256": "93ea35854c648fd9c4a66d02868b338ab2838b4595a42a86242f99acb3d4de27"
11696
11696
  },
11697
+ {
11698
+ "path": "modules/ui-design/public/brand.css",
11699
+ "mode": "0000644",
11700
+ "sha256": "02e8b06b640a7eebd905ecac18bde4b22b5718c0cde8d6abe3c99b17f6230e96"
11701
+ },
11702
+ {
11703
+ "path": "modules/ui-design/public/brand.html",
11704
+ "mode": "0000644",
11705
+ "sha256": "718ed7bf511946f9878a894d6019cf5ae667b970c7e45371515c357310ceb9fb"
11706
+ },
11707
+ {
11708
+ "path": "modules/ui-design/public/brand.states.json",
11709
+ "mode": "0000644",
11710
+ "sha256": "e09f19c17d383da42aa64d485e2281a8448c21883b1adb4e2c43297875c9d607"
11711
+ },
11697
11712
  {
11698
11713
  "path": "modules/ui-design/public/brand/apple-touch-icon.png",
11699
11714
  "mode": "0000644",
@@ -11754,6 +11769,11 @@
11754
11769
  "mode": "0000644",
11755
11770
  "sha256": "5d38655b42d1f0e27607b2a026676d46253a869eae8fdccd1aa6e1bac0c288c7"
11756
11771
  },
11772
+ {
11773
+ "path": "modules/ui-design/scripts/gen-design-reference.js",
11774
+ "mode": "0000644",
11775
+ "sha256": "f372163aa83a75e5d9ec5650d28e6a1c11a43c084a5db16d4284bbd60b68e70e"
11776
+ },
11757
11777
  {
11758
11778
  "path": "modules/ui-design/scripts/gen-design-tokens.js",
11759
11779
  "mode": "0000644",
@@ -11872,12 +11892,12 @@
11872
11892
  {
11873
11893
  "path": "package-lock.json",
11874
11894
  "mode": "0000644",
11875
- "sha256": "4e260983c396326d6050f5bf8b82e3ca1bb9d3b14d355a97cd46dd1b28a9e7f1"
11895
+ "sha256": "da6da45b121ab5b65f6f5281520d6344c50252ff5325f1736e64f2501616c0ac"
11876
11896
  },
11877
11897
  {
11878
11898
  "path": "package.json",
11879
11899
  "mode": "0000644",
11880
- "sha256": "a064c411ce3cc3267076cbe05edee314ef7ce674d45651c9312b1242df981f52"
11900
+ "sha256": "b60272204a1ca2996fa122dbb81bad4fc6d200d783b0cf3247a2fcb48df9969c"
11881
11901
  },
11882
11902
  {
11883
11903
  "path": "public-docs/index.html",
@@ -11897,7 +11917,7 @@
11897
11917
  {
11898
11918
  "path": "release-notes.json",
11899
11919
  "mode": "0000644",
11900
- "sha256": "060392a335624b929951dc284a27d7ddd82fbffdb6ce4f917eae4459d1977ab2"
11920
+ "sha256": "c4bc76b71c94da1bbf1c3e4730581b448ce4537219fcfbd3452d896abd078f03"
11901
11921
  },
11902
11922
  {
11903
11923
  "path": "scripts/bongos/CLAUDE.md",
@@ -15832,7 +15852,7 @@
15832
15852
  {
15833
15853
  "path": "src/module-api.js",
15834
15854
  "mode": "0000644",
15835
- "sha256": "b605a0a15f013ef95257a2c25363b41493c006f066f199965c952df9f4b997f2"
15855
+ "sha256": "ef77f7a92cf3dfec0b57200f62bd0bdcc03b4a8df263a4707dd783195cf92896"
15836
15856
  },
15837
15857
  {
15838
15858
  "path": "src/module-loader/catalog.js",
@@ -15882,7 +15902,7 @@
15882
15902
  {
15883
15903
  "path": "src/platform-server.js",
15884
15904
  "mode": "0000644",
15885
- "sha256": "8bcbb0682fe33073abdba7cc3e6923cf9aa02c9763dcc5d71edb5267dc28a1ba"
15905
+ "sha256": "57713cdf702f436cd5440d0d08afd403cbb1be71ede94fa95e4dd994cddcfeaf"
15886
15906
  },
15887
15907
  {
15888
15908
  "path": "src/role-responsibilities.js",
@@ -17359,6 +17379,11 @@
17359
17379
  "mode": "0000644",
17360
17380
  "sha256": "7a492f806c867e71e52a9d90b3c0b30d47327c83d95e989ad09377c958457dd5"
17361
17381
  },
17382
+ {
17383
+ "path": "tests/design_reference.mjs",
17384
+ "mode": "0000644",
17385
+ "sha256": "64a500720aa3d0179700366ce948a93a84dcf4e975a4e261ff95eb6575412c0e"
17386
+ },
17362
17387
  {
17363
17388
  "path": "tests/design_tokens_generated.mjs",
17364
17389
  "mode": "0000644",
@@ -3362,5 +3362,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
3362
3362
  landed since 1.24.52 with no explicit bump. run 42365574840. (task 1002620)
3363
3363
  1.24.54 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3364
3364
  landed since 1.24.53 with no explicit bump. run 42372833171. (task 1002620)
3365
+ 1.24.55 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3366
+ landed since 1.24.54 with no explicit bump. run 42375689581. (task 1002620)
3365
3367
  ---------------------------------------------------------------------------
3366
3368
  ```
@@ -0,0 +1,168 @@
1
+ /* modules/ui-design/public/brand.css — the design-system reference's own LAYOUT
2
+ (task 1005083). brand.html is generated (scripts/gen-design-reference.js); this
3
+ sheet only lays its sections out. Every colour, size, corner and duration is a
4
+ token: the pack's thirteen, the hall's derived tier (loaded from
5
+ /modules/hall-ui/public/style.css, which brings the --ds-* scales and the shared
6
+ controls), or a --ds-* step. No page :root; page values live on body. The dark
7
+ tier comes from the hall's sheet (both twins), so nothing here is mode-specific. */
8
+
9
+ body[data-page="brand"] {
10
+ --ref-max: 1040px;
11
+ --ref-swatch: 34px;
12
+ margin: 0;
13
+ color: var(--ink);
14
+ background: var(--bg);
15
+ font-family: var(--font-body);
16
+ }
17
+
18
+ /* ---- the bar: the mark, the name, the sections ---- */
19
+ .ref-head {
20
+ box-sizing: border-box;
21
+ display: flex;
22
+ flex-wrap: wrap;
23
+ align-items: center;
24
+ gap: var(--ds-space-xs) var(--ds-space-md);
25
+ max-width: var(--ref-max);
26
+ margin: 0 auto;
27
+ padding: var(--ds-space-sm) var(--ds-space-md);
28
+ }
29
+ .ref-head__brand {
30
+ display: inline-flex;
31
+ align-items: center;
32
+ gap: var(--ds-space-xs);
33
+ min-height: 32px;
34
+ font-family: var(--font-display);
35
+ font-weight: 300;
36
+ font-size: var(--ds-text-lg);
37
+ text-transform: lowercase;
38
+ color: var(--ink);
39
+ }
40
+ .ref-head__brand:hover { text-decoration: none; }
41
+ .ref-head__mark, .ref-lockup__mark { display: block; flex: none; --mark-ink: var(--ink); }
42
+ .ref-head__mark { width: 28px; height: 28px; }
43
+ .ref-toc { display: flex; flex-wrap: wrap; gap: 0 var(--ds-space-md); margin: 0 0 var(--ds-space-md); }
44
+ .ref-toc a {
45
+ display: inline-flex;
46
+ align-items: center;
47
+ min-height: 32px;
48
+ font-size: var(--ds-text-sm);
49
+ color: var(--ink-soft);
50
+ }
51
+ .ref-toc a:hover { color: var(--ink); }
52
+
53
+ /* ---- the page ---- */
54
+ .ref-main {
55
+ box-sizing: border-box;
56
+ max-width: var(--ref-max);
57
+ margin: 0 auto;
58
+ padding: var(--ds-space-md) var(--ds-space-md) var(--ds-space-xl);
59
+ }
60
+ .ref-title {
61
+ margin: 0 0 var(--ds-space-xs);
62
+ font-family: var(--font-display);
63
+ font-size: var(--ds-text-headline);
64
+ font-weight: 300;
65
+ line-height: 1.1;
66
+ }
67
+ .ref-lede {
68
+ max-width: var(--measure);
69
+ margin: 0 0 var(--ds-space-sm);
70
+ font-size: var(--ds-text-lg);
71
+ color: var(--ink-soft);
72
+ }
73
+ .ref-sec { scroll-margin-top: var(--ds-space-md); }
74
+ .ref-sub {
75
+ margin: var(--ds-space-lg) 0 var(--ds-space-xs);
76
+ font-size: var(--ds-text-md);
77
+ font-weight: 600;
78
+ }
79
+ .ref-note { max-width: var(--measure); margin: 0 0 var(--ds-space-xs); font-size: var(--ds-text-sm); color: var(--ink-soft); }
80
+ .ref-main code { overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
81
+
82
+ /* ---- the token rows: one line per token, wrapping on a phone ---- */
83
+ .ref-list { margin: 0; padding: 0; list-style: none; }
84
+ .ref-row {
85
+ display: flex;
86
+ flex-wrap: wrap;
87
+ align-items: center;
88
+ gap: var(--ds-space-hair) var(--ds-space-sm);
89
+ min-width: 0;
90
+ padding: var(--ds-space-xs) 0;
91
+ border-top: var(--ds-line) solid var(--rule-soft);
92
+ }
93
+ .ref-row:first-child { border-top: 0; }
94
+ .ref-name { flex: 0 0 13rem; min-width: 0; font-size: var(--ds-text-sm); color: var(--ink); }
95
+ .ref-val { flex: 1 1 12rem; min-width: 0; font-size: var(--ds-text-sm); color: var(--ink-soft); }
96
+ .ref-val code { color: var(--ink); }
97
+ .ref-val__k {
98
+ font-family: var(--label-face);
99
+ font-size: var(--label-size);
100
+ letter-spacing: var(--label-space);
101
+ text-transform: var(--label-case);
102
+ color: var(--ink-faint);
103
+ }
104
+ .ref-swatch {
105
+ flex: none;
106
+ width: var(--ref-swatch);
107
+ height: var(--ref-swatch);
108
+ border-radius: var(--ds-radius-tile);
109
+ border: var(--ds-line) solid var(--rule);
110
+ }
111
+ .ref-face { flex: 1 1 14rem; min-width: 0; font-size: var(--ds-text-3xl); }
112
+ .ref-row--face .ref-val { flex-basis: 100%; }
113
+ .ref-sample { flex: 1 1 100%; min-width: 0; line-height: 1.2; overflow-wrap: anywhere; }
114
+ .ref-bar { flex: none; height: 14px; border-radius: var(--ds-radius-pill); background: var(--accent-tint); border: var(--ds-line) solid var(--accent-deep); }
115
+ .ref-hair { flex: 0 1 8rem; border-top: var(--ds-line) solid var(--rule); }
116
+ .ref-corner, .ref-float {
117
+ flex: none;
118
+ width: 56px;
119
+ height: 40px;
120
+ background: var(--bg-card);
121
+ border: var(--ds-line) solid var(--rule);
122
+ }
123
+ .ref-float { border-radius: var(--ds-radius-tile); margin: var(--ds-space-xs) var(--ds-space-xs) var(--ds-space-md) 0; }
124
+ .ref-what { flex: 1 1 14rem; min-width: 0; font-size: var(--ds-text-sm); color: var(--ink-soft); }
125
+ .ref-file { display: inline-flex; align-items: center; flex: 0 0 13rem; min-height: 24px; }
126
+
127
+ /* ---- the logo and the naming rule ---- */
128
+ .ref-lockup {
129
+ display: flex;
130
+ align-items: center;
131
+ gap: var(--ds-space-sm);
132
+ margin: var(--ds-space-sm) 0 var(--ds-space-md);
133
+ font-family: var(--font-display);
134
+ font-weight: 300;
135
+ font-size: var(--ds-text-headline);
136
+ letter-spacing: -0.01em;
137
+ color: var(--ink);
138
+ }
139
+ .ref-lockup__mark { width: 72px; height: 72px; }
140
+ .ref-naming { max-width: var(--measure); font-size: var(--ds-text-md); color: var(--ink-soft); }
141
+ .ref-naming p { margin: 0 0 var(--ds-space-xs); }
142
+ .ref-naming ul { margin: 0; padding-left: var(--ds-space-md); }
143
+ .ref-naming li { margin: 0 0 var(--ds-space-hair); }
144
+ .ref-naming strong { color: var(--ink); font-weight: 600; }
145
+
146
+ /* ---- the components, each at rest where it can be looked at ---- */
147
+ .ref-specimen { display: flex; flex-wrap: wrap; align-items: center; gap: var(--ds-space-xs); margin: var(--ds-space-xs) 0; }
148
+ .ref-fields { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr)); gap: var(--ds-space-sm); }
149
+ .ref-field { display: flex; flex-direction: column; gap: var(--ds-space-hair); min-width: 0; font-size: var(--ds-text-sm); color: var(--ink-soft); }
150
+ .ref-field input, .ref-field select, .ref-field textarea { width: 100%; }
151
+ .ref-tile { margin: var(--ds-space-xs) 0; }
152
+ .ref-toasts { display: flex; flex-direction: column; align-items: flex-start; gap: var(--ds-space-xs); }
153
+ .ref-toast.toast { position: static; transform: none; max-width: 100%; box-sizing: border-box; }
154
+ .ref-toast__k { color: inherit; }
155
+ .ref-dialog { padding: var(--ds-space-sm); border-radius: var(--ds-radius-tile); background: var(--scrim); }
156
+ .ref-dialog .hall-dialog { position: static; width: 100%; max-width: 480px; margin: 0 auto; box-shadow: var(--ds-shadow-pop); }
157
+
158
+ /* ---- the sources ---- */
159
+ .ref-sources { margin-top: var(--ds-space-lg); font-size: var(--ds-text-sm); color: var(--ink-soft); }
160
+ .ref-sources__h { margin: 0 0 var(--ds-space-xs); font-size: var(--ds-text-md); font-weight: 600; color: var(--ink); }
161
+ .ref-sources ul { margin: 0; padding-left: var(--ds-space-md); }
162
+
163
+ @media (max-width: 760px) {
164
+ .ref-head, .ref-main { padding-left: var(--ds-space-sm); padding-right: var(--ds-space-sm); }
165
+ .ref-main .scroll { padding: var(--ds-space-sm); }
166
+ .ref-name, .ref-file { flex-basis: 100%; }
167
+ .ref-row--colour .ref-name { flex: 1 1 8rem; }
168
+ }
@@ -0,0 +1,162 @@
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>{{productName}} — Design system</title>
7
+ <meta name="description" content="The colours, type, spacing, corners, motion and shared components every {{productName}} page is drawn from.">
8
+ <!-- GENERATED by node modules/ui-design/scripts/gen-design-reference.js (task 1005083); do not edit by hand.
9
+ Every value is read from its source file; every component is drawn by the hall's shared rules.
10
+ kit-ignore: tell: tiny type (the type scale shows its smallest step at its own size) -->
11
+ <link rel="stylesheet" href="/modules/hall-ui/public/style.css">
12
+ <link rel="stylesheet" href="/modules/ui-design/public/brand.css">
13
+ </head>
14
+ <body data-page="brand">
15
+ <header class="ref-head">
16
+ <a class="ref-head__brand" href="/"><svg class="ref-head__mark" viewBox="0 0 200 200" aria-hidden="true"><use href="/modules/ui-design/public/brand/mark.svg#mark"/></svg><span>cloud bongos</span></a>
17
+ </header>
18
+ <main class="ref-main">
19
+ <h1 class="ref-title">Design system</h1>
20
+ <p class="ref-lede">The colours, type, spacing, corners, motion and shared components every {{productName}} page is drawn from. This page is generated from those same files, so it cannot drift from them. It follows your device’s light or dark setting; each value is shown for both.</p>
21
+ <nav class="ref-toc" aria-label="On this page">
22
+ <a href="#ref-logo">Logo</a><a href="#ref-colour">Colour</a><a href="#ref-type">Type</a><a href="#ref-space">Spacing</a><a href="#ref-shape">Corners</a><a href="#ref-motion">Motion</a><a href="#ref-components">Components</a>
23
+ </nav>
24
+ <section class="scroll ref-sec" id="ref-logo" aria-labelledby="ref-logo-h">
25
+ <h2 class="scroll__h" id="ref-logo-h">Logo and name</h2>
26
+ <p class="scroll__lede">The mark is one file every page draws from. The ring and the planet take the ink; the moon takes the accent.</p>
27
+
28
+ <div class="ref-lockup">
29
+ <svg class="ref-lockup__mark" viewBox="0 0 200 200" aria-hidden="true"><use href="/modules/ui-design/public/brand/mark.svg#mark"/></svg>
30
+ <span class="ref-lockup__name">cloud bongos</span>
31
+ </div>
32
+ <div class="ref-naming">
33
+ <p><strong>The wordmark is lowercase; the name in a sentence is capitalised</strong> (owner, 2026-11-06):</p>
34
+ <ul>
35
+ <li>In the logo and the wordmark beside the mark: <strong>cloud bongos</strong>, all lowercase, the display voice.</li>
36
+ <li>In running text, titles, metadata, emails and Discord: <strong>Cloud Bongos</strong>, two capitals. Never &quot;CloudBongos&quot;, &quot;Cloud bongos&quot; or &quot;CB&quot;.</li>
37
+ <li>The domain is written <code>cloudbongos.com</code>, lowercase, one word.</li>
38
+ <li>An instance&#39;s own product name comes from its branding pack (<code>identity.productName</code>) and follows that instance&#39;s rules, not these. Platform code never hardcodes either spelling. It reads the pack.</li>
39
+ </ul>
40
+ </div>
41
+ <h3 class="ref-sub">The files</h3>
42
+ <ul class="ref-list" role="list"><li class="ref-row ref-row--file"><a class="ref-file" href="/modules/ui-design/public/brand/mark.svg"><code>mark.svg</code></a><span class="ref-what">the one source: drawn inline by reference, coloured by the page</span></li><li class="ref-row ref-row--file"><a class="ref-file" href="/modules/ui-design/public/brand/mark-light.svg"><code>mark-light.svg</code></a><span class="ref-what">the mark in Ink with the accent moon (light grounds)</span></li><li class="ref-row ref-row--file"><a class="ref-file" href="/modules/ui-design/public/brand/mark-dark.svg"><code>mark-dark.svg</code></a><span class="ref-what">the mark in the Night ink with the accent moon (dark grounds)</span></li><li class="ref-row ref-row--file"><a class="ref-file" href="/modules/ui-design/public/brand/mark-mono.svg"><code>mark-mono.svg</code></a><span class="ref-what">the whole mark in one ink (black), for a print or a stamp</span></li><li class="ref-row ref-row--file"><a class="ref-file" href="/modules/ui-design/public/brand/mark-body.svg"><code>mark-body.svg</code></a><span class="ref-what">the ring and planet only, one ink</span></li><li class="ref-row ref-row--file"><a class="ref-file" href="/modules/ui-design/public/brand/mark-dot.svg"><code>mark-dot.svg</code></a><span class="ref-what">the moon only, one ink</span></li><li class="ref-row ref-row--file"><a class="ref-file" href="/modules/ui-design/public/brand/favicon.svg"><code>favicon.svg</code></a><span class="ref-what">the small cut, Ink by day and the Night ink by night</span></li><li class="ref-row ref-row--file"><a class="ref-file" href="/modules/ui-design/public/brand/apple-touch-icon.png"><code>apple-touch-icon.png</code></a><span class="ref-what">180x180, the light mark on the pack&#39;s ground</span></li><li class="ref-row ref-row--file"><a class="ref-file" href="/modules/ui-design/public/brand/og-card.png"><code>og-card.png</code></a><span class="ref-what">1200x630, the link-preview card every public page names</span></li></ul>
43
+ </section>
44
+ <section class="scroll ref-sec" id="ref-colour" aria-labelledby="ref-colour-h">
45
+ <h2 class="scroll__h" id="ref-colour-h">Colour</h2>
46
+ <p class="scroll__lede">The thirteen colours of the branding pack. An instance replaces them in its own pack; in dark mode only the neutrals flip, and the accent, sea and terracotta stay the pack’s.</p>
47
+
48
+ <ul class="ref-list" role="list"><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--bg)" aria-hidden="true"></span><code class="ref-name">--bg</code><span class="ref-val"><span class="ref-val__k">light</span> <code>#edecee</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>#000000</code></span></li><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--bg-card)" aria-hidden="true"></span><code class="ref-name">--bg-card</code><span class="ref-val"><span class="ref-val__k">light</span> <code>#fbfafc</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>#101012</code></span></li><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--bg-deep)" aria-hidden="true"></span><code class="ref-name">--bg-deep</code><span class="ref-val"><span class="ref-val__k">light</span> <code>#e2e1e4</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>#000000</code></span></li><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--bg-frame)" aria-hidden="true"></span><code class="ref-name">--bg-frame</code><span class="ref-val"><span class="ref-val__k">light</span> <code>#f6f5f7</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>#08080a</code></span></li><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--ink)" aria-hidden="true"></span><code class="ref-name">--ink</code><span class="ref-val"><span class="ref-val__k">light</span> <code>#17181a</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>#ffffff</code></span></li><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--ink-soft)" aria-hidden="true"></span><code class="ref-name">--ink-soft</code><span class="ref-val"><span class="ref-val__k">light</span> <code>#55565a</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>#c0c0c0</code></span></li><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--ink-faint)" aria-hidden="true"></span><code class="ref-name">--ink-faint</code><span class="ref-val"><span class="ref-val__k">light</span> <code>#65656a</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>#a8a8a8</code></span></li><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--rule)" aria-hidden="true"></span><code class="ref-name">--rule</code><span class="ref-val"><span class="ref-val__k">light</span> <code>#c6c6c8</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>rgba(255, 255, 255, .16)</code></span></li><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--rule-soft)" aria-hidden="true"></span><code class="ref-name">--rule-soft</code><span class="ref-val"><span class="ref-val__k">light</span> <code>#dcdbdd</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>rgba(255, 255, 255, .08)</code></span></li><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--accent)" aria-hidden="true"></span><code class="ref-name">--accent</code><span class="ref-val"><span class="ref-val__k">light</span> <code>#fb9569</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>same as light</code></span></li><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--accent-deep)" aria-hidden="true"></span><code class="ref-name">--accent-deep</code><span class="ref-val"><span class="ref-val__k">light</span> <code>#a3481a</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>same as light</code></span></li><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--sea)" aria-hidden="true"></span><code class="ref-name">--sea</code><span class="ref-val"><span class="ref-val__k">light</span> <code>#5b6fd0</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>same as light</code></span></li><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--terracotta)" aria-hidden="true"></span><code class="ref-name">--terracotta</code><span class="ref-val"><span class="ref-val__k">light</span> <code>#c9512c</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>same as light</code></span></li></ul>
49
+ <h3 class="ref-sub">The status vocabulary</h3>
50
+ <p class="ref-note">Fixed, not taken from the pack, and the one exception to the fifteen tokens: a status is always a word as well as a colour.</p>
51
+ <ul class="ref-list" role="list"><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--ok)" aria-hidden="true"></span><code class="ref-name">--ok</code><span class="ref-val"><span class="ref-val__k">light</span> <code>#187444</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>#4ade80</code></span></li><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--ok-weak)" aria-hidden="true"></span><code class="ref-name">--ok-weak</code><span class="ref-val"><span class="ref-val__k">light</span> <code>color-mix(in srgb, #187444 12%, var(--bg-card))</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>color-mix(in srgb, #4ade80 14%, var(--bg-card))</code></span></li><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--warn)" aria-hidden="true"></span><code class="ref-name">--warn</code><span class="ref-val"><span class="ref-val__k">light</span> <code>#895c00</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>#fbbf24</code></span></li><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--warn-weak)" aria-hidden="true"></span><code class="ref-name">--warn-weak</code><span class="ref-val"><span class="ref-val__k">light</span> <code>color-mix(in srgb, #895c00 14%, var(--bg-card))</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>color-mix(in srgb, #fbbf24 14%, var(--bg-card))</code></span></li><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--danger)" aria-hidden="true"></span><code class="ref-name">--danger</code><span class="ref-val"><span class="ref-val__k">light</span> <code>#b42318</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>#f87171</code></span></li><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--danger-weak)" aria-hidden="true"></span><code class="ref-name">--danger-weak</code><span class="ref-val"><span class="ref-val__k">light</span> <code>color-mix(in srgb, #b42318 10%, var(--bg-card))</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>color-mix(in srgb, #f87171 14%, var(--bg-card))</code></span></li><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--info)" aria-hidden="true"></span><code class="ref-name">--info</code><span class="ref-val"><span class="ref-val__k">light</span> <code>color-mix(in srgb, var(--sea) 86%, #000000)</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>color-mix(in srgb, var(--sea) 80%, #ffffff)</code></span></li><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--info-weak)" aria-hidden="true"></span><code class="ref-name">--info-weak</code><span class="ref-val"><span class="ref-val__k">light</span> <code>color-mix(in srgb, var(--info) 9%, var(--bg-card))</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>color-mix(in srgb, var(--info) 14%, var(--bg-card))</code></span></li></ul>
52
+ <h3 class="ref-sub">Derived colours</h3>
53
+ <p class="ref-note">Mixed from the thirteen, never typed as a new colour. Black and white with alpha appear only as a scrim, a shadow or glass.</p>
54
+ <ul class="ref-list" role="list"><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--glass)" aria-hidden="true"></span><code class="ref-name">--glass</code><span class="ref-val"><span class="ref-val__k">light</span> <code>rgba(23, 24, 26, .05)</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>rgba(255, 255, 255, .11)</code></span></li><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--glass-strong)" aria-hidden="true"></span><code class="ref-name">--glass-strong</code><span class="ref-val"><span class="ref-val__k">light</span> <code>rgba(23, 24, 26, .08)</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>rgba(255, 255, 255, .17)</code></span></li><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--glass-edge)" aria-hidden="true"></span><code class="ref-name">--glass-edge</code><span class="ref-val"><span class="ref-val__k">light</span> <code>rgba(23, 24, 26, .12)</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>rgba(255, 255, 255, .13)</code></span></li><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--void)" aria-hidden="true"></span><code class="ref-name">--void</code><span class="ref-val"><span class="ref-val__k">light</span> <code>#050508</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>#000000</code></span></li><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--void-ink)" aria-hidden="true"></span><code class="ref-name">--void-ink</code><span class="ref-val"><span class="ref-val__k">light</span> <code>#ffffff</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>var(--ink)</code></span></li><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--void-edge)" aria-hidden="true"></span><code class="ref-name">--void-edge</code><span class="ref-val"><span class="ref-val__k">light</span> <code>rgba(23, 24, 26, .12)</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>rgba(255, 255, 255, .16)</code></span></li><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--skeleton)" aria-hidden="true"></span><code class="ref-name">--skeleton</code><span class="ref-val"><span class="ref-val__k">light</span> <code>color-mix(in srgb, var(--ink) 10%, var(--bg-card))</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>color-mix(in srgb, var(--ink) 10%, var(--bg-card))</code></span></li><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--accent-ink)" aria-hidden="true"></span><code class="ref-name">--accent-ink</code><span class="ref-val"><span class="ref-val__k">light</span> <code>color-mix(in srgb, var(--accent) 45%, #240e00)</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>var(--accent)</code></span></li><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--accent-lift)" aria-hidden="true"></span><code class="ref-name">--accent-lift</code><span class="ref-val"><span class="ref-val__k">light</span> <code>color-mix(in srgb, var(--accent) 58%, #ffffff)</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>color-mix(in srgb, var(--accent) 58%, #ffffff)</code></span></li><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--accent-tint)" aria-hidden="true"></span><code class="ref-name">--accent-tint</code><span class="ref-val"><span class="ref-val__k">light</span> <code>color-mix(in srgb, var(--accent) 30%, #fffdf9)</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>color-mix(in srgb, var(--accent) 22%, #0b0a09)</code></span></li><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--accent-on)" aria-hidden="true"></span><code class="ref-name">--accent-on</code><span class="ref-val"><span class="ref-val__k">light</span> <code>#2a1305</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>#2a1305</code></span></li><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--focus)" aria-hidden="true"></span><code class="ref-name">--focus</code><span class="ref-val"><span class="ref-val__k">light</span> <code>color-mix(in srgb, var(--accent-deep) 82%, #3a1400)</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>var(--accent)</code></span></li><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--scrim)" aria-hidden="true"></span><code class="ref-name">--scrim</code><span class="ref-val"><span class="ref-val__k">light</span> <code>rgba(0, 0, 0, .55)</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>rgba(0, 0, 0, .55)</code></span></li><li class="ref-row ref-row--colour"><span class="ref-swatch" style="background: var(--surface-2)" aria-hidden="true"></span><code class="ref-name">--surface-2</code><span class="ref-val"><span class="ref-val__k">light</span> <code>color-mix(in srgb, var(--ink) 5%, var(--bg-card))</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>color-mix(in srgb, var(--ink) 6%, var(--bg-card))</code></span></li></ul>
55
+ </section>
56
+ <section class="scroll ref-sec" id="ref-type" aria-labelledby="ref-type-h">
57
+ <h2 class="scroll__h" id="ref-type-h">Type</h2>
58
+ <p class="scroll__lede">Two faces from the pack and one mono face for what the machine says: task ids, labels, figures.</p>
59
+
60
+ <ul class="ref-list" role="list"><li class="ref-row ref-row--face"><code class="ref-name">--font-display</code><span class="ref-face" style="font-family: var(--font-display)">Claim the next task</span><span class="ref-val"><span class="ref-val__k">display</span> <code>&quot;Manrope&quot;, &quot;Segoe UI&quot;, system-ui, -apple-system, sans-serif</code></span></li><li class="ref-row ref-row--face"><code class="ref-name">--font-body</code><span class="ref-face" style="font-family: var(--font-body)">Claim the next task</span><span class="ref-val"><span class="ref-val__k">body</span> <code>&quot;Manrope&quot;, &quot;Segoe UI&quot;, system-ui, -apple-system, sans-serif</code></span></li><li class="ref-row ref-row--face"><code class="ref-name">--font-mono</code><span class="ref-face" style="font-family: var(--font-mono)">Claim the next task</span><span class="ref-val"><span class="ref-val__k">mono</span> <code>&quot;JetBrains Mono&quot;, ui-monospace, &quot;Cascadia Mono&quot;, Consolas, monospace</code></span></li></ul>
61
+ <h3 class="ref-sub">The type scale</h3>
62
+ <ul class="ref-list" role="list"><li class="ref-row ref-row--type"><code class="ref-name">--ds-text-3xs</code><span class="ref-val"><span class="ref-val__k">size</span> <code>10.5px</code></span><span class="ref-sample" style="font-size: var(--ds-text-3xs)">Claim the next task</span></li><li class="ref-row ref-row--type"><code class="ref-name">--ds-text-2xs</code><span class="ref-val"><span class="ref-val__k">size</span> <code>11px</code></span><span class="ref-sample" style="font-size: var(--ds-text-2xs)">Claim the next task</span></li><li class="ref-row ref-row--type"><code class="ref-name">--ds-text-2xs-plus</code><span class="ref-val"><span class="ref-val__k">size</span> <code>11.5px</code></span><span class="ref-sample" style="font-size: var(--ds-text-2xs-plus)">Claim the next task</span></li><li class="ref-row ref-row--type"><code class="ref-name">--ds-text-xs</code><span class="ref-val"><span class="ref-val__k">size</span> <code>12px</code></span><span class="ref-sample" style="font-size: var(--ds-text-xs)">Claim the next task</span></li><li class="ref-row ref-row--type"><code class="ref-name">--ds-text-xs-plus</code><span class="ref-val"><span class="ref-val__k">size</span> <code>12.5px</code></span><span class="ref-sample" style="font-size: var(--ds-text-xs-plus)">Claim the next task</span></li><li class="ref-row ref-row--type"><code class="ref-name">--ds-text-sm</code><span class="ref-val"><span class="ref-val__k">size</span> <code>13px</code></span><span class="ref-sample" style="font-size: var(--ds-text-sm)">Claim the next task</span></li><li class="ref-row ref-row--type"><code class="ref-name">--ds-text-sm-plus</code><span class="ref-val"><span class="ref-val__k">size</span> <code>13.5px</code></span><span class="ref-sample" style="font-size: var(--ds-text-sm-plus)">Claim the next task</span></li><li class="ref-row ref-row--type"><code class="ref-name">--ds-text-md</code><span class="ref-val"><span class="ref-val__k">size</span> <code>14px</code></span><span class="ref-sample" style="font-size: var(--ds-text-md)">Claim the next task</span></li><li class="ref-row ref-row--type"><code class="ref-name">--ds-text-md-plus</code><span class="ref-val"><span class="ref-val__k">size</span> <code>14.5px</code></span><span class="ref-sample" style="font-size: var(--ds-text-md-plus)">Claim the next task</span></li><li class="ref-row ref-row--type"><code class="ref-name">--ds-text-lg</code><span class="ref-val"><span class="ref-val__k">size</span> <code>15px</code></span><span class="ref-sample" style="font-size: var(--ds-text-lg)">Claim the next task</span></li><li class="ref-row ref-row--type"><code class="ref-name">--ds-text-xl</code><span class="ref-val"><span class="ref-val__k">size</span> <code>16px</code></span><span class="ref-sample" style="font-size: var(--ds-text-xl)">Claim the next task</span></li><li class="ref-row ref-row--type"><code class="ref-name">--ds-text-2xl</code><span class="ref-val"><span class="ref-val__k">size</span> <code>17px</code></span><span class="ref-sample" style="font-size: var(--ds-text-2xl)">Claim the next task</span></li><li class="ref-row ref-row--type"><code class="ref-name">--ds-text-3xl</code><span class="ref-val"><span class="ref-val__k">size</span> <code>20px</code></span><span class="ref-sample" style="font-size: var(--ds-text-3xl)">Claim the next task</span></li><li class="ref-row ref-row--type"><code class="ref-name">--ds-text-4xl</code><span class="ref-val"><span class="ref-val__k">size</span> <code>22px</code></span><span class="ref-sample" style="font-size: var(--ds-text-4xl)">Claim the next task</span></li><li class="ref-row ref-row--type"><code class="ref-name">--ds-text-5xl</code><span class="ref-val"><span class="ref-val__k">size</span> <code>26px</code></span><span class="ref-sample" style="font-size: var(--ds-text-5xl)">Claim the next task</span></li><li class="ref-row ref-row--type"><code class="ref-name">--ds-text-title</code><span class="ref-val"><span class="ref-val__k">size</span> <code>clamp(23px, 2.4vw, 32px)</code></span><span class="ref-sample" style="font-size: var(--ds-text-title)">Claim the next task</span></li><li class="ref-row ref-row--type"><code class="ref-name">--ds-text-headline</code><span class="ref-val"><span class="ref-val__k">size</span> <code>clamp(26px, 3.1vw, 42px)</code></span><span class="ref-sample" style="font-size: var(--ds-text-headline)">Claim the next task</span></li><li class="ref-row ref-row--type"><code class="ref-name">--ds-text-figure</code><span class="ref-val"><span class="ref-val__k">size</span> <code>clamp(30px, 3.7vw, 52px)</code></span><span class="ref-sample" style="font-size: var(--ds-text-figure)">Claim the next task</span></li><li class="ref-row ref-row--type"><code class="ref-name">--ds-text-display</code><span class="ref-val"><span class="ref-val__k">size</span> <code>clamp(34px, 6.7vw, 96px)</code></span><span class="ref-sample" style="font-size: var(--ds-text-display)">Claim the next task</span></li></ul>
63
+ </section>
64
+ <section class="scroll ref-sec" id="ref-space" aria-labelledby="ref-space-h">
65
+ <h2 class="scroll__h" id="ref-space-h">Spacing</h2>
66
+ <p class="scroll__lede">The rhythm every gap and padding is taken from.</p>
67
+
68
+ <ul class="ref-list" role="list"><li class="ref-row ref-row--space"><code class="ref-name">--ds-space-hair</code><span class="ref-val"><span class="ref-val__k">size</span> <code>6px</code></span><span class="ref-bar" style="width: var(--ds-space-hair)" aria-hidden="true"></span></li><li class="ref-row ref-row--space"><code class="ref-name">--ds-space-xs</code><span class="ref-val"><span class="ref-val__k">size</span> <code>10px</code></span><span class="ref-bar" style="width: var(--ds-space-xs)" aria-hidden="true"></span></li><li class="ref-row ref-row--space"><code class="ref-name">--ds-space-sm</code><span class="ref-val"><span class="ref-val__k">size</span> <code>14px</code></span><span class="ref-bar" style="width: var(--ds-space-sm)" aria-hidden="true"></span></li><li class="ref-row ref-row--space"><code class="ref-name">--ds-space-md</code><span class="ref-val"><span class="ref-val__k">size</span> <code>22px</code></span><span class="ref-bar" style="width: var(--ds-space-md)" aria-hidden="true"></span></li><li class="ref-row ref-row--space"><code class="ref-name">--ds-space-lg</code><span class="ref-val"><span class="ref-val__k">size</span> <code>34px</code></span><span class="ref-bar" style="width: var(--ds-space-lg)" aria-hidden="true"></span></li><li class="ref-row ref-row--space"><code class="ref-name">--ds-space-xl</code><span class="ref-val"><span class="ref-val__k">size</span> <code>52px</code></span><span class="ref-bar" style="width: var(--ds-space-xl)" aria-hidden="true"></span></li><li class="ref-row ref-row--space"><code class="ref-name">--ds-space-gutter</code><span class="ref-val"><span class="ref-val__k">size</span> <code>26px</code></span><span class="ref-bar" style="width: var(--ds-space-gutter)" aria-hidden="true"></span></li><li class="ref-row ref-row--space"><code class="ref-name">--ds-space-gutter-narrow</code><span class="ref-val"><span class="ref-val__k">size</span> <code>20px</code></span><span class="ref-bar" style="width: var(--ds-space-gutter-narrow)" aria-hidden="true"></span></li></ul>
69
+ <h3 class="ref-sub">The hairline</h3>
70
+ <ul class="ref-list" role="list"><li class="ref-row ref-row--space"><code class="ref-name">--ds-line</code><span class="ref-val"><span class="ref-val__k">width</span> <code>1px</code></span><span class="ref-hair" aria-hidden="true"></span></li></ul>
71
+ </section>
72
+ <section class="scroll ref-sec" id="ref-shape" aria-labelledby="ref-shape-h">
73
+ <h2 class="scroll__h" id="ref-shape-h">Corners and shadow</h2>
74
+ <p class="scroll__lede">A control is a pill or square; a panel and a tile take their own corner. The one shadow is spent only on what floats.</p>
75
+
76
+ <ul class="ref-list" role="list"><li class="ref-row ref-row--shape"><span class="ref-corner" style="border-radius: var(--ds-radius-pill)" aria-hidden="true"></span><code class="ref-name">--ds-radius-pill</code><span class="ref-val"><span class="ref-val__k">radius</span> <code>999px</code></span></li><li class="ref-row ref-row--shape"><span class="ref-corner" style="border-radius: var(--ds-radius-panel)" aria-hidden="true"></span><code class="ref-name">--ds-radius-panel</code><span class="ref-val"><span class="ref-val__k">radius</span> <code>16px</code></span></li><li class="ref-row ref-row--shape"><span class="ref-corner" style="border-radius: var(--ds-radius-tile)" aria-hidden="true"></span><code class="ref-name">--ds-radius-tile</code><span class="ref-val"><span class="ref-val__k">radius</span> <code>10px</code></span></li></ul>
77
+ <ul class="ref-list" role="list"><li class="ref-row ref-row--shape"><span class="ref-float" style="box-shadow: var(--ds-shadow-pop)" aria-hidden="true"></span><code class="ref-name">--ds-shadow-pop</code><span class="ref-val"><span class="ref-val__k">light</span> <code>0 18px 40px rgba(0, 0, 0, .28)</code></span><span class="ref-val"><span class="ref-val__k">dark</span> <code>0 18px 40px rgba(0, 0, 0, .62)</code></span></li></ul>
78
+ </section>
79
+ <section class="scroll ref-sec" id="ref-motion" aria-labelledby="ref-motion-h">
80
+ <h2 class="scroll__h" id="ref-motion-h">Motion and layers</h2>
81
+ <p class="scroll__lede">State changes take the one easing; an entrance takes the slow ease-out. With reduced motion asked for, every duration is zero.</p>
82
+
83
+ <ul class="ref-list" role="list"><li class="ref-row ref-row--plain"><code class="ref-name">--ds-dur-fast</code><span class="ref-val"><span class="ref-val__k">value</span> <code>150ms</code></span></li><li class="ref-row ref-row--plain"><code class="ref-name">--ds-dur-base</code><span class="ref-val"><span class="ref-val__k">value</span> <code>200ms</code></span></li><li class="ref-row ref-row--plain"><code class="ref-name">--ds-dur-slow</code><span class="ref-val"><span class="ref-val__k">value</span> <code>320ms</code></span></li><li class="ref-row ref-row--plain"><code class="ref-name">--ds-dur-slower</code><span class="ref-val"><span class="ref-val__k">value</span> <code>420ms</code></span></li><li class="ref-row ref-row--plain"><code class="ref-name">--ds-dur-arrive</code><span class="ref-val"><span class="ref-val__k">value</span> <code>700ms</code></span></li><li class="ref-row ref-row--plain"><code class="ref-name">--ds-stagger</code><span class="ref-val"><span class="ref-val__k">value</span> <code>70ms</code></span></li><li class="ref-row ref-row--plain"><code class="ref-name">--ds-ease</code><span class="ref-val"><span class="ref-val__k">value</span> <code>cubic-bezier(.2, .7, .3, 1)</code></span></li><li class="ref-row ref-row--plain"><code class="ref-name">--ds-ease-arrive</code><span class="ref-val"><span class="ref-val__k">value</span> <code>cubic-bezier(.16, 1, .3, 1)</code></span></li></ul>
84
+ <h3 class="ref-sub">Stacking layers, low to high</h3>
85
+ <ul class="ref-list" role="list"><li class="ref-row ref-row--plain"><code class="ref-name">--ds-z-progress</code><span class="ref-val"><span class="ref-val__k">z</span> <code>25</code></span></li><li class="ref-row ref-row--plain"><code class="ref-name">--ds-z-topbar</code><span class="ref-val"><span class="ref-val__k">z</span> <code>30</code></span></li><li class="ref-row ref-row--plain"><code class="ref-name">--ds-z-sidebar</code><span class="ref-val"><span class="ref-val__k">z</span> <code>40</code></span></li><li class="ref-row ref-row--plain"><code class="ref-name">--ds-z-scrim</code><span class="ref-val"><span class="ref-val__k">z</span> <code>140</code></span></li><li class="ref-row ref-row--plain"><code class="ref-name">--ds-z-drawer</code><span class="ref-val"><span class="ref-val__k">z</span> <code>150</code></span></li><li class="ref-row ref-row--plain"><code class="ref-name">--ds-z-modal</code><span class="ref-val"><span class="ref-val__k">z</span> <code>160</code></span></li><li class="ref-row ref-row--plain"><code class="ref-name">--ds-z-diagram-full</code><span class="ref-val"><span class="ref-val__k">z</span> <code>9999</code></span></li></ul>
86
+ </section>
87
+ <section class="scroll ref-sec" id="ref-components" aria-labelledby="ref-components-h">
88
+ <h2 class="scroll__h" id="ref-components-h">Components</h2>
89
+ <p class="scroll__lede">Drawn by the same shared rules every page of the hall loads, in each of their states.</p>
90
+
91
+ <h3 class="ref-sub">Buttons</h3>
92
+ <div class="ref-specimen">
93
+ <button type="button" class="btn">claim it</button>
94
+ <button type="button" class="btn btn--ghost">later</button>
95
+ <button type="button" class="btn btn--danger">release</button>
96
+ <button type="button" class="btn" disabled>claimed</button>
97
+ </div>
98
+ <div class="ref-specimen">
99
+ <button type="button" class="btn btn--sm">claim</button>
100
+ <button type="button" class="btn btn--ghost btn--sm">release</button>
101
+ <button type="button" class="btn btn--text">read the brief</button>
102
+ </div>
103
+ <h3 class="ref-sub">Fields</h3>
104
+ <div class="ref-fields">
105
+ <label class="ref-field">A field <input type="text" value="A sentence someone typed"></label>
106
+ <label class="ref-field">A choice <select><option>ready</option><option>active</option></select></label>
107
+ <label class="ref-field">A longer answer <textarea rows="2">Two lines of what changed.</textarea></label>
108
+ <label class="ref-field">Not open yet <input type="text" value="Read only" disabled></label>
109
+ </div>
110
+ <h3 class="ref-sub">Status pills</h3>
111
+ <div class="ref-specimen" data-ref="tones"><span class="status-pill status-pill--ok">ok</span> <span class="status-pill status-pill--warn">warn</span> <span class="status-pill status-pill--danger">danger</span> <span class="status-pill status-pill--info">info</span> <span class="status-pill status-pill--neutral">neutral</span> <span class="status-pill status-pill--accent">accent</span></div>
112
+ <div class="ref-specimen" data-ref="tones-sm"><span class="status-pill status-pill--ok status-pill--sm">ok</span> <span class="status-pill status-pill--warn status-pill--sm">warn</span> <span class="status-pill status-pill--danger status-pill--sm">danger</span> <span class="status-pill status-pill--info status-pill--sm">info</span> <span class="status-pill status-pill--neutral status-pill--sm">neutral</span> <span class="status-pill status-pill--accent status-pill--sm">accent</span></div>
113
+ <p class="ref-note">A task’s status, from the one map:</p>
114
+ <div class="ref-specimen" data-ref="task-statuses"><span class="status-pill status-pill--neutral">backlog</span> <span class="status-pill status-pill--info">ready</span> <span class="status-pill status-pill--info">active</span> <span class="status-pill status-pill--warn">blocked</span> <span class="status-pill status-pill--info">completed</span> <span class="status-pill status-pill--ok">confirmed</span> <span class="status-pill status-pill--ok">shipped</span> <span class="status-pill status-pill--neutral">abandoned</span></div>
115
+ <h3 class="ref-sub">Tabs and the view toggle</h3>
116
+ <div class="kit-tabs" role="tablist" aria-label="Example tabs">
117
+ <button type="button" class="setup__tab is-active" role="tab" aria-selected="true">ready <span class="count-pill">4</span></button>
118
+ <button type="button" class="setup__tab" role="tab" aria-selected="false">active</button>
119
+ <button type="button" class="setup__tab" role="tab" aria-selected="false">done</button>
120
+ </div>
121
+ <div class="viewtoggle" role="group" aria-label="View">
122
+ <button type="button" class="viewtoggle__btn" aria-pressed="true">rows</button>
123
+ <button type="button" class="viewtoggle__btn" aria-pressed="false">cards</button>
124
+ </div>
125
+ <h3 class="ref-sub">Panel, tile and the empty state</h3>
126
+ <div class="scroll scroll--quiet ref-tile"><p class="ref-note">This section is a panel; a quiet tile inside it recedes by tone.</p></div>
127
+ <div class="empty-state"><p>Nothing is waiting for you.</p><a class="empty-state__action" href="#ref-components">See the whole queue</a></div>
128
+ <h3 class="ref-sub">Loading</h3>
129
+ <p class="hall-loading" role="status">Reading the queue…</p>
130
+ <p class="ref-specimen"><span class="hall-loading hall-loading--inline" role="status">Saving…</span></p>
131
+ <h3 class="ref-sub">Toasts</h3>
132
+ <div class="ref-toasts">
133
+ <p class="toast ref-toast" data-visible="1" data-kind="info">Copied the prompt. <span class="ref-toast__k">(info, 3.2s)</span></p>
134
+ <p class="toast ref-toast" data-visible="1" data-kind="ok">Task claimed. <span class="ref-toast__k">(ok, 2.6s)</span></p>
135
+ <p class="toast ref-toast" data-visible="1" data-kind="warn">The board is paused while the owner reviews it. <span class="ref-toast__k">(warn, 5s)</span></p>
136
+ <p class="toast ref-toast" data-visible="1" data-kind="error">Could not reach the server: try again in a moment. <span class="ref-toast__k">(error, 6.5s)</span></p>
137
+ </div>
138
+ <h3 class="ref-sub">Dialog</h3>
139
+ <div class="ref-dialog">
140
+ <div class="hall-dialog" role="dialog" aria-modal="false" aria-labelledby="ref-dialog-t">
141
+ <div class="hall-dialog__head"><h4 class="hall-dialog__title" id="ref-dialog-t">Release this task?</h4></div>
142
+ <div class="hall-dialog__body"><p>It returns to the queue and no credits are earned.</p></div>
143
+ <div class="hall-dialog__actions"><button type="button" class="btn btn--ghost">cancel</button><button type="button" class="btn btn--danger">release</button></div>
144
+ </div>
145
+ </div>
146
+ </section>
147
+ <section class="ref-sources" aria-labelledby="ref-sources-h">
148
+ <h2 class="ref-sources__h" id="ref-sources-h">Generated from</h2>
149
+ <ul>
150
+ <li><code>config/branding.neutral.json</code></li>
151
+ <li><code>modules/ui-design/public/tokens.css</code></li>
152
+ <li><code>modules/hall-ui/public/style.css</code></li>
153
+ <li><code>modules/hall-ui/public/controls.css</code></li>
154
+ <li><code>modules/hall-ui/public/dom-utils.js</code></li>
155
+ <li><code>modules/ui-design/public/brand</code></li>
156
+ <li><code>scripts/bongos/gen-brand.js</code></li>
157
+ <li><code>DESIGN.md</code></li>
158
+ </ul>
159
+ </section>
160
+ </main>
161
+ </body>
162
+ </html>
@@ -0,0 +1,35 @@
1
+ {
2
+ "_": "The public design-system reference (task 1005083, goal 1000138): every token with its light and dark values, the scales, the logo and naming rule, and every shared component in its states. The page is generated (node modules/ui-design/scripts/gen-design-reference.js) and follows the device's mode, so the kit renders it in both: node modules/ui-design/kit/render.js --page /brand --surface modules/ui-design/public",
3
+ "page": "/brand",
4
+ "surface": "modules/ui-design/public",
5
+ "stub": {
6
+ "prefix": "/"
7
+ },
8
+ "modeQuery": false,
9
+ "ignoreRequests": [
10
+ "/api/"
11
+ ],
12
+ "ignoreControls": [
13
+ "a.empty-state__action"
14
+ ],
15
+ "states": {
16
+ "reference": {
17
+ "expect": {
18
+ "visible": [
19
+ ".ref-lockup",
20
+ "#ref-colour .ref-swatch",
21
+ ".status-pill--ok",
22
+ ".btn--danger",
23
+ ".hall-dialog",
24
+ ".toast[data-kind=\"error\"]"
25
+ ],
26
+ "count": [
27
+ [
28
+ "[data-ref=\"tones\"] .status-pill",
29
+ 6
30
+ ]
31
+ ]
32
+ }
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,354 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // modules/ui-design/scripts/gen-design-reference.js — build the public design-system
4
+ // reference page, modules/ui-design/public/brand.html, served at /brand on the hub
5
+ // (task 1005083, goal 1000138; DESIGN.md "Naming" → the reference is public).
6
+ //
7
+ // WHY GENERATED. A hand-written reference drifts from the sheets the day after it is
8
+ // written. Nothing on the page is typed by hand except its own layout and the short
9
+ // headings: every value is read from the file that owns it, every component is drawn by
10
+ // the shared rule that every hall page uses.
11
+ //
12
+ // the thirteen colours + the two faces ← config/branding.neutral.json theme.ui
13
+ // their dark values, the derived tier, ← modules/hall-ui/public/style.css (the hall's
14
+ // the status vocabulary, the mono face :root and its html[data-theme="dark"] twin)
15
+ // type, spacing, line, corners, shadow, ← modules/ui-design/public/tokens.css (--ds-*,
16
+ // motion, layers and its dark block for the shadow)
17
+ // buttons, field, pill, dialog, toast… ← drawn live by modules/hall-ui/public/style.css
18
+ // + controls.css; each class is checked to exist
19
+ // task statuses, toast durations ← modules/hall-ui/public/dom-utils.js (the one map)
20
+ // the logo files ← modules/ui-design/public/brand/ + the file list
21
+ // in scripts/bongos/gen-brand.js
22
+ // the naming rule ← DESIGN.md "## Naming"
23
+ //
24
+ // node modules/ui-design/scripts/gen-design-reference.js write the page
25
+ // node modules/ui-design/scripts/gen-design-reference.js --check exit 1 if it is stale
26
+ //
27
+ // Guarded by tests/design_reference.mjs. The kit renders it with
28
+ // modules/ui-design/public/brand.states.json.
29
+
30
+ const fs = require('node:fs');
31
+ const path = require('node:path');
32
+
33
+ const ROOT = path.resolve(__dirname, '..', '..', '..');
34
+ const P = (...a) => path.join(ROOT, ...a);
35
+ const SRC = Object.freeze({
36
+ pack: P('config', 'branding.neutral.json'),
37
+ sheet: P('modules', 'ui-design', 'public', 'tokens.css'),
38
+ hall: P('modules', 'hall-ui', 'public', 'style.css'),
39
+ controls: P('modules', 'hall-ui', 'public', 'controls.css'),
40
+ domUtils: P('modules', 'hall-ui', 'public', 'dom-utils.js'),
41
+ brandDir: P('modules', 'ui-design', 'public', 'brand'),
42
+ genBrand: P('scripts', 'bongos', 'gen-brand.js'),
43
+ design: P('DESIGN.md'),
44
+ });
45
+ const OUT = P('modules', 'ui-design', 'public', 'brand.html');
46
+ const BRAND_URL = '/modules/ui-design/public/brand/';
47
+
48
+ // the hall's one escaper (dom-utils.js also loads under require(); fitness: one escaper per side)
49
+ const esc = (s) => require(SRC.domUtils).escapeHtml(String(s));
50
+ const kebab = (k) => '--' + k.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase());
51
+ const stripComments = (css) => css.replace(/\/\*[\s\S]*?\*\//g, '');
52
+
53
+ // The declarations of the first block whose selector matches `selectorRe`, in source
54
+ // order, as [[prop, value], …]. Values are whitespace-collapsed.
55
+ function blockVars(css, selectorRe, what) {
56
+ const m = stripComments(css).match(new RegExp(selectorRe.source + '\\s*\\{([^}]*)\\}'));
57
+ if (!m) throw new Error(`${what}: no block matching ${selectorRe}`);
58
+ const out = [];
59
+ for (const decl of m[1].split(';')) {
60
+ const d = decl.match(/^\s*(--[a-z0-9-]+)\s*:\s*([\s\S]+?)\s*$/i);
61
+ if (d) out.push([d[1], d[2].replace(/\s+/g, ' ')]);
62
+ }
63
+ return out;
64
+ }
65
+
66
+ // Turn a DESIGN.md line's inline markdown (**bold**, `code`) into escaped HTML.
67
+ function inline(md) {
68
+ return esc(md).replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>').replace(/`([^`]+)`/g, '<code>$1</code>');
69
+ }
70
+
71
+ // DESIGN.md "## Naming", up to its first sub-heading: the lead paragraph and the bullets.
72
+ function readNaming(design) {
73
+ const m = design.match(/\n## Naming\n([\s\S]*?)(?=\n### |\n## |$)/);
74
+ if (!m) throw new Error('DESIGN.md: no "## Naming" chapter');
75
+ const lines = m[1].split('\n').map((l) => l.trim()).filter(Boolean);
76
+ const lead = lines.filter((l) => !l.startsWith('- ')).map(inline);
77
+ const rules = lines.filter((l) => l.startsWith('- ')).map((l) => inline(l.slice(2)));
78
+ if (!rules.length) throw new Error('DESIGN.md: the Naming chapter has no rules');
79
+ return { lead, rules };
80
+ }
81
+
82
+ // The logo files and what each is for, from gen-brand.js's own header (the one list
83
+ // of what it writes), kept only where the file is actually on disk.
84
+ function readBrandFiles(genBrand, dir) {
85
+ const files = [];
86
+ for (const line of genBrand.split('\n')) {
87
+ const m = line.match(/^\/\/\s{2,}([a-z-]+\.(?:svg|png))\s{2,}(.+?)\s*$/);
88
+ if (!m || !fs.existsSync(path.join(dir, m[1]))) continue;
89
+ // the first column of the description only; drop a build note "(--png …)" and an
90
+ // aside the header continues on the next line (an unclosed "(")
91
+ const what = m[2].split(/\s{2,}/)[0].replace(/\s*\(--png[^)]*\)/, '').replace(/\s*\([^)]*$/, '')
92
+ .replace(/\s*\(task \d+\)/, '').replace(/[:;,]\s*$/, '');
93
+ files.push({ file: m[1], what });
94
+ }
95
+ if (!files.length) throw new Error('gen-brand.js: no file list found in its header');
96
+ return [{ file: 'mark.svg', what: 'the one source: drawn inline by reference, coloured by the page' }, ...files];
97
+ }
98
+
99
+ // The classes the components section draws with: each must exist in the hall's sheets,
100
+ // so a renamed rule fails the generator instead of shipping an unstyled specimen.
101
+ const NEEDS = ['.btn', '.btn--ghost', '.btn--danger', '.btn--sm', '.btn--text', '.status-pill', '.status-pill--sm',
102
+ '.scroll', '.scroll--quiet', '.scroll__h', '.scroll__lede', '.empty-state', '.empty-state__action', '.hall-loading',
103
+ '.hall-loading--inline', '.toast', '.kit-tabs', '.setup__tab', '.count-pill', '.viewtoggle', '.viewtoggle__btn',
104
+ '.hall-dialog', '.hall-dialog__head', '.hall-dialog__title', '.hall-dialog__body', '.hall-dialog__actions'];
105
+
106
+ // Which hall :root props count as the derived COLOUR tier: anything whose value is a
107
+ // colour expression (not a length, a face, a gradient veil or a plate).
108
+ const COLOURISH = /^(#|rgba?\(|color-mix\()/;
109
+ const STATUS = ['--ok', '--ok-weak', '--warn', '--warn-weak', '--danger', '--danger-weak', '--info', '--info-weak'];
110
+
111
+ function read() {
112
+ return {
113
+ pack: JSON.parse(fs.readFileSync(SRC.pack, 'utf8')),
114
+ sheet: fs.readFileSync(SRC.sheet, 'utf8'),
115
+ hall: fs.readFileSync(SRC.hall, 'utf8'),
116
+ controls: fs.readFileSync(SRC.controls, 'utf8'),
117
+ design: fs.readFileSync(SRC.design, 'utf8'),
118
+ genBrand: fs.readFileSync(SRC.genBrand, 'utf8'),
119
+ otb: require(SRC.domUtils),
120
+ };
121
+ }
122
+
123
+ // ---- the page's parts ---------------------------------------------------------------
124
+
125
+ const row = (cells, cls = '') => `<li class="ref-row${cls ? ' ' + cls : ''}">${cells.join('')}</li>`;
126
+ const name = (prop) => `<code class="ref-name">${esc(prop)}</code>`;
127
+ const val = (label, v) => `<span class="ref-val"><span class="ref-val__k">${label}</span> <code>${esc(v)}</code></span>`;
128
+ const swatch = (prop) => `<span class="ref-swatch" style="background: var(${prop})" aria-hidden="true"></span>`;
129
+ const list = (rows, cls = '') => `<ul class="ref-list${cls ? ' ' + cls : ''}" role="list">${rows.join('')}</ul>`;
130
+ function section(id, title, lede, body) {
131
+ return `<section class="scroll ref-sec" id="${id}" aria-labelledby="${id}-h">
132
+ <h2 class="scroll__h" id="${id}-h">${esc(title)}</h2>
133
+ ${lede ? `<p class="scroll__lede">${lede}</p>` : ''}
134
+ ${body}
135
+ </section>`;
136
+ }
137
+
138
+ function colourRows(entries) {
139
+ return entries.map(({ prop, light, dark }) => row([
140
+ swatch(prop), name(prop), val('light', light), val('dark', dark),
141
+ ], 'ref-row--colour'));
142
+ }
143
+
144
+ function build(src = read()) {
145
+ const ui = src.pack.theme && src.pack.theme.ui;
146
+ if (!ui || !ui.colors || !ui.fonts) throw new Error('branding.neutral.json: theme.ui has no colours or fonts');
147
+ const hallRoot = new Map(blockVars(src.hall, /(?:^|\n|\})\s*:root/, 'style.css'));
148
+ const hallDark = new Map(blockVars(src.hall, /html\[data-theme="dark"\]/, 'style.css'));
149
+ const ds = blockVars(src.sheet, /(?:^|\n|\})\s*:root/, 'tokens.css');
150
+ const dsDark = new Map(blockVars(src.sheet, /html\[data-theme="dark"\], :root\[data-mode="dark"\]/, 'tokens.css'));
151
+ const allCss = src.hall + '\n' + src.controls;
152
+ for (const cls of NEEDS) {
153
+ if (!new RegExp(cls.replace(/[.[\]-]/g, '\\$&') + '(?![a-z0-9_-])').test(allCss)) throw new Error(`the hall's sheets no longer define ${cls}`);
154
+ }
155
+ const product = (src.pack.identity && src.pack.identity.productName) || '';
156
+ const naming = readNaming(src.design);
157
+ const files = readBrandFiles(src.genBrand, SRC.brandDir);
158
+ const OTB = src.otb;
159
+
160
+ // 1. the pack's thirteen
161
+ const packKeys = Object.keys(ui.colors);
162
+ const pack = packKeys.map((k) => {
163
+ const prop = kebab(k);
164
+ if (!hallRoot.has(prop)) throw new Error(`style.css :root does not declare ${prop} (pack colour ${k})`);
165
+ return { prop, light: ui.colors[k], dark: hallDark.get(prop) || 'same as light' };
166
+ });
167
+ const packProps = new Set(pack.map((c) => c.prop));
168
+ // 2. the status vocabulary
169
+ const status = STATUS.map((prop) => {
170
+ if (!hallRoot.has(prop)) throw new Error(`style.css :root does not declare ${prop}`);
171
+ return { prop, light: hallRoot.get(prop), dark: hallDark.get(prop) || hallRoot.get(prop) };
172
+ });
173
+ // 3. the rest of the derived colour tier
174
+ const derived = [...hallRoot].filter(([prop, v]) => !packProps.has(prop) && !STATUS.includes(prop) && COLOURISH.test(v))
175
+ .map(([prop, light]) => ({ prop, light, dark: hallDark.get(prop) || light }));
176
+
177
+ // the scales, grouped by their prefix in tokens.css
178
+ const group = (re) => ds.filter(([p]) => re.test(p));
179
+ const text = group(/^--ds-text-/);
180
+ const space = group(/^--ds-space-/);
181
+ const line = group(/^--ds-line$/);
182
+ const radius = group(/^--ds-radius-/);
183
+ const shadow = group(/^--ds-shadow-/);
184
+ const motion = group(/^--ds-(dur|stagger|ease)/);
185
+ const layers = group(/^--ds-z-/);
186
+ const covered = new Set([...text, ...space, ...line, ...radius, ...shadow, ...motion, ...layers].map(([p]) => p));
187
+ const loose = ds.filter(([p]) => !covered.has(p));
188
+ if (loose.length) throw new Error(`tokens.css declares ${loose.map(([p]) => p).join(', ')} in no group the reference shows: add its group here`);
189
+
190
+ const faces = [
191
+ { prop: '--font-display', v: ui.fonts.display, role: 'display' },
192
+ { prop: '--font-body', v: ui.fonts.body, role: 'body' },
193
+ { prop: '--font-mono', v: hallRoot.get('--font-mono'), role: 'mono' },
194
+ ];
195
+
196
+ const sample = 'Claim the next task';
197
+ const parts = [];
198
+
199
+ parts.push(section('ref-logo', 'Logo and name', 'The mark is one file every page draws from. The ring and the planet take the ink; the moon takes the accent.', `
200
+ <div class="ref-lockup">
201
+ <svg class="ref-lockup__mark" viewBox="0 0 200 200" aria-hidden="true"><use href="${BRAND_URL}mark.svg#mark"/></svg>
202
+ <span class="ref-lockup__name">${esc(product.toLowerCase())}</span>
203
+ </div>
204
+ <div class="ref-naming">
205
+ ${naming.lead.map((p) => `<p>${p}</p>`).join('\n ')}
206
+ <ul>
207
+ ${naming.rules.map((r) => `<li>${r}</li>`).join('\n ')}
208
+ </ul>
209
+ </div>
210
+ <h3 class="ref-sub">The files</h3>
211
+ ${list(files.map((f) => row([`<a class="ref-file" href="${BRAND_URL}${esc(f.file)}"><code>${esc(f.file)}</code></a>`, `<span class="ref-what">${esc(f.what)}</span>`], 'ref-row--file')))}`));
212
+
213
+ parts.push(section('ref-colour', 'Colour', 'The thirteen colours of the branding pack. An instance replaces them in its own pack; in dark mode only the neutrals flip, and the accent, sea and terracotta stay the pack’s.', `
214
+ ${list(colourRows(pack))}
215
+ <h3 class="ref-sub">The status vocabulary</h3>
216
+ <p class="ref-note">Fixed, not taken from the pack, and the one exception to the fifteen tokens: a status is always a word as well as a colour.</p>
217
+ ${list(colourRows(status))}
218
+ <h3 class="ref-sub">Derived colours</h3>
219
+ <p class="ref-note">Mixed from the thirteen, never typed as a new colour. Black and white with alpha appear only as a scrim, a shadow or glass.</p>
220
+ ${list(colourRows(derived))}`));
221
+
222
+ parts.push(section('ref-type', 'Type', 'Two faces from the pack and one mono face for what the machine says: task ids, labels, figures.', `
223
+ ${list(faces.map((f) => row([name(f.prop), `<span class="ref-face" style="font-family: var(${f.prop})">${esc(sample)}</span>`, val(f.role, f.v)], 'ref-row--face')))}
224
+ <h3 class="ref-sub">The type scale</h3>
225
+ ${list(text.map(([p, v]) => row([name(p), val('size', v), `<span class="ref-sample" style="font-size: var(${p})">${esc(sample)}</span>`], 'ref-row--type')))}`));
226
+
227
+ parts.push(section('ref-space', 'Spacing', 'The rhythm every gap and padding is taken from.', `
228
+ ${list(space.map(([p, v]) => row([name(p), val('size', v), `<span class="ref-bar" style="width: var(${p})" aria-hidden="true"></span>`], 'ref-row--space')))}
229
+ <h3 class="ref-sub">The hairline</h3>
230
+ ${list(line.map(([p, v]) => row([name(p), val('width', v), '<span class="ref-hair" aria-hidden="true"></span>'], 'ref-row--space')))}`));
231
+
232
+ parts.push(section('ref-shape', 'Corners and shadow', 'A control is a pill or square; a panel and a tile take their own corner. The one shadow is spent only on what floats.', `
233
+ ${list(radius.map(([p, v]) => row([`<span class="ref-corner" style="border-radius: var(${p})" aria-hidden="true"></span>`, name(p), val('radius', v)], 'ref-row--shape')))}
234
+ ${list(shadow.map(([p, v]) => row([`<span class="ref-float" style="box-shadow: var(${p})" aria-hidden="true"></span>`, name(p), val('light', v), val('dark', dsDark.get(p) || v)], 'ref-row--shape')))}`));
235
+
236
+ parts.push(section('ref-motion', 'Motion and layers', 'State changes take the one easing; an entrance takes the slow ease-out. With reduced motion asked for, every duration is zero.', `
237
+ ${list(motion.map(([p, v]) => row([name(p), val('value', v)], 'ref-row--plain')))}
238
+ <h3 class="ref-sub">Stacking layers, low to high</h3>
239
+ ${list(layers.map(([p, v]) => row([name(p), val('z', v)], 'ref-row--plain')))}`));
240
+
241
+ const pills = OTB.STATUS_TONES.map((t) => OTB.statusPill(t, { tone: t })).join(' ');
242
+ const pillsSm = OTB.STATUS_TONES.map((t) => OTB.statusPill(t, { tone: t, small: true })).join(' ');
243
+ const taskPills = Object.keys(OTB.STATUS_TONE).map((s) => OTB.statusPill(s)).join(' ');
244
+ const toasts = [['info', 'Copied the prompt.'], ['ok', 'Task claimed.'], ['warn', 'The board is paused while the owner reviews it.'], ['error', 'Could not reach the server: try again in a moment.']]
245
+ .map(([k, msg]) => `<p class="toast ref-toast" data-visible="1" data-kind="${k}">${esc(OTB.plainMessage(msg))} <span class="ref-toast__k">(${k}, ${OTB.TOAST_MS[k] / 1000}s)</span></p>`).join('\n ');
246
+
247
+ parts.push(section('ref-components', 'Components', 'Drawn by the same shared rules every page of the hall loads, in each of their states.', `
248
+ <h3 class="ref-sub">Buttons</h3>
249
+ <div class="ref-specimen">
250
+ <button type="button" class="btn">claim it</button>
251
+ <button type="button" class="btn btn--ghost">later</button>
252
+ <button type="button" class="btn btn--danger">release</button>
253
+ <button type="button" class="btn" disabled>claimed</button>
254
+ </div>
255
+ <div class="ref-specimen">
256
+ <button type="button" class="btn btn--sm">claim</button>
257
+ <button type="button" class="btn btn--ghost btn--sm">release</button>
258
+ <button type="button" class="btn btn--text">read the brief</button>
259
+ </div>
260
+ <h3 class="ref-sub">Fields</h3>
261
+ <div class="ref-fields">
262
+ <label class="ref-field">A field <input type="text" value="A sentence someone typed"></label>
263
+ <label class="ref-field">A choice <select><option>ready</option><option>active</option></select></label>
264
+ <label class="ref-field">A longer answer <textarea rows="2">Two lines of what changed.</textarea></label>
265
+ <label class="ref-field">Not open yet <input type="text" value="Read only" disabled></label>
266
+ </div>
267
+ <h3 class="ref-sub">Status pills</h3>
268
+ <div class="ref-specimen" data-ref="tones">${pills}</div>
269
+ <div class="ref-specimen" data-ref="tones-sm">${pillsSm}</div>
270
+ <p class="ref-note">A task’s status, from the one map:</p>
271
+ <div class="ref-specimen" data-ref="task-statuses">${taskPills}</div>
272
+ <h3 class="ref-sub">Tabs and the view toggle</h3>
273
+ <div class="kit-tabs" role="tablist" aria-label="Example tabs">
274
+ <button type="button" class="setup__tab is-active" role="tab" aria-selected="true">ready <span class="count-pill">4</span></button>
275
+ <button type="button" class="setup__tab" role="tab" aria-selected="false">active</button>
276
+ <button type="button" class="setup__tab" role="tab" aria-selected="false">done</button>
277
+ </div>
278
+ <div class="viewtoggle" role="group" aria-label="View">
279
+ <button type="button" class="viewtoggle__btn" aria-pressed="true">rows</button>
280
+ <button type="button" class="viewtoggle__btn" aria-pressed="false">cards</button>
281
+ </div>
282
+ <h3 class="ref-sub">Panel, tile and the empty state</h3>
283
+ <div class="scroll scroll--quiet ref-tile"><p class="ref-note">This section is a panel; a quiet tile inside it recedes by tone.</p></div>
284
+ <div class="empty-state"><p>Nothing is waiting for you.</p><a class="empty-state__action" href="#ref-components">See the whole queue</a></div>
285
+ <h3 class="ref-sub">Loading</h3>
286
+ <p class="hall-loading" role="status">Reading the queue…</p>
287
+ <p class="ref-specimen"><span class="hall-loading hall-loading--inline" role="status">Saving…</span></p>
288
+ <h3 class="ref-sub">Toasts</h3>
289
+ <div class="ref-toasts">
290
+ ${toasts}
291
+ </div>
292
+ <h3 class="ref-sub">Dialog</h3>
293
+ <div class="ref-dialog">
294
+ <div class="hall-dialog" role="dialog" aria-modal="false" aria-labelledby="ref-dialog-t">
295
+ <div class="hall-dialog__head"><h4 class="hall-dialog__title" id="ref-dialog-t">Release this task?</h4></div>
296
+ <div class="hall-dialog__body"><p>It returns to the queue and no credits are earned.</p></div>
297
+ <div class="hall-dialog__actions"><button type="button" class="btn btn--ghost">cancel</button><button type="button" class="btn btn--danger">release</button></div>
298
+ </div>
299
+ </div>`));
300
+
301
+ const sources = Object.values(SRC).map((f) => `<li><code>${esc(path.relative(ROOT, f).split(path.sep).join('/'))}</code></li>`).join('\n ');
302
+
303
+ return `<!doctype html>
304
+ <html lang="en">
305
+ <head>
306
+ <meta charset="utf-8">
307
+ <meta name="viewport" content="width=device-width, initial-scale=1">
308
+ <title>{{productName}} — Design system</title>
309
+ <meta name="description" content="The colours, type, spacing, corners, motion and shared components every {{productName}} page is drawn from.">
310
+ <!-- GENERATED by node modules/ui-design/scripts/gen-design-reference.js (task 1005083); do not edit by hand.
311
+ Every value is read from its source file; every component is drawn by the hall's shared rules.
312
+ kit-ignore: tell: tiny type (the type scale shows its smallest step at its own size) -->
313
+ <link rel="stylesheet" href="/modules/hall-ui/public/style.css">
314
+ <link rel="stylesheet" href="/modules/ui-design/public/brand.css">
315
+ </head>
316
+ <body data-page="brand">
317
+ <header class="ref-head">
318
+ <a class="ref-head__brand" href="/"><svg class="ref-head__mark" viewBox="0 0 200 200" aria-hidden="true"><use href="${BRAND_URL}mark.svg#mark"/></svg><span>${esc(product.toLowerCase())}</span></a>
319
+ </header>
320
+ <main class="ref-main">
321
+ <h1 class="ref-title">Design system</h1>
322
+ <p class="ref-lede">The colours, type, spacing, corners, motion and shared components every {{productName}} page is drawn from. This page is generated from those same files, so it cannot drift from them. It follows your device’s light or dark setting; each value is shown for both.</p>
323
+ <nav class="ref-toc" aria-label="On this page">
324
+ <a href="#ref-logo">Logo</a><a href="#ref-colour">Colour</a><a href="#ref-type">Type</a><a href="#ref-space">Spacing</a><a href="#ref-shape">Corners</a><a href="#ref-motion">Motion</a><a href="#ref-components">Components</a>
325
+ </nav>
326
+ ${parts.join('\n ')}
327
+ <section class="ref-sources" aria-labelledby="ref-sources-h">
328
+ <h2 class="ref-sources__h" id="ref-sources-h">Generated from</h2>
329
+ <ul>
330
+ ${sources}
331
+ </ul>
332
+ </section>
333
+ </main>
334
+ </body>
335
+ </html>
336
+ `;
337
+ }
338
+
339
+ module.exports = { build, blockVars };
340
+
341
+ if (require.main === module) {
342
+ const next = build();
343
+ const current = fs.existsSync(OUT) ? fs.readFileSync(OUT, 'utf8') : '';
344
+ if (process.argv.includes('--check')) {
345
+ if (current === next) process.stdout.write('brand.html: current\n');
346
+ else {
347
+ process.stderr.write('modules/ui-design/public/brand.html is STALE: run node modules/ui-design/scripts/gen-design-reference.js\n');
348
+ process.exitCode = 1;
349
+ }
350
+ } else {
351
+ if (current !== next) fs.writeFileSync(OUT, next);
352
+ process.stdout.write((current === next ? 'brand.html: unchanged' : 'brand.html: written') + '\n');
353
+ }
354
+ }
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.24.54",
3
+ "version": "1.24.55",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.24.54",
9
+ "version": "1.24.55",
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.54",
3
+ "version": "1.24.55",
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",
@@ -11295,5 +11295,11 @@
11295
11295
  "id": "1005086",
11296
11296
  "text": "A task's status now looks the same on every hall page: one pill with six colours, chosen in one place. Error pop-up messages stay longer and never show a server's address, and a new specimen page shows every shared piece in ev"
11297
11297
  }
11298
+ ],
11299
+ "1.24.55": [
11300
+ {
11301
+ "id": "1005083",
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
+ }
11298
11304
  ]
11299
11305
  }
package/src/module-api.js CHANGED
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
81
81
  // MAJOR (see allowBoxScope below): passes the request through untouched.
82
82
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
83
83
 
84
- const CORE_VERSION = '1.24.54'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.24.55'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
85
85
 
86
86
  // A namespaced logger so a module's log lines are attributable + consistent.
87
87
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -165,6 +165,7 @@ function landingAssetCache(urlPath, v) {
165
165
  return null;
166
166
  }
167
167
 
168
+ let _brandHtml; // the /brand reference, branded once (task 1005083); '' if the file is absent
168
169
  const _landingPages = new Map(); // memoized per file; '' if absent (instance ships no landing)
169
170
  function landingPage(file) {
170
171
  if (!_landingPages.has(file)) {
@@ -317,6 +318,23 @@ function buildPlatformApp() {
317
318
  res.type('html').send(html);
318
319
  });
319
320
  }
321
+ // THE DESIGN-SYSTEM REFERENCE (task 1005083; DESIGN.md "Naming": the reference is
322
+ // public). A generated page — modules/ui-design/scripts/gen-design-reference.js writes
323
+ // modules/ui-design/public/brand.html from the token sheets and the shared components —
324
+ // served signed-out at /brand, branding-stamped like the footer doors. It describes the
325
+ // platform's own world, so it is the hub's alone (platform-identity IS the hub flag, as
326
+ // for /account below) and needs ui-design on, the module that ships it.
327
+ app.get('/brand', (req, res, next) => {
328
+ if (!isApexHost(req) || !isModuleEnabled('platform-identity') || !isModuleEnabled('ui-design')) return next();
329
+ if (_brandHtml === undefined) {
330
+ try {
331
+ _brandHtml = injectBrandingGlobal(fs.readFileSync(path.join(__dirname, '..', 'modules', 'ui-design', 'public', 'brand.html'), 'utf8'));
332
+ } catch { _brandHtml = ''; }
333
+ }
334
+ if (!_brandHtml) return next();
335
+ res.set('Cache-Control', 'public, max-age=300');
336
+ res.type('html').send(_brandHtml);
337
+ });
320
338
  // THE RAISE PAGE (task 1004847, ADR 0384): the instance's investor page at
321
339
  // /raise, served by the raise module through its `raise.web` port — looked up
322
340
  // per request, because modules mount inside mountInternalSurfaces above. No
@@ -0,0 +1,175 @@
1
+ // tests/design_reference.mjs — the public design-system reference (task 1005083, goal
2
+ // 1000138): modules/ui-design/public/brand.html is GENERATED from the files that own
3
+ // each value, and the hub serves it signed-out at /brand.
4
+ //
5
+ // Pins:
6
+ // 1. the committed page is exactly what the generator writes (a hand edit, or a
7
+ // token change nobody regenerated, fails here; task 1005093 adds the ship gate);
8
+ // 2. every --ds-* token, every pack colour with its light AND dark value, and the
9
+ // status vocabulary are on the page — read from the sheets, not a copy;
10
+ // 3. a source change reaches the page, and a source losing what the page needs is
11
+ // an error rather than a silent gap;
12
+ // 4. the page keeps the platform floors: no page :root, no colour literal in its
13
+ // layout sheet, a states file the kit can render in both modes;
14
+ // 5. the route: 200 signed-out on the hub's apex, stamped; never on a child
15
+ // instance, the builders host or the status host.
16
+ //
17
+ // DB-free. Run: node --test tests/design_reference.mjs
18
+
19
+ import assert from 'node:assert/strict';
20
+ import { test, before, after } from 'node:test';
21
+ import http from 'node:http';
22
+ import fs from 'node:fs';
23
+ import path from 'node:path';
24
+ import { fileURLToPath } from 'node:url';
25
+ import { createRequire } from 'node:module';
26
+
27
+ const require = createRequire(import.meta.url);
28
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
29
+ const gen = require(path.join(ROOT, 'modules', 'ui-design', 'scripts', 'gen-design-reference.js'));
30
+ const PUB = path.join(ROOT, 'modules', 'ui-design', 'public');
31
+ const PAGE = fs.readFileSync(path.join(PUB, 'brand.html'), 'utf8');
32
+ const SHEET = fs.readFileSync(path.join(PUB, 'tokens.css'), 'utf8');
33
+ const HALL = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'style.css'), 'utf8');
34
+ const PACK = JSON.parse(fs.readFileSync(path.join(ROOT, 'config', 'branding.neutral.json'), 'utf8'));
35
+
36
+ function sources() {
37
+ return {
38
+ pack: JSON.parse(JSON.stringify(PACK)),
39
+ sheet: SHEET,
40
+ hall: HALL,
41
+ controls: fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'controls.css'), 'utf8'),
42
+ design: fs.readFileSync(path.join(ROOT, 'DESIGN.md'), 'utf8'),
43
+ genBrand: fs.readFileSync(path.join(ROOT, 'scripts', 'bongos', 'gen-brand.js'), 'utf8'),
44
+ otb: require(path.join(ROOT, 'modules', 'hall-ui', 'public', 'dom-utils.js')),
45
+ };
46
+ }
47
+
48
+ // ── 1. generated ─────────────────────────────────────────────────────────────
49
+
50
+ test('the committed page is exactly what the generator writes', () => {
51
+ assert.equal(PAGE, gen.build(),
52
+ 'modules/ui-design/public/brand.html is stale or hand-edited: run node modules/ui-design/scripts/gen-design-reference.js');
53
+ });
54
+
55
+ // ── 2. every value is on the page ────────────────────────────────────────────
56
+
57
+ // the text of the row a token's name opens (one <li> per token)
58
+ function rowOf(html, prop) {
59
+ const m = html.match(new RegExp(`<li class="ref-row[^"]*">(?:(?!</li>).)*?<code class="ref-name">${prop}</code>(?:(?!</li>).)*</li>`));
60
+ assert.ok(m, `the page has a row for ${prop}`);
61
+ return m[0];
62
+ }
63
+
64
+ test('every --ds-* token of the shared sheet is on the page with its value', () => {
65
+ const vars = gen.blockVars(SHEET, /(?:^|\n|\})\s*:root/, 'tokens.css');
66
+ assert.ok(vars.length > 40, 'the sheet was read');
67
+ for (const [prop, v] of vars) assert.ok(rowOf(PAGE, prop).includes(`<code>${v}</code>`), `${prop} shows ${v}`);
68
+ // the one mode-dependent scale shows its dark twin too
69
+ assert.match(rowOf(PAGE, '--ds-shadow-pop'), /dark<\/span> <code>0 18px 40px rgba\(0, 0, 0, \.62\)<\/code>/);
70
+ });
71
+
72
+ test('every pack colour shows its light value and its dark value; the status vocabulary both modes', () => {
73
+ for (const [key, light] of Object.entries(PACK.theme.ui.colors)) {
74
+ const prop = '--' + key.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase());
75
+ const r = rowOf(PAGE, prop);
76
+ assert.ok(r.includes(`light</span> <code>${light}</code>`), `${prop} light ${light}`);
77
+ assert.match(r, /dark<\/span> <code>[^<]+<\/code>/, `${prop} has a dark value`);
78
+ }
79
+ assert.ok(rowOf(PAGE, '--bg').includes('dark</span> <code>#000000</code>'), 'the dark ground is read from the hall\'s dark tier');
80
+ assert.ok(rowOf(PAGE, '--ok').includes('light</span> <code>#187444</code>') && rowOf(PAGE, '--ok').includes('dark</span> <code>#4ade80</code>'));
81
+ for (const s of ['--warn', '--danger', '--info', '--ok-weak', '--warn-weak', '--danger-weak', '--info-weak']) rowOf(PAGE, s);
82
+ });
83
+
84
+ test('the shared components, the logo and the naming rule are on the page', () => {
85
+ for (const sel of ['class="btn"', 'class="btn btn--ghost"', 'class="btn btn--danger"', 'class="btn btn--text"', 'class="hall-dialog"',
86
+ 'data-kind="error"', 'class="viewtoggle"', 'class="hall-loading"', '<select>', '<textarea']) {
87
+ assert.ok(PAGE.includes(sel), `the page draws ${sel}`);
88
+ }
89
+ assert.equal((PAGE.match(/data-ref="tones"[^>]*>(.*?)<\/div>/) || [])[1].match(/status-pill--/g).length, 6, 'six tones');
90
+ assert.match(PAGE, /<use href="\/modules\/ui-design\/public\/brand\/mark\.svg#mark"\/>/, 'the mark is drawn from its one source');
91
+ assert.match(PAGE, /<span class="ref-lockup__name">cloud bongos<\/span>/, 'the wordmark is lowercase (DESIGN.md Naming)');
92
+ assert.match(PAGE, /Never &quot;CloudBongos&quot;/, 'the naming rule is read from DESIGN.md');
93
+ for (const f of ['mark-light.svg', 'mark-dark.svg', 'mark-mono.svg', 'favicon.svg']) assert.ok(PAGE.includes(`brand/${f}"`), `lists ${f}`);
94
+ });
95
+
96
+ // ── 3. the sources drive it ──────────────────────────────────────────────────
97
+
98
+ test('a source change reaches the page (the generator reads the sources, not a copy)', () => {
99
+ const src = sources();
100
+ src.sheet = src.sheet.replace('--ds-space-hair: 6px;', '--ds-space-hair: 7px;');
101
+ src.pack.theme.ui.colors.accent = '#ff8855';
102
+ src.design = src.design.replace('- The domain is written', '- A new naming rule.\n- The domain is written');
103
+ const html = gen.build(src);
104
+ assert.ok(rowOf(html, '--ds-space-hair').includes('<code>7px</code>'));
105
+ assert.ok(rowOf(html, '--accent').includes('<code>#ff8855</code>'));
106
+ assert.match(html, /<li>A new naming rule\.<\/li>/);
107
+ });
108
+
109
+ test('a source losing what the page needs is an error, not a silent gap', () => {
110
+ const lostRule = sources();
111
+ lostRule.controls = lostRule.controls.split('.btn--danger').join('.btn--gone');
112
+ assert.throws(() => gen.build(lostRule), /no longer define \.btn--danger/);
113
+ const lostNaming = sources();
114
+ lostNaming.design = lostNaming.design.replace('\n## Naming\n', '\n## Names\n');
115
+ assert.throws(() => gen.build(lostNaming), /Naming/);
116
+ const newGroup = sources();
117
+ newGroup.sheet = newGroup.sheet.replace('--ds-line: 1px;', '--ds-line: 1px;\n --ds-blur-glass: 18px;');
118
+ assert.throws(() => gen.build(newGroup), /--ds-blur-glass in no group/);
119
+ });
120
+
121
+ // ── 4. the floors ────────────────────────────────────────────────────────────
122
+
123
+ test('no page :root, no colour literal in the layout sheet, and a kit states file for both modes', () => {
124
+ const css = fs.readFileSync(path.join(PUB, 'brand.css'), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '');
125
+ assert.doesNotMatch(css, /:root/);
126
+ assert.doesNotMatch(css, /#[0-9a-f]{3,8}\b|rgba?\(/i, 'every colour is a token');
127
+ assert.doesNotMatch(PAGE, /<style>/, 'the page carries no style block of its own');
128
+ assert.doesNotMatch(PAGE, /cloudbongos\.com"/, 'no baked apex link');
129
+ const lib = require(path.join(ROOT, 'modules', 'ui-design', 'kit', 'lib.js'));
130
+ const states = JSON.parse(fs.readFileSync(path.join(PUB, 'brand.states.json'), 'utf8'));
131
+ assert.deepEqual(lib.validateStatesFile(states).errors || [], []);
132
+ assert.equal(states.page, '/brand');
133
+ assert.equal(states.modes, undefined, 'both modes: the page follows the device');
134
+ });
135
+
136
+ // ── 5. the route ─────────────────────────────────────────────────────────────
137
+
138
+ const { buildPlatformApp } = require('../src/platform-server.js');
139
+ const modules = require('../src/modules.js');
140
+ let server;
141
+ let port;
142
+ before(async () => {
143
+ server = http.createServer(buildPlatformApp());
144
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
145
+ port = server.address().port;
146
+ });
147
+ after(async () => {
148
+ modules.setModuleOverrides({});
149
+ await new Promise((resolve) => server.close(resolve));
150
+ });
151
+ function get(pathname, host) {
152
+ return new Promise((resolve, reject) => {
153
+ http.get({ host: '127.0.0.1', port, path: pathname, headers: { Host: host } }, (res) => {
154
+ let body = '';
155
+ res.on('data', (c) => (body += c));
156
+ res.on('end', () => resolve({ status: res.statusCode, body }));
157
+ }).on('error', reject);
158
+ });
159
+ }
160
+
161
+ test('GET /brand: 200 signed-out on the hub\'s apex, branded; never on a child, the builders or status host', async () => {
162
+ modules.setModuleOverrides({ 'platform-identity': false });
163
+ assert.equal((await get('/brand', 'game.test.local')).status, 404, 'a child instance serves no platform reference');
164
+ modules.setModuleOverrides({ 'platform-identity': true, 'ui-design': true });
165
+ const hub = await get('/brand', 'game.test.local');
166
+ assert.equal(hub.status, 200, 'reachable with no session cookie');
167
+ assert.match(hub.body, /— Design system<\/title>/);
168
+ assert.match(hub.body, /window\.__BRANDING__=/, 'the pack is spliced, so the swatches take the instance colours');
169
+ assert.doesNotMatch(hub.body, /\{\{[a-zA-Z]+\}\}/, 'every token stamps');
170
+ for (const host of ['builders.test.local', 'status.test.local']) {
171
+ assert.doesNotMatch((await get('/brand', host)).body, /— Design system<\/title>/, `${host} must not serve /brand`);
172
+ }
173
+ const css = await get('/modules/ui-design/public/brand.css', 'game.test.local');
174
+ assert.equal(css.status, 200, 'the layout sheet is served at its module path');
175
+ });