@bongos/core 1.21.113 → 1.21.115

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 (265) hide show
  1. package/.bongos-core.json +608 -188
  2. package/.claude/skills/ask-for-help/SKILL.md +1 -1
  3. package/.claude/skills/bongos-feedback/SKILL.md +1 -1
  4. package/.claude/skills/builder-sequence/SKILL.md +1 -1
  5. package/.claude/skills/builder-ship/SKILL.md +1 -0
  6. package/GOVERNMENT.md +128 -11
  7. package/clients/bongos-client/README.md +1 -1
  8. package/clients/bongos-client/bongos-client.global.js +28 -0
  9. package/clients/bongos-client/index.cjs +28 -0
  10. package/clients/bongos-client/index.d.ts +45 -2
  11. package/clients/bongos-client/index.mjs +28 -0
  12. package/docs/adr/0372-an-ask-is-stored-per-docket-matter-and-the-server-refuses-slop.md +33 -0
  13. package/docs/adr/0373-an-ai-session-files-a-blocker-with-an-ask.md +31 -0
  14. package/docs/adr/0374-a-building-lead-is-a-held-position-appointed-by-a-people-matter.md +30 -0
  15. package/docs/adr/0375-who-may-build-without-a-vote-comes-from-the-form-of-government.md +38 -0
  16. package/docs/adr/0376-every-ask-is-routed-to-a-real-seat-on-the-server.md +30 -0
  17. package/docs/adr/0377-governance-is-a-held-position-read-on-me.md +24 -0
  18. package/docs/adr/0378-a-leads-build-outs-are-rows-built-at-once-or-by-a-passed-vote.md +33 -0
  19. package/docs/adr/0379-the-treasury-is-pots-per-goal-a-reserve-and-borrowing-over-real-spend.md +32 -0
  20. package/docs/adr/0380-a-strictness-preset-is-a-bundle-of-laws-switched-by-one-laws-vote.md +26 -0
  21. package/docs/adr/README.md +10 -1
  22. package/docs/api/openapi.json +989 -87
  23. package/docs/api-reference.md +18 -4
  24. package/docs/copy-inventory.md +709 -626
  25. package/docs/copy-registry.json +1673 -858
  26. package/docs/file-map.md +2 -1
  27. package/docs/module-api-changelog.md +4 -0
  28. package/docs/packs/engineer.md +2 -1
  29. package/docs/page-inventory.json +91 -6
  30. package/docs/page-readings.json +4875 -4748
  31. package/modules/builder-settings/law-locks.js +202 -0
  32. package/modules/builder-settings/routes/settings.js +66 -6
  33. package/modules/builder-settings/settings.js +7 -0
  34. package/modules/discord/ship-broadcast.js +22 -13
  35. package/modules/government/asks-db.js +54 -0
  36. package/modules/government/asks.js +269 -0
  37. package/modules/government/board-idea-reads.js +68 -0
  38. package/modules/government/board.js +21 -57
  39. package/modules/government/build-rights.js +148 -0
  40. package/modules/government/buildouts.js +160 -0
  41. package/modules/government/counts.js +81 -0
  42. package/modules/government/db.js +242 -0
  43. package/modules/government/decision-board.js +234 -8
  44. package/modules/government/decision-rules.js +2 -0
  45. package/modules/government/docket.js +114 -6
  46. package/modules/government/law-board.js +131 -0
  47. package/modules/government/laws.js +241 -0
  48. package/modules/government/leads.js +170 -0
  49. package/modules/government/migrations/government_027_laws.sql +143 -0
  50. package/modules/government/migrations/government_028_asks.sql +57 -0
  51. package/modules/government/migrations/government_029_leads.sql +135 -0
  52. package/modules/government/migrations/government_030_buildouts.sql +111 -0
  53. package/modules/government/migrations/government_031_treasury.sql +107 -0
  54. package/modules/government/module.json +2 -1
  55. package/modules/government/pollers/treasury-borrow.js +62 -0
  56. package/modules/government/positions.js +107 -0
  57. package/modules/government/presets.js +149 -0
  58. package/modules/government/routes/government.js +425 -2
  59. package/modules/government/routing.js +154 -0
  60. package/modules/government/treasury-db.js +166 -0
  61. package/modules/government/treasury.js +241 -0
  62. package/modules/hall-ui/public/atlas.html +2 -2
  63. package/modules/hall-ui/public/blockers.html +2 -2
  64. package/modules/hall-ui/public/board-room.html +2 -2
  65. package/modules/hall-ui/public/board-room.js +2 -0
  66. package/modules/hall-ui/public/city-draw.js +604 -11
  67. package/modules/hall-ui/public/city.css +157 -1
  68. package/modules/hall-ui/public/city.html +37 -7
  69. package/modules/hall-ui/public/city.js +441 -21
  70. package/modules/hall-ui/public/city.states.json +110 -5
  71. package/modules/hall-ui/public/collab.html +2 -2
  72. package/modules/hall-ui/public/commands.html +2 -2
  73. package/modules/hall-ui/public/copy-desk.html +2 -2
  74. package/modules/hall-ui/public/craft-mode.js +112 -29
  75. package/modules/hall-ui/public/deploy.html +2 -2
  76. package/modules/hall-ui/public/diagram-viewer.css +12 -0
  77. package/modules/hall-ui/public/diagram-viewer.js +4 -0
  78. package/modules/hall-ui/public/diagrams.html +2 -2
  79. package/modules/hall-ui/public/drachmae.html +2 -2
  80. package/modules/hall-ui/public/fleet.html +2 -2
  81. package/modules/hall-ui/public/gate.html +2 -2
  82. package/modules/hall-ui/public/goal-map.html +2 -2
  83. package/modules/hall-ui/public/goals.html +2 -2
  84. package/modules/hall-ui/public/gov-feeds.js +141 -0
  85. package/modules/hall-ui/public/gov-living.js +1002 -0
  86. package/modules/hall-ui/public/gov-positions.js +97 -0
  87. package/modules/hall-ui/public/government.html +2 -2
  88. package/modules/hall-ui/public/hall-render.js +3 -0
  89. package/modules/hall-ui/public/idea.html +2 -2
  90. package/modules/hall-ui/public/ideas.html +2 -2
  91. package/modules/hall-ui/public/index.html +3 -3
  92. package/modules/hall-ui/public/modules.html +2 -2
  93. package/modules/hall-ui/public/my-agents.css +334 -0
  94. package/modules/hall-ui/public/my-agents.html +72 -0
  95. package/modules/hall-ui/public/my-agents.js +603 -0
  96. package/modules/hall-ui/public/my-agents.states.json +37 -0
  97. package/modules/hall-ui/public/primer.html +2 -2
  98. package/modules/hall-ui/public/profile-connections.js +40 -28
  99. package/modules/hall-ui/public/profile.html +2 -2
  100. package/modules/hall-ui/public/project-settings.html +2 -2
  101. package/modules/hall-ui/public/ranks.html +2 -2
  102. package/modules/hall-ui/public/roadmap.html +2 -2
  103. package/modules/hall-ui/public/room.css +8 -0
  104. package/modules/hall-ui/public/roster.html +2 -2
  105. package/modules/hall-ui/public/sessions.html +2 -2
  106. package/modules/hall-ui/public/settings.html +2 -2
  107. package/modules/hall-ui/public/shell.js +116 -16
  108. package/modules/hall-ui/public/street-card.js +18 -1
  109. package/modules/hall-ui/public/street-deck.css +299 -0
  110. package/modules/hall-ui/public/street-deck.js +1145 -0
  111. package/modules/hall-ui/public/street-depth.js +712 -0
  112. package/modules/hall-ui/public/street-desk.js +5 -2
  113. package/modules/hall-ui/public/street-draw.js +109 -16
  114. package/modules/hall-ui/public/street-inside.js +137 -22
  115. package/modules/hall-ui/public/street-office.js +705 -0
  116. package/modules/hall-ui/public/street.css +161 -11
  117. package/modules/hall-ui/public/street.html +41 -11
  118. package/modules/hall-ui/public/street.js +78 -66
  119. package/modules/hall-ui/public/street.states.json +129 -21
  120. package/modules/hall-ui/public/studio.html +2 -2
  121. package/modules/hall-ui/public/style.css +5 -1
  122. package/modules/hall-ui/public/task.html +2 -2
  123. package/modules/hall-ui/public/thinking.html +2 -2
  124. package/modules/hall-ui/public/tweak-editor.html +2 -2
  125. package/modules/hall-ui/public/watch.html +2 -2
  126. package/modules/hall-ui/public/watch.js +14 -1
  127. package/modules/hall-ui/public/work.html +2 -2
  128. package/modules/hall-ui/records/living-city.md +57 -0
  129. package/modules/hall-ui/records/my-agents.md +21 -0
  130. package/modules/hall-ui/records/oversight-family.md +1 -0
  131. package/modules/hall-ui/records/profile-connections.md +10 -0
  132. package/modules/hall-ui/records/reading-rooms.md +2 -0
  133. package/modules/ideas/routes/blockers.js +92 -3
  134. package/modules/lifecycle/module.json +2 -1
  135. package/modules/lifecycle/moving.js +17 -1
  136. package/modules/lifecycle/routes/moving.js +6 -0
  137. package/modules/lifecycle/routes/project-settings.js +23 -0
  138. package/modules/onboarding/approval-broadcast.js +8 -0
  139. package/modules/onboarding/routes/access-requests.js +16 -6
  140. package/modules/platform-identity/connections.js +13 -2
  141. package/modules/platform-identity/routes/connections.js +7 -1
  142. package/modules/public-landing/public/account-private.states.json +2 -2
  143. package/modules/public-landing/public/account.html +65 -5
  144. package/modules/public-landing/public/account.states.json +3 -3
  145. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
  146. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
  147. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +13 -0
  148. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
  149. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
  150. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +13 -0
  151. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
  152. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
  153. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +13 -0
  154. package/modules/public-landing/public/assets/world.css +4 -4
  155. package/modules/public-landing/public/connections-empty.states.json +14 -0
  156. package/modules/public-landing/public/connections-inactive.states.json +14 -0
  157. package/modules/public-landing/public/connections.html +450 -0
  158. package/modules/public-landing/public/connections.probes.json +414 -0
  159. package/modules/public-landing/public/connections.states.json +17 -0
  160. package/modules/public-landing/public/contact.html +2 -2
  161. package/modules/public-landing/public/guild.html +2 -2
  162. package/modules/public-landing/public/index.html +2 -2
  163. package/modules/public-landing/public/invitations.html +2 -2
  164. package/modules/public-landing/public/privacy.html +2 -2
  165. package/modules/public-landing/public/projects-hall-live.states.json +0 -5
  166. package/modules/public-landing/public/projects.html +169 -6
  167. package/modules/public-landing/public/projects.probes.json +10 -6
  168. package/modules/public-landing/public/projects.states.json +13 -13
  169. package/modules/public-landing/public/terms.html +2 -2
  170. package/modules/ui-design/kit/fixtures/connections-empty.json +12 -0
  171. package/modules/ui-design/kit/fixtures/connections-inactive.json +7 -0
  172. package/modules/ui-design/kit/fixtures/connections.json +22 -0
  173. package/modules/ui-design/kit/serve.js +95 -0
  174. package/package-lock.json +2 -2
  175. package/package.json +1 -1
  176. package/release-notes.json +128 -0
  177. package/scripts/discord/README.md +8 -5
  178. package/scripts/discord/post.js +12 -1
  179. package/scripts/gds/artifact-audit.js +20 -3
  180. package/scripts/gds/ask.js +164 -0
  181. package/scripts/gds/autobongos-verify.js +21 -2
  182. package/scripts/gds/government-walk.js +638 -4
  183. package/scripts/gds/pulse.js +9 -6
  184. package/scripts/gds/ship-escalation.js +22 -3
  185. package/scripts/gds/ship-finish.js +5 -4
  186. package/scripts/gds/ship-merge-gate.js +18 -6
  187. package/scripts/hall-preview/README.md +41 -1
  188. package/scripts/hall-preview/pref-writes.js +140 -0
  189. package/scripts/hall-preview/refresh-fixtures.js +101 -7
  190. package/scripts/hall-preview/scrub-identity.js +366 -7
  191. package/scripts/hall-preview/server.js +27 -1
  192. package/scripts/render-diagrams.sh +4 -2
  193. package/src/bongos/auth-idp.js +6 -1
  194. package/src/bongos/project-settings.js +41 -0
  195. package/src/bongos/route-rank-check.js +27 -0
  196. package/src/bongos/routes/me.js +68 -7
  197. package/src/bongos/serve-internal.js +5 -1
  198. package/src/module-api.js +7 -1
  199. package/src/platform-server.js +10 -0
  200. package/tests/application_lifecycle.mjs +47 -5
  201. package/tests/conductor_main_e2e.mjs +21 -0
  202. package/tests/connections_api.mjs +18 -0
  203. package/tests/craft_mode_nav.mjs +5 -3
  204. package/tests/craft_mode_tallies.mjs +52 -16
  205. package/tests/discord_approvals.mjs +11 -0
  206. package/tests/discord_channels.mjs +20 -0
  207. package/tests/fixtures/gov-seated.mjs +64 -0
  208. package/tests/fixtures/government-ask-cards.json +1284 -0
  209. package/tests/government_act_alone_buildings.mjs +1 -0
  210. package/tests/government_agent_asks.mjs +322 -0
  211. package/tests/government_asks.mjs +408 -0
  212. package/tests/government_build_rights.mjs +253 -0
  213. package/tests/government_buildouts.mjs +382 -0
  214. package/tests/government_counts.mjs +143 -0
  215. package/tests/government_decision_rules.mjs +5 -1
  216. package/tests/government_laws.mjs +488 -0
  217. package/tests/government_laws_session.mjs +321 -0
  218. package/tests/government_leads.mjs +375 -0
  219. package/tests/government_positions.mjs +127 -0
  220. package/tests/government_presets.mjs +215 -0
  221. package/tests/government_routes.mjs +8 -1
  222. package/tests/government_routing.mjs +118 -0
  223. package/tests/government_seed.mjs +5 -0
  224. package/tests/government_treasury.mjs +394 -0
  225. package/tests/governor_docket.mjs +26 -2
  226. package/tests/hall_buildouts.mjs +208 -0
  227. package/tests/hall_city.mjs +14 -6
  228. package/tests/hall_city_living.mjs +617 -0
  229. package/tests/hall_gov_counts.mjs +336 -0
  230. package/tests/hall_gov_living.mjs +636 -0
  231. package/tests/hall_gov_real_site.mjs +655 -0
  232. package/tests/hall_matter_card.mjs +6 -1
  233. package/tests/hall_my_agents.mjs +481 -0
  234. package/tests/hall_nav.mjs +107 -4
  235. package/tests/hall_page_gate_map.mjs +4 -0
  236. package/tests/hall_preview_governor_fixtures.mjs +30 -3
  237. package/tests/hall_preview_scrub.mjs +59 -1
  238. package/tests/hall_preview_security.mjs +169 -0
  239. package/tests/hall_reading_world.mjs +44 -0
  240. package/tests/hall_roster_world.mjs +6 -2
  241. package/tests/hall_street.mjs +18 -2
  242. package/tests/hall_street_amend.mjs +1 -1
  243. package/tests/hall_street_deck.mjs +609 -0
  244. package/tests/hall_street_depth.mjs +559 -0
  245. package/tests/hall_street_desk.mjs +1 -1
  246. package/tests/hall_street_inside.mjs +29 -5
  247. package/tests/hall_tweak_editor.mjs +4 -1
  248. package/tests/hub_account_page.mjs +94 -3
  249. package/tests/hub_connections_page.mjs +507 -0
  250. package/tests/hub_invite_badge.mjs +1 -1
  251. package/tests/idp_device_exchange.mjs +18 -0
  252. package/tests/landing_legal_pages.mjs +8 -5
  253. package/tests/landing_page.mjs +23 -4
  254. package/tests/nav_permission_atoms.mjs +4 -1
  255. package/tests/profile_connections_ui.mjs +88 -25
  256. package/tests/projects_hub.mjs +15 -0
  257. package/tests/projects_hub_pre_uat.mjs +4 -2
  258. package/tests/render_check.mjs +2 -2
  259. package/tests/ship_broadcast_routing.mjs +57 -16
  260. package/tests/ship_broadcast_server.mjs +40 -7
  261. package/tests/ship_merge_gate.mjs +19 -0
  262. package/tests/ui_design_kit.mjs +2 -2
  263. package/tests/watch_applications_queue.mjs +23 -0
  264. package/tests/wizard_physics.mjs +1 -1
  265. package/tests/wizard_physics_more.mjs +133 -0
@@ -1,19 +1,28 @@
1
1
  // modules/hall-ui/public/city.js — THE GOVERNOR CITY's home, wired (task
2
- // 1004524, goal 1000125, R02).
2
+ // 1004524, goal 1000125, R02; alive since task 1004685).
3
3
  //
4
- // city-draw.js computes the city as data; this file reads the three feeds,
5
- // hands them to GovCity.cityModel(), and turns its lists into DOM: SVG paths
6
- // for the ground and the buildings, one span per parcel (CSS offset-path) and
7
- // one per pin. It also owns the sun/moon switch and the stage's fit to the
8
- // window. No innerHTML anywhere: every node is created, and every docket string
9
- // lands as text or an attribute.
4
+ // city-draw.js computes the city as data; this file reads the feeds, hands
5
+ // them to GovCity.cityModel(), and turns its lists into DOM: SVG paths for
6
+ // the ground and the buildings, one span per parcel and per sky pod (CSS
7
+ // offset-path), one per pin, light shaft and Lead plate, and one button per
8
+ // civic building that opens its card in place. It also owns the sun/moon
9
+ // switch and the stage's fit to the window. No innerHTML anywhere: every node
10
+ // is created, and every docket string lands as text or an attribute.
10
11
  //
11
12
  // THE FEEDS (all GETs, all gated server-side):
12
13
  // /api/bongos/government/docket the pins and the one line (task 1004523)
13
14
  // /api/bongos/goals the open goals: one district each
14
15
  // /api/bongos/goals/rollups each goal's task counts: a building per task
15
- // A feed that fails leaves its part of the city out (no pins, or no districts)
16
- // and the line says the docket could not be read; it never draws sample data.
16
+ // and, for the living city (task 1004685), the reads gov-living.js takes:
17
+ // government/constitution (the plaque's form of government), tasks/claimable
18
+ // (the active claims: one sky pod each), tasks?status=active (which goal a
19
+ // claim's task is in), public/cost-summary (the spend conduit and the
20
+ // plaque), builders/roster and /me (who leads, who the viewer is), blockers,
21
+ // autonomy/fence, moving, and the government's own reads (laws, Leads, the
22
+ // treasury, the presets, the counts). All of it is real: the prototype
23
+ // overlay is gone (task 1004717).
24
+ // A feed that fails leaves its part of the city out; it never draws sample
25
+ // data. Without gov-living.js the page draws the city it drew before.
17
26
  //
18
27
  // THE SWITCH. Light and dark use the hall's own preference ('hall-theme' in
19
28
  // localStorage, else the OS setting) and the hall's own attribute
@@ -22,7 +31,12 @@
22
31
  // the mode, so a toggle redraws the city from the last feeds (no refetch).
23
32
  //
24
33
  // MOTION is CSS only (city.css); nothing here animates. The scale write on
25
- // resize is layout, not motion.
34
+ // resize is layout, not motion; the card changes a class.
35
+ //
36
+ // ANSWERING. A building's card takes its first ask in place (task 1004702, the
37
+ // owner's pick) through the deck's own sender (street-deck.js GovDeck.sendAnswer:
38
+ // the desk row's real route, never a new one); its door leads to the decision
39
+ // deck on the street (/street#deck=<building>) for the rest.
26
40
 
27
41
  (function () {
28
42
  'use strict';
@@ -36,10 +50,33 @@
36
50
  goals: '/api/bongos/goals',
37
51
  rollups: '/api/bongos/goals/rollups',
38
52
  };
53
+ // the living city's reads (task 1004685), fetched only when gov-living.js is on the page
54
+ var LIVING_FEEDS = {
55
+ // the counts every surface prints, answered once by the server (task 1004716)
56
+ counts: '/api/bongos/government/counts',
57
+ constitution: '/api/bongos/government/constitution',
58
+ // the project's laws in force (task 1004706)
59
+ laws: '/api/bongos/government/laws',
60
+ // who leads each building, and the owner seat an unseated one falls to (task 1004709)
61
+ leads: '/api/bongos/government/leads',
62
+ // the pots each goal draws from, the reserve, the spend cap (task 1004712)
63
+ treasury: '/api/bongos/government/treasury',
64
+ claims: '/api/bongos/tasks/claimable?limit=1',
65
+ tasks: '/api/bongos/tasks?status=active&limit=200',
66
+ cost: '/api/bongos/public/cost-summary',
67
+ roster: '/api/bongos/builders/roster',
68
+ me: '/api/bongos/me',
69
+ blockers: '/api/bongos/blockers?limit=100',
70
+ fence: '/api/bongos/autonomy/fence',
71
+ moving: '/api/bongos/moving',
72
+ };
39
73
 
40
74
  var $ = function (id) { return document.getElementById(id); };
41
75
  var media = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
42
- var data = { docket: null, goals: [], rollups: [], loaded: false };
76
+ var data = { docket: null, goals: [], rollups: [], loaded: false, raw: {} };
77
+ var model = null;
78
+ var openKey = null;
79
+ var drewLiving = false;
43
80
 
44
81
  // ---------- light / dark ----------
45
82
  // A choice made on this page when storage is blocked (private mode) still
@@ -95,7 +132,9 @@
95
132
  // (task 1004544); the full-screen switch resizes it and fires a resize.
96
133
  var main = $('hall-main');
97
134
  var w = (main && main.clientWidth) || window.innerWidth || 1440, h = (main && main.clientHeight) || window.innerHeight || 900;
98
- var s = Math.min(w / 1000, h / 900);
135
+ // On a phone the city stands closer (task 1004685): the civic quarter fills the
136
+ // width and the far corners of the ground bleed off, as a plate would.
137
+ var s = Math.min(w < 640 ? w / 700 : w / 1000, h / 900);
99
138
  stage.style.setProperty('--city-scale', String(Math.round(s * 1000) / 1000));
100
139
  }
101
140
 
@@ -105,15 +144,50 @@
105
144
  .then(function (r) { return r && r.ok ? r.json() : null; })
106
145
  .catch(function () { return null; });
107
146
  }
147
+ // Through gov-feeds.js when it is on the page: one read per feed per page view,
148
+ // shared with the shell's Governing tally, which therefore prints these counts.
149
+ function readFeeds() {
150
+ var F = window.GovFeeds;
151
+ var keys = ['docket', 'goals', 'rollups'];
152
+ if (window.GovLiving) Object.keys(LIVING_FEEDS).forEach(function (k) { keys.push(k); });
153
+ if (F && typeof F.read === 'function') return F.read(keys);
154
+ return Promise.all(keys.map(function (k) { return getJson(FEEDS[k] || LIVING_FEEDS[k]); })).then(function (res) {
155
+ var raw = {};
156
+ keys.forEach(function (k, i) { raw[k] = res[i]; });
157
+ return raw;
158
+ });
159
+ }
108
160
  function load() {
109
- return Promise.all([getJson(FEEDS.docket), getJson(FEEDS.goals), getJson(FEEDS.rollups)]).then(function (res) {
110
- data.docket = res[0] && Array.isArray(res[0].matters) ? res[0] : null;
111
- data.goals = res[1] && Array.isArray(res[1].goals) ? res[1].goals : [];
112
- data.rollups = res[2] && Array.isArray(res[2].rollups) ? res[2].rollups : [];
161
+ return readFeeds().then(function (raw) {
162
+ data.raw = raw;
163
+ data.docket = raw.docket && Array.isArray(raw.docket.matters) ? raw.docket : null;
164
+ data.goals = raw.goals && Array.isArray(raw.goals.goals) ? raw.goals.goals : [];
165
+ data.rollups = raw.rollups && Array.isArray(raw.rollups.rollups) ? raw.rollups.rollups : [];
166
+ data.living = livingOf(raw);
113
167
  data.loaded = true;
114
168
  draw(effectiveTheme());
115
169
  });
116
170
  }
171
+ // The living model (gov-living.js) over the same reads; null when the page has no model.
172
+ function livingOf(raw) {
173
+ var G = window.GovLiving;
174
+ if (!G || typeof G.livingModel !== 'function') return null;
175
+ if (window.GovFeeds) return window.GovFeeds.model(raw, Date.now());
176
+ try {
177
+ return G.livingModel({
178
+ docket: raw.docket, constitution: raw.constitution, goals: raw.goals, rollups: raw.rollups, blockers: raw.blockers,
179
+ fence: raw.fence, claims: raw.claims, tasks: raw.tasks, moving: raw.moving, cost: raw.cost, roster: raw.roster,
180
+ me: raw.me, laws: raw.laws, leads: raw.leads, counts: raw.counts, now: Date.now(),
181
+ });
182
+ } catch (_) { return null; }
183
+ }
184
+ // the counts this page prints: the one rule
185
+ function countsNow() {
186
+ var F = window.GovFeeds, L = window.GovLiving;
187
+ if (!data.living) return null;
188
+ if (F) return F.countsFrom(data.living);
189
+ return L ? L.countsOf(data.living) : null;
190
+ }
117
191
 
118
192
  // ---------- drawing ----------
119
193
  function clear(el) { while (el && el.firstChild) el.removeChild(el.firstChild); }
@@ -124,9 +198,19 @@
124
198
  el.setAttribute('stroke', p.s);
125
199
  if (p.dash && p.dash !== 'none') el.setAttribute('stroke-dasharray', p.dash);
126
200
  el.setAttribute('stroke-width', String(p.w || 1));
201
+ if (p.cls) el.setAttribute('class', p.cls);
127
202
  return el;
128
203
  }
204
+ // A part of a building that moves (or stands at its still frame) is its own group.
205
+ function partEl(part) {
206
+ var g = document.createElementNS(SVG_NS, 'g');
207
+ if (part.cls) g.setAttribute('class', part.cls);
208
+ if (part.style) Object.keys(part.style).forEach(function (k) { g.style.setProperty(k, part.style[k]); });
209
+ part.polys.forEach(function (p) { g.appendChild(pathEl(p)); });
210
+ return g;
211
+ }
129
212
  function span(cls) { var el = document.createElement('span'); el.className = cls; return el; }
213
+ function node(tag, cls, text) { var el = document.createElement(tag); if (cls) el.className = cls; if (text != null) el.textContent = String(text); return el; }
130
214
 
131
215
  function drawPin(pin) {
132
216
  var el = span('city-pin city-pin--' + pin.state);
@@ -140,6 +224,24 @@
140
224
  var head = span('city-pin__head');
141
225
  // only the urgent tier pulses; a settled pin is a plain grey dot, no mark (task 1004569)
142
226
  if (pin.state === 'urgent') head.appendChild(span('city-pin__ring'));
227
+ // the age ring: how long the oldest ask on you has waited, full at two weeks (task 1004685)
228
+ if (pin.age != null) {
229
+ var ring = document.createElementNS(SVG_NS, 'svg');
230
+ ring.setAttribute('class', 'city-pin__age');
231
+ ring.setAttribute('viewBox', '0 0 32 32');
232
+ ring.setAttribute('aria-hidden', 'true');
233
+ var track = document.createElementNS(SVG_NS, 'circle');
234
+ track.setAttribute('cx', '16'); track.setAttribute('cy', '16'); track.setAttribute('r', '14');
235
+ track.setAttribute('class', 'city-pin__age-track');
236
+ var arc = document.createElementNS(SVG_NS, 'circle');
237
+ arc.setAttribute('cx', '16'); arc.setAttribute('cy', '16'); arc.setAttribute('r', '14');
238
+ arc.setAttribute('class', 'city-pin__age-arc');
239
+ arc.setAttribute('pathLength', '100');
240
+ arc.setAttribute('stroke-dasharray', Math.round(pin.age * 100) + ' 100');
241
+ ring.appendChild(track);
242
+ ring.appendChild(arc);
243
+ head.appendChild(ring);
244
+ }
143
245
  if (pin.state !== 'settled') {
144
246
  var svg = document.createElementNS(SVG_NS, 'svg');
145
247
  svg.setAttribute('width', '14');
@@ -172,26 +274,333 @@
172
274
  return el;
173
275
  }
174
276
 
277
+ // one chrome pod per agent; its still frame stands at --pod-at along the lane
278
+ function drawPod(lane, pod) {
279
+ var el = span(lane.orbit ? 'city-pod city-pod--orbit' : 'city-pod');
280
+ el.setAttribute('data-lane', lane.key);
281
+ if (pod.task_id) el.setAttribute('data-task', pod.task_id);
282
+ el.style.offsetPath = "path('" + lane.d + "')";
283
+ el.style.setProperty('--pod-at', pod.at + '%');
284
+ el.style.animationDuration = pod.dur + 's';
285
+ el.style.animationDelay = pod.delay + 's';
286
+ return el;
287
+ }
288
+
289
+ function drawShaft(sh) {
290
+ var el = span('city-shaft');
291
+ el.setAttribute('data-building', sh.building);
292
+ el.style.left = sh.x + 'px';
293
+ el.style.top = (sh.y - sh.h) + 'px';
294
+ el.style.height = sh.h + 'px';
295
+ el.appendChild(span('city-shaft__ring'));
296
+ return el;
297
+ }
298
+
299
+ // A seated Lead's plate stands at rest; "owner" (a building nobody leads yet)
300
+ // shows only while its building is pointed at or focused, or its card is open,
301
+ // so five plates saying "owner" do not crowd the quarter (a class, no motion).
302
+ function showPlate(key, on) {
303
+ var sky = $('citySky');
304
+ var el = sky && sky.querySelector ? sky.querySelector('.city-plate--owner[data-building="' + key + '"]') : null;
305
+ if (!el) return;
306
+ var base = String(el.className || '').replace(/\s*\bcity-plate--shown\b/g, '');
307
+ el.className = on || openKey === key ? base + ' city-plate--shown' : base;
308
+ }
309
+ function hoverReveals(el, key) {
310
+ el.addEventListener('mouseenter', function () { showPlate(key, true); });
311
+ el.addEventListener('mouseleave', function () { showPlate(key, false); });
312
+ el.addEventListener('focus', function () { showPlate(key, true); });
313
+ el.addEventListener('blur', function () { showPlate(key, false); });
314
+ }
315
+ function drawPlate(c) {
316
+ var el = span(c.plate.lead ? 'city-plate' : 'city-plate city-plate--owner' + (openKey === c.key ? ' city-plate--shown' : ''));
317
+ el.setAttribute('data-building', c.key);
318
+ el.style.left = c.plate.x + 'px';
319
+ el.style.top = c.plate.y + 'px';
320
+ el.textContent = c.plate.text;
321
+ el.addEventListener('click', function () { openCard(c.key); });
322
+ return el;
323
+ }
324
+
325
+ // a count in the label every surface prints (gov-living.js COUNT_WORDS)
326
+ function countText(c) {
327
+ var L = window.GovLiving;
328
+ var w = function (k, n) { return L ? L.countWords(k, n) : k === 'on_you' ? 'to do' : 'wait on others'; };
329
+ return c.on_you ? c.on_you + ' ' + w('on_you', c.on_you) : c.on_others ? c.on_others + ' ' + w('on_others', c.on_others) : c.open ? c.open + ' moving' : 'nothing open';
330
+ }
331
+ // who holds it: a Lead, or the owner's seat (GET /government/leads); unknown names nobody
332
+ function hitLabel(c) {
333
+ var who = c.card.lead ? ', led by ' + c.card.plate : c.card.plate ? ', held by the owner' : '';
334
+ return c.name + who + ': ' + c.card.fact.text + ', ' + countText(c);
335
+ }
336
+ function drawHit(c) {
337
+ var el = node('button', 'city-hit');
338
+ el.setAttribute('type', 'button');
339
+ el.setAttribute('data-building', c.key);
340
+ el.setAttribute('aria-label', hitLabel(c));
341
+ el.setAttribute('aria-haspopup', 'dialog');
342
+ el.style.left = c.hit.x + 'px';
343
+ el.style.top = c.hit.y + 'px';
344
+ el.style.width = c.hit.w + 'px';
345
+ el.style.height = c.hit.h + 'px';
346
+ el.addEventListener('click', function () { openCard(c.key, el); });
347
+ hoverReveals(el, c.key);
348
+ return el;
349
+ }
350
+
351
+ function drawPlaque(tiles) {
352
+ var el = $('cityPlaque');
353
+ if (!el) return;
354
+ clear(el);
355
+ (tiles || []).forEach(function (t) {
356
+ var tile = node('span', 'city-plaque__tile');
357
+ tile.setAttribute('data-key', t.key);
358
+ tile.appendChild(node('span', 'city-plaque__figure', t.figure));
359
+ tile.appendChild(node('span', 'city-plaque__words', t.words));
360
+ el.appendChild(tile);
361
+ });
362
+ if (tiles && tiles.length) el.removeAttribute('hidden'); else el.setAttribute('hidden', '');
363
+ }
364
+
175
365
  function draw(mode) {
176
- var model = City.cityModel({ docket: data.docket, goals: data.goals, rollups: data.rollups, mode: mode, now: Date.now() });
177
- var flat = $('cityFlat'), blds = $('cityBlds'), parcels = $('cityParcels'), pins = $('cityPins');
178
- clear(flat); clear(blds); clear(parcels); clear(pins);
366
+ model = City.cityModel({
367
+ docket: data.docket, goals: data.goals, rollups: data.rollups, mode: mode, now: Date.now(),
368
+ living: data.living || null, constitution: data.raw.constitution || null, counts: countsNow(),
369
+ });
370
+ var flat = $('cityFlat'), blds = $('cityBlds'), sky = $('citySkyLanes'), parcels = $('cityParcels'), pods = $('citySky'), pins = $('cityPins'), hits = $('cityHits');
371
+ clear(flat); clear(blds); clear(sky); clear(parcels); clear(pods); clear(pins); clear(hits);
179
372
  model.flat.forEach(function (p) { flat.appendChild(pathEl(p)); });
373
+ var civicKeys = {};
374
+ (model.civic || []).forEach(function (c) { civicKeys[c.key] = true; });
180
375
  model.blds.forEach(function (b) {
181
376
  var g = document.createElementNS(SVG_NS, 'g');
182
377
  g.setAttribute('data-key', b.key);
378
+ if (b.quiet) g.setAttribute('data-quiet', '');
379
+ // a pointer finds a building by its own painted faces, so the one in front wins
380
+ if (civicKeys[b.key]) { g.setAttribute('data-civic', b.key); g.addEventListener('click', function () { openCard(b.key); }); hoverReveals(g, b.key); }
183
381
  b.polys.forEach(function (p) { g.appendChild(pathEl(p)); });
382
+ (b.parts || []).forEach(function (part) { g.appendChild(partEl(part)); });
184
383
  blds.appendChild(g);
185
384
  });
186
385
  model.parcels.forEach(function (pc) { parcels.appendChild(drawParcel(pc)); });
187
- model.pins.forEach(function (pin) { pins.appendChild(drawPin(pin)); });
386
+ if (model.living) {
387
+ // the capsules plug in once, on the first living drawing; a redraw does not replay it
388
+ var st = $('cityStage');
389
+ if (drewLiving && st) st.setAttribute('data-drawn', '');
390
+ drewLiving = true;
391
+ if (sky) model.sky.forEach(function (p) { sky.appendChild(pathEl(p)); });
392
+ if (pods) {
393
+ model.shafts.forEach(function (sh) { pods.appendChild(drawShaft(sh)); });
394
+ model.lanes.forEach(function (L) {
395
+ L.pods.forEach(function (pod) { pods.appendChild(drawPod(L, pod)); });
396
+ // a lane past its convoy says how many more agents ride it
397
+ if (L.more && L.more.x != null) {
398
+ var more = span('city-lane__more');
399
+ more.textContent = '+' + L.more.n;
400
+ more.style.left = L.more.x + 'px';
401
+ more.style.top = L.more.y + 'px';
402
+ pods.appendChild(more);
403
+ }
404
+ });
405
+ model.civic.forEach(function (c) { if (!c.plate.hidden) pods.appendChild(drawPlate(c)); });
406
+ }
407
+ if (hits) model.civic.forEach(function (c) { hits.appendChild(drawHit(c)); });
408
+ }
409
+ model.pins.forEach(function (pin) {
410
+ var el = drawPin(pin);
411
+ if (model.living) el.firstChild.addEventListener('click', function () { openCard(pin.building); });
412
+ pins.appendChild(el);
413
+ });
188
414
  var lineEl = $('cityLine');
189
415
  if (lineEl && data.loaded) lineEl.textContent = model.line;
190
416
  var stage = $('cityStage');
191
417
  if (stage) stage.setAttribute('aria-label', model.label);
418
+ var start = $('cityStart');
419
+ if (start && model.cta) {
420
+ start.textContent = model.cta.label;
421
+ start.setAttribute('href', model.cta.href);
422
+ start.setAttribute('aria-label', model.cta.aria);
423
+ }
424
+ drawPlaque(model.plaque || null);
425
+ if (openKey) renderCard(openKey);
192
426
  return model;
193
427
  }
194
428
 
429
+ // ---------- the card a building opens in place (task 1004702) ----------
430
+ // A glass panel beside the building, over a light hush with the building left
431
+ // clear: its name, who holds it, one fact and what waits there; the building's
432
+ // first ask, answered in place (the owner's pick, 2026-10-13) through the same
433
+ // route the deck takes (GovDeck.sendAnswer: the desk row's own request); then
434
+ // two doors, "decide all N in a row" to the deck and "go inside" to the
435
+ // building on the street. Without the deck's scripts it is the row and doors.
436
+ var here = { done: {}, busy: false, said: '', room: null, roomAsked: false, cv: null };
437
+ function civicOf(key) { return model && model.civic ? model.civic.filter(function (c) { return c.key === key; })[0] || null : null; }
438
+ // The building's button: the keyboard's way in, and where the card is placed beside.
439
+ function hitFor(key) {
440
+ var hits = $('cityHits');
441
+ return hits && hits.querySelector ? hits.querySelector('[data-building="' + key + '"]') : null;
442
+ }
443
+ // the building's pin head, else its button: what the hairline and the halo find
444
+ function anchorFor(key) {
445
+ var pins = $('cityPins');
446
+ var pin = pins && pins.querySelector ? pins.querySelector('[data-building="' + key + '"]') : null;
447
+ return (pin && pin.firstChild) || hitFor(key);
448
+ }
449
+ function openCard(key, from) {
450
+ var was = openKey;
451
+ openKey = key;
452
+ if (was !== key) { here.said = ''; here.cv = null; }
453
+ if (was && was !== key) showPlate(was, false);
454
+ showPlate(key, true);
455
+ var el = renderCard(key);
456
+ if (!el) return;
457
+ place(el, from || hitFor(key));
458
+ var first = el.querySelector ? el.querySelector('.ask__opt, .city-card__go, .city-card__enter') : null;
459
+ if (first && first.focus) first.focus();
460
+ }
461
+ function closeCard() {
462
+ var el = $('cityCard');
463
+ var key = openKey;
464
+ openKey = null;
465
+ here.said = ''; here.cv = null;
466
+ if (key) showPlate(key, false);
467
+ if (el) { clear(el); el.setAttribute('hidden', ''); }
468
+ clear($('citySpot'));
469
+ var hits = $('cityHits');
470
+ if (key && hits && hits.querySelector) { var b = hits.querySelector('[data-building="' + key + '"]'); if (b && b.focus) b.focus(); }
471
+ }
472
+ // the building's first ask, as the deck draws it (needs-you first, then the oldest)
473
+ function firstAsk(key) {
474
+ var K = window.GovDeck;
475
+ if (!K || !K.drawAsk || !data.living || !data.docket) return null;
476
+ var q = K.queueOf(data.living, { done: here.done, delegated: {}, skipped: [], extra: [] }, key);
477
+ if (!q.length) return null;
478
+ var cv = K.cardView(q[0], { docket: data.docket, room: here.room, constitution: data.raw.constitution, goals: (data.goals || []).filter(function (g) { return g && g.status === 'open'; }), now: Date.now(), viewerId: data.docket.viewer ? String(data.docket.viewer.id) : null });
479
+ // a sitting's ballot is read once, when a vote's card first needs it
480
+ if (cv && cv.card && cv.card.loading && !here.roomAsked) {
481
+ here.roomAsked = true;
482
+ getJson('/api/bongos/government/board/items').then(function (r) {
483
+ here.room = r && (Array.isArray(r.open) || Array.isArray(r.closed)) ? r : { open: [], closed: [] };
484
+ if (openKey) renderCard(openKey);
485
+ });
486
+ }
487
+ return cv;
488
+ }
489
+ function sentence(s) { var t = String(s || '').trim(); return t ? t.charAt(0).toUpperCase() + t.slice(1) + (/[.!?]$/.test(t) ? '' : '.') : ''; }
490
+ function renderCard(key) {
491
+ var el = $('cityCard');
492
+ var c = civicOf(key);
493
+ if (!el || !c) return null;
494
+ var card = c.card, K = window.GovDeck;
495
+ clear(el);
496
+ el.removeAttribute('hidden');
497
+ el.className = 'city-card gov-glass';
498
+ el.setAttribute('data-building', key);
499
+ el.setAttribute('aria-label', card.name);
500
+ var head = node('div', 'city-card__head');
501
+ var top = node('div', 'city-card__top');
502
+ top.appendChild(node('p', 'city-card__name', card.name));
503
+ var close = node('button', 'city-card__close');
504
+ close.setAttribute('type', 'button');
505
+ close.setAttribute('aria-label', 'Close');
506
+ close.setAttribute('aria-keyshortcuts', 'Escape');
507
+ close.appendChild(node('kbd', null, 'esc'));
508
+ close.addEventListener('click', closeCard);
509
+ top.appendChild(close);
510
+ head.appendChild(top);
511
+ // who holds it, one fact, and what waits there (salmon only while it is yours)
512
+ var hold = K && K.holdWords ? K.holdWords(data.living, key) : card.plate ? (card.lead ? 'Led by ' + card.plate + '.' : 'The owner holds it until a Lead is seated.') : '';
513
+ var words = [hold, card.fact && card.fact.text ? sentence(card.fact.text) : ''].filter(Boolean).join(' ');
514
+ var line = node('p', 'city-card__f', words);
515
+ var waits = K && K.waitWords ? K.waitWords(card.on_you, card.on_others) : sentence(countText(card));
516
+ line.appendChild(node('span', card.on_you ? 'city-card__count city-card__count--you' : 'city-card__count', (words ? ' ' : '') + waits));
517
+ head.appendChild(line);
518
+ el.appendChild(head);
519
+ var cv = firstAsk(key);
520
+ here.cv = cv;
521
+ if (cv) el.appendChild(K.drawAsk(document, cv, { pick: function (op) { answer(key, cv, op); } }, { inner: true, busy: here.busy, cls: 'city-ask' }));
522
+ if (here.said) {
523
+ var said = node('p', 'city-card__said', here.said);
524
+ said.setAttribute('role', 'status');
525
+ el.appendChild(said);
526
+ }
527
+ var acts = node('div', 'city-card__acts');
528
+ var n = card.on_you || card.on_others;
529
+ if (n) {
530
+ var go = node('a', 'city-card__go', n === 1 ? 'decide it on the deck' : 'decide all ' + n + ' in a row');
531
+ go.setAttribute('href', card.deck_href);
532
+ acts.appendChild(go);
533
+ }
534
+ var enter = node('a', 'city-card__enter', 'go inside');
535
+ enter.setAttribute('href', card.enter_href);
536
+ acts.appendChild(enter);
537
+ el.appendChild(acts);
538
+ return el;
539
+ }
540
+ // One answer, through the deck's own sender: the same request its desk row sends.
541
+ function send(req) {
542
+ return fetch(req.url, { method: req.method, credentials: 'same-origin', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: JSON.stringify(req.body || {}) })
543
+ .then(function (r) {
544
+ return Promise.resolve(r && r.json ? r.json() : {}).catch(function () { return {}; })
545
+ .then(function (b) { return { ok: !!(r && r.ok), status: r ? r.status : 0, body: b || {} }; });
546
+ })
547
+ .catch(function () { return { ok: false, status: 0, body: {} }; });
548
+ }
549
+ function answer(key, cv, op) {
550
+ var K = window.GovDeck;
551
+ if (!K || here.busy || !op) return Promise.resolve(false);
552
+ if (op.plan && op.plan.mode === 'record') { here.said = 'This one is signed off with a recording: decide it on the deck.'; renderCard(key); return Promise.resolve(false); }
553
+ here.busy = true;
554
+ renderCard(key);
555
+ return K.sendAnswer(op, send).then(function (res) {
556
+ here.busy = false;
557
+ here.said = res.toast;
558
+ if (res.ok) {
559
+ here.done[cv.id] = true;
560
+ // every count on the page leaves out what was answered here (gov-feeds.js)
561
+ if (window.GovFeeds && window.GovFeeds.changed) window.GovFeeds.changed();
562
+ }
563
+ if (res.ok && res.stored) return load().then(function () { return true; });
564
+ if (openKey) { draw(painted || effectiveTheme()); var f = $('cityCard'); var first = f && f.querySelector ? f.querySelector('.ask__opt, .city-card__go') : null; if (first && first.focus) first.focus(); }
565
+ return res.ok;
566
+ });
567
+ }
568
+ // 1 / 2 / 3 answer the card's ask, as on the deck
569
+ function cardKey(e) {
570
+ if (!e) return;
571
+ if (e.key === 'Escape') { closeCard(); return; }
572
+ if (!here.cv || !openKey || e.ctrlKey || e.metaKey || e.altKey || !/^[1-3]$/.test(String(e.key))) return;
573
+ var op = here.cv.options[Number(e.key) - 1];
574
+ if (!op) return;
575
+ if (e.preventDefault) e.preventDefault();
576
+ answer(openKey, here.cv, op);
577
+ }
578
+ // Beside the building on a wide stage, over a light hush with the building
579
+ // clear and a hairline to its pin; a sheet along the foot on a phone (city.css).
580
+ function place(el, from) {
581
+ var main = $('hall-main');
582
+ if (!from || !main || typeof from.getBoundingClientRect !== 'function' || typeof main.getBoundingClientRect !== 'function') return;
583
+ var narrow = window.matchMedia ? window.matchMedia('(max-width: 640px)').matches : false;
584
+ if (narrow) { el.style.left = ''; el.style.top = ''; }
585
+ var K = window.GovDeck, layer = $('citySpot');
586
+ if (!layer && K && K.spot) {
587
+ layer = node('div', 'gov-spot city-spot');
588
+ layer.setAttribute('id', 'citySpot');
589
+ layer.setAttribute('aria-hidden', 'true');
590
+ main.appendChild(layer);
591
+ }
592
+ var c = civicOf(openKey);
593
+ if (K && K.spot && layer) { K.spot(document, layer, main, anchorFor(openKey) || from, el, { place: narrow ? null : el, top: 62, you: !!(c && c.card.on_you) }); return; }
594
+ if (narrow) return;
595
+ var r = from.getBoundingClientRect(), m = main.getBoundingClientRect();
596
+ var w = el.offsetWidth || 360, h = el.offsetHeight || 320;
597
+ var left = r.right - m.left + 14;
598
+ if (left + w > m.width - 16) left = r.left - m.left - w - 14;
599
+ var top = Math.max(76, Math.min(r.top - m.top - 12, m.height - h - 16));
600
+ el.style.left = Math.max(16, Math.round(left)) + 'px';
601
+ el.style.top = Math.round(top) + 'px';
602
+ }
603
+
195
604
  function boot() {
196
605
  var start = $('cityStart');
197
606
  if (start) start.setAttribute('href', City.STREET_HREF);
@@ -199,6 +608,17 @@
199
608
  if (btn) btn.addEventListener('click', toggleTheme);
200
609
  if (media && media.addEventListener) media.addEventListener('change', function () { if (!storedTheme()) draw(applyTheme()); });
201
610
  window.addEventListener('resize', fit);
611
+ var card = $('cityCard');
612
+ if (card) card.addEventListener('keydown', cardKey);
613
+ // the page's counts leave out what the card answered (gov-feeds.js)
614
+ if (window.GovFeeds && typeof window.GovFeeds.useSession === 'function') window.GovFeeds.useSession(function () { return { answered: Object.keys(here.done), extra: [] }; });
615
+ var frame = $('cityFrame');
616
+ // a click anywhere on the city but a building (its faces, its pin, its plate) closes the card
617
+ if (frame) frame.addEventListener('click', function (e) {
618
+ var t = e && e.target;
619
+ if (!openKey || (t && t.closest && t.closest('.city-hit, [data-civic], .city-pin__head, .city-plate'))) return;
620
+ closeCard();
621
+ });
202
622
  fit();
203
623
  painted = applyTheme();
204
624
  draw(painted);
@@ -206,6 +626,6 @@
206
626
  return load();
207
627
  }
208
628
 
209
- window.GovCityPage = { boot: boot, draw: draw, load: load, fit: fit, FEEDS: FEEDS, THEME_KEY: THEME_KEY };
629
+ window.GovCityPage = { boot: boot, draw: draw, load: load, fit: fit, openCard: openCard, closeCard: closeCard, FEEDS: FEEDS, LIVING_FEEDS: LIVING_FEEDS, THEME_KEY: THEME_KEY };
210
630
  window.GovCityPage.ready = boot();
211
631
  }());