@bongos/core 1.24.86 → 1.24.87

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 (62) hide show
  1. package/.bongos-core.json +76 -66
  2. package/docs/adr/0402-helm-is-the-halls-look-and-the-hall-is-dark-only.md +15 -0
  3. package/docs/copy-inventory.md +6 -6
  4. package/docs/copy-registry.json +6 -6
  5. package/docs/module-api-changelog.md +2 -0
  6. package/docs/page-inventory.json +2 -0
  7. package/docs/page-readings.json +36 -36
  8. package/modules/hall-ui/public/ai-studio.html +1 -1
  9. package/modules/hall-ui/public/atlas.html +1 -1
  10. package/modules/hall-ui/public/blockers.html +1 -1
  11. package/modules/hall-ui/public/board-room.html +1 -1
  12. package/modules/hall-ui/public/city-draw.js +127 -69
  13. package/modules/hall-ui/public/city.css +37 -3
  14. package/modules/hall-ui/public/city.html +16 -5
  15. package/modules/hall-ui/public/city.js +26 -1
  16. package/modules/hall-ui/public/collab.html +1 -1
  17. package/modules/hall-ui/public/commands.html +1 -1
  18. package/modules/hall-ui/public/copy-desk.html +1 -1
  19. package/modules/hall-ui/public/deploy.html +1 -1
  20. package/modules/hall-ui/public/goal-map.html +2 -2
  21. package/modules/hall-ui/public/goals.html +1 -1
  22. package/modules/hall-ui/public/government.html +1 -1
  23. package/modules/hall-ui/public/home-city-draw.js +88 -54
  24. package/modules/hall-ui/public/home-city.css +25 -3
  25. package/modules/hall-ui/public/idea.html +1 -1
  26. package/modules/hall-ui/public/ideas.html +1 -1
  27. package/modules/hall-ui/public/index.html +6 -4
  28. package/modules/hall-ui/public/market-watch-item.html +1 -1
  29. package/modules/hall-ui/public/market-watch-rules.html +1 -1
  30. package/modules/hall-ui/public/market-watch.html +1 -1
  31. package/modules/hall-ui/public/modules.html +1 -1
  32. package/modules/hall-ui/public/money.html +1 -1
  33. package/modules/hall-ui/public/my-agents.html +1 -1
  34. package/modules/hall-ui/public/primer.html +1 -1
  35. package/modules/hall-ui/public/profile.html +1 -1
  36. package/modules/hall-ui/public/project-settings.html +1 -1
  37. package/modules/hall-ui/public/roster.html +1 -1
  38. package/modules/hall-ui/public/settings.html +1 -1
  39. package/modules/hall-ui/public/sky-time.js +206 -0
  40. package/modules/hall-ui/public/street.html +2 -2
  41. package/modules/hall-ui/public/studio.html +1 -1
  42. package/modules/hall-ui/public/style.css +86 -1
  43. package/modules/hall-ui/public/task.html +1 -1
  44. package/modules/hall-ui/public/thinking.html +2 -2
  45. package/modules/hall-ui/public/tweak-editor.html +1 -1
  46. package/modules/hall-ui/public/visor.css +13 -14
  47. package/modules/hall-ui/public/watch.html +1 -1
  48. package/modules/hall-ui/public/work.html +1 -1
  49. package/modules/hall-ui/records/home-city.md +13 -0
  50. package/modules/hall-ui/records/living-city.md +12 -0
  51. package/modules/hall-ui/records/visor.md +1 -1
  52. package/modules/ui-design/public/brand.html +1 -1
  53. package/package-lock.json +2 -2
  54. package/package.json +1 -1
  55. package/release-notes.json +6 -0
  56. package/src/module-api.js +1 -1
  57. package/tests/hall_city.mjs +72 -22
  58. package/tests/hall_city_living.mjs +7 -2
  59. package/tests/hall_sky_time.mjs +361 -0
  60. package/tests/hall_street.mjs +2 -1
  61. package/tests/hall_visor.mjs +18 -8
  62. package/tests/home_city.mjs +54 -5
@@ -17,11 +17,13 @@
17
17
  // so tests/hall_city.mjs can pin the model without a browser, and city.js only
18
18
  // turns the lists into SVG nodes and spans.
19
19
  //
20
- // THE PALETTE. isoTheme() is the prototype's theme().iso sub-palette for both
21
- // modes, verbatim. It is drawing data (the faces of a white architectural model
22
- // on paper in light, dark blocks in the black in dark), the way the sky's
23
- // canvas carries its own ink channels; the page chrome around the drawing takes
24
- // the hall's tokens (city.css).
20
+ // THE PALETTE (task 1005196: the city in the clouds). isoTheme() names every
21
+ // face and line as one of the hour's sky tokens (--sky-now-*, style.css; set by
22
+ // sky-time.js) or a mix of two, so this file types in no colour and the same
23
+ // path list is right at every hour. There is no ground: a face that stands in
24
+ // the cloud fades to the cloud's colour at its foot (skyDefs: the gradients a
25
+ // page puts in its <defs>), cloud lies between the rows (cloudsFor) and the
26
+ // light comes from below. The geometry is still the prototype's, path for path.
25
27
  //
26
28
  // REAL DATA, AND WHERE IT HAD TO BEND (each said once, here and in the ship
27
29
  // notes):
@@ -51,39 +53,49 @@
51
53
  // the living model's one count rule (gov-living.js), so the city never counts for itself
52
54
  const Living = root.GovLiving || (typeof require === 'function' ? require('./gov-living.js') : null);
53
55
 
54
- // ---------- the iso sub-palette (Street.dc.html theme(mode).iso) ----------
55
- function isoTheme(mode) {
56
- if (mode === 'light') {
57
- return {
58
- civic: { t: 'var(--bg-card)', l: '#ecebee', r: '#d9d8dd', s: 'color-mix(in srgb, var(--ink) 34%, transparent)' },
59
- work: { t: '#f4f3f6', l: '#e6e5e9', r: '#d4d3d8', s: 'color-mix(in srgb, var(--ink) 24%, transparent)' },
60
- plinth: { t: '#efeef1', l: '#e2e1e5', r: '#cfced3', s: 'color-mix(in srgb, var(--ink) 28%, transparent)' },
61
- roof: { t: '#e9e8ec', l: '#dddce1', r: '#cbcad0', s: 'color-mix(in srgb, var(--ink) 34%, transparent)' },
62
- glass: { t: 'color-mix(in srgb, var(--sea) 10%, transparent)', l: 'color-mix(in srgb, var(--sea) 14%, transparent)', r: 'color-mix(in srgb, var(--sea) 22%, transparent)', s: 'rgba(60,80,180,0.50)' },
63
- scaffold: { t: 'color-mix(in srgb, var(--ink) 3%, transparent)', l: 'color-mix(in srgb, var(--ink) 2%, transparent)', r: 'color-mix(in srgb, var(--ink) 2%, transparent)', s: 'color-mix(in srgb, var(--ink) 45%, transparent)', dash: '3 3' },
64
- crate: { t: 'color-mix(in srgb, var(--ink) 6%, transparent)', l: 'color-mix(in srgb, var(--ink) 4%, transparent)', r: 'color-mix(in srgb, var(--ink) 7%, transparent)', s: 'color-mix(in srgb, var(--ink) 55%, transparent)', dash: '3 2' },
65
- sign: { t: '#e2e1e5', l: '#e2e1e5', r: '#cfced3', s: 'color-mix(in srgb, var(--ink) 45%, transparent)' },
66
- ground: 'color-mix(in srgb, var(--ink) 2.5%, transparent)', groundLine: 'color-mix(in srgb, var(--ink) 10%, transparent)', road: 'color-mix(in srgb, var(--ink) 5%, transparent)', lane: 'color-mix(in srgb, var(--ink) 22%, transparent)',
67
- plaza: 'color-mix(in srgb, var(--ink) 5%, transparent)', plazaLine: 'color-mix(in srgb, var(--ink) 16%, transparent)', plot: 'color-mix(in srgb, var(--ink) 3.5%, transparent)', plotLine: 'color-mix(in srgb, var(--ink) 14%, transparent)', grid: 'color-mix(in srgb, var(--ink) 6%, transparent)',
68
- detail: 'color-mix(in srgb, var(--ink) 18%, transparent)', wc: 'color-mix(in srgb, var(--ink) 16%, transparent)', wcLit: 'rgba(214,140,80,0.60)', door: '#cfced3', beacon: 'var(--accent)',
69
- vacantFill: 'color-mix(in srgb, var(--ink) 2%, transparent)', vacantLine: 'color-mix(in srgb, var(--ink) 45%, transparent)', tree: 'color-mix(in srgb, var(--sea) 22%, transparent)', treeLine: 'rgba(60,80,180,0.45)',
70
- };
71
- }
56
+ // ---------- the iso sub-palette: the hour's sky tokens, never a literal ----------
57
+ // now('lit') is the token the hour's blend is written to; mix() is a share of one
58
+ // colour in another (or in nothing: a glaze or a thin line).
59
+ const now = (role) => 'var(--sky-now-' + role + ')';
60
+ const mix = (a, pct, b) => 'color-mix(in srgb, ' + a + ' ' + pct + '%, ' + (b || 'transparent') + ')';
61
+ // A face that stands in the cloud: the page's gradient for that side (skyDefs), then
62
+ // the plain face colour for a drawing that carries no <defs> (an SVG paint's fallback).
63
+ const SIDES = Object.freeze({ l: 'lit', r: 'shade' });
64
+ const hazeId = (t) => (t > 0 ? '-h' + Math.round(t * 100) : '');
65
+ const faceFill = (side, t) => 'url(#sky-face-' + side + hazeId(t) + ') ' + (t > 0 ? mix(now(SIDES[side]), Math.round((1 - t) * 100), now('haze')) : now(SIDES[side]));
66
+ // One palette: the hall is dark only (ADR 0402) and the hour, not a mode, changes it.
67
+ function isoTheme() {
68
+ const roof = now('roof'), lit = now('lit'), shade = now('shade'), line = now('line');
69
+ const edge = mix(roof, 46);
72
70
  return {
73
- civic: { t: '#3b3b41', l: '#25252a', r: '#18181c', s: 'rgba(255,255,255,0.14)' },
74
- work: { t: '#2d2d32', l: '#19191c', r: '#0f0f12', s: 'rgba(255,255,255,0.12)' },
75
- plinth: { t: '#27272c', l: '#141417', r: '#0b0b0d', s: 'rgba(255,255,255,0.10)' },
76
- roof: { t: '#38383e', l: '#2b2b31', r: '#1d1d22', s: 'rgba(255,255,255,0.14)' },
77
- glass: { t: 'rgba(150,170,255,0.22)', l: 'rgba(120,140,230,0.17)', r: 'rgba(100,120,210,0.12)', s: 'rgba(170,190,255,0.42)' },
78
- scaffold: { t: 'rgba(255,255,255,0.03)', l: 'rgba(255,255,255,0.02)', r: 'rgba(255,255,255,0.015)', s: 'rgba(255,255,255,0.45)', dash: '3 3' },
79
- crate: { t: 'rgba(255,255,255,0.07)', l: 'rgba(255,255,255,0.04)', r: 'rgba(255,255,255,0.03)', s: 'rgba(255,255,255,0.55)', dash: '3 2' },
80
- sign: { t: '#202024', l: '#202024', r: '#141417', s: 'rgba(255,255,255,0.4)' },
81
- ground: 'rgba(255,255,255,0.012)', groundLine: 'rgba(255,255,255,0.05)', road: 'rgba(255,255,255,0.025)', lane: 'rgba(255,255,255,0.12)',
82
- plaza: 'rgba(255,255,255,0.045)', plazaLine: 'rgba(255,255,255,0.16)', plot: 'rgba(255,255,255,0.03)', plotLine: 'rgba(255,255,255,0.13)', grid: 'rgba(255,255,255,0.045)',
83
- detail: 'rgba(255,255,255,0.10)', wc: 'rgba(255,255,255,0.10)', wcLit: 'rgba(255,214,170,0.80)', door: '#050506', beacon: '#fff4e6',
84
- vacantFill: 'rgba(255,255,255,0.02)', vacantLine: 'rgba(255,255,255,0.4)', tree: 'color-mix(in srgb, var(--sea) 32%, transparent)', treeLine: 'rgba(140,155,230,0.45)',
71
+ civic: { t: roof, l: lit, r: shade, lg: faceFill('l', 0), rg: faceFill('r', 0), s: edge },
72
+ work: { t: mix(roof, 86, now('haze')), l: lit, r: shade, lg: faceFill('l', 0), rg: faceFill('r', 0), s: mix(roof, 36) },
73
+ plinth: { t: mix(roof, 60, now('base')), l: lit, r: shade, lg: faceFill('l', 0), rg: faceFill('r', 0), s: mix(roof, 30) },
74
+ roof: { t: roof, l: mix(lit, 62, roof), r: mix(shade, 78, lit), s: edge },
75
+ glass: { t: mix(line, 16), l: mix(line, 24), r: mix(line, 32), s: mix(line, 62) },
76
+ scaffold: { t: mix(line, 5), l: mix(line, 3), r: mix(line, 3), s: mix(line, 72), dash: '3 3' },
77
+ crate: { t: mix(line, 14), l: mix(line, 9), r: mix(line, 11), s: mix(line, 82), dash: '3 2' },
78
+ sign: { t: lit, l: lit, r: shade, s: mix(line, 62) },
79
+ lane: mix(line, 30), plaza: mix(now('cloud'), 34), plazaLine: mix(line, 34), plot: mix(now('cloud'), 12), plotLine: mix(line, 30), grid: mix(line, 12),
80
+ detail: mix(roof, 34), wc: mix(now('glow'), 58), wcLit: now('glow'), door: shade, beacon: now('lamp'),
81
+ vacantFill: mix(line, 5), vacantLine: mix(line, 72), tree: mix(now('cloud-shade'), 72), treeLine: mix(line, 52),
85
82
  };
86
83
  }
84
+ // The gradients a page puts in its <defs> for the faces that stand in the cloud: the
85
+ // face colour at the top, the cloud's at the foot. One pair per haze share in use
86
+ // (0 is the clear city). Each is { id, stops: [[offset, colour], …] }, top to bottom.
87
+ function skyDefs(hazes) {
88
+ const seen = {}, out = [];
89
+ [0].concat(Array.isArray(hazes) ? hazes : []).forEach((h) => {
90
+ const t = Math.max(0, Math.min(1, Number(h) || 0)), id = hazeId(t);
91
+ if (seen[id]) return;
92
+ seen[id] = true;
93
+ const far = (c) => (t > 0 ? mix(c, Math.round((1 - t) * 100), now('haze')) : c);
94
+ out.push({ id: 'sky-face-l' + id, stops: [[0, far(now('lit'))], [0.5, far(mix(now('lit'), 84, now('base')))], [1, far(now('base'))]] });
95
+ out.push({ id: 'sky-face-r' + id, stops: [[0, far(now('shade'))], [0.5, far(mix(now('shade'), 86, now('base')))], [1, far(mix(now('base'), 78, now('shade')))]] });
96
+ });
97
+ return out;
98
+ }
87
99
 
88
100
  // ---------- the prototype's primitives ----------
89
101
  function push(out, d, f, s, dash, w) { out.push({ d: d, f: f, s: s, dash: dash || 'none', w: w || 1 }); }
@@ -104,8 +116,10 @@
104
116
  function isoBox(out, k, x0, y0, x1, y1, z0, h, tone, o) {
105
117
  o = o || {};
106
118
  const P = k.P, z1 = z0 + h, dash = tone.dash || 'none';
107
- push(out, k.poly([P(x0, y1, z1), P(x1, y1, z1), P(x1, y1, z0), P(x0, y1, z0)]), tone.l, tone.s, dash);
108
- push(out, k.poly([P(x1, y1, z1), P(x1, y0, z1), P(x1, y0, z0), P(x1, y1, z0)]), tone.r, tone.s, dash);
119
+ // a box on the ground stands in the cloud: its two seen faces fade into it
120
+ const inCloud = !(z0 > 0);
121
+ push(out, k.poly([P(x0, y1, z1), P(x1, y1, z1), P(x1, y1, z0), P(x0, y1, z0)]), (inCloud && tone.lg) || tone.l, tone.s, dash);
122
+ push(out, k.poly([P(x1, y1, z1), P(x1, y0, z1), P(x1, y0, z0), P(x1, y1, z0)]), (inCloud && tone.rg) || tone.r, tone.s, dash);
109
123
  push(out, k.poly([P(x0, y0, z1), P(x1, y0, z1), P(x1, y1, z1), P(x0, y1, z1)]), tone.t, tone.s, dash);
110
124
  let det = '';
111
125
  const ix = Math.min(0.08, (x1 - x0) / 6), iy = Math.min(0.08, (y1 - y0) / 6);
@@ -420,40 +434,34 @@
420
434
  const HAZE = Object.freeze([0.62, 0.48, 0.34]);
421
435
  const ACTIVE_HAZE = 0.4; // a district with an agent at work keeps this share of its band's haze
422
436
  const QUIET_HAZE = 0.3; // a civic building with no agent and no ask sits darker
423
- // the haze the far bands mix toward: the void in dark, and on paper a cool sea-tinted
424
- // paper, so distance reads as atmosphere (fainter, cooler, lower contrast) in both modes
425
- const VOID = Object.freeze({ dark: '#000000', light: '#cbd3e4' }); // light: a cooler, deeper sea so the haze reads on paper (task 1004718)
437
+ // The far bands mix toward the haze between sky and cloud (--sky-now-haze), so distance
438
+ // reads as atmosphere at every hour.
426
439
 
427
440
  const clamp01 = (n) => Math.max(0, Math.min(1, Number(n) || 0));
428
441
  const r1 = (n) => Math.round(n * 10) / 10;
429
442
 
430
- // ---------- haze: a tone mixed toward the void (drawing data, like isoTheme) ----------
431
- function parseColor(c) {
432
- let m = /^#([0-9a-f]{6})$/i.exec(String(c));
433
- if (m) { const n = parseInt(m[1], 16); return [(n >> 16) & 255, (n >> 8) & 255, n & 255, 1]; }
434
- m = /^rgba?\(([^)]+)\)$/i.exec(String(c));
435
- if (m) { const p = m[1].split(',').map((x) => Number(x.trim())); return [p[0], p[1], p[2], p.length > 3 ? p[3] : 1]; }
436
- return null;
443
+ // ---------- haze: a tone mixed toward the haze (drawing data, like isoTheme) ----------
444
+ // One tone, a share t of the way into the haze: a face that fades into the cloud takes
445
+ // the hazed gradient of its side, a glaze or a line (anything mixed with nothing) thins
446
+ // out, and a solid face mixes toward the haze.
447
+ function hazed(c, t) {
448
+ const s = String(c);
449
+ if (s === 'none') return s;
450
+ const face = /^url\(#sky-face-([lr])\)/.exec(s);
451
+ if (face) return faceFill(face[1], t);
452
+ const keep = Math.round((1 - t) * 100);
453
+ return s.indexOf('transparent') >= 0 ? mix(s, keep) : mix(s, keep, now('haze'));
437
454
  }
438
- const hex2 = (n) => ('0' + Math.max(0, Math.min(255, Math.round(n))).toString(16)).slice(-2);
439
- // A solid face mixes toward the ground; a line or a glaze (alpha) thins out.
440
- function mixToward(c, target, t) {
441
- const a = parseColor(c), b = parseColor(target);
442
- if (!a || !b || !(t > 0)) return c;
443
- if (a[3] < 1) return 'rgba(' + a[0] + ',' + a[1] + ',' + a[2] + ',' + Math.round(a[3] * (1 - t) * 1000) / 1000 + ')';
444
- return '#' + hex2(a[0] + (b[0] - a[0]) * t) + hex2(a[1] + (b[1] - a[1]) * t) + hex2(a[2] + (b[2] - a[2]) * t);
445
- }
446
- function hazeTheme(I, t, mode) {
455
+ function hazeTheme(I, t) {
447
456
  if (!(t > 0)) return I;
448
- const to = VOID[mode] || VOID.dark;
449
457
  const out = {};
450
458
  Object.keys(I).forEach((key) => {
451
459
  const v = I[key];
452
460
  if (v && typeof v === 'object') {
453
461
  const o = {};
454
- Object.keys(v).forEach((f) => { o[f] = f === 'dash' ? v[f] : mixToward(v[f], to, t); });
462
+ Object.keys(v).forEach((f) => { o[f] = f === 'dash' ? v[f] : hazed(v[f], t); });
455
463
  out[key] = o;
456
- } else out[key] = typeof v === 'string' ? mixToward(v, to, t) : v;
464
+ } else out[key] = typeof v === 'string' ? hazed(v, t) : v;
457
465
  });
458
466
  return out;
459
467
  }
@@ -883,11 +891,60 @@
883
891
  return parts.join(' · ');
884
892
  }
885
893
 
894
+ // ---------- the cloud the city stands in (task 1005196) ----------
895
+ // One cloud: six soft ellipses, as one path, centred on (x, y) at scale s.
896
+ const CLOUD = Object.freeze([[70, 34, 80, 26], [160, 16, 90, 40], [280, 4, 110, 52], [400, 18, 100, 40], [510, 32, 90, 28], [290, 44, 300, 28]]);
897
+ function cloudAt(x, y, s) {
898
+ return CLOUD.map((e) => ellipse(r1(x + (e[0] - 290) * s), r1(y + (e[1] - 20) * s), r1(e[2] * s), r1(e[3] * s))).join('');
899
+ }
900
+ // One puff at a building's foot: a flat ellipse centred on (x, y), r wide. Home's city
901
+ // draws its banks and puffs with cloudAt and this, so the cloud's shape lives here alone.
902
+ function puffAt(x, y, r) { return ellipse(r1(x), r1(y), r, r1(r * 0.36)); }
903
+ // Where the banks lie on the 1440 x 900 plate: far (the sky's own shade, behind the city),
904
+ // back (lit, behind the far corner), then in front of the near corner: lit, shaded, dark.
905
+ const BANKS = Object.freeze({
906
+ far: [[200, 190, 1.1], [1240, 190, 1.1], [720, 70, 1.2]],
907
+ back: [[360, 300, 0.9], [1080, 300, 0.9], [720, 196, 0.8]],
908
+ near: [[300, 600, 0.8], [520, 706, 0.8], [720, 758, 0.9], [920, 706, 0.8], [1140, 600, 0.8]],
909
+ shade: [[170, 700, 1.1], [460, 816, 1.1], [980, 816, 1.1], [1270, 700, 1.1]],
910
+ dark: [[50, 856, 1.5], [720, 916, 1.6], [1390, 856, 1.5]],
911
+ });
912
+ // the two softening filters a page defines for the cloud: { id, blur } (an SVG blur, in px of the plate)
913
+ const SOFT = Object.freeze([Object.freeze({ id: 'sky-soft', blur: 8 }), Object.freeze({ id: 'sky-softer', blur: 14 })]);
914
+ // The cloud as data: the five banks (each one path, its class the layer it is), and the
915
+ // puffs that lie between the rows: one at the foot of every standing building, gathered
916
+ // by depth so the page lays each row's over that row and under the next. The page's sheet
917
+ // gives every layer its colour (a sky token); `soft` names the filter that softens it.
918
+ function cloudsFor(k, feet) {
919
+ // near and row cloud is soft; the far, back, shaded and dark banks softer (SOFT: the page's two filters)
920
+ const bank = (key) => ({ d: BANKS[key].map((c) => cloudAt(c[0], c[1], c[2])).join(''), f: 'none', s: 'none', dash: 'none', w: 1, cls: 'city-cloud city-cloud--' + key, soft: key === 'near' ? SOFT[0] : SOFT[1] });
921
+ const rows = {};
922
+ (feet || []).forEach((f) => {
923
+ const at = k.P(f.x + 0.25, f.y + 0.25, 0), row = Math.floor(f.depth / 2) * 2 + 1;
924
+ rows[row] = (rows[row] || '') + puffAt(at[0], at[1] + 5, f.r);
925
+ });
926
+ return {
927
+ back: [bank('far'), bank('back')],
928
+ rows: Object.keys(rows).map(Number).sort((a, b) => a - b).map((depth) => ({ depth: depth, d: rows[depth], f: 'none', s: 'none', dash: 'none', w: 1, cls: 'city-cloud city-cloud--row', soft: SOFT[0] })),
929
+ front: [bank('near'), bank('shade'), bank('dark')],
930
+ };
931
+ }
932
+ // The stars: a fixed scatter over the top of the plate, one path. The sheet shows them at
933
+ // the hour's star strength, so they are out at night and gone by day.
934
+ function starField() {
935
+ let d = '';
936
+ for (let i = 0; i < 46; i++) {
937
+ const x = 30 + rnd(i * 3 + 11) * 1380, y = 16 + rnd(i * 5 + 29) * 330, r = 0.7 + rnd(i * 7 + 2) * 0.8;
938
+ d += ellipse(r1(x), r1(y), r1(r), r1(r));
939
+ }
940
+ return { d: d, f: 'none', s: 'none', dash: 'none', w: 1, cls: 'city-stars' };
941
+ }
942
+
886
943
  // The whole home view as data.
887
944
  // docket the GET /government/docket body, or null when it failed
888
945
  // goals GET /goals goals[] (any status; only 'open' ones are drawn)
889
946
  // rollups GET /goals/rollups rollups[] (goal_id → task counts)
890
- // mode 'dark' | 'light'
947
+ // mode ignored: the hall is dark only, and the hour (sky-time.js) paints the city
891
948
  // now ms since the epoch (a deadline inside a day is urgent)
892
949
  // and, for the living city (task 1004685):
893
950
  // living GovLiving.livingModel() over the same reads (gov-living.js)
@@ -896,8 +953,8 @@
896
953
  function cityModel(input) {
897
954
  const opts = input || {};
898
955
  const docket = opts.docket && typeof opts.docket === 'object' ? opts.docket : null;
899
- const mode = opts.mode === 'light' ? 'light' : 'dark';
900
- const I = isoTheme(mode);
956
+ const mode = 'dark';
957
+ const I = isoTheme();
901
958
  const counts = docket && docket.counts ? docket.counts : { needs: 0, moving: 0, settled: 0 };
902
959
  const living = opts.living && Array.isArray(opts.living.buildings) ? Object.assign({}, opts.living, { byKey: {} }) : null;
903
960
  if (living) living.buildings.forEach((b) => { if (b && b.key) living.byKey[b.key] = b; });
@@ -914,12 +971,11 @@
914
971
  const k = isoKit(60, 30, 720, 176, 30 / 28, I, evening);
915
972
  const P = k.P;
916
973
  const kits = {};
917
- const kitFor = (t) => (t > 0 ? (kits[t] = kits[t] || isoKit(60, 30, 720, 176, 30 / 28, hazeTheme(I, t, mode), evening)) : k);
974
+ const kitFor = (t) => (t > 0 ? (kits[t] = kits[t] || isoKit(60, 30, 720, 176, 30 / 28, hazeTheme(I, t), evening)) : k);
918
975
 
919
- // ground, the ring road and its lanes, the plaza
976
+ // No ground and no road fill: the city stands in cloud. The ring road keeps its lane
977
+ // marks (the parcels and the spend conduit run along them), then the plaza, lit from below.
920
978
  const flat = [];
921
- push(flat, k.poly([P(0, 0), P(16, 0), P(16, 16), P(0, 16)]), I.ground, I.groundLine);
922
- [[4, 0, 5, 16], [11, 0, 12, 16], [0, 4, 16, 5], [0, 11, 16, 12]].forEach((q) => push(flat, k.poly([P(q[0], q[1]), P(q[2], q[1]), P(q[2], q[3]), P(q[0], q[3])]), I.road, 'none'));
923
979
  push(flat, k.seg(P(4.5, 0), P(4.5, 16)) + k.seg(P(11.5, 0), P(11.5, 16)) + k.seg(P(0, 4.5), P(16, 4.5)) + k.seg(P(0, 11.5), P(16, 11.5)), 'none', I.lane, '2 7');
924
980
  push(flat, k.poly([P(5, 5), P(11, 5), P(11, 11), P(5, 11)]), I.plaza, I.plazaLine);
925
981
 
@@ -973,6 +1029,8 @@
973
1029
  });
974
1030
  });
975
1031
  blds.sort((a, b) => a.depth - b.depth);
1032
+ const clouds = cloudsFor(k, CIVIC_SLOTS.map((s) => ({ x: s[1] + 1, y: s[2] + 1, r: 58, depth: s[1] + s[2] + 2 }))
1033
+ .concat(districts.reduce((all, D) => all.concat(D.work.filter((w) => w.kind !== 'trees' && w.kind !== 'slab').map((w) => ({ x: w.gx + 0.5, y: w.gy + 0.5, r: 31, depth: w.gx + w.gy + 1 }))), [])));
976
1034
 
977
1035
  // one pin over each building with a matter, in its most important matter's tier
978
1036
  const slotOf = {};
@@ -1022,7 +1080,7 @@
1022
1080
  + (districts.length === 1 ? 'one district' : districts.length + ' districts') + ', one for each open goal'
1023
1081
  + (pins.length ? '. Pins: ' + pinCount.needs + ' need you, ' + pinCount.moving + ' moving, ' + pinCount.settled + ' settled.' : '.');
1024
1082
 
1025
- const out = { mode: mode, evening: evening, crate: { fill: I.crate.t, edge: I.crate.s }, flat: flat, blds: blds, pins: pins, parcels: parcels, districts: districts, unplaced: unplaced, line: line, label: label };
1083
+ const out = { mode: mode, evening: evening, crate: { fill: I.crate.t, edge: I.crate.s }, defs: skyDefs(Object.keys(kits).map(Number)), clouds: clouds, stars: starField(), flat: flat, blds: blds, pins: pins, parcels: parcels, districts: districts, unplaced: unplaced, line: line, label: label };
1026
1084
  if (!living) return out;
1027
1085
 
1028
1086
  // ---- the living city's three levels (task 1004685) ----
@@ -1074,14 +1132,14 @@
1074
1132
  const STREET_HREF = '/builders/street';
1075
1133
 
1076
1134
  const api = {
1077
- isoTheme: isoTheme, isoKit: isoKit, isoBox: isoBox, isoGable: isoGable, isoHip: isoHip, isoSaw: isoSaw,
1135
+ isoTheme: isoTheme, skyDefs: skyDefs, SOFT: SOFT, cloudsFor: cloudsFor, cloudAt: cloudAt, puffAt: puffAt, starField: starField, isoKit: isoKit, isoBox: isoBox, isoGable: isoGable, isoHip: isoHip, isoSaw: isoSaw,
1078
1136
  isoTree: isoTree, isoLight: isoLight, isoInstitution: isoInstitution, isoWork: isoWork, glyph: glyph, rnd: rnd,
1079
1137
  lotsFor: lotsFor, districtWork: districtWork, cityModel: cityModel, rulesAtWork: rulesAtWork,
1080
1138
  matterTier: matterTier, buildingPin: buildingPin, pinAria: pinAria, TIERS: TIERS, URGENT_MS: URGENT_MS,
1081
1139
  CIVIC_SLOTS: CIVIC_SLOTS, ROOF: ROOF, BASE_LOTS: BASE_LOTS, MAX_DISTRICTS: MAX_DISTRICTS,
1082
1140
  FALLBACK_LINE: FALLBACK_LINE, STREET_HREF: STREET_HREF,
1083
1141
  // the living city (task 1004685)
1084
- hazeTheme: hazeTheme, mixToward: mixToward, hazeBand: hazeBand, structureFloors: structureFloors, asksAt: asksAt,
1142
+ hazeTheme: hazeTheme, hazeBand: hazeBand, structureFloors: structureFloors, asksAt: asksAt,
1085
1143
  spendRate: spendRate, trunkWidth: trunkWidth, plaqueFor: plaqueFor, ctaFor: ctaFor, livingLine: livingLine,
1086
1144
  FLOOR_H: FLOOR_H, BUILD_FLOORS: BUILD_FLOORS, CORE: CORE, BODY: BODY, LANE_PERIOD: LANE_PERIOD, POD_GAP: POD_GAP,
1087
1145
  MAX_LANE_PODS: MAX_LANE_PODS, MAX_PODS: MAX_PODS, ACTIVE_HAZE: ACTIVE_HAZE,
@@ -6,8 +6,9 @@
6
6
  prototype wrote its theme() values inline; here every colour is one of the
7
7
  hall's tokens (style.css + the branding pack), whose values ARE the
8
8
  prototype's (bg #000 / #edecee, ink, ink-soft, the rule, the accent
9
- #fb9569 and its dark ink #2a1305). The drawing's own faces are drawing data
10
- in city-draw.js (the prototype's iso sub-palette).
9
+ #fb9569 and its dark ink #2a1305). The drawing's faces and the sky behind
10
+ it are the hour's sky tokens (--sky-now-*, task 1005196: the foot of this
11
+ sheet); city-draw.js types in no colour.
11
12
  No :root here (tests/hall_reading_world.mjs): page tokens live on the body.
12
13
  The two dark blocks below are byte-identical twins (the hall's switch is
13
14
  html[data-theme], set by city.js from the same 'hall-theme' preference
@@ -169,7 +170,7 @@ html[data-fullscreen="on"] .city-top { right: 184px; }
169
170
  .city-beacon--stem { fill: none; stroke: var(--sea); }
170
171
  .city-beacon--ring { fill: none; stroke: color-mix(in srgb, var(--sea) 60%, transparent); }
171
172
  .city-sky, .city-hits { position: absolute; left: 0; top: 0; width: 1440px; height: 900px; pointer-events: none; }
172
- .city-pod { position: absolute; left: 0; top: 0; width: 11px; height: 5px; margin: -2.5px 0 0 -5.5px; box-sizing: border-box; border-radius: 999px; background: linear-gradient(180deg, var(--city-pod-hi), var(--city-pod-lo)); box-shadow: 0 0 0 1px color-mix(in srgb, var(--bg) 55%, transparent); offset-rotate: auto; offset-distance: var(--pod-at, 30%); }
173
+ .city-pod { position: absolute; left: 0; top: 0; width: 11px; height: 5px; margin: -2.5px 0 0 -5.5px; box-sizing: border-box; border-radius: 999px; background: linear-gradient(180deg, var(--city-pod-hi), var(--city-pod-lo)); box-shadow: 0 0 0 1.5px var(--bg); offset-rotate: auto; offset-distance: var(--pod-at, 30%); }
173
174
 
174
175
  .city-lane__more { position: absolute; transform: translate(-50%, -150%); font-size: 10.5px; font-weight: 600; font-variant-numeric: tabular-nums; color: var(--ink-soft); }
175
176
 
@@ -458,3 +459,36 @@ html[data-fullscreen="on"] .city-top { right: 184px; }
458
459
  .ask-full summary { display: inline-flex; align-items: center; min-height: 24px; cursor: pointer; color: var(--ink); text-decoration: underline; text-underline-offset: 3px; text-decoration-color: var(--gov-chip-edge-pick); }
459
460
  .ask-full__text { margin: 6px 0 0; max-height: 240px; overflow-y: auto; white-space: pre-wrap; line-height: 1.5; overflow-wrap: anywhere; }
460
461
  .ask-full summary:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
462
+
463
+ /* ======================================================================
464
+ THE CITY IN THE CLOUDS, AND THE HOUR (task 1005196; ADR 0402)
465
+ The same city, with no ground: it stands in cloud, lit from below, and the
466
+ sky behind it is the hour's (sky-time.js sets --sky-now-*; they rest at
467
+ night). These are art tokens: they paint the scene and nothing you read or
468
+ press. The sky and its cloud run to all four edges of the view at every
469
+ hour: nothing darkens the frame. The wordmark, the plaque, the line and
470
+ the archive note stay Helm night by each standing on a small plate of the
471
+ night (--night-plate), sized to its words.
472
+ ====================================================================== */
473
+ .city-frame { background: radial-gradient(ellipse 82% 82% at 50% 56%, var(--sky-now-cloud), var(--sky-now-base) 24%, var(--sky-now-haze) 46%, var(--sky-now-mid) 80%, color-mix(in srgb, var(--sky-now-mid) 60%, var(--sky-now-top))); }
474
+ /* what is drawn over the scene takes the hour's line colour, so it reads on the bright cloud of midday and on the night */
475
+ .city-stage { --city-lane: color-mix(in srgb, var(--sky-now-line) 62%, transparent); --city-crane: color-mix(in srgb, var(--sky-now-line) 86%, transparent); --city-win-lit: var(--sky-now-glow); }
476
+ .city-cloud { stroke: none; }
477
+ /* each bank is softened by one of the stage's two SVG filters (city.html), named on the path; nothing that moves is */
478
+ .city-cloud--row { fill: var(--sky-now-cloud); opacity: .5; }
479
+ .city-cloud--far { fill: color-mix(in srgb, var(--sky-now-mid) 62%, var(--sky-now-cloud-shade)); opacity: .6; }
480
+ .city-cloud--back { fill: var(--sky-now-cloud); opacity: .8; }
481
+ .city-cloud--near { fill: var(--sky-now-cloud); opacity: .95; }
482
+ .city-cloud--shade { fill: var(--sky-now-cloud-shade); }
483
+ .city-cloud--dark { fill: color-mix(in srgb, var(--sky-now-cloud-shade) 62%, var(--sky-now-mid)); }
484
+ .city-stars { fill: var(--sky-now-lamp); stroke: none; opacity: var(--sky-now-stars); }
485
+ .city-stage .city-tally--yes { fill: var(--sky-now-line); }
486
+ .city-stage .city-tally--no { fill: color-mix(in srgb, var(--sky-now-line) 40%, transparent); }
487
+ .city-stage .city-tally--open { stroke: var(--sky-now-line); }
488
+ /* a marker keeps its meaning's colour and gains a night edge, so it reads on any sky */
489
+ .city-pin--moving .city-pin__head, .city-pin--settled .city-pin__head { outline: 2px solid var(--bg); }
490
+ .city-line, .city-mark, .city-plaque, .city-archived { background: var(--night-plate); clip-path: var(--cut-path-sm); }
491
+ .city-line { width: fit-content; margin-inline: auto; padding: 4px 14px; }
492
+ .city-mark { padding: 4px 12px; }
493
+ .city-plaque { padding: 6px 14px; margin-left: -14px; }
494
+ .city-archived { left: 50%; right: auto; width: max-content; max-width: calc(100% - 32px); transform: translateX(-50%); padding: 0 12px; opacity: 1; }
@@ -11,9 +11,9 @@
11
11
  links, injected into this head by the server like every hall page's
12
12
  (tests/hall_tokens.mjs: no hall page hardcodes a web-font stylesheet).
13
13
  The prototype carried its own Google Fonts link for the same two faces. -->
14
- <link rel="stylesheet" href="/builders/style.css?v=2026-11-11-helm3">
15
- <link rel="stylesheet" href="/builders/visor.css?v=2026-11-11-helm3">
16
- <link rel="stylesheet" href="/builders/city.css?v=2026-11-11-helm3">
14
+ <link rel="stylesheet" href="/builders/style.css?v=2026-11-11-helm4d">
15
+ <link rel="stylesheet" href="/builders/visor.css?v=2026-11-11-helm4c">
16
+ <link rel="stylesheet" href="/builders/city.css?v=2026-11-11-helm4c">
17
17
  <!-- the ask card in the glass language, shared with the street's deck (task 1004702) -->
18
18
  <link rel="stylesheet" href="/builders/street-deck.css?v=2026-10-17-tidy">
19
19
  </head>
@@ -78,8 +78,17 @@
78
78
  <div class="city-zoom">
79
79
  <div class="city-drift">
80
80
  <svg class="city-svg" id="citySvg" width="1440" height="900" viewBox="0 0 1440 900" aria-hidden="true" focusable="false">
81
+ <!-- the city in the clouds (task 1005196): the face gradients, the stars and the far
82
+ banks behind the city, and the near banks in front of it -->
83
+ <defs>
84
+ <filter id="sky-soft" x="-0.2" y="-0.8" width="1.4" height="2.6"><feGaussianBlur stdDeviation="8"></feGaussianBlur></filter>
85
+ <filter id="sky-softer" x="-0.2" y="-0.8" width="1.4" height="2.6"><feGaussianBlur stdDeviation="14"></feGaussianBlur></filter>
86
+ </defs>
87
+ <defs id="cityDefs"></defs>
88
+ <g id="citySkyBack"></g>
81
89
  <g id="cityFlat"></g>
82
90
  <g id="cityBlds" class="city-blds"></g>
91
+ <g id="cityCloudFront"></g>
83
92
  <g id="citySkyLanes" class="city-sky-lanes"></g>
84
93
  </svg>
85
94
  <div class="city-parcels" id="cityParcels" aria-hidden="true"></div>
@@ -114,7 +123,9 @@
114
123
  <script src="/builders/palette.js" defer></script>
115
124
  <script src="/builders/gov-living.js?v=2026-10-09-positions1" defer></script>
116
125
  <script src="/builders/gov-feeds.js?v=2026-10-09-living3" defer></script>
117
- <script src="/builders/city-draw.js?v=2026-10-17-tidy" defer></script>
126
+ <!-- task 1005196: the hour of the city's sky (sets the --sky-now-* tokens the drawing spends) -->
127
+ <script src="/builders/sky-time.js?v=2026-11-11-helm4" defer></script>
128
+ <script src="/builders/city-draw.js?v=2026-11-11-helm4b" defer></script>
118
129
  <!-- a building's card answers its first ask in place through the deck's own
119
130
  route (task 1004702): the desk rows (street-desk.js over the street's kit)
120
131
  and the deck's card and sender (street-deck.js) -->
@@ -126,6 +137,6 @@
126
137
  <script src="/builders/street-deck.js?v=2026-10-13-glass1" defer></script>
127
138
  <script src="/builders/city-decide.js?v=2026-10-23-decide12" defer></script>
128
139
  <script src="/builders/city-switchboard.js?v=2026-10-23-board5" defer></script>
129
- <script src="/builders/city.js?v=2026-11-11-helm" defer></script>
140
+ <script src="/builders/city.js?v=2026-11-11-helm4" defer></script>
130
141
  </body>
131
142
  </html>
@@ -158,6 +158,8 @@
158
158
  if (p.dash && p.dash !== 'none') el.setAttribute('stroke-dasharray', p.dash);
159
159
  el.setAttribute('stroke-width', String(p.w || 1));
160
160
  if (p.cls) el.setAttribute('class', p.cls);
161
+ // a bank of cloud is softened by one of the stage's two filters (city.html)
162
+ if (p.soft) el.setAttribute('filter', 'url(#' + p.soft.id + ')');
161
163
  return el;
162
164
  }
163
165
  // A part of a building that moves (or stands at its still frame) is its own group.
@@ -168,6 +170,19 @@
168
170
  part.polys.forEach(function (p) { g.appendChild(pathEl(p)); });
169
171
  return g;
170
172
  }
173
+ // A gradient for the faces that stand in the cloud (city-draw.js skyDefs): top to bottom.
174
+ function gradEl(def) {
175
+ var g = document.createElementNS(SVG_NS, 'linearGradient');
176
+ g.setAttribute('id', def.id);
177
+ g.setAttribute('x1', '0'); g.setAttribute('y1', '0'); g.setAttribute('x2', '0'); g.setAttribute('y2', '1');
178
+ def.stops.forEach(function (st) {
179
+ var s = document.createElementNS(SVG_NS, 'stop');
180
+ s.setAttribute('offset', String(st[0]));
181
+ s.style.setProperty('stop-color', st[1]);
182
+ g.appendChild(s);
183
+ });
184
+ return g;
185
+ }
171
186
  function span(cls) { var el = document.createElement('span'); el.className = cls; return el; }
172
187
  function node(tag, cls, text) { var el = document.createElement(tag); if (cls) el.className = cls; if (text != null) el.textContent = String(text); return el; }
173
188
 
@@ -333,11 +348,20 @@
333
348
  living: data.living || null, constitution: data.raw.constitution || null, counts: countsNow(),
334
349
  });
335
350
  var flat = $('cityFlat'), blds = $('cityBlds'), sky = $('citySkyLanes'), parcels = $('cityParcels'), pods = $('citySky'), pins = $('cityPins'), hits = $('cityHits');
336
- clear(flat); clear(blds); clear(sky); clear(parcels); clear(pods); clear(pins); clear(hits);
351
+ var defs = $('cityDefs'), back = $('citySkyBack'), front = $('cityCloudFront');
352
+ clear(flat); clear(blds); clear(sky); clear(parcels); clear(pods); clear(pins); clear(hits); clear(defs); clear(back); clear(front);
353
+ // the cloud city (task 1005196): the stars and the far banks behind, a row of cloud
354
+ // after each row of buildings, the near banks in front
355
+ if (defs) (model.defs || []).forEach(function (d) { defs.appendChild(gradEl(d)); });
356
+ var clouds = model.clouds || { back: [], rows: [], front: [] };
357
+ if (back) { if (model.stars) back.appendChild(pathEl(model.stars)); clouds.back.forEach(function (p) { back.appendChild(pathEl(p)); }); }
358
+ if (front) clouds.front.forEach(function (p) { front.appendChild(pathEl(p)); });
359
+ var rows = clouds.rows.slice();
337
360
  model.flat.forEach(function (p) { flat.appendChild(pathEl(p)); });
338
361
  var civicKeys = {};
339
362
  (model.civic || []).forEach(function (c) { civicKeys[c.key] = true; });
340
363
  model.blds.forEach(function (b) {
364
+ while (rows.length && rows[0].depth < b.depth) blds.appendChild(pathEl(rows.shift()));
341
365
  var g = document.createElementNS(SVG_NS, 'g');
342
366
  g.setAttribute('data-key', b.key);
343
367
  if (b.quiet) g.setAttribute('data-quiet', '');
@@ -347,6 +371,7 @@
347
371
  (b.parts || []).forEach(function (part) { g.appendChild(partEl(part)); });
348
372
  blds.appendChild(g);
349
373
  });
374
+ rows.forEach(function (p) { blds.appendChild(pathEl(p)); });
350
375
  model.parcels.forEach(function (pc) { parcels.appendChild(drawParcel(pc)); });
351
376
  if (model.living) {
352
377
  // the capsules plug in once, on the first living drawing; a redraw does not replay it
@@ -8,7 +8,7 @@
8
8
  <link rel="icon" href="/modules/ui-design/public/brand/favicon.svg" type="image/svg+xml">
9
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
- <link rel="stylesheet" href="/builders/style.css?v=2026-11-11-helm3">
11
+ <link rel="stylesheet" href="/builders/style.css?v=2026-11-11-helm4d">
12
12
  <link rel="stylesheet" href="/builders/oversight.css">
13
13
  <link rel="stylesheet" href="/builders/collab.css">
14
14
  </head>
@@ -8,7 +8,7 @@
8
8
  <link rel="icon" href="/modules/ui-design/public/brand/favicon.svg" type="image/svg+xml">
9
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
- <link rel="stylesheet" href="/builders/style.css?v=2026-11-11-helm3">
11
+ <link rel="stylesheet" href="/builders/style.css?v=2026-11-11-helm4d">
12
12
  <link rel="stylesheet" href="/builders/commands.css">
13
13
  </head>
14
14
  <body data-page="commands" data-page-title="Commands">
@@ -8,7 +8,7 @@
8
8
  <link rel="icon" href="/modules/ui-design/public/brand/favicon.svg" type="image/svg+xml">
9
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
- <link rel="stylesheet" href="/builders/style.css?v=2026-11-11-helm3">
11
+ <link rel="stylesheet" href="/builders/style.css?v=2026-11-11-helm4d">
12
12
  <link rel="stylesheet" href="/builders/oversight.css">
13
13
  <link rel="stylesheet" href="/builders/copy-desk.css">
14
14
  </head>
@@ -8,7 +8,7 @@
8
8
  <link rel="icon" href="/modules/ui-design/public/brand/favicon.svg" type="image/svg+xml">
9
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
- <link rel="stylesheet" href="/builders/style.css?v=2026-11-11-helm3">
11
+ <link rel="stylesheet" href="/builders/style.css?v=2026-11-11-helm4d">
12
12
  <link rel="stylesheet" href="/builders/oversight.css">
13
13
  <link rel="stylesheet" href="/builders/hall-ask.css">
14
14
  <link rel="stylesheet" href="/builders/project-settings.css">
@@ -11,8 +11,8 @@
11
11
  <!-- The faces (Manrope, JetBrains Mono) come from the branding pack's font
12
12
  links, injected into this head by the server like every hall page's
13
13
  (tests/hall_tokens.mjs: no hall page hardcodes a web-font stylesheet). -->
14
- <link rel="stylesheet" href="/builders/style.css?v=2026-11-11-helm3">
15
- <link rel="stylesheet" href="/builders/visor.css?v=2026-11-11-helm3">
14
+ <link rel="stylesheet" href="/builders/style.css?v=2026-11-11-helm4d">
15
+ <link rel="stylesheet" href="/builders/visor.css?v=2026-11-11-helm4c">
16
16
  <link rel="stylesheet" href="/builders/thinking.css?v=2026-11-10-visor">
17
17
  </head>
18
18
  <!-- THE PROJECT SKY (task 1004232 / sky part 4 of 5, goal 1000095; ADR 0331,
@@ -8,7 +8,7 @@
8
8
  <link rel="icon" href="/modules/ui-design/public/brand/favicon.svg" type="image/svg+xml">
9
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
- <link rel="stylesheet" href="/builders/style.css?v=2026-11-11-helm3">
11
+ <link rel="stylesheet" href="/builders/style.css?v=2026-11-11-helm4d">
12
12
  <!-- task 1003470: the family sheet the list's figure band and group heads
13
13
  come from (.ov-figs, .ov-group__head), linked before the page's own. -->
14
14
  <link rel="stylesheet" href="/builders/oversight.css">
@@ -8,7 +8,7 @@
8
8
  <link rel="icon" href="/modules/ui-design/public/brand/favicon.svg" type="image/svg+xml">
9
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
- <link rel="stylesheet" href="/builders/style.css?v=2026-11-11-helm3">
11
+ <link rel="stylesheet" href="/builders/style.css?v=2026-11-11-helm4d">
12
12
  <link rel="stylesheet" href="/builders/oversight.css">
13
13
  <link rel="stylesheet" href="/builders/government.css">
14
14
  </head>