@bongos/core 1.19.458 → 1.19.460

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/.bongos-core.json +117 -42
  2. package/.claude/skills/design/SKILL.md +10 -0
  3. package/docs/adr/0219-a-look-is-a-branding-pack-the-style-library.md +30 -0
  4. package/docs/adr/README.md +1 -0
  5. package/docs/branding-contract.md +1 -1
  6. package/docs/copy-inventory.md +52 -61
  7. package/docs/copy-registry.json +92 -203
  8. package/docs/file-map.md +1 -1
  9. package/docs/module-api-changelog.md +4 -0
  10. package/docs/recipes/ui-look-before-you-ship.md +2 -0
  11. package/modules/hall-ui/public/collab-lib.js +40 -4
  12. package/modules/hall-ui/public/collab.css +18 -34
  13. package/modules/hall-ui/public/collab.html +17 -17
  14. package/modules/hall-ui/public/collab.js +75 -39
  15. package/modules/hall-ui/public/collab.states.json +2 -2
  16. package/modules/hall-ui/public/copy-desk.css +64 -109
  17. package/modules/hall-ui/public/copy-desk.html +76 -69
  18. package/modules/hall-ui/public/copy-desk.js +175 -80
  19. package/modules/hall-ui/public/copy-desk.states.json +59 -2
  20. package/modules/hall-ui/public/oversight.css +11 -2
  21. package/modules/lifecycle/help-requests.js +18 -8
  22. package/modules/ui-design/kit/README.md +7 -5
  23. package/modules/ui-design/kit/check-mock.js +12 -5
  24. package/modules/ui-design/kit/contrast.js +13 -6
  25. package/modules/ui-design/kit/lib.js +39 -2
  26. package/modules/ui-design/kit/probe.js +4 -1
  27. package/modules/ui-design/kit/render.js +7 -4
  28. package/modules/ui-design/kit/serve.js +9 -2
  29. package/modules/ui-design/styles/README.md +54 -2
  30. package/modules/ui-design/styles/chrome-world/DESIGN.md +56 -0
  31. package/modules/ui-design/styles/chrome-world/mock.html +275 -0
  32. package/modules/ui-design/styles/chrome-world/mock.states.json +11 -0
  33. package/modules/ui-design/styles/chrome-world/pack.json +33 -0
  34. package/modules/ui-design/styles/expedition/DESIGN.md +141 -0
  35. package/modules/ui-design/styles/expedition/mock.html +285 -0
  36. package/modules/ui-design/styles/expedition/mock.states.json +11 -0
  37. package/modules/ui-design/styles/expedition/pack.json +33 -0
  38. package/modules/ui-design/styles/grove/DESIGN.md +148 -0
  39. package/modules/ui-design/styles/grove/mock.html +294 -0
  40. package/modules/ui-design/styles/grove/mock.states.json +11 -0
  41. package/modules/ui-design/styles/grove/pack.json +33 -0
  42. package/package-lock.json +2 -2
  43. package/package.json +1 -1
  44. package/src/module-api.js +1 -1
  45. package/tests/collab_page.mjs +13 -7
  46. package/tests/cosmos_token_contrast.mjs +9 -2
  47. package/tests/hall_collab_world.mjs +214 -0
  48. package/tests/hall_tokens.mjs +15 -6
  49. package/tests/ui_design_styles.mjs +218 -0
  50. package/tests/world_tokens.mjs +9 -2
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
  // contrast.js — the contrast probe (task 1003320, ADR 0197), three ways:
4
- // --pack the instance's branding pack: every ink on every ground,
5
- // light values from the pack (pure; no browser)
4
+ // --pack [name] the instance's branding pack — or, named, a look from the
5
+ // style library (modules/ui-design/styles/<name>/pack.json, or a
6
+ // pack.json path): every ink on every ground, light values
7
+ // from the pack (pure; no browser)
6
8
  // --url <url> | --file <html> walk every visible text element in dark + light
7
9
  // and compute the real WCAG ratio against its COMPOSITED
8
10
  // background (the degraded static detector cannot do this)
@@ -11,7 +13,7 @@
11
13
  // ground's median — for text over a moving or textured
12
14
  // ground where the stylesheet cannot say what is under it
13
15
  //
14
- // node modules/ui-design/kit/contrast.js --pack
16
+ // node modules/ui-design/kit/contrast.js --pack [expedition]
15
17
  // node modules/ui-design/kit/contrast.js --url http://127.0.0.1:3412/projects [--modes dark,light] [--glyphs "#projTrig,.foot a"]
16
18
  // node modules/ui-design/kit/contrast.js --file docs/design/mocks/hub/v2.html
17
19
  //
@@ -110,12 +112,17 @@ async function glyphContrast(page, selector) {
110
112
 
111
113
  async function main() {
112
114
  const args = process.argv.slice(2);
113
- if (lib.hasFlag(args, '--help')) { process.stderr.write('usage: node modules/ui-design/kit/contrast.js --pack | --url <url> | --file <html> [--modes dark,light] [--glyphs sel,sel]\n'); process.exit(lib.EXIT.USAGE); }
115
+ if (lib.hasFlag(args, '--help')) { process.stderr.write('usage: node modules/ui-design/kit/contrast.js --pack [name] | --url <url> | --file <html> [--modes dark,light] [--glyphs sel,sel]\n'); process.exit(lib.EXIT.USAGE); }
114
116
  const url = lib.argOf(args, '--url', null), file = lib.argOf(args, '--file', null);
115
117
  let failures = 0;
116
118
  if (lib.hasFlag(args, '--pack') || (!url && !file)) {
117
- const rows = packContrast(lib.instancePack());
118
- process.stdout.write('the pack (light values):\n');
119
+ // `--pack` alone is the instance's; `--pack <name>` is a look from the style library
120
+ const packArg = lib.argOf(args, '--pack', null);
121
+ const named = packArg && !packArg.startsWith('--') ? packArg : null;
122
+ const style = named ? lib.resolveStyle(named) : null;
123
+ if (named && !style) { process.stderr.write(`contrast: --pack "${named}" is not a style under modules/ui-design/styles/ (${lib.listStyles().join(', ') || 'none'}) nor a pack.json path\n`); process.exit(lib.EXIT.USAGE); }
124
+ const rows = packContrast(style ? style.pack : lib.instancePack());
125
+ process.stdout.write(`${style ? `the ${style.name} pack` : 'the pack'} (light values):\n`);
119
126
  for (const r of rows) { process.stdout.write(` ${r.ok === null ? 'SKIP' : r.ok ? 'PASS' : 'FAIL'} ${String(r.fg).padEnd(11)} on ${String(r.bg).padEnd(8)} ${r.ratio === null ? r.note : r.ratio + ':1 (need ' + r.need + ')'}\n`); if (r.ok === false) failures++; }
120
127
  if (!url && !file) { process.stdout.write(failures ? `\n${failures} pack pair(s) below the floor\n` : '\nPACK CLEAN\n'); process.exit(failures ? lib.EXIT.PROBLEMS : lib.EXIT.CLEAN); }
121
128
  }
@@ -10,12 +10,14 @@
10
10
  // Exit codes shared by every kit CLI: 0 clean · 1 problems/FAILs · 2 usage or a
11
11
  // contract error (a bad file, a missing page) · 3 no browser / no playwright-core.
12
12
  const fs = require('node:fs');
13
+ const os = require('node:os');
13
14
  const path = require('node:path');
14
15
  const net = require('node:net');
15
16
  const { spawn } = require('node:child_process');
16
17
 
17
18
  const REPO_ROOT = path.resolve(__dirname, '..', '..', '..');
18
19
  const KIT_DIR = __dirname;
20
+ const STYLES_DIR = path.join(__dirname, '..', 'styles');
19
21
  const EXIT = Object.freeze({ CLEAN: 0, PROBLEMS: 1, USAGE: 2, NO_BROWSER: 3 });
20
22
 
21
23
  // The floors (modules/ui-design/CLAUDE.md "The platform's floors"). The names are
@@ -342,10 +344,11 @@ function freePort() {
342
344
  s.listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => resolve(p)); });
343
345
  });
344
346
  }
345
- async function startStub({ surface, auth = false, port, prefix, child = false, fixtures } = {}) {
347
+ async function startStub({ surface, auth = false, port, prefix, child = false, fixtures, style } = {}) {
346
348
  const p = port || await freePort();
347
349
  const env = { ...process.env, STUB_PORT: String(p), AUTH: auth ? '1' : '0', CHILD: child ? '1' : '0' };
348
350
  if (surface) env.SURFACE = surface;
351
+ if (style) env.STYLE = style;
349
352
  if (prefix) env.PREFIX = prefix;
350
353
  if (fixtures) env.FIXTURE_ALIAS = JSON.stringify(fixtures);
351
354
  const proc = spawn(process.execPath, [path.join(KIT_DIR, 'serve.js')], { env, stdio: ['ignore', 'pipe', 'pipe'] });
@@ -375,6 +378,39 @@ function composite(fg, bg) { const a = fg[3] == null ? 1 : fg[3]; return [fg[0]
375
378
  function luminance(c) { const f = (v) => { v /= 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); }; return 0.2126 * f(c[0]) + 0.7152 * f(c[1]) + 0.0722 * f(c[2]); }
376
379
  function contrastRatio(a, b) { const l1 = luminance(a), l2 = luminance(b); const [hi, lo] = l1 > l2 ? [l1, l2] : [l2, l1]; return (hi + 0.05) / (lo + 0.05); }
377
380
 
381
+ // ── the style library (task 1003323) ──────────────────────────────────────────
382
+ // A look is a branding-pack overlay under modules/ui-design/styles/<name>/pack.json
383
+ // (theme.ui: the thirteen colours + the two font stacks), never a fork of a sheet.
384
+ function listStyles(dir = STYLES_DIR) {
385
+ return fs.existsSync(dir) ? fs.readdirSync(dir).filter((n) => fs.existsSync(path.join(dir, n, 'pack.json'))).sort() : [];
386
+ }
387
+ // A style by NAME (a dir under styles/) or by PATH (a pack.json anywhere) → { name, dir, file, pack }, or null.
388
+ function resolveStyle(nameOrPath, dir = STYLES_DIR) {
389
+ if (!nameOrPath || typeof nameOrPath !== 'string') return null;
390
+ const byName = /^[a-z0-9][a-z0-9-]*$/.test(nameOrPath) ? path.join(dir, nameOrPath, 'pack.json') : null;
391
+ const file = byName && fs.existsSync(byName) ? byName : (nameOrPath.endsWith('.json') && fs.existsSync(nameOrPath) ? path.resolve(nameOrPath) : null);
392
+ if (!file) return null;
393
+ const pack = readJson(file);
394
+ return { name: pack.name || path.basename(path.dirname(file)), dir: path.dirname(file), file, pack };
395
+ }
396
+ // The pack the stub serves under a style: the instance's own pack (config/branding.json,
397
+ // where it has one) with the style's theme.ui laid over it, written to a temp file the
398
+ // core-side transform reads through GDS_BRANDING_FILE (src/branding.js's fixed knob — the
399
+ // one that lets one checkout run two instances). The instance's identity strings stay;
400
+ // only the fifteen change, which is the whole point: a look is a pack, never a page edit.
401
+ function styleBrandingFile(style, root = REPO_ROOT) {
402
+ const instanceFile = path.join(root, 'config', 'branding.json');
403
+ const instance = fs.existsSync(instanceFile) ? readJson(instanceFile) : {};
404
+ const theme = instance.theme || {};
405
+ const merged = { ...instance, theme: { ...theme, ui: { ...(theme.ui || {}), ...((style.pack.theme || {}).ui || {}) } } };
406
+ // a pack.json loaded by PATH names itself; the temp file takes a slug of that name, never the name
407
+ const slug = String(style.name).replace(/[^a-z0-9-]/gi, '_').slice(0, 64) || 'style';
408
+ const out = path.join(os.tmpdir(), 'ui-design-kit', 'styles', `${slug}.branding.json`);
409
+ fs.mkdirSync(path.dirname(out), { recursive: true });
410
+ fs.writeFileSync(out, JSON.stringify(merged));
411
+ return out;
412
+ }
413
+
378
414
  // The instance's resolved branding pack through the doorway — the only core
379
415
  // reach the kit makes (task 1003320). Loaded lazily: the pure helpers above never
380
416
  // touch it, so the test pins them without booting the doorway.
@@ -391,9 +427,10 @@ function packColors(pack) {
391
427
  }
392
428
 
393
429
  module.exports = {
394
- REPO_ROOT, KIT_DIR, EXIT, FLOORS, SIZES, MODES, CHANNELS, SETTLE_MS, ACTIONS, EXPECT_KEYS,
430
+ REPO_ROOT, KIT_DIR, STYLES_DIR, EXIT, FLOORS, SIZES, MODES, CHANNELS, SETTLE_MS, ACTIONS, EXPECT_KEYS,
395
431
  argOf, hasFlag, listArg, pageName, normalizePageArg, resolveSurfaceDir, sidecarPath, readJson,
396
432
  validateActions, runActions, validateExpect, factsInPage, expectProblems, auditFloors, reducedMotionProblems,
397
433
  validateStatesFile, validateProbesFile, NoBrowserError, launchBrowser, attachCollectors, withMode,
398
434
  freePort, startStub, parseColor, composite, luminance, contrastRatio, instancePack, packColors,
435
+ listStyles, resolveStyle, styleBrandingFile,
399
436
  };
@@ -10,6 +10,7 @@
10
10
  // dialog yields Escape; signed out nothing throws.
11
11
  //
12
12
  // node modules/ui-design/kit/probe.js --page /projects [--surface <dir|key>] [--probes <file>] [--only <substring>]
13
+ // --style <name> drive the page under a look from the style library (task 1003323)
13
14
  // --base-out <url> --base-in <url> use running stubs instead of starting two
14
15
  //
15
16
  // A probe with `continue: true` keeps the previous probe's page (a contract is a
@@ -52,7 +53,9 @@ async function main() {
52
53
 
53
54
  const stubs = [];
54
55
  let baseOut = lib.argOf(args, '--base-out', null), baseIn = lib.argOf(args, '--base-in', null);
55
- const stubOpts = { surface, prefix: json.stub && json.stub.prefix, child: json.stub && json.stub.child, fixtures: json.stub && json.stub.fixtures };
56
+ const style = lib.argOf(args, '--style', null);
57
+ if (style && !lib.resolveStyle(style)) usage(`--style "${style}" is not a style under modules/ui-design/styles/ (${lib.listStyles().join(', ') || 'none'})`);
58
+ const stubOpts = { surface, prefix: json.stub && json.stub.prefix, child: json.stub && json.stub.child, fixtures: json.stub && json.stub.fixtures, style };
56
59
  if (!baseOut) { const s = await lib.startStub({ ...stubOpts, auth: false }); stubs.push(s); baseOut = s.base; }
57
60
  if (!baseIn) { const s = await lib.startStub({ ...stubOpts, auth: true }); stubs.push(s); baseIn = s.base; }
58
61
  const stop = () => stubs.forEach((s) => s.stop());
@@ -12,6 +12,7 @@
12
12
  // node modules/ui-design/kit/render.js --page /builders/ranks --surface hall-ui
13
13
  // node modules/ui-design/kit/render.js --states <file> (explicit file)
14
14
  // --modes dark,light --sizes 1440,390,320 --only a,b --out <dir>
15
+ // --style <name> render under a look from the style library (STYLE= on the stubs; task 1003323)
15
16
  // --base-out <url> --base-in <url> use running stubs instead of starting two
16
17
  //
17
18
  // Exit 0 ALL CLEAN · 1 problems · 2 usage/contract · 3 no browser.
@@ -21,7 +22,7 @@ const path = require('node:path');
21
22
  const lib = require('./lib.js');
22
23
 
23
24
  function usage(msg) {
24
- process.stderr.write(`render: ${msg}\n node modules/ui-design/kit/render.js --page /projects [--surface <dir|key>] [--states <file>] [--modes dark,light] [--sizes 1440,390,320] [--only a,b] [--out <dir>] [--base-out <url> --base-in <url>]\n`);
25
+ process.stderr.write(`render: ${msg}\n node modules/ui-design/kit/render.js --page /projects [--surface <dir|key>] [--states <file>] [--modes dark,light] [--sizes 1440,390,320] [--only a,b] [--style <name>] [--out <dir>] [--base-out <url> --base-in <url>]\n`);
25
26
  process.exit(lib.EXIT.USAGE);
26
27
  }
27
28
 
@@ -71,12 +72,14 @@ async function main() {
71
72
  const only = lib.listArg(args, '--only', null);
72
73
  const names = Object.keys(json.states).filter((n) => !only || only.includes(n));
73
74
  if (!names.length) usage(`--only names no state of ${path.basename(file)}`);
74
- const out = path.resolve(lib.argOf(args, '--out', path.join(os.tmpdir(), 'ui-design-kit', surface.replace(/[\\/]/g, '_'), lib.pageName(json.page))));
75
+ const style = lib.argOf(args, '--style', null);
76
+ if (style && !lib.resolveStyle(style)) usage(`--style "${style}" is not a style under modules/ui-design/styles/ (${lib.listStyles().join(', ') || 'none'})`);
77
+ const out = path.resolve(lib.argOf(args, '--out', path.join(os.tmpdir(), 'ui-design-kit', surface.replace(/[\\/]/g, '_'), lib.pageName(json.page) + (style ? `@${style}` : ''))));
75
78
  fs.mkdirSync(out, { recursive: true });
76
79
 
77
80
  const stubs = [];
78
81
  let baseOut = lib.argOf(args, '--base-out', null), baseIn = lib.argOf(args, '--base-in', null);
79
- const stubOpts = { surface, prefix: json.stub && json.stub.prefix, child: json.stub && json.stub.child, fixtures: json.stub && json.stub.fixtures };
82
+ const stubOpts = { surface, prefix: json.stub && json.stub.prefix, child: json.stub && json.stub.child, fixtures: json.stub && json.stub.fixtures, style };
80
83
  if (!baseOut) { const s = await lib.startStub({ ...stubOpts, auth: false }); stubs.push(s); baseOut = s.base; }
81
84
  if (!baseIn) { const s = await lib.startStub({ ...stubOpts, auth: true }); stubs.push(s); baseIn = s.base; }
82
85
  const stop = () => stubs.forEach((s) => s.stop());
@@ -85,7 +88,7 @@ async function main() {
85
88
  try { browser = await lib.launchBrowser(); }
86
89
  catch (e) { stop(); process.stderr.write(`render: ${e.message}\n`); process.exit(e instanceof lib.NoBrowserError ? lib.EXIT.NO_BROWSER : lib.EXIT.USAGE); }
87
90
 
88
- const report = { page: json.page, surface, out, renders: {}, reducedMotion: {} };
91
+ const report = { page: json.page, surface, style: style || null, out, renders: {}, reducedMotion: {} };
89
92
  const problems = [];
90
93
  const collectorOpts = { ignoreRequests: json.ignoreRequests, ignoreConsole: json.ignoreConsole };
91
94
  try {
@@ -21,6 +21,8 @@
21
21
  // CHILD=1 the child-instance shape: apex-only regions stripped
22
22
  // LIVE=1 | LIVE=<origin> proxy the public feeds to the pack's public origin (or the named one)
23
23
  // FAIL=<feed,feed> those feeds answer 500 (the names in FEEDS below)
24
+ // STYLE=<name | pack.json> serve under a look from the style library (modules/ui-design/
25
+ // styles/<name>/pack.json laid over the instance's pack; task 1003323)
24
26
  //
25
27
  // Exit 2 on a surface that does not resolve. Prints ONE line when listening
26
28
  // ("stub on http://…") — render.js and probe.js wait for it.
@@ -40,6 +42,7 @@ const AUTH = process.env.AUTH === '1';
40
42
  const CHILD = process.env.CHILD === '1';
41
43
  const LIVE = process.env.LIVE || '';
42
44
  const FAIL = new Set(String(process.env.FAIL || '').split(',').map((s) => s.trim()).filter(Boolean));
45
+ const STYLE = process.env.STYLE || '';
43
46
  // a states file may point a fixture NAME at another FILE (stub.fixtures, task 1003345) — the
44
47
  // way the mine view's empty state is rendered without a second stub shape
45
48
  const ALIAS = JSON.parse(process.env.FIXTURE_ALIAS || '{}');
@@ -51,6 +54,10 @@ const DIR = lib.resolveSurfaceDir(SURFACE, ROOT);
51
54
  if (!DIR) { process.stderr.write(`serve: SURFACE "${SURFACE}" is neither a directory of ${ROOT} nor a module key with a webSurfaces dir\n`); process.exit(lib.EXIT.USAGE); }
52
55
  if (!PREFIX.startsWith('/') || !PREFIX.endsWith('/')) { process.stderr.write('serve: PREFIX must start and end with "/"\n'); process.exit(lib.EXIT.USAGE); }
53
56
  if (!fs.existsSync(TRANSFORM)) { process.stderr.write(`serve: the transform command is missing: ${TRANSFORM}\n`); process.exit(lib.EXIT.USAGE); }
57
+ const style = STYLE ? lib.resolveStyle(STYLE) : null;
58
+ if (STYLE && !style) { process.stderr.write(`serve: STYLE "${STYLE}" is neither a style under modules/ui-design/styles/ (${lib.listStyles().join(', ') || 'none'}) nor a pack.json path\n`); process.exit(lib.EXIT.USAGE); }
59
+ // the transform resolves the pack itself (src/branding.js); under a style it reads the merged file
60
+ const TRANSFORM_ENV = style ? { ...process.env, GDS_BRANDING_FILE: lib.styleBrandingFile(style, ROOT) } : process.env;
54
61
 
55
62
  const MIME = {
56
63
  '.html': 'text/html; charset=utf-8', '.css': 'text/css; charset=utf-8', '.js': 'application/javascript', '.mjs': 'application/javascript',
@@ -61,7 +68,7 @@ const MIME = {
61
68
 
62
69
  // The real transforms, as the instance runs them (see ui-kit-transform.js).
63
70
  function transform(file) {
64
- const r = spawnSync(process.execPath, [TRANSFORM, path.join(DIR, file), ...(CHILD ? ['--child'] : []), '--json'], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
71
+ const r = spawnSync(process.execPath, [TRANSFORM, path.join(DIR, file), ...(CHILD ? ['--child'] : []), '--json'], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, env: TRANSFORM_ENV });
65
72
  if (r.status !== 0) throw new Error(`ui-kit-transform exited ${r.status}: ${(r.stderr || r.stdout || '').trim().slice(0, 600)}`);
66
73
  return JSON.parse(r.stdout);
67
74
  }
@@ -303,5 +310,5 @@ const server = http.createServer((req, res) => {
303
310
  });
304
311
  server.listen(PORT, '127.0.0.1', () => {
305
312
  const port = server.address().port;
306
- process.stdout.write(`stub on http://127.0.0.1:${port}${PREFIX} surface=${path.relative(ROOT, DIR) || '.'} auth=${AUTH} child=${CHILD} live=${LIVE || '-'} fail=${[...FAIL].join(',') || '-'}\n`);
313
+ process.stdout.write(`stub on http://127.0.0.1:${port}${PREFIX} surface=${path.relative(ROOT, DIR) || '.'} auth=${AUTH} child=${CHILD} live=${LIVE || '-'} fail=${[...FAIL].join(',') || '-'} style=${style ? style.name : '-'}\n`);
307
314
  });
@@ -1,3 +1,55 @@
1
- # `modules/ui-design/styles/` — reserved home for the style library
1
+ # `modules/ui-design/styles/` — the style library: looks as branding packs
2
2
 
3
- Lands with [task 1003323](https://cloudbongos.com/builders#/task/1003323): looks shipped as branding packs (the fifteen host-overridable tokens plus a `DESIGN.md` per look), with two alternates beside the chrome world, so an instance picks a world instead of inheriting Cloud Bongos's. A look is a pack; it is never a page's private palette.
3
+ A **look** is something a builder or an instance owner picks for a new surface or a whole instance. The library ships looks as **branding packs** ([task 1003323](https://cloudbongos.com/builders#/task/1003323), [ADR 0219](../../../docs/adr/<redacted>.md)): the thirteen colours and the two font stacks of `theme.ui` (the fifteen host-overridable tokens, [ADR 0062 §3](../../../docs/adr/<redacted>.md) / [ADR 0196](../../../docs/adr/<redacted>.md)), a `DESIGN.md` of the look's derived recipes in the frontmatter shape the chrome world uses, and one specimen mock. **Never a fork of a sheet.** The drift tests already prove every served sheet reads only the contract, so a look that needs a sixteenth token is a contract change and an ADR, not a style.
4
+
5
+ ## The catalogue
6
+
7
+ | Style | `pack.json` | What it is | Origin |
8
+ |---|---|---|---|
9
+ | `chrome-world` | the neutral pack, byte for byte | The canonical entry: one iridescent chrome sculpture in a black void, pale grey paper in light, one warm salmon that lights only the pressable and the live. Its recipes are the repo-root [`DESIGN.md`](../../../DESIGN.md); [`chrome-world/DESIGN.md`](chrome-world/DESIGN.md) points there. | the world round's pick (task 1003296 → 1003298) |
10
+ | `expedition` | warm paper, lamp yellow, Archivo | A printed field journal: a halftone screen over the paper, hairline frames instead of a grid, big heavy uppercase headlines, the yellow as a fill only. [`expedition/DESIGN.md`](expedition/DESIGN.md) | `docs/design/mocks/world/natgeo-editorial` (kept in round 1) |
11
+ | `grove` | pale sage, tan, sea green, Nunito | A sculpted paper garden: soft corners on everything that holds content, tinted shadows, the sea as a slab a whole band sits on, the tan as the one lit thing. [`grove/DESIGN.md`](grove/DESIGN.md) | `docs/design/mocks/world/organic-cards` (kept in round 1) |
12
+
13
+ The owner looked at both alternates on the sheet on 2026-09-01 and kept them ("styles look good for a start"). Both alternates were **retuned against the contract**, which is a measurement and not a taste: a pack's thirteen are laid through three sheets that already exist (the landing's `world.css`, the hub's `cosmos.css`, the hall's `style.css`) and every pair those sheets paint must clear AA under it. The grove's sage ground lifted and its inks deepened until the hub's fixed status vocabulary and the hall's chips cleared; the expedition's `ink-faint` deepened one step. What a pack cannot carry is **dark**: the contract is light values only, and every sheet's dark tier is its own black, which a look reaches through its accent and its faces. So the grove's green dark ground stays in the world round's mock and does not travel.
14
+
15
+ ## What a style dir is (the contract `tests/ui_design_styles.mjs` enforces)
16
+
17
+ ```
18
+ modules/ui-design/styles/<name>/
19
+ pack.json { name, title, summary, origin, theme: { ui: { colors: <the thirteen>, fonts: { display, body, links } } } }
20
+ DESIGN.md frontmatter: colors (the thirteen, value for value with pack.json; the dark facts; the derived recipes),
21
+ typography, rounded, spacing, components; then the look's prose in the root DESIGN.md's sections
22
+ mock.html the specimen: no :root, tokens on body, dark written twice, every colour a var()/color-mix() of the pack,
23
+ one header / main / h1, a skip link, ?state= and ?mode=, served through the stub with the pack spliced in
24
+ ```
25
+
26
+ - `name` is the dir name, kebab-case. `theme.ui.colors` carries **exactly** the thirteen keys of the neutral pack, `fonts` the two stacks and a `links` line for the faces.
27
+ - `chrome-world/pack.json` equals `config/branding.neutral.json`'s `theme.ui` and is pinned to it.
28
+ - Every pack clears the pack floor (every ink on every ground at 4.5:1) and the three token suites in both modes; every mock passes `check-mock`'s platform rules under its own colours and its own `DESIGN.md` bans.
29
+ - The roster above, the module's [`CLAUDE.md`](../CLAUDE.md) and [`/design`](../../../.claude/skills/design/SKILL.md) name every style; a dir that is not named is not a look.
30
+
31
+ ## Using a look
32
+
33
+ **On the kit** (any surface, any look, no page edit): `STYLE=<name>` on the stub lays the pack over the instance's own (its identity strings stay; only the fifteen change) through `GDS_BRANDING_FILE`, the fixed knob `src/branding.js` already has for running one checkout as two instances.
34
+
35
+ ```
36
+ STYLE=grove node modules/ui-design/kit/serve.js # the front door under the grove
37
+ node modules/ui-design/kit/render.js --page / --style expedition # every state × width × mode, audited
38
+ node modules/ui-design/kit/render.js --page /builders/ranks --surface hall-ui --style grove
39
+ node modules/ui-design/kit/probe.js --page /projects --style grove
40
+ node modules/ui-design/kit/contrast.js --pack expedition # the pack floor, no browser
41
+ node modules/ui-design/kit/check-mock.js --file modules/ui-design/styles/grove/mock.html --pack grove
42
+ SURFACE=modules/ui-design/styles/expedition STYLE=expedition node modules/ui-design/kit/serve.js # the specimen at /mock
43
+ ```
44
+
45
+ **On an instance** (the switch is the contract, not a page): copy the look's `pack.json` `theme` into `config/branding.json`, the file `cloudbongos init` writes ([`docs/branding-contract.md`](../../../docs/branding-contract.md) §Branding a new instance). Every served page re-themes at once, because the server splices `theme.ui` into `:root` at the end of `<head>` and every sheet reads only that. To leave a look, remove the block: the neutral pack (the chrome world) resolves.
46
+
47
+ **What stays the platform's, whichever look is on:** the dark ground (black), the hall's fixed green / amber / red **verification** vocabulary and the status ledger's pillars (a look must not repaint a status chip, ADR 0196 Context §2), the derived recipes each sheet carries (a corner radius, a shadow, a glass are a sheet's tier, not a pack's), and the platform floors.
48
+
49
+ ## Adding a look
50
+
51
+ 1. Make the dir with the three files; start from the alternate nearest to it.
52
+ 2. `node modules/ui-design/kit/contrast.js --pack <name>` until the floor is clean; then `node --test tests/ui_design_styles.mjs`, which runs the world / cosmos / hall suites under the pack and tells you the exact pair that fails. Tune the thirteen, never the sheets.
53
+ 3. `node modules/ui-design/kit/check-mock.js --file modules/ui-design/styles/<name>/mock.html --pack <name>` until ALL RULES PASS.
54
+ 4. `node modules/ui-design/kit/render.js --page / --style <name>` and look at the shots; the hall's `/builders/ranks` shows the light tier.
55
+ 5. Add the row above, the line in the module's `CLAUDE.md` and in `/design`. Ship under a claimed task.
@@ -0,0 +1,56 @@
1
+ ---
2
+ name: The Chrome World
3
+ description: One iridescent chrome sculpture in a black void, one warm accent that lights only what is pressable or live. The canonical entry of the style library; its recipes are the repo-root DESIGN.md.
4
+ colors:
5
+ # ---- contract tier, LIGHT: the thirteen this look's pack.json carries, value for value
6
+ # (tests/ui_design_styles.mjs holds the two in lockstep). The other two host-overridable
7
+ # tokens are the font stacks, under `typography`.
8
+ bg: "#edecee"
9
+ bg-card: "#fbfafc"
10
+ bg-deep: "#e2e1e4"
11
+ bg-frame: "#f6f5f7"
12
+ ink: "#17181a"
13
+ ink-soft: "#55565a"
14
+ ink-faint: "#65656a"
15
+ rule: "#c6c6c8"
16
+ rule-soft: "#dcdbdd"
17
+ accent: "#fb9569"
18
+ accent-deep: "#a3481a"
19
+ sea: "#5b6fd0"
20
+ terracotta: "#c9512c"
21
+ typography:
22
+ display: { fontFamily: "Manrope, Segoe UI, system-ui, -apple-system, sans-serif", fontSize: "clamp(34px, 6.7vw, 96px)", fontWeight: 300, lineHeight: 0.92, letterSpacing: "-0.042em", textTransform: "lowercase" }
23
+ body: { fontFamily: "Manrope, Segoe UI, system-ui, -apple-system, sans-serif", fontSize: "17px", fontWeight: 400, lineHeight: 1.5 }
24
+ label: { fontFamily: "JetBrains Mono, ui-monospace, Cascadia Mono, Consolas, monospace", fontSize: "11px", fontWeight: 400, letterSpacing: "0.12em", textTransform: "uppercase" }
25
+ rounded:
26
+ pill: "999px"
27
+ panel: "16px"
28
+ tile: "10px"
29
+ ---
30
+
31
+ # Style: The Chrome World (the canonical look)
32
+
33
+ This is the look Cloud Bongos itself runs on and the **first entry** of the style library ([task 1003323](https://cloudbongos.com/builders#/task/1003323)). It is the canonical one in a precise sense: its `pack.json` is byte-for-byte the `theme.ui` block of [`config/branding.neutral.json`](../../../../config/branding.neutral.json), the neutral pack every instance resolves over, and its recipes are **not restated here**. The world's derived tiers, faces, materials, motion grammar, components, Do's and Don'ts and accessibility floors are the repo-root [`DESIGN.md`](../../../../DESIGN.md), recorded from the built landing ([ADR 0196](../../../../docs/adr/<redacted>.md)). Read that file; this one only holds the thirteen so the catalogue's lockstep test has something to hold.
34
+
35
+ ## What this entry is for
36
+
37
+ - **The reference every alternate is measured against.** An alternate is a different set of the same fifteen tokens laid through the same sheets; the chrome world is what those sheets were built for, so `STYLE=chrome-world` on the kit's stub is the same render as no `STYLE=` at all on a neutral instance.
38
+ - **The specimen.** [`mock.html`](mock.html) beside this file is the world's specimen sheet (the hairline bar, the lowercase display voice, the door pill, the lifecycle rail, three ledger rows), served through the stub with the pack spliced in:
39
+
40
+ ```
41
+ SURFACE=modules/ui-design/styles/chrome-world STYLE=chrome-world node modules/ui-design/kit/serve.js
42
+ # http://127.0.0.1:3412/mock (?mode=light · ?state=signed-in)
43
+ ```
44
+
45
+ ## Do's and Don'ts
46
+
47
+ The root `DESIGN.md`'s, restated here only so the kit reads the look's bans from this dir (`check-mock.js --pack chrome-world`); the root file is the authority.
48
+
49
+ ### Don't
50
+
51
+ - **Don't** use an em dash or en dash in visible copy, an emoji, gradient text, a coloured text glow, or a box shadow under content.
52
+ - **Don't** declare a `:root` in a page, pin the accent in a dark block, or spend the accent on anything that is not pressable or live.
53
+
54
+ ## Adopting it
55
+
56
+ An instance that starts from the neutral pack already has this look; there is nothing to copy. An instance that adopted an alternate and wants back: copy this `pack.json`'s `theme` into its `config/branding.json` (or delete the `theme.ui` override so the neutral values resolve). No page changes: the sheets read the contract and nothing else.
@@ -0,0 +1,275 @@
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}} · the chrome world, on a sheet</title>
7
+ <script>
8
+ (function(){try{var q=new URLSearchParams(location.search);var m=q.get('mode');if(m==='light'||m==='dark')document.documentElement.setAttribute('data-mode',m);}catch(e){}})();
9
+ </script>
10
+ <style>
11
+ html { color-scheme: light; }
12
+ body {
13
+ /* the chrome world's derived tier, by the recipes in this style's DESIGN.md (the root
14
+ DESIGN.md's frontmatter). The fifteen arrive from the pack the stub splices after this
15
+ sheet; nothing below is a colour of its own: a var(), a color-mix(), or black or white
16
+ with alpha. */
17
+ --font-mono: "JetBrains Mono", ui-monospace, "Cascadia Mono", Consolas, monospace;
18
+ --glass: color-mix(in srgb, var(--ink) 5%, transparent);
19
+ --glass-strong: color-mix(in srgb, var(--ink) 8%, transparent);
20
+ --glass-edge: color-mix(in srgb, var(--ink) 12%, transparent);
21
+ --void-pill: rgba(0, 0, 0, .5);
22
+ --accent-ink: color-mix(in srgb, var(--accent) 45%, #000000);
23
+ --accent-lift: color-mix(in srgb, var(--accent) 58%, #ffffff);
24
+ --accent-tint: color-mix(in srgb, var(--accent) 30%, #ffffff);
25
+ --accent-on: color-mix(in srgb, var(--accent) 15%, #000000);
26
+ --accent-glow: 0 0 0 0 transparent;
27
+ --accent-halo: color-mix(in srgb, var(--accent) 20%, transparent);
28
+ --focus: color-mix(in srgb, var(--accent-deep) 82%, #000000);
29
+ --type-halo: none;
30
+ --label-face: var(--font-mono);
31
+ --label-size: 11px;
32
+ --label-space: .12em;
33
+ --radius: 16px;
34
+ --radius-sm: 10px;
35
+ --radius-pill: 999px;
36
+ --top-h: 64px;
37
+ --gutter: clamp(16px, 4vw, 26px);
38
+ --band: clamp(40px, 7vh, 72px);
39
+ margin: 0;
40
+ background: var(--bg);
41
+ color: var(--ink);
42
+ font-family: var(--font-body);
43
+ font-size: 17px;
44
+ line-height: 1.5;
45
+ -webkit-font-smoothing: antialiased;
46
+ }
47
+ :root[data-mode="dark"] body {
48
+ color-scheme: dark;
49
+ --bg: #000000;
50
+ --bg-card: color-mix(in srgb, #ffffff 6%, #000000);
51
+ --bg-deep: #000000;
52
+ --bg-frame: color-mix(in srgb, #ffffff 3%, #000000);
53
+ --ink: #ffffff;
54
+ --ink-soft: rgba(255, 255, 255, .75);
55
+ --ink-faint: rgba(255, 255, 255, .66);
56
+ --rule: rgba(255, 255, 255, .16);
57
+ --rule-soft: rgba(255, 255, 255, .08);
58
+ --accent-deep: color-mix(in srgb, var(--accent) 70%, #ffffff);
59
+ --glass: rgba(255, 255, 255, .11);
60
+ --glass-strong: rgba(255, 255, 255, .17);
61
+ --glass-edge: rgba(255, 255, 255, .13);
62
+ --accent-ink: var(--accent);
63
+ --accent-tint: color-mix(in srgb, var(--accent) 22%, #000000);
64
+ --accent-glow: 0 8px 28px color-mix(in srgb, var(--accent) 38%, transparent);
65
+ --focus: var(--accent);
66
+ --type-halo: 0 2px 18px rgba(0, 0, 0, .94), 0 0 54px rgba(0, 0, 0, .72);
67
+ --label-face: var(--font-body);
68
+ --label-size: 11.5px;
69
+ --label-space: .1em;
70
+ }
71
+ @media (prefers-color-scheme: dark) {
72
+ :root:not([data-mode="light"]) body {
73
+ color-scheme: dark;
74
+ --bg: #000000;
75
+ --bg-card: color-mix(in srgb, #ffffff 6%, #000000);
76
+ --bg-deep: #000000;
77
+ --bg-frame: color-mix(in srgb, #ffffff 3%, #000000);
78
+ --ink: #ffffff;
79
+ --ink-soft: rgba(255, 255, 255, .75);
80
+ --ink-faint: rgba(255, 255, 255, .66);
81
+ --rule: rgba(255, 255, 255, .16);
82
+ --rule-soft: rgba(255, 255, 255, .08);
83
+ --accent-deep: color-mix(in srgb, var(--accent) 70%, #ffffff);
84
+ --glass: rgba(255, 255, 255, .11);
85
+ --glass-strong: rgba(255, 255, 255, .17);
86
+ --glass-edge: rgba(255, 255, 255, .13);
87
+ --accent-ink: var(--accent);
88
+ --accent-tint: color-mix(in srgb, var(--accent) 22%, #000000);
89
+ --accent-glow: 0 8px 28px color-mix(in srgb, var(--accent) 38%, transparent);
90
+ --focus: var(--accent);
91
+ --type-halo: 0 2px 18px rgba(0, 0, 0, .94), 0 0 54px rgba(0, 0, 0, .72);
92
+ --label-face: var(--font-body);
93
+ --label-size: 11.5px;
94
+ --label-space: .1em;
95
+ }
96
+ }
97
+
98
+ *, *::before, *::after { box-sizing: border-box; }
99
+ a { color: inherit; }
100
+ :focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }
101
+ .skip { position: absolute; left: 12px; top: -60px; padding: 11px 18px; border-radius: var(--radius-pill); background: var(--bg-card); color: var(--ink); text-decoration: none; }
102
+ .skip:focus { top: 12px; }
103
+ .wrap { max-width: 1180px; margin: 0 auto; padding: 0 var(--gutter); }
104
+
105
+ /* the hairline bar */
106
+ .bar { height: var(--top-h); border-bottom: 1px solid var(--rule); }
107
+ .bar .wrap { height: 100%; display: flex; align-items: center; gap: 18px; }
108
+ .mark { font-weight: 500; letter-spacing: -.015em; text-decoration: none; padding: 10px 0; }
109
+ .nav { margin-left: auto; display: flex; align-items: center; gap: 4px; }
110
+ .nav a { text-decoration: none; text-transform: lowercase; color: var(--ink-soft); padding: 10px 8px; display: inline-block; line-height: 20px; }
111
+ .nav a:hover { color: var(--ink); }
112
+ .pill { display: inline-flex; align-items: center; height: 46px; padding: 0 22px; border-radius: var(--radius-pill); text-decoration: none; text-transform: lowercase; font-weight: 500; transition: background-color 200ms cubic-bezier(.2, .7, .3, 1); }
113
+ .pill.in { background: color-mix(in srgb, var(--ink) 92%, var(--bg)); color: var(--bg); }
114
+ .who { display: none; align-items: center; gap: 8px; height: 46px; padding: 0 6px; border-radius: var(--radius-pill); background: var(--glass); }
115
+ .who .dot { width: 30px; height: 30px; border-radius: 50%; background: var(--glass-strong); border: 1px solid var(--glass-edge); }
116
+ body[data-state="signed-in"] .pill.in { display: none; }
117
+ body[data-state="signed-in"] .who { display: inline-flex; }
118
+
119
+ /* the bands */
120
+ .band { padding: var(--band) 0; border-top: 1px solid var(--rule-soft); }
121
+ .band:first-of-type { border-top: 0; }
122
+ h1 { font-family: var(--font-display); font-weight: 300; font-size: clamp(34px, 6.7vw, 88px); line-height: .92; letter-spacing: -.042em; text-transform: lowercase; margin: 0 0 18px; text-shadow: var(--type-halo); max-width: 12ch; }
123
+ h2 { font-family: var(--font-display); font-weight: 300; font-size: clamp(26px, 3.1vw, 42px); line-height: 1.06; letter-spacing: -.035em; margin: 0 0 14px; }
124
+ .lede { color: var(--ink-soft); max-width: 58ch; margin: 0; }
125
+ .label { font-family: var(--label-face); font-size: var(--label-size); letter-spacing: var(--label-space); text-transform: uppercase; color: var(--ink-faint); }
126
+
127
+ /* the thirteen */
128
+ .sw { display: grid; grid-template-columns: repeat(auto-fill, minmax(72px, 1fr)); gap: 14px 10px; margin: 0; padding: 0; list-style: none; }
129
+ .sw li { margin: 0; }
130
+ .sw i { display: block; height: 56px; border-radius: var(--radius-sm); border: 1px solid var(--rule); background: var(--c); }
131
+ .sw b { display: block; margin-top: 6px; font-family: var(--font-mono); font-weight: 400; font-size: 11px; color: var(--ink-faint); overflow-wrap: anywhere; }
132
+
133
+ /* the door pill */
134
+ .doors { display: inline-flex; flex-wrap: wrap; gap: 6px; padding: 7px; border-radius: var(--radius-pill); background: var(--glass); }
135
+ .door { display: inline-flex; align-items: center; height: 48px; padding: 0 30px; border-radius: var(--radius-pill); text-decoration: none; text-transform: lowercase; font-weight: 500; transition: background-color 200ms cubic-bezier(.2, .7, .3, 1); }
136
+ .door.go { background: var(--accent); color: var(--accent-on); box-shadow: var(--accent-glow); }
137
+ .door.go:hover { background: var(--accent-lift); }
138
+ .door.more { color: var(--ink); }
139
+ .door.more:hover { background: var(--glass-strong); }
140
+
141
+ /* the lifecycle rail */
142
+ .rail { display: grid; grid-template-columns: repeat(5, 1fr); gap: 14px; margin: 0; padding: 0; list-style: none; }
143
+ .rail li { border-top: 1px solid var(--rule); padding-top: 12px; }
144
+ .rail .dot { display: block; width: 7px; height: 7px; border-radius: 50%; border: 1px solid var(--ink-faint); margin-bottom: 10px; }
145
+ .rail .lit { border-top-color: var(--accent); }
146
+ .rail .lit .dot { background: var(--accent); border-color: var(--accent); box-shadow: 0 0 0 4px var(--accent-halo); }
147
+ .rail .lit .label { color: var(--ink); }
148
+ .rail p { margin: 6px 0 0; color: var(--ink-soft); font-size: 15px; }
149
+
150
+ /* ledger rows */
151
+ .ledger { margin: 0; padding: 0; list-style: none; }
152
+ .ledger li { display: grid; grid-template-columns: 88px 1fr 152px 96px; gap: 20px; align-items: baseline; padding: 15px 0; border-top: 1px solid var(--rule); }
153
+ .ledger .id { font-family: var(--font-mono); font-size: 13px; color: var(--ink-faint); font-variant-numeric: tabular-nums; }
154
+ .ledger .by, .ledger .when { color: var(--ink-soft); }
155
+ .ledger .when { text-align: right; font-variant-numeric: tabular-nums; }
156
+ .ledger a { color: var(--accent-ink); text-underline-offset: 3px; display: inline-block; padding: 2px 0; }
157
+
158
+ footer { border-top: 1px solid var(--rule-soft); padding: 26px 0 40px; }
159
+ footer .wrap { display: flex; flex-wrap: wrap; gap: 8px 22px; align-items: baseline; color: var(--ink-soft); font-size: 15px; }
160
+ footer p { margin: 0; flex: 1 1 320px; }
161
+ footer a { color: var(--accent-ink); display: inline-block; padding: 3px 0; }
162
+
163
+ @media (max-width: 900px) {
164
+ body { --top-h: 60px; }
165
+ .rail { grid-template-columns: 1fr; }
166
+ }
167
+ @media (max-width: 600px) {
168
+ .ledger li { grid-template-columns: 1fr; gap: 4px; }
169
+ .ledger .when { text-align: left; }
170
+ .nav a { display: none; }
171
+ }
172
+ @media (prefers-reduced-motion: reduce) {
173
+ *, *::before, *::after { animation: none !important; transition: none !important; }
174
+ }
175
+ </style>
176
+ </head>
177
+ <body>
178
+ <!-- THESIS: the chrome world as a specimen sheet. The thirteen colours and the two faces the pack carries, laid through the world's own derived recipes (the root DESIGN.md frontmatter): the hairline bar, the lowercase display voice, the door pill, the lifecycle rail with one lit step, three ledger rows, the footer. This page declares no :root and no colour of its own: the pack is spliced by the stub (STYLE=chrome-world) and every value below is a var() or a color-mix() of it. Dark restates the world's black tier on body, since a mock links no sheet. STATES: rest (signed out), signed-in (the identity chip replaces the sign-in door). MODES: ?mode=dark|light. -->
179
+ <a class="skip" href="#main">Skip to content</a>
180
+ <header class="bar">
181
+ <div class="wrap">
182
+ <a class="mark" href="/">{{productName}}</a>
183
+ <nav class="nav" aria-label="Primary">
184
+ <a href="#swatches">Projects</a>
185
+ <a href="#type">Docs</a>
186
+ <a class="pill in" href="#main">Sign in</a>
187
+ <span class="who"><span class="dot" aria-hidden="true"></span><span class="label">builder</span></span>
188
+ </nav>
189
+ </div>
190
+ </header>
191
+ <main id="main" tabindex="-1">
192
+ <section class="band">
193
+ <div class="wrap">
194
+ <p class="label">A look from the style library</p>
195
+ <h1>the chrome world</h1>
196
+ <p class="lede">One sculpture in an empty room. Pure black in dark, pale grey paper in light, and exactly one warm thing on the page: the salmon, spent only on what is pressable or live.</p>
197
+ </div>
198
+ </section>
199
+ <section class="band" id="swatches">
200
+ <div class="wrap">
201
+ <h2>The thirteen</h2>
202
+ <ul class="sw">
203
+ <li><i style="--c: var(--bg)"></i><b>bg</b></li>
204
+ <li><i style="--c: var(--bg-card)"></i><b>bg-card</b></li>
205
+ <li><i style="--c: var(--bg-deep)"></i><b>bg-deep</b></li>
206
+ <li><i style="--c: var(--bg-frame)"></i><b>bg-frame</b></li>
207
+ <li><i style="--c: var(--ink)"></i><b>ink</b></li>
208
+ <li><i style="--c: var(--ink-soft)"></i><b>ink-soft</b></li>
209
+ <li><i style="--c: var(--ink-faint)"></i><b>ink-faint</b></li>
210
+ <li><i style="--c: var(--rule)"></i><b>rule</b></li>
211
+ <li><i style="--c: var(--rule-soft)"></i><b>rule-soft</b></li>
212
+ <li><i style="--c: var(--accent)"></i><b>accent</b></li>
213
+ <li><i style="--c: var(--accent-deep)"></i><b>accent-deep</b></li>
214
+ <li><i style="--c: var(--sea)"></i><b>sea</b></li>
215
+ <li><i style="--c: var(--terracotta)"></i><b>terracotta</b></li>
216
+ </ul>
217
+ </div>
218
+ </section>
219
+ <section class="band" id="type">
220
+ <div class="wrap">
221
+ <h2>Two faces, one voice</h2>
222
+ <p class="lede">The display face is set light, lowercase and tight so a headline reads as a shape crossing the sculpture. Body copy is the same family at reading weight. The mono appears only where the page quotes the machine: labels, ids, figures.</p>
223
+ <p class="label" style="margin-top: 22px">Label face, tracked and uppercase</p>
224
+ <p style="margin: 6px 0 0; font-family: var(--font-mono); font-size: 13px; font-variant-numeric: tabular-nums">1003323 · 14,987 credits · 7 ships in 24h</p>
225
+ </div>
226
+ </section>
227
+ <section class="band">
228
+ <div class="wrap">
229
+ <h2>The door pill</h2>
230
+ <p class="lede" style="margin-bottom: 22px">Two doors joined into one object. The primary carries the accent with dark ink on it; the secondary is glass on hover.</p>
231
+ <div class="doors">
232
+ <a class="door more" href="#swatches">Open the map</a>
233
+ <a class="door go" href="#main">Start a project</a>
234
+ </div>
235
+ </div>
236
+ </section>
237
+ <section class="band">
238
+ <div class="wrap">
239
+ <h2>The lifecycle rail</h2>
240
+ <ol class="rail">
241
+ <li><span class="dot"></span><span class="label">Claim</span><p>A builder locks a task.</p></li>
242
+ <li><span class="dot"></span><span class="label">Build</span><p>The code is written in a worktree.</p></li>
243
+ <li class="lit" aria-current="step"><span class="dot"></span><span class="label">Grade</span><p>An adversarial panel reads the diff.</p></li>
244
+ <li><span class="dot"></span><span class="label">Ship</span><p>Green checks land it on main.</p></li>
245
+ <li><span class="dot"></span><span class="label">Credit</span><p>The ledger pays on landing.</p></li>
246
+ </ol>
247
+ </div>
248
+ </section>
249
+ <section class="band">
250
+ <div class="wrap">
251
+ <h2>Ledger rows</h2>
252
+ <ul class="ledger">
253
+ <li><span class="id">1003320</span><span>The look-before-you-ship kit: a stub, a renderer, the probes</span><span class="by">jaxri</span><span class="when">3d ago</span></li>
254
+ <li><span class="id">1003321</span><span>The vetted UI skills: the vendoring policy and the platform preamble</span><span class="by">jaxri</span><span class="when">3d ago</span></li>
255
+ <li><span class="id">1003323</span><span><a href="#main">The style library: looks as branding packs</a></span><span class="by">jaxri</span><span class="when">today</span></li>
256
+ </ul>
257
+ </div>
258
+ </section>
259
+ </main>
260
+ <footer>
261
+ <div class="wrap">
262
+ <p>{{productName}} is built by AI agents claiming tasks from a ledger. This sheet is served through the kit with the look's pack spliced in.</p>
263
+ <a href="#main">Privacy</a>
264
+ <a href="#main">Terms</a>
265
+ <a href="#main">Contact</a>
266
+ </div>
267
+ </footer>
268
+ <script>
269
+ (function () {
270
+ var state = new URLSearchParams(location.search).get('state');
271
+ if (state === 'signed-in') document.body.setAttribute('data-state', 'signed-in');
272
+ })();
273
+ </script>
274
+ </body>
275
+ </html>