@bongos/core 1.21.5 → 1.21.7

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 (82) hide show
  1. package/.bongos-core.json +152 -72
  2. package/clients/bongos-client/index.d.ts +1 -1
  3. package/docs/adr/0361-merges-publish-as-candidates-release-decides-what-others-are-offered.md +1 -0
  4. package/docs/api/openapi.json +2 -1
  5. package/docs/api-reference.md +1 -1
  6. package/docs/copy-inventory.md +156 -144
  7. package/docs/copy-registry.json +286 -158
  8. package/docs/module-api-changelog.md +4 -0
  9. package/docs/onboarding/diagrams/03-drachmae-karma.mmd +1 -1
  10. package/docs/onboarding/diagrams/assertions.json +1 -1
  11. package/docs/page-inventory.json +35 -6
  12. package/docs/page-readings.json +1056 -1013
  13. package/modules/autonomy/config-idle.js +192 -0
  14. package/modules/autonomy/routes/autonomy.js +22 -0
  15. package/modules/autonomy/runner-health.js +22 -1
  16. package/modules/hall-ui/public/city-draw.js +438 -0
  17. package/modules/hall-ui/public/city.css +110 -0
  18. package/modules/hall-ui/public/city.html +71 -0
  19. package/modules/hall-ui/public/city.js +177 -0
  20. package/modules/hall-ui/public/city.states.json +30 -0
  21. package/modules/hall-ui/public/gate.js +11 -4
  22. package/modules/hall-ui/public/profile.css +4 -0
  23. package/modules/hall-ui/public/profile.js +25 -1
  24. package/modules/hall-ui/public/settings-autobongos.js +19 -1
  25. package/modules/hall-ui/public/settings.css +26 -0
  26. package/modules/hall-ui/public/settings.html +8 -0
  27. package/modules/hall-ui/public/settings.js +43 -3
  28. package/modules/platform-identity/craft-rollup.js +72 -0
  29. package/modules/platform-identity/migrations/platform_identity_029_activity_crafts.sql +23 -0
  30. package/modules/platform-identity/platform-identity.js +12 -6
  31. package/modules/platform-identity/routes/sso.js +4 -0
  32. package/modules/platform-identity/tests/platform-identity.mjs +1 -1
  33. package/modules/provisioning/core-upgrade.js +34 -7
  34. package/modules/provisioning/module.json +2 -1
  35. package/modules/provisioning/seams.js +2 -0
  36. package/modules/public-landing/public/projects-hall-live.states.json +216 -0
  37. package/modules/public-landing/public/projects.html +43 -13
  38. package/modules/public-landing/public/projects.states.json +2 -1
  39. package/modules/ui-design/kit/fixtures/provisioning-instances-hall-live.json +106 -0
  40. package/package-lock.json +2 -2
  41. package/package.json +1 -1
  42. package/release-notes.json +64 -0
  43. package/scripts/gds/autobongos-grade-cap.js +187 -0
  44. package/scripts/gds/autobongos-loop.js +4 -0
  45. package/scripts/gds/autobongos-run.js +154 -13
  46. package/scripts/gds/autobongos-verify.js +211 -14
  47. package/scripts/gds/provision-core-upgrade.js +14 -2
  48. package/scripts/gds/provision-repo.js +77 -13
  49. package/scripts/gds/ship-flow.js +5 -0
  50. package/scripts/gds/ship-preflight-steps.js +2 -1
  51. package/scripts/gds/ship.js +10 -0
  52. package/scripts/gds/update-sweep.js +24 -8
  53. package/scripts/gds/upgrade-outcome.js +1 -0
  54. package/src/bongos/auth-admission.js +95 -13
  55. package/src/bongos/db-kernel.js +1 -1
  56. package/src/bongos/routes/core-update.js +27 -2
  57. package/src/bongos/serve-internal.js +14 -0
  58. package/src/bongos/software-update.js +3 -2
  59. package/src/module-api.js +1 -1
  60. package/tests/activity_rollup_order_db.mjs +3 -1
  61. package/tests/activity_snapshots_db.mjs +2 -0
  62. package/tests/autobongos_grade_cap.mjs +125 -0
  63. package/tests/autobongos_loop.mjs +230 -1
  64. package/tests/autobongos_verify.mjs +239 -3
  65. package/tests/autonomy_config_idle.mjs +272 -0
  66. package/tests/core_update_banner.mjs +29 -0
  67. package/tests/core_upgrade_door.mjs +31 -1
  68. package/tests/core_upgrade_runner.mjs +20 -0
  69. package/tests/hall_audit.mjs +10 -1
  70. package/tests/hall_city.mjs +369 -0
  71. package/tests/hall_page_gate_map.mjs +5 -0
  72. package/tests/hub_craft_rollup.mjs +350 -0
  73. package/tests/profile_rollup_consent.mjs +2 -0
  74. package/tests/profile_route.mjs +16 -2
  75. package/tests/project_invite_ui.mjs +65 -7
  76. package/tests/projects_hub_dns_ready.mjs +7 -1
  77. package/tests/provision.mjs +78 -0
  78. package/tests/settings_main_role.mjs +190 -0
  79. package/tests/software_update.mjs +56 -1
  80. package/tests/ui_design_kit.mjs +1 -1
  81. package/tests/update_subscription_engine.mjs +55 -3
  82. package/tests/wizard_physics_more.mjs +19 -0
@@ -0,0 +1,438 @@
1
+ // modules/hall-ui/public/city-draw.js — THE GOVERNOR CITY's drawing, as data
2
+ // (task 1004524, goal 1000125; R02 of the Governor City).
3
+ //
4
+ // WHAT THIS IS. The home view of the reference design, docs/design/mocks/
5
+ // governor-city/Street.dc.html: the isometric kit (isoKit / P(), isoBox,
6
+ // isoGable, isoHip, isoSaw, isoTree, isoLight), the nine civic recipes
7
+ // (isoInstitution), the district work (isoWork), the decision glyphs (glyph)
8
+ // and homeView(), which lays out the 16 x 16 city. Every function below is the
9
+ // prototype's, ported verbatim (same tile maths, same heights, same tones),
10
+ // with ONE change of input: homeView() read the prototype's sample docket and
11
+ // its four hand-placed districts; cityModel() reads the real governor docket
12
+ // (GET /api/bongos/government/docket, task 1004523) for the pins and the open
13
+ // goals with their task rollups (GET /goals, GET /goals/rollups) for the
14
+ // districts. Anything else that differs from the prototype is a defect.
15
+ //
16
+ // PURE. No DOM, no fetch, no clock: the same inputs give the same path list,
17
+ // so tests/hall_city.mjs can pin the model without a browser, and city.js only
18
+ // turns the lists into SVG nodes and spans.
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).
25
+ //
26
+ // REAL DATA, AND WHERE IT HAD TO BEND (each said once, here and in the ship
27
+ // notes):
28
+ // * Districts. The prototype has four hand-placed goal districts (north, east,
29
+ // west, south of the ring road). The city draws one per OPEN goal: the four
30
+ // prototype plots first, then the four empty corners of the 16 x 16 ground,
31
+ // and past eight goals plots are halved along their long side, in the same
32
+ // order, up to sixteen. Goals past sixteen are not drawn (cityModel reports
33
+ // them in `unplaced`).
34
+ // * A building per task. Each task of a goal is one building on its plot
35
+ // (shipped work built as a tower, setback or shed in the prototype's shares,
36
+ // work being built as a scaffold, work not started as a staked slab) until
37
+ // the plot is full. Past that, one building stands for several tasks and
38
+ // grows taller, which is the reference design's own answer to scale
39
+ // (Growth.dc.html: the city fills, then rises).
40
+ // * Review. The prototype's story opens with Review a vacant lot; in this
41
+ // project Review is held (the grader and the testers sign work off), so the
42
+ // held recipe is drawn and the parcels leave through Release.
43
+ // * Pins. A building can hold more matters than the prototype's one; up to
44
+ // three pins stand side by side over it (needs first, the docket's own
45
+ // order), and the line under the button carries the full counts.
46
+
47
+ (function (root) {
48
+ 'use strict';
49
+
50
+ // ---------- the iso sub-palette (Street.dc.html theme(mode).iso) ----------
51
+ function isoTheme(mode) {
52
+ if (mode === 'light') {
53
+ return {
54
+ civic: { t: '#fbfafc', l: '#ecebee', r: '#d9d8dd', s: 'rgba(23,24,26,0.34)' },
55
+ work: { t: '#f4f3f6', l: '#e6e5e9', r: '#d4d3d8', s: 'rgba(23,24,26,0.24)' },
56
+ plinth: { t: '#efeef1', l: '#e2e1e5', r: '#cfced3', s: 'rgba(23,24,26,0.28)' },
57
+ roof: { t: '#e9e8ec', l: '#dddce1', r: '#cbcad0', s: 'rgba(23,24,26,0.34)' },
58
+ glass: { t: 'rgba(91,111,208,0.10)', l: 'rgba(91,111,208,0.14)', r: 'rgba(91,111,208,0.22)', s: 'rgba(60,80,180,0.50)' },
59
+ scaffold: { t: 'rgba(23,24,26,0.03)', l: 'rgba(23,24,26,0.02)', r: 'rgba(23,24,26,0.02)', s: 'rgba(23,24,26,0.45)', dash: '3 3' },
60
+ crate: { t: 'rgba(23,24,26,0.06)', l: 'rgba(23,24,26,0.04)', r: 'rgba(23,24,26,0.07)', s: 'rgba(23,24,26,0.55)', dash: '3 2' },
61
+ sign: { t: '#e2e1e5', l: '#e2e1e5', r: '#cfced3', s: 'rgba(23,24,26,0.45)' },
62
+ ground: 'rgba(23,24,26,0.025)', groundLine: 'rgba(23,24,26,0.10)', road: 'rgba(23,24,26,0.05)', lane: 'rgba(23,24,26,0.22)',
63
+ plaza: 'rgba(23,24,26,0.05)', plazaLine: 'rgba(23,24,26,0.16)', plot: 'rgba(23,24,26,0.035)', plotLine: 'rgba(23,24,26,0.14)', grid: 'rgba(23,24,26,0.06)',
64
+ detail: 'rgba(23,24,26,0.18)', wc: 'rgba(23,24,26,0.16)', wcLit: 'rgba(214,140,80,0.60)', door: '#cfced3', beacon: '#fb9569',
65
+ vacantFill: 'rgba(23,24,26,0.02)', vacantLine: 'rgba(23,24,26,0.45)', tree: 'rgba(91,111,208,0.22)', treeLine: 'rgba(60,80,180,0.45)',
66
+ };
67
+ }
68
+ return {
69
+ civic: { t: '#3b3b41', l: '#25252a', r: '#18181c', s: 'rgba(255,255,255,0.14)' },
70
+ work: { t: '#2d2d32', l: '#19191c', r: '#0f0f12', s: 'rgba(255,255,255,0.12)' },
71
+ plinth: { t: '#27272c', l: '#141417', r: '#0b0b0d', s: 'rgba(255,255,255,0.10)' },
72
+ roof: { t: '#38383e', l: '#2b2b31', r: '#1d1d22', s: 'rgba(255,255,255,0.14)' },
73
+ 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)' },
74
+ 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' },
75
+ 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' },
76
+ sign: { t: '#202024', l: '#202024', r: '#141417', s: 'rgba(255,255,255,0.4)' },
77
+ 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)',
78
+ 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)',
79
+ 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',
80
+ vacantFill: 'rgba(255,255,255,0.02)', vacantLine: 'rgba(255,255,255,0.4)', tree: 'rgba(91,111,208,0.32)', treeLine: 'rgba(140,155,230,0.45)',
81
+ };
82
+ }
83
+
84
+ // ---------- the prototype's primitives ----------
85
+ function push(out, d, f, s, dash, w) { out.push({ d: d, f: f, s: s, dash: dash || 'none', w: w || 1 }); }
86
+ function ellipse(cx, cy, rx, ry) { return 'M' + (cx - rx) + ' ' + cy + 'a' + rx + ' ' + ry + ' 0 1 0 ' + (2 * rx) + ' 0a' + rx + ' ' + ry + ' 0 1 0 ' + (-2 * rx) + ' 0Z'; }
87
+ function rnd(n) {
88
+ const s = Math.sin(n * 12.9898 + 78.233) * 43758.5453;
89
+ return s - Math.floor(s);
90
+ }
91
+
92
+ function isoKit(TW, TH, OX, OY, S, I, lit) {
93
+ const r1 = (n) => Math.round(n * 10) / 10;
94
+ const P = (x, y, z) => [OX + (x - y) * TW / 2, OY + (x + y) * TH / 2 - (z || 0) * S];
95
+ const poly = (pts) => 'M' + pts.map((p) => r1(p[0]) + ' ' + r1(p[1])).join('L') + 'Z';
96
+ const seg = (a, b) => 'M' + r1(a[0]) + ' ' + r1(a[1]) + 'L' + r1(b[0]) + ' ' + r1(b[1]);
97
+ return { P: P, poly: poly, seg: seg, S: S, I: I, wc: lit ? I.wcLit : I.wc };
98
+ }
99
+
100
+ function isoBox(out, k, x0, y0, x1, y1, z0, h, tone, o) {
101
+ o = o || {};
102
+ const P = k.P, z1 = z0 + h, dash = tone.dash || 'none';
103
+ 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);
104
+ 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);
105
+ 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);
106
+ let det = '';
107
+ const ix = Math.min(0.08, (x1 - x0) / 6), iy = Math.min(0.08, (y1 - y0) / 6);
108
+ if (o.rows || o.grid) for (let z = z0 + 8; z < z1 - 5; z += 8) det += k.seg(P(x0 + ix, y1, z), P(x1 - ix, y1, z)) + k.seg(P(x1, y1 - iy, z), P(x1, y0 + iy, z));
109
+ if (o.fins || o.grid) {
110
+ const n = o.n || 6;
111
+ for (let i = 1; i < n; i++) { const fx = x0 + (x1 - x0) * i / n, fy = y0 + (y1 - y0) * i / n; det += k.seg(P(fx, y1, z0 + 2), P(fx, y1, z1 - 2)) + k.seg(P(x1, fy, z0 + 2), P(x1, fy, z1 - 2)); }
112
+ }
113
+ if (det) push(out, det, 'none', o.wc || (o.rows ? k.wc : k.I.detail), o.rows && !o.grid ? '4 3' : 'none');
114
+ if (o.door) {
115
+ const xm = (x0 + x1) / 2, dw = Math.min(0.12, (x1 - x0) / 5), dh = Math.min(14, h * 0.45);
116
+ push(out, k.poly([P(xm - dw, y1, z0), P(xm + dw, y1, z0), P(xm + dw, y1, z0 + dh), P(xm - dw, y1, z0 + dh)]), k.I.door, tone.s);
117
+ }
118
+ }
119
+
120
+ function isoGable(out, k, x0, y0, x1, y1, z, hr, tone) {
121
+ const P = k.P, ym = (y0 + y1) / 2;
122
+ push(out, k.poly([P(x0, y0, z), P(x1, y0, z), P(x1, ym, z + hr), P(x0, ym, z + hr)]), tone.r, tone.s);
123
+ push(out, k.poly([P(x0, ym, z + hr), P(x1, ym, z + hr), P(x1, y1, z), P(x0, y1, z)]), tone.t, tone.s);
124
+ push(out, k.poly([P(x1, y0, z), P(x1, ym, z + hr), P(x1, y1, z)]), tone.l, tone.s);
125
+ }
126
+
127
+ function isoHip(out, k, x0, y0, x1, y1, z, hr, tone) {
128
+ const P = k.P, a = P((x0 + x1) / 2, (y0 + y1) / 2, z + hr);
129
+ push(out, k.poly([P(x0, y0, z), P(x1, y0, z), a]), tone.r, tone.s);
130
+ push(out, k.poly([P(x0, y0, z), P(x0, y1, z), a]), tone.l, tone.s);
131
+ push(out, k.poly([P(x0, y1, z), P(x1, y1, z), a]), tone.t, tone.s);
132
+ push(out, k.poly([P(x1, y1, z), P(x1, y0, z), a]), tone.l, tone.s);
133
+ }
134
+
135
+ function isoSaw(out, k, x0, y0, x1, y1, z, hr, n, tone, glass) {
136
+ const P = k.P, step = (x1 - x0) / n;
137
+ for (let i = 0; i < n; i++) {
138
+ const xa = x0 + step * i, xb = xa + step;
139
+ push(out, k.poly([P(xa, y0, z), P(xb, y0, z + hr), P(xb, y1, z + hr), P(xa, y1, z)]), tone.t, tone.s);
140
+ push(out, k.poly([P(xa, y1, z), P(xb, y1, z + hr), P(xb, y1, z)]), tone.l, tone.s);
141
+ push(out, k.poly([P(xb, y0, z), P(xb, y0, z + hr), P(xb, y1, z + hr), P(xb, y1, z)]), glass.r, glass.s);
142
+ }
143
+ }
144
+
145
+ function isoTree(out, k, x, y, size) {
146
+ const P = k.P, c = P(x, y, size * 1.15);
147
+ push(out, k.seg(P(x, y, 0), P(x, y, size * 0.9)), 'none', k.I.treeLine);
148
+ push(out, ellipse(Math.round(c[0]), Math.round(c[1]), Math.round(size * 0.42 * k.S * 10) / 10, Math.round(size * 0.42 * k.S * 10) / 10), k.I.tree, k.I.treeLine);
149
+ }
150
+
151
+ function isoLight(out, k, x, y, z, r) {
152
+ const c = k.P(x, y, z);
153
+ push(out, ellipse(Math.round(c[0]), Math.round(c[1]), r * 2.6, r * 2.6), 'rgba(251,149,105,0.10)', 'none');
154
+ push(out, ellipse(Math.round(c[0]), Math.round(c[1]), r, r), k.I.beacon, 'none');
155
+ }
156
+
157
+ function isoInstitution(out, k, kind, ox, oy, st) {
158
+ const I = k.I;
159
+ const b = (x0, y0, x1, y1, z, h, t, o) => isoBox(out, k, ox + x0, oy + y0, ox + x1, oy + y1, z, h, t, o);
160
+ if (kind === 'charter') {
161
+ b(0.15, 0.15, 1.85, 1.85, 0, 6, I.plinth); b(0.3, 0.3, 1.7, 1.7, 6, 46, I.civic, { rows: true }); b(0.45, 0.45, 1.55, 1.55, 52, 40, I.civic, { rows: true });
162
+ b(0.6, 0.6, 1.4, 1.4, 92, 34, I.civic, { fins: true, n: 5 }); b(0.75, 0.75, 1.25, 1.25, 126, 18, I.glass, { grid: true, n: 3 }); isoLight(out, k, ox + 1, oy + 1, 152, 3.2);
163
+ } else if (kind === 'rules') {
164
+ b(0.1, 0.5, 1.9, 1.5, 0, 4, I.plinth); b(0.22, 0.62, 1.78, 1.38, 4, 28, I.civic, { fins: true, n: 12, wc: st.fixes ? I.wcLit : null }); b(0.12, 0.52, 1.88, 1.48, 32, 4, I.roof);
165
+ } else if (kind === 'treasury') {
166
+ b(0.22, 0.22, 1.78, 1.78, 0, 8, I.plinth); b(0.4, 0.4, 1.6, 1.6, 8, 44, I.civic, { door: true }); b(0.33, 0.33, 1.67, 1.67, 52, 5, I.roof); b(0.62, 0.62, 1.38, 1.38, 57, 9, I.civic);
167
+ } else if (kind === 'planning') {
168
+ b(0.3, 0.3, 1.7, 1.7, 0, 38, I.civic, { rows: true, door: true }); isoSaw(out, k, ox + 0.3, oy + 0.3, ox + 1.7, oy + 1.7, 38, 13, 3, I.roof, I.glass);
169
+ } else if (kind === 'review') {
170
+ b(0.3, 0.3, 1.7, 1.7, 0, 14, I.civic, { door: true }); b(0.38, 0.38, 1.62, 1.62, 14, 36, I.glass, { grid: true, n: 5 }); b(0.34, 0.34, 1.66, 1.66, 50, 3, I.roof);
171
+ } else if (kind === 'release') {
172
+ b(0.3, 0.75, 0.75, 1.25, 0, 66, I.civic, { rows: true }); isoLight(out, k, ox + 0.52, oy + 1, 72, 2.6);
173
+ b(0.75, 0.85, 1.25, 1.15, 44, 12, I.civic); b(1.25, 0.75, 1.7, 1.25, 0, 66, I.civic, { rows: true }); isoLight(out, k, ox + 1.47, oy + 1, 72, 2.6);
174
+ } else if (kind === 'people') {
175
+ b(0.2, 0.2, 0.85, 0.8, 0, 16, I.civic, { door: true }); isoGable(out, k, ox + 0.2, oy + 0.2, ox + 0.85, oy + 0.8, 16, 11, I.roof);
176
+ b(1.1, 0.3, 1.75, 0.9, 0, 20, I.civic, { door: true }); isoGable(out, k, ox + 1.1, oy + 0.3, ox + 1.75, oy + 0.9, 20, 11, I.roof);
177
+ isoTree(out, k, ox + 1.45, oy + 1.45, 12);
178
+ b(0.35, 1.1, 1.0, 1.7, 0, 14, I.civic, { door: true }); isoGable(out, k, ox + 0.35, oy + 1.1, ox + 1.0, oy + 1.7, 14, 10, I.roof);
179
+ } else if (kind === 'disputes') {
180
+ b(0.25, 0.7, 0.75, 1.3, 0, 22, I.civic, { rows: true }); b(0.75, 0.55, 1.25, 1.45, 0, 48, I.civic, { fins: true, n: 4, door: true });
181
+ isoHip(out, k, ox + 0.75, oy + 0.55, ox + 1.25, oy + 1.45, 48, 12, I.roof); b(1.25, 0.7, 1.75, 1.3, 0, 22, I.civic, { rows: true });
182
+ } else if (kind === 'commons') {
183
+ b(0.85, 0.85, 1.15, 1.15, 0, 3, I.plinth);
184
+ isoTree(out, k, ox + 0.45, oy + 0.5, 14); isoTree(out, k, ox + 1.5, oy + 0.45, 12); isoTree(out, k, ox + 0.5, oy + 1.5, 12); isoTree(out, k, ox + 1.5, oy + 1.55, 15);
185
+ } else if (kind === 'automation') {
186
+ b(0.25, 0.3, 1.75, 1.6, 0, 6, I.plinth); b(0.35, 0.4, 1.65, 1.5, 6, 30, I.civic, { rows: true, door: true });
187
+ b(1.15, 0.55, 1.45, 0.85, 36, 42, I.civic); isoLight(out, k, ox + 1.3, oy + 0.7, 84, 2.8);
188
+ b(0.5, 0.9, 1.0, 1.4, 36, 8, I.roof, { fins: true, n: 4 });
189
+ } else if (kind === 'vacant') {
190
+ const P = k.P;
191
+ push(out, k.poly([P(ox + 0.2, oy + 0.2, 0), P(ox + 1.8, oy + 0.2, 0), P(ox + 1.8, oy + 1.8, 0), P(ox + 0.2, oy + 1.8, 0)]), I.vacantFill, I.vacantLine, '4 4');
192
+ push(out, k.seg(P(ox + 0.45, oy + 1.55, 0), P(ox + 0.45, oy + 1.55, 30)), 'none', I.vacantLine);
193
+ b(0.3, 1.52, 0.6, 1.58, 22, 12, I.sign);
194
+ b(1.0, 0.7, 1.35, 1.05, 0, 9, I.crate); b(1.05, 0.75, 1.3, 1.0, 9, 8, I.crate); b(1.25, 1.05, 1.55, 1.35, 0, 9, I.crate);
195
+ }
196
+ }
197
+
198
+ // The prototype's five work kinds, plus `slab`: a task not started yet, drawn
199
+ // as the staked plot the README names for backlog (a low dashed footing).
200
+ function isoWork(out, k, x, y, kind, h) {
201
+ const I = k.I;
202
+ if (kind === 'tower') isoBox(out, k, x + 0.16, y + 0.16, x + 0.84, y + 0.84, 0, h, I.work, { rows: true });
203
+ else if (kind === 'setback') {
204
+ isoBox(out, k, x + 0.12, y + 0.12, x + 0.88, y + 0.88, 0, Math.round(h * 0.55), I.work, { rows: true });
205
+ isoBox(out, k, x + 0.3, y + 0.3, x + 0.7, y + 0.7, Math.round(h * 0.55), Math.round(h * 0.45), I.work, { rows: true });
206
+ } else if (kind === 'shed') {
207
+ isoBox(out, k, x + 0.1, y + 0.2, x + 0.9, y + 0.8, 0, 12, I.work, { door: true });
208
+ isoGable(out, k, x + 0.1, y + 0.2, x + 0.9, y + 0.8, 12, 8, I.roof);
209
+ } else if (kind === 'trees') { isoTree(out, k, x + 0.35, y + 0.4, 10); isoTree(out, k, x + 0.68, y + 0.7, 8); }
210
+ else if (kind === 'scaffold') isoBox(out, k, x + 0.16, y + 0.16, x + 0.84, y + 0.84, 0, h, I.scaffold);
211
+ else if (kind === 'slab') isoBox(out, k, x + 0.2, y + 0.2, x + 0.8, y + 0.8, 0, 3, I.scaffold);
212
+ }
213
+
214
+ // The decision glyphs (Street.dc.html glyph()), with Guide.dc.html's two extras
215
+ // (first, signoff), verbatim, so every rule the docket can carry has its mark.
216
+ function glyph(kind, cx, cy, s) {
217
+ const c = (x, y, r) => 'M' + (x - r) + ' ' + y + 'a' + r + ' ' + r + ' 0 1 0 ' + (2 * r) + ' 0a' + r + ' ' + r + ' 0 1 0 ' + (-2 * r) + ' 0Z';
218
+ const arcDots = (n, rad, r, yo) => { let d = ''; for (let i = 0; i < n; i++) { const a = Math.PI - (Math.PI * i) / (n - 1); d += c(cx + Math.cos(a) * rad, cy + yo - Math.sin(a) * rad, r); } return d; };
219
+ let fill = '', line = '';
220
+ if (kind === 'decide') fill = c(cx, cy, s * 0.3);
221
+ else if (kind === 'majority') fill = arcDots(5, s * 0.4, s * 0.085, s * 0.2);
222
+ else if (kind === 'unanimous') { const rad = s * 0.4, yo = s * 0.2; fill = arcDots(5, rad, s * 0.085, yo); line = 'M' + (cx - rad) + ' ' + (cy + yo) + 'A' + rad + ' ' + rad + ' 0 0 1 ' + (cx + rad) + ' ' + (cy + yo); }
223
+ else if (kind === 'consent') { fill = c(cx, cy, s * 0.1); const r = s * 0.36; line = 'M' + cx + ' ' + (cy - r) + 'A' + r + ' ' + r + ' 0 1 1 ' + (cx - r) + ' ' + cy; }
224
+ else if (kind === 'rule') { const h = s * 0.22; fill = 'M' + (cx - h) + ' ' + (cy - h) + 'h' + (2 * h) + 'v' + (2 * h) + 'h' + (-2 * h) + 'Z'; }
225
+ else if (kind === 'first') { fill = c(cx - s * 0.3, cy, s * 0.12); line = c(cx, cy, s * 0.1) + c(cx + s * 0.3, cy, s * 0.1); }
226
+ else if (kind === 'signoff') { fill = c(cx - s * 0.26, cy, s * 0.11) + c(cx + s * 0.26, cy, s * 0.11); line = 'M' + (cx - s * 0.15) + ' ' + cy + 'L' + (cx + s * 0.15) + ' ' + cy; }
227
+ return { fill: fill, line: line };
228
+ }
229
+
230
+ // ---------- the city's fixed layout (Street.dc.html homeView) ----------
231
+ const CIVIC_SLOTS = Object.freeze([['planning', 5, 5], ['rules', 7, 5], ['treasury', 9, 5], ['people', 5, 7], ['charter', 7, 7], ['release', 9, 7], ['disputes', 5, 9], ['automation', 7, 9], ['review', 9, 9]]);
232
+ const ROOF = Object.freeze({ charter: 158, rules: 36, treasury: 66, planning: 51, people: 31, release: 72, disputes: 60, review: 53, automation: 86 });
233
+
234
+ // The four prototype districts (route = the parcel road it feeds), then the
235
+ // four empty corners of the ground. Tiles x0..x1, y0..y1 inclusive.
236
+ const BASE_LOTS = Object.freeze([
237
+ Object.freeze({ x0: 5, x1: 10, y0: 0, y1: 3, tall: 60, route: 0 }),
238
+ Object.freeze({ x0: 12, x1: 15, y0: 5, y1: 10, tall: 100, route: 1 }),
239
+ Object.freeze({ x0: 0, x1: 3, y0: 5, y1: 10, tall: 120, route: 2 }),
240
+ Object.freeze({ x0: 5, x1: 10, y0: 12, y1: 15, tall: 50, route: 3 }),
241
+ Object.freeze({ x0: 0, x1: 3, y0: 0, y1: 3, tall: 80, route: null }),
242
+ Object.freeze({ x0: 12, x1: 15, y0: 0, y1: 3, tall: 90, route: null }),
243
+ Object.freeze({ x0: 0, x1: 3, y0: 12, y1: 15, tall: 70, route: null }),
244
+ Object.freeze({ x0: 12, x1: 15, y0: 12, y1: 15, tall: 60, route: null }),
245
+ ]);
246
+ const MAX_DISTRICTS = BASE_LOTS.length * 2;
247
+ const MAX_PINS_PER_BUILDING = 3;
248
+
249
+ function halve(L) {
250
+ const w = L.x1 - L.x0 + 1, h = L.y1 - L.y0 + 1;
251
+ if (w >= h) {
252
+ const m = L.x0 + Math.floor(w / 2) - 1;
253
+ return [Object.assign({}, L, { x1: m }), Object.assign({}, L, { x0: m + 1 })];
254
+ }
255
+ const m = L.y0 + Math.floor(h / 2) - 1;
256
+ return [Object.assign({}, L, { y1: m }), Object.assign({}, L, { y0: m + 1 })];
257
+ }
258
+
259
+ // n open goals → n plots (at most sixteen), in the order the goals come.
260
+ function lotsFor(n) {
261
+ const count = Math.max(0, Math.min(Math.floor(Number(n) || 0), MAX_DISTRICTS));
262
+ if (count <= BASE_LOTS.length) return BASE_LOTS.slice(0, count).map((L) => Object.assign({}, L));
263
+ let splits = count - BASE_LOTS.length;
264
+ const out = [];
265
+ BASE_LOTS.forEach((L) => {
266
+ if (splits > 0) { splits -= 1; halve(L).forEach((h) => out.push(h)); } else out.push(Object.assign({}, L));
267
+ });
268
+ return out;
269
+ }
270
+
271
+ // A goal's tasks in the order they fill its plot: shipped first, then the
272
+ // work being built, then what has not started.
273
+ const TASK_ORDER = Object.freeze(['done', 'active', 'ready', 'blocked', 'backlog']);
274
+ function taskList(rollup) {
275
+ const list = [];
276
+ if (!rollup) return list;
277
+ TASK_ORDER.forEach((s) => {
278
+ const n = Math.max(0, Math.floor(Number(rollup[s]) || 0));
279
+ for (let i = 0; i < n; i++) list.push(s);
280
+ });
281
+ return list;
282
+ }
283
+
284
+ // One goal on one plot → the buildings on it. A building per task until the
285
+ // plot is full; past that each building carries `per` tasks and rises.
286
+ function districtWork(lot, rollup) {
287
+ const tasks = taskList(rollup);
288
+ const tiles = [];
289
+ for (let gx = lot.x0; gx <= lot.x1; gx++) for (let gy = lot.y0; gy <= lot.y1; gy++) tiles.push({ gx: gx, gy: gy, r: rnd(gx * 31 + gy * 7 + 1) });
290
+ tiles.sort((a, b) => a.r - b.r || a.gx - b.gx || a.gy - b.gy);
291
+ const per = Math.max(1, Math.ceil(tasks.length / Math.max(1, tiles.length)));
292
+ // The rise is capped low: the civic quarter stays the tallest thing in the city.
293
+ const grow = per > 1 ? Math.min(1.25, 1 + 0.05 * (per - 1)) : 1;
294
+ const out = [];
295
+ let t = 0;
296
+ tiles.forEach((tile) => {
297
+ const gx = tile.gx, gy = tile.gy;
298
+ const r2 = rnd(gx * 13 + gy * 29 + 5);
299
+ if (t >= tasks.length) {
300
+ // An empty tile keeps the prototype's trees where its draw would have planted them.
301
+ if (tile.r >= 0.52 && tile.r < 0.6) out.push({ gx: gx, gy: gy, kind: 'trees', h: 0, tasks: 0 });
302
+ return;
303
+ }
304
+ const chunk = tasks.slice(t, t + per);
305
+ t += per;
306
+ let kind;
307
+ if (chunk.indexOf('active') >= 0) kind = 'scaffold';
308
+ else if (chunk.every((s) => s === 'done')) {
309
+ const r3 = rnd(gx * 17 + gy * 11 + 3);
310
+ kind = r3 < 0.58 ? 'tower' : r3 < 0.8 ? 'setback' : 'shed';
311
+ } else kind = 'slab';
312
+ out.push({ gx: gx, gy: gy, kind: kind, h: Math.round((20 + r2 * lot.tall) * grow), tasks: chunk.length });
313
+ });
314
+ return out;
315
+ }
316
+
317
+ function pinState(state) {
318
+ return state === 'needs' || state === 'moving' || state === 'settled' ? state : 'moving';
319
+ }
320
+
321
+ // The home line, when the docket could not be read.
322
+ const FALLBACK_LINE = 'the docket could not be read just now';
323
+
324
+ // The whole home view as data.
325
+ // docket the GET /government/docket body, or null when it failed
326
+ // goals GET /goals goals[] (any status; only 'open' ones are drawn)
327
+ // rollups GET /goals/rollups rollups[] (goal_id → task counts)
328
+ // mode 'dark' | 'light'
329
+ function cityModel(input) {
330
+ const opts = input || {};
331
+ const docket = opts.docket && typeof opts.docket === 'object' ? opts.docket : null;
332
+ const mode = opts.mode === 'light' ? 'light' : 'dark';
333
+ const I = isoTheme(mode);
334
+ const counts = docket && docket.counts ? docket.counts : { needs: 0, moving: 0, settled: 0 };
335
+ const evening = !!docket && !(Number(counts.needs) > 0);
336
+ const k = isoKit(60, 30, 720, 176, 30 / 28, I, evening);
337
+ const P = k.P, r1 = (n) => Math.round(n * 10) / 10;
338
+
339
+ // ground, the ring road and its lanes, the plaza
340
+ const flat = [];
341
+ push(flat, k.poly([P(0, 0), P(16, 0), P(16, 16), P(0, 16)]), I.ground, I.groundLine);
342
+ [[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'));
343
+ 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');
344
+ push(flat, k.poly([P(5, 5), P(11, 5), P(11, 11), P(5, 11)]), I.plaza, I.plazaLine);
345
+
346
+ // one district per open goal
347
+ const goals = (Array.isArray(opts.goals) ? opts.goals : []).filter((g) => g && g.status === 'open' && g.id != null);
348
+ const byGoal = {};
349
+ (Array.isArray(opts.rollups) ? opts.rollups : []).forEach((r) => { if (r && r.goal_id != null) byGoal[String(r.goal_id)] = r; });
350
+ const lots = lotsFor(goals.length);
351
+ const districts = lots.map((lot, i) => ({ goal_id: String(goals[i].id), title: String(goals[i].title || ''), lot: lot, work: districtWork(lot, byGoal[String(goals[i].id)]) }));
352
+ const unplaced = goals.slice(lots.length).map((g) => String(g.id));
353
+
354
+ let grid = '';
355
+ districts.forEach((D) => {
356
+ const L = D.lot;
357
+ push(flat, k.poly([P(L.x0, L.y0), P(L.x1 + 1, L.y0), P(L.x1 + 1, L.y1 + 1), P(L.x0, L.y1 + 1)]), I.plot, I.plotLine);
358
+ for (let gx = L.x0 + 1; gx <= L.x1; gx++) grid += k.seg(P(gx, L.y0), P(gx, L.y1 + 1));
359
+ for (let gy = L.y0 + 1; gy <= L.y1; gy++) grid += k.seg(P(L.x0, gy), P(L.x1 + 1, gy));
360
+ });
361
+ if (grid) push(flat, grid, 'none', I.grid);
362
+
363
+ // the civic quarter, then the districts' work, painted back to front
364
+ const blds = [];
365
+ CIVIC_SLOTS.forEach((s) => {
366
+ const polys = [];
367
+ isoInstitution(polys, k, s[0], s[1], s[2], { fixes: false });
368
+ blds.push({ key: s[0], depth: s[1] + s[2] + 2, polys: polys });
369
+ });
370
+ districts.forEach((D) => {
371
+ D.work.forEach((w) => {
372
+ const polys = [];
373
+ isoWork(polys, k, w.gx, w.gy, w.kind, w.h);
374
+ blds.push({ key: w.gx + ',' + w.gy, depth: w.gx + w.gy + 1, polys: polys });
375
+ });
376
+ });
377
+ blds.sort((a, b) => a.depth - b.depth);
378
+
379
+ // a pin over each open matter's building (up to three abreast)
380
+ const slotOf = {};
381
+ CIVIC_SLOTS.forEach((s) => { slotOf[s[0]] = s; });
382
+ const matters = docket && Array.isArray(docket.matters) ? docket.matters : [];
383
+ const atBuilding = {};
384
+ matters.forEach((m) => {
385
+ if (!m || !slotOf[m.building]) return;
386
+ (atBuilding[m.building] = atBuilding[m.building] || []).push(m);
387
+ });
388
+ const pins = [];
389
+ let n = 0;
390
+ CIVIC_SLOTS.forEach((s) => {
391
+ const here = (atBuilding[s[0]] || []).slice(0, MAX_PINS_PER_BUILDING);
392
+ const top = P(s[1] + 1, s[2] + 1, ROOF[s[0]] + 8);
393
+ here.forEach((m, i) => {
394
+ const g = glyph(m.rule, 7, 7, 13);
395
+ const dx = (i - (here.length - 1) / 2) * 26;
396
+ pins.push({ key: String(m.id), building: s[0], state: pinState(m.state), title: String(m.short || m.title || ''), x: r1(top[0] - 15 + dx), y: r1(top[1] - 52), h: 52, gf: g.fill, gl: g.line, delay: -(n * 0.9) });
397
+ n += 1;
398
+ });
399
+ });
400
+
401
+ // work on the move: parcels ride the ring road to Review, and leave through Release
402
+ const route = (pts) => pts.map((p, i) => { const q = P(p[0], p[1]); return (i ? 'L' : 'M') + r1(q[0]) + ' ' + r1(q[1]); }).join('');
403
+ const toReview = [
404
+ route([[8, 3.6], [8, 4.5], [11.5, 4.5], [11.5, 10], [10.7, 10]]),
405
+ route([[12.4, 8], [11.5, 8], [11.5, 10.2], [10.7, 10.2]]),
406
+ route([[3.6, 7.5], [4.5, 7.5], [4.5, 11.5], [9.8, 11.5], [9.8, 10.8]]),
407
+ route([[7.5, 12.4], [7.5, 11.5], [10.2, 11.5], [10.2, 10.8]]),
408
+ ];
409
+ const fed = {};
410
+ districts.forEach((D) => { if (D.lot.route != null) fed[D.lot.route] = true; });
411
+ const parcels = [];
412
+ toReview.forEach((d, i) => { if (fed[i]) [0, 1].forEach((j) => parcels.push({ key: 'p' + i + j, d: d, dur: 12 + i, delay: -(i * 2.7 + j * 6.2), out: false })); });
413
+ if (districts.length) [0, 1].forEach((j) => parcels.push({ key: 'out' + j, d: route([[10.6, 9.6], [11.5, 9.6], [11.5, 8], [11.5, 0]]), dur: 10, delay: -(j * 5), out: true }));
414
+
415
+ const line = docket && docket.line ? String(docket.line) : FALLBACK_LINE;
416
+ const pinCount = { needs: 0, moving: 0, settled: 0 };
417
+ pins.forEach((p) => { pinCount[p.state] += 1; });
418
+ const label = 'The governor city: the nine civic buildings round the plaza and '
419
+ + (districts.length === 1 ? 'one district' : districts.length + ' districts') + ', one for each open goal'
420
+ + (pins.length ? '. Pins: ' + pinCount.needs + ' need you, ' + pinCount.moving + ' moving, ' + pinCount.settled + ' settled.' : '.');
421
+
422
+ return { 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 };
423
+ }
424
+
425
+ // Where the start governing button goes: the street (task 1004525 builds it),
426
+ // a hall page of its own beside this one.
427
+ const STREET_HREF = '/builders/street';
428
+
429
+ const api = {
430
+ isoTheme: isoTheme, isoKit: isoKit, isoBox: isoBox, isoGable: isoGable, isoHip: isoHip, isoSaw: isoSaw,
431
+ isoTree: isoTree, isoLight: isoLight, isoInstitution: isoInstitution, isoWork: isoWork, glyph: glyph, rnd: rnd,
432
+ lotsFor: lotsFor, districtWork: districtWork, cityModel: cityModel,
433
+ CIVIC_SLOTS: CIVIC_SLOTS, ROOF: ROOF, BASE_LOTS: BASE_LOTS, MAX_DISTRICTS: MAX_DISTRICTS, MAX_PINS_PER_BUILDING: MAX_PINS_PER_BUILDING,
434
+ FALLBACK_LINE: FALLBACK_LINE, STREET_HREF: STREET_HREF,
435
+ };
436
+ root.GovCity = api;
437
+ if (typeof module !== 'undefined' && module.exports) module.exports = api;
438
+ }(typeof window !== 'undefined' ? window : globalThis));
@@ -0,0 +1,110 @@
1
+ /* modules/hall-ui/public/city.css — THE GOVERNOR CITY's home (task 1004524,
2
+ goal 1000125, R02). The home branch of the <style> in
3
+ docs/design/mocks/governor-city/Street.dc.html: the same keyframes (ring,
4
+ zoom, travel, bob, drift), the same wordmark, mode switch, start button and
5
+ line, the same sizes. What changed is where the colours come from: the
6
+ prototype wrote its theme() values inline; here every colour is one of the
7
+ hall's tokens (style.css + the branding pack), whose values ARE the
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).
11
+ No :root here (tests/hall_reading_world.mjs): page tokens live on the body.
12
+ The two dark blocks below are byte-identical twins (the hall's switch is
13
+ html[data-theme], set by city.js from the same 'hall-theme' preference
14
+ every hall page reads; the media block is first paint). */
15
+
16
+ body[data-page="city"] {
17
+ --city-contact: drop-shadow(0 6px 8px color-mix(in srgb, var(--ink) 10%, transparent));
18
+ --city-glow: none;
19
+ --city-crate: color-mix(in srgb, var(--ink) 7%, transparent);
20
+ --city-crate-edge: color-mix(in srgb, var(--ink) 55%, transparent);
21
+ /* the hall's ground plate and its dark veil (style.css body::before/::after) are
22
+ the hall chrome's; the city is its own ground, so both are off here */
23
+ --plate: none;
24
+ --plate-veil: none;
25
+ margin: 0;
26
+ height: 100%;
27
+ overflow: hidden;
28
+ background: var(--bg);
29
+ color: var(--ink);
30
+ font-family: var(--font-body);
31
+ }
32
+ html[data-theme="dark"] body[data-page="city"] {
33
+ --city-contact: none;
34
+ --city-glow: 0 0 18px color-mix(in srgb, var(--accent) 50%, transparent);
35
+ }
36
+ @media (prefers-color-scheme: dark) {
37
+ html:not([data-theme]) body[data-page="city"] {
38
+ --city-contact: none;
39
+ --city-glow: 0 0 18px color-mix(in srgb, var(--accent) 50%, transparent);
40
+ }
41
+ }
42
+ html:has(body[data-page="city"]) { height: 100%; }
43
+
44
+ /* the header: wordmark left, the sun/moon switch right */
45
+ .city-top { position: absolute; left: 32px; right: 32px; top: 0; height: 64px; display: flex; align-items: center; justify-content: space-between; z-index: 3; }
46
+ .city-mark { font-weight: 500; font-size: 17px; letter-spacing: -0.015em; text-transform: lowercase; }
47
+ .city-mode { background: none; border: 1px solid var(--rule); border-radius: 999px; height: 36px; padding: 0 11px; display: flex; align-items: center; gap: 8px; cursor: pointer; color: var(--ink); font-family: var(--font-body); }
48
+ .city-mode svg { display: block; stroke-width: 1.4; }
49
+
50
+ /* the stage: the prototype's 1440 x 900 artboard, scaled to the window by
51
+ city.js (--city-scale) and centred, so 1440 x 900 is the prototype pixel
52
+ for pixel */
53
+ .city-main { position: fixed; inset: 0; z-index: 1; }
54
+ .city-frame { position: absolute; inset: 0; overflow: hidden; }
55
+ .city-stage { position: absolute; left: 50%; top: 50%; width: 1440px; height: 900px; transform: translate(-50%, -50%) scale(var(--city-scale, 1)); transform-origin: 50% 50%; }
56
+ .city-zoom, .city-drift { position: absolute; left: 0; top: 0; width: 1440px; height: 900px; }
57
+ .city-svg { position: absolute; left: 0; top: 0; pointer-events: none; overflow: visible; }
58
+ .city-blds { filter: var(--city-contact); }
59
+ .city-parcels, .city-pins { position: absolute; left: 0; top: 0; width: 1440px; height: 900px; pointer-events: none; }
60
+
61
+ /* parcels: crates riding the ring road on offset-path */
62
+ .city-parcel { position: absolute; left: 0; top: 0; width: 9px; height: 7px; margin: -4px 0 0 -5px; box-sizing: border-box; border-radius: 2px; background: var(--city-crate); border: 1px solid var(--city-crate-edge); offset-rotate: 0deg; }
63
+ .city-parcel--out { background: var(--accent); border-color: var(--accent); }
64
+
65
+ /* pins: orange and pulsing waits on you; ink and bobbing is moving; grey is settled */
66
+ .city-pin { position: absolute; width: 30px; height: 52px; display: flex; flex-direction: column; align-items: center; }
67
+ .city-pin__head { position: relative; width: 22px; height: 22px; flex-shrink: 0; border-radius: 999px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; }
68
+ .city-pin__head svg { display: block; }
69
+ .city-pin__head path { fill: currentColor; }
70
+ .city-pin__head path + path { fill: none; stroke: currentColor; stroke-width: 1.2; }
71
+ .city-pin__stem { width: 0; flex-grow: 1; border-left: 1px solid transparent; }
72
+ .city-pin__ring { position: absolute; inset: -1px; border-radius: 999px; border: 1px solid var(--accent); }
73
+ .city-pin--needs .city-pin__head { background: var(--accent); color: var(--accent-on); box-shadow: var(--city-glow); }
74
+ .city-pin--needs .city-pin__stem { border-left-color: color-mix(in srgb, var(--accent) 60%, transparent); }
75
+ .city-pin--moving .city-pin__head { background: var(--ink); color: var(--bg-card); }
76
+ .city-pin--moving .city-pin__stem { border-left-color: color-mix(in srgb, var(--ink) 45%, transparent); }
77
+ .city-pin--settled .city-pin__head { background: color-mix(in srgb, var(--ink) 10%, var(--bg)); border: 1px solid color-mix(in srgb, var(--ink) 22%, transparent); color: var(--ink-faint); }
78
+ .city-pin--settled .city-pin__stem { border-left-color: color-mix(in srgb, var(--ink) 22%, transparent); }
79
+
80
+ /* the one door and the one line */
81
+ .city-door { position: absolute; left: 0; right: 0; bottom: 70px; display: flex; flex-direction: column; align-items: center; gap: 14px; z-index: 2; }
82
+ .city-start { display: inline-flex; align-items: center; box-sizing: border-box; font-family: var(--font-body); font-size: 17px; font-weight: 500; height: 56px; padding: 0 38px; border-radius: 999px; border: 0; cursor: pointer; background: var(--accent); color: var(--accent-on); text-transform: lowercase; text-decoration: none; transition: transform 200ms cubic-bezier(.2,.7,.3,1); }
83
+ .city-start:hover { background: var(--accent-lift); transform: translateY(-2px); }
84
+ .city-line { margin: 0; font-size: 14px; font-weight: 400; line-height: 1.4; color: var(--ink-soft); font-variant-numeric: tabular-nums; text-align: center; }
85
+ .city-mode:focus-visible, .city-start:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
86
+
87
+ @media (max-width: 560px) {
88
+ .city-top { left: 16px; right: 16px; }
89
+ .city-door { bottom: 40px; padding: 0 16px; }
90
+ }
91
+
92
+ /* the prototype's motion, verbatim */
93
+ @keyframes city-ring { 0% { transform: scale(.8); opacity: .9; } 100% { transform: scale(2.3); opacity: 0; } }
94
+ @keyframes city-zoom { from { opacity: 0; transform: scale(.97); } to { opacity: 1; transform: none; } }
95
+ @keyframes city-travel { 0% { offset-distance: 0%; opacity: 0; } 8% { opacity: 1; } 90% { opacity: 1; } 100% { offset-distance: 100%; opacity: 0; } }
96
+ @keyframes city-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-5px); } }
97
+ @keyframes city-drift { from { transform: translate(0, 0); } to { transform: translate(-8px, 5px); } }
98
+ .city-zoom { animation: city-zoom 420ms cubic-bezier(.16,1,.3,1) both; transform-origin: 50% 60%; }
99
+ .city-drift { animation: city-drift 46s ease-in-out infinite alternate; }
100
+ .city-pin { animation: city-bob 6s ease-in-out infinite; }
101
+ .city-pin__ring { animation: city-ring 2.8s cubic-bezier(.2,.7,.3,1) infinite; }
102
+ .city-parcel { animation-name: city-travel; animation-timing-function: linear; animation-iteration-count: infinite; }
103
+
104
+ /* The Kill Switch: under reduced motion nothing moves. The rest frame is the
105
+ whole city with its pins standing still; the parcels, which only mean
106
+ something while they travel, are left out of it. */
107
+ @media (prefers-reduced-motion: reduce) {
108
+ body[data-page="city"] *, body[data-page="city"] *::before, body[data-page="city"] *::after { animation: none !important; transition: none !important; }
109
+ .city-parcels { display: none; }
110
+ }
@@ -0,0 +1,71 @@
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, viewport-fit=cover">
6
+ <title>The city — {{worldName}}</title>
7
+ <meta name="description" content="The governor's home: the project drawn as a small city, a pin over every matter that waits on you, and one door into the street.">
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <!-- The faces (Manrope, JetBrains Mono) come from the branding pack's font
11
+ links, injected into this head by the server like every hall page's
12
+ (tests/hall_tokens.mjs: no hall page hardcodes a web-font stylesheet).
13
+ The prototype carried its own Google Fonts link for the same two faces. -->
14
+ <link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
15
+ <link rel="stylesheet" href="/builders/city.css?v=2026-10-02-city1">
16
+ </head>
17
+ <!-- THE GOVERNOR CITY, HOME (task 1004524, goal 1000125, R02). The home view
18
+ of docs/design/mocks/governor-city/Street.dc.html (homeView and the home
19
+ branch of its template), built over the governor docket (GET
20
+ /api/bongos/government/docket, task 1004523) and the open goals with their
21
+ task rollups. The markup below is the prototype's home: the wordmark and
22
+ the sun/moon switch in the header, the city, one button and one line.
23
+ Nothing else is on the page, on purpose: no hall sidebar, no top bar.
24
+
25
+ The drawing is city-draw.js (the prototype's iso kit, verbatim, as pure
26
+ data); city.js reads the three feeds and turns the lists into SVG paths
27
+ and the pin and parcel spans; city.css is the template's <style> on the
28
+ hall's tokens.
29
+
30
+ The button is a link to /builders/street, the street's own hall page,
31
+ which task 1004525 builds. It is an <a>, not the prototype's <button>,
32
+ because pressing it goes to another page.
33
+
34
+ The page is gated on board.vote.cast (src/bongos/serve-internal.js), the
35
+ same atom the docket read checks, so whoever reaches the city can read
36
+ what it draws. -->
37
+ <body data-page="city" class="city">
38
+
39
+ <header class="city-top">
40
+ <span class="city-mark">{{productName}}</span>
41
+ <button type="button" class="city-mode" id="cityMode" aria-label="Switch to light mode" title="Switch to light mode">
42
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" aria-hidden="true"><path id="cityModeIcon" d=""></path></svg>
43
+ </button>
44
+ </header>
45
+
46
+ <main class="city-main" id="cityMain">
47
+ <div class="city-frame" id="cityFrame">
48
+ <div class="city-stage" id="cityStage" role="img" aria-label="The governor city">
49
+ <div class="city-zoom">
50
+ <div class="city-drift">
51
+ <svg class="city-svg" id="citySvg" width="1440" height="900" viewBox="0 0 1440 900" aria-hidden="true" focusable="false">
52
+ <g id="cityFlat"></g>
53
+ <g id="cityBlds" class="city-blds"></g>
54
+ </svg>
55
+ <div class="city-parcels" id="cityParcels" aria-hidden="true"></div>
56
+ <div class="city-pins" id="cityPins" aria-hidden="true"></div>
57
+ </div>
58
+ </div>
59
+ </div>
60
+ </div>
61
+
62
+ <div class="city-door">
63
+ <a class="city-start" id="cityStart" href="/builders/street" aria-label="Start governing: enter the street view">start governing</a>
64
+ <h1 class="city-line" id="cityLine" aria-live="polite">reading the docket</h1>
65
+ </div>
66
+ </main>
67
+
68
+ <script src="/builders/city-draw.js?v=2026-10-02-city1"></script>
69
+ <script src="/builders/city.js?v=2026-10-02-city1"></script>
70
+ </body>
71
+ </html>