@bongos/core 1.25.31 → 1.25.32

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.
@@ -20,8 +20,8 @@
20
20
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
21
21
  <link rel="stylesheet" href="/builders/style.css?v=2026-11-11-helm4d">
22
22
  <link rel="stylesheet" href="/builders/visor.css?v=2026-11-14-world">
23
- <link rel="stylesheet" href="/builders/welcome-boot.css?v=2026-11-07-brand-mark">
24
- <link rel="stylesheet" href="/builders/shell-game.css?v=2026-11-11-view2">
23
+ <link rel="stylesheet" href="/builders/welcome-boot.css?v=2026-11-14-cloud">
24
+ <link rel="stylesheet" href="/builders/shell-game.css?v=2026-11-14-cloud">
25
25
  <link rel="stylesheet" href="/builders/hall-ask.css">
26
26
  <link rel="stylesheet" href="/builders/home-city.css?v=2026-11-14-world">
27
27
  <link rel="stylesheet" href="/builders/home-gallery.css">
@@ -102,8 +102,8 @@
102
102
  BEFORE builders.js so the chrome ids exist when events are wired. -->
103
103
  <!-- task 1004785: the first-visit-of-the-day welcome (the tower). Before shell.js, which asks it
104
104
  before the craft picker so the two never stack. Home only. -->
105
- <script src="/builders/welcome-boot.js?v=2026-11-07-brand-mark" defer></script>
106
- <script src="/builders/shell-game.js?v=2026-11-11-view2" defer></script>
105
+ <script src="/builders/welcome-boot.js?v=2026-11-14-cloud" defer></script>
106
+ <script src="/builders/shell-game.js?v=2026-11-14-cloud" defer></script>
107
107
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers" defer></script>
108
108
  <script src="/builders/craft-mode.js?v=2026-11-10-visor" defer></script>
109
109
  <script src="/builders/shell.js?v=2026-11-13-founding" defer></script>
@@ -1,5 +1,5 @@
1
1
  /* shell-game.css — the game's start screen and load screen (task 1005143; shell-game.js start()),
2
- in the Helm look (task 1005192, ADR 0402). Both stand over the welcome's painting. The start
2
+ in the Helm look (task 1005192, ADR 0402). Both stand over the city in the clouds (task 1005308). The start
3
3
  screen is one full-height night panel on the left, closed left and right by an ice hairline:
4
4
  the mark and the project's name in the name voice, the five choices as full-width rows, one
5
5
  line at the foot. The load screen is the same panel language, centred: the week in the name
@@ -18,11 +18,10 @@ html.gs-open { overflow: hidden; }
18
18
  .gs--leaving { opacity: 0; pointer-events: none; }
19
19
  .gs--load { align-items: center; justify-content: center; padding: var(--ds-space-lg) var(--ds-space-gutter-narrow); }
20
20
 
21
- /* the still: the painting, covering the screen. The start screen leaves it bare (the panel is
21
+ /* the still: the city in the clouds (.cloud-scene, welcome-boot.css), covering the screen. The start screen leaves it bare (the panel is
22
22
  its own ground); the load screen lays the scrim over it so the centred panel reads anywhere. */
23
23
  .gs__art { position: absolute; inset: 0; }
24
24
  .gs--load .gs__art::after { content: ""; position: absolute; inset: 0; background: var(--scrim); }
25
- .gs__img { width: 100%; height: 100%; object-fit: cover; display: block; }
26
25
 
27
26
  /* the two panels: night over the art, an ice hairline left and right, no corner, no shadow */
28
27
  .gs__menu, .gs__load {
@@ -87,9 +87,9 @@
87
87
 
88
88
  // ---------- the start screen and the load screen (task 1005143, BONGOS-V3 R03) ----------
89
89
  // A fresh sign-in opens on the start screen: Start, Options, Achievements, Extras, Quit over a
90
- // still. Start turns game mode on and passes through the load screen, where the project city
90
+ // the city in the clouds (task 1005308; the painting before it is retired). Start turns game mode on and passes through the load screen, where the project city
91
91
  // builds itself up week by week under a tip, and then the world is there. welcome-boot.js calls
92
- // start() on Home (where a sign-in lands) and hands in the still; this file draws both screens.
92
+ // start() on Home (where a sign-in lands) and hands in the scene's painter; this file draws both screens.
93
93
  // Rules are in modules/hall-ui/records/start-screen.md.
94
94
  var MENU = [
95
95
  { id: 'start', label: 'Start' },
@@ -155,7 +155,7 @@
155
155
  }
156
156
  // The Helm panel (task 1005192, ADR 0402): the mark and the name, the choices, one line at the foot.
157
157
  function screenHtml(o) {
158
- return '<div class="gs__art" aria-hidden="true">' + (o.art ? '<img class="gs__img" alt="" src="' + esc(o.art) + '" decoding="async">' : '') + '</div>' +
158
+ return '<div class="gs__art" aria-hidden="true"></div>' +
159
159
  '<div class="gs__menu">' +
160
160
  '<div class="gs__top">' +
161
161
  '<div class="gs__head">' + markHtml() +
@@ -369,8 +369,8 @@
369
369
  return v === 'game' || v === 'classic' ? v : null;
370
370
  }
371
371
 
372
- // Put the start screen up. `o`: { name, art, done, signIn } — the project's name, the still to
373
- // stand behind both screens, what to do once the world is showing, and (signed out) how to sign
372
+ // Put the start screen up. `o`: { name, scene, done, signIn } — the project's name, the painter of
373
+ // the city that stands behind both screens (scene(host), welcome-boot.js paintScene), what to do once the world is showing, and (signed out) how to sign
374
374
  // in once a view is picked. True when it is up; false for a classic pick made before sign-in,
375
375
  // which goes straight to the hall.
376
376
  function start(o) {
@@ -387,6 +387,7 @@
387
387
  root.setAttribute('aria-modal', 'true');
388
388
  root.setAttribute('aria-labelledby', 'gs-title');
389
389
  root.innerHTML = screenHtml(opts);
390
+ if (typeof opts.scene === 'function') { try { opts.scene(root.querySelector('.gs__art')); } catch (_) { /* the ground stands */ } }
390
391
  root.addEventListener('click', onClick);
391
392
  document.addEventListener('keydown', onKey, true);
392
393
  document.body.appendChild(root);
@@ -1,7 +1,8 @@
1
1
  /* welcome-boot.css — the first-visit-of-the-day welcome (tasks 1004785, 1004818; welcome-boot.js).
2
- The scene is the owner's painting of a circuit city (welcome-day.webp / welcome-night.webp) with
3
- motion laid over it. Its palette is the painting's, by time of day rather than by hall theme, so its
4
- ink is fixed here rather than --ink (which flips with the theme). */
2
+ The scene is the city in the clouds, drawn in the hour's sky (task 1005308; the circuit-city
3
+ painting it replaced is retired). Its ink follows day or night rather than the hall theme, so it
4
+ is fixed here rather than --ink. .cloud-scene is also the ground of the game's start and load
5
+ screens (shell-game.js), which this sheet shares the page with. */
5
6
  html.wb-open { overflow: hidden; }
6
7
  .wb {
7
8
  position: fixed; inset: 0; z-index: 1000; overflow: hidden;
@@ -10,35 +11,21 @@ html.wb-open { overflow: hidden; }
10
11
  }
11
12
  .wb--day { color: #14161c; background: linear-gradient(180deg, #a9d6dc 0%, #f1d9d0 55%, #e9cfc6 100%); }
12
13
 
13
- /* the painting: covers the screen at its own 1024x572 shape, so the motion layer stays on its marks */
14
+ /* the scene: the city in the clouds (welcome-boot.js paintScene), on the hour's sky. It covers the
15
+ screen; the city stands on the bottom edge, so a tall screen shows more sky and a wide one crops it. */
14
16
  .wb__art {
15
- position: absolute; left: 50%; top: 50%; z-index: 1;
16
- width: max(100vw, calc(100vh * 1024 / 572)); aspect-ratio: 1024 / 572;
17
- transform: translate(-50%, -50%);
17
+ position: absolute; inset: 0; z-index: 1;
18
18
  transition: transform 2.2s cubic-bezier(.45, 0, .25, 1);
19
19
  }
20
- .wb__pan { position: absolute; inset: 0; transform-origin: 62% 42%; animation: wb-pan 40s ease-in-out infinite alternate; }
21
- @keyframes wb-pan { from { transform: scale(1); } to { transform: scale(1.07) translateX(-1.2%); } }
22
- .wb__img, .wb__fx, .wb-motion { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
23
- .wb__img { object-fit: cover; }
24
- .wb__img[hidden] { display: none; }
25
-
26
- /* the motion: pulses climb the circuit towers, cars cross, and at night the lights blink */
27
- .wb-pulses { opacity: 0; transition: opacity 1.6s ease-out; }
28
- .wb--live .wb-pulses { opacity: 1; }
29
- .wb-pulse { fill: none; stroke-width: 2.2; stroke-linecap: round; stroke-dasharray: 7 100; stroke-dashoffset: 7; animation: wb-climb linear infinite; }
30
- @keyframes wb-climb { to { stroke-dashoffset: -100; } }
31
- .wb--night .wb-pulse { filter: drop-shadow(0 0 3px currentColor); }
32
- .wb--day .wb-pulse { opacity: .8; stroke-width: 1.8; }
33
- .wb-car { animation: wb-car linear infinite; }
34
- @keyframes wb-car {
35
- 0% { transform: translate(0, 0); opacity: 0; }
36
- 10%, 88% { opacity: 1; }
37
- 100% { transform: translate(var(--wb-dx), var(--wb-dy)); opacity: 0; }
20
+ .wb__pan { position: absolute; inset: 0; transform-origin: 50% 70%; animation: wb-pan 40s ease-in-out infinite alternate; }
21
+ @keyframes wb-pan { from { transform: scale(1); } to { transform: scale(1.05) translateX(-1%); } }
22
+ .cloud-scene { background: linear-gradient(to bottom, var(--sky-now-top), var(--sky-now-mid) 46%, var(--sky-now-haze)); }
23
+ .cloud-scene__svg {
24
+ position: absolute; inset: 0; width: 100%; height: 100%; display: block; pointer-events: none;
25
+ background: radial-gradient(ellipse 70% 60% at 50% 72%, var(--sky-now-cloud), var(--sky-now-base) 30%, var(--sky-now-haze) 62%, transparent);
38
26
  }
39
- .wb-star { opacity: .6; }
40
- .wb-moon { animation: wb-moon 6s ease-in-out infinite alternate; filter: blur(6px); }
41
- @keyframes wb-moon { from { opacity: .35; } to { opacity: .8; } }
27
+ .cloud-scene__svg .hc-stars { opacity: var(--sky-now-stars); }
28
+
42
29
  .wb__cloud { position: absolute; z-index: 2; border-radius: 50%; background: rgba(255, 255, 255, .06); filter: blur(30px); animation: wb-drift linear infinite; pointer-events: none; }
43
30
  .wb--day .wb__cloud { background: rgba(255, 255, 255, .35); filter: blur(28px); }
44
31
  .wb__cloud--a { top: 8vh; width: 40vw; height: 7vh; animation-duration: 80s; animation-delay: -20s; }
@@ -71,7 +58,7 @@ html.wb-open { overflow: hidden; }
71
58
  font-family: var(--font-display); font-size: clamp(18px, 2.6vw, 26px); font-weight: 300;
72
59
  letter-spacing: -.01em; text-transform: lowercase; white-space: nowrap;
73
60
  }
74
- /* the mark's ink follows the painting: pale by night, dark by day; the moon stays salmon */
61
+ /* the mark's ink follows the hour: pale by night, dark by day; the moon stays salmon */
75
62
  .wb__mark { --mark-ink: rgba(255, 255, 255, .88); }
76
63
  .wb__planet { transform-box: fill-box; transform-origin: center; }
77
64
  .wb__moonlet { transform-box: view-box; transform-origin: 100px 100px; }
@@ -137,7 +124,7 @@ html.wb-open { overflow: hidden; }
137
124
  .wb__role small { display: block; margin-top: 3px; }
138
125
  .wb__role i { display: inline-block; width: 9px; height: 9px; border-radius: 50%; margin-right: 7px; }
139
126
 
140
- /* the veil: white by day, black by night, lifting slowly once the painting is in */
127
+ /* the veil: white by day, black by night, lifting slowly once the scene is painted */
141
128
  .wb__veil { position: absolute; inset: 0; z-index: 6; pointer-events: none; background: #000; opacity: 1; transition: opacity 1.6s ease-out; }
142
129
  .wb--day .wb__veil { background: #fff; }
143
130
  .wb--lit .wb__veil { opacity: 0; }
@@ -145,7 +132,7 @@ html.wb-open { overflow: hidden; }
145
132
  /* the way out: the view sinks past the rooftops and the welcome fades into the hall underneath;
146
133
  bound for another room, the veil comes back instead */
147
134
  .wb--leaving { pointer-events: none; }
148
- .wb--leaving .wb__art { transform: translate(-50%, calc(-50% - 30vh)) scale(1.12); }
135
+ .wb--leaving .wb__art { transform: translateY(-30vh) scale(1.12); }
149
136
  .wb--leaving .wb__banner { transform: translate(-50%, -40vh); }
150
137
  .wb--done.wb--leaving .wb__after { opacity: 0; transform: translateY(24px); transition: opacity .5s, transform .5s; }
151
138
  .wb--leaving:not(.wb--to-room) { opacity: 0; transition: opacity 1.8s ease-in .5s; }
@@ -1,13 +1,16 @@
1
1
  // welcome-boot.js — the welcome on a builder's first visit of the day (tasks 1004785, 1004818; idea 1001262).
2
2
  //
3
- // Home only. The scene is the owner's own painting of a circuit city (welcome-day.webp and
4
- // welcome-night.webp, 2026-10-23), brought to life: a slow drift across the skyline, data pulses
5
- // climbing the circuit towers, hover cars crossing, and by night blinking aircraft lights, a breathing
6
- // moon and twinkling stars. The banner at the top is the logo, animated, and a "Built on
7
- // cloudbongos.com" credit sits in the bottom-right corner (task 1004953). The picture follows the local time of day: the day painting from 06:00 to 17:59, the
8
- // night one otherwise. It opens from a white (day) or black (night) veil, then "Good morning, <name>"
9
- // comes up with two choices. The way out is slow: the view sinks past the rooftops and fades into the
10
- // hall underneath.
3
+ // Home only. The scene is the city in the clouds (task 1005196), drawn by home-city-draw.js and
4
+ // city-draw.js in the hour's sky (sky-time.js): the Cloud Bongos core and the museum standing in
5
+ // cloud, with a slow drift. It replaced the owner's circuit-city painting (a day and a
6
+ // night WebP still), retired everywhere by task 1005308 (owner, 2026-11-14), and with it the
7
+ // lights that were pinned to the painting's pixels. paintScene() is also what the game's start and
8
+ // load screens stand on (shell-game.js), so every screen before the hall shows the same city.
9
+ // The banner at the top is the logo, animated, and a "Built on cloudbongos.com" credit sits in the
10
+ // bottom-right corner (task 1004953). The ink follows the local time of day: day from 06:00 to
11
+ // 17:59, night otherwise. It opens from a white (day) or black (night) veil, then
12
+ // "Good morning, <name>" comes up with two choices. The way out is slow: the view sinks past the
13
+ // rooftops and fades into the hall underneath.
11
14
  //
12
15
  // Rules:
13
16
  // - A signed-in builder sees it once per LOCAL day. A fresh sign-in (the sign-in click leaves a
@@ -22,10 +25,11 @@
22
25
  // picker asked (OTBCraftMode.markAsked), so the two never stack.
23
26
  // - Picking a role also sets the hall's craft mode. Governing is a seat (craft-mode.js), so the
24
27
  // Owner role shows only when OTBCraftMode offers it.
25
- // - A painting that will not load leaves the sky gradient behind it, and the welcome works the same.
26
- // - prefers-reduced-motion: no motion; the still painting and the greeting at once, leaving is immediate.
28
+ // - Without the city's painters (a page that does not load them) the hour's sky gradient stands alone,
29
+ // and the welcome works the same.
30
+ // - prefers-reduced-motion: no motion; the still city and the greeting at once, leaving is immediate.
27
31
  //
28
- // The pure parts (the day rule, the logo, the roles, the motion layout) are exported for
32
+ // The pure parts (the day rule, the logo, the roles, the scene's node rewrite) are exported for
29
33
  // tests/welcome_boot.mjs; tests/welcome_boot_browser.mjs runs the overlay itself in a real browser.
30
34
  (function () {
31
35
  'use strict';
@@ -33,8 +37,7 @@
33
37
  var DAY_KEY = 'hall-welcome-day:';
34
38
  var SIGNIN_KEY = 'hall-welcome-signin';
35
39
  var SIGNIN_HREF = '/api/bongos/auth/web/start';
36
- var ART = { day: '/builders/welcome-day.webp', night: '/builders/welcome-night.webp' };
37
- var ART_WAIT = 1500; // the veil lifts once the painting is in, or after this long
40
+ var ART_WAIT = 1500; // the veil lifts once the scene is painted, or after this long
38
41
  var RUN_MS = 3400; // the city powering up before the greeting
39
42
  var LEAVE_MS = 2400; // the way down into the hall
40
43
 
@@ -46,9 +49,9 @@
46
49
  var h = now.getHours();
47
50
  return h < 12 ? 'Good morning' : h < 18 ? 'Good afternoon' : 'Good evening';
48
51
  }
49
- // Day is 06:00 to 17:59 local: a white veil and the day painting. Night: a black veil and the night one.
52
+ // Day is 06:00 to 17:59 local: a white veil and dark ink. Night: a black veil and pale ink.
53
+ // (The scene's own colours follow the hour more finely: sky-time.js.)
50
54
  function timeOfDay(now) { var h = now.getHours(); return h >= 6 && h < 18 ? 'day' : 'night'; }
51
- function artFor(when) { return when === 'day' ? ART.day : ART.night; }
52
55
  function nameOf(builder) {
53
56
  return String((builder && (builder.display_name || builder.github_login)) || 'builder');
54
57
  }
@@ -88,73 +91,61 @@
88
91
  }
89
92
  function roleById(id) { return ROLES.filter(function (r) { return r.id === id; })[0] || null; }
90
93
 
91
- // ---------- the motion, laid over the painting ----------
92
- // In the painting's own pixels (1024x572; both paintings share one layout), so every light sits on
93
- // the thing it belongs to however the picture is cropped to the screen.
94
- var AW = 1024, AH = 572;
95
- // The circuit towers: a box each, and the colour its traces glow in the night painting.
96
- var TOWERS = [
97
- { x: 140, y: 168, w: 62, h: 118, glow: '#60a5fa' },
98
- { x: 280, y: 150, w: 58, h: 200, glow: '#22d3ee' },
99
- { x: 184, y: 296, w: 100, h: 196, glow: 'var(--accent)' },
100
- { x: 636, y: 222, w: 42, h: 140, glow: 'color-mix(in srgb, var(--sea) 55%, #ffffff)' },
101
- { x: 702, y: 212, w: 84, h: 190, glow: 'color-mix(in srgb, var(--sea) 55%, #ffffff)' },
102
- { x: 2, y: 196, w: 52, h: 150, glow: 'color-mix(in srgb, var(--sea) 55%, #ffffff)' },
103
- { x: 910, y: 410, w: 110, h: 160, glow: '#22d3ee' },
104
- ];
105
- // The hover-car lanes on the right (the masts' blinking lights went in task 1005087).
106
- var LANES = [
107
- { x1: 770, y1: 350, x2: 1040, y2: 362, dur: 5.5 },
108
- { x1: 1040, y1: 340, x2: 760, y2: 352, dur: 7 },
109
- { x1: 820, y1: 258, x2: 1040, y2: 254, dur: 6.5 },
110
- ];
111
- var MOON = [665, 82];
112
- function seeded(s) { return function () { s = (s * 16807) % 2147483647; return (s - 1) / 2147483646; }; }
113
- // Where each data pulse climbs: a vertical run inside a tower, bottom to top. Pure, so it is testable.
114
- function pulseRuns(towers, perTower, seed) {
115
- var rnd = seeded(seed || 7), out = [];
116
- towers.forEach(function (t, ti) {
117
- for (var i = 0; i < perTower; i++) {
118
- var x = Math.round((t.x + 6 + rnd() * (t.w - 12)) * 10) / 10;
119
- var y1 = Math.round((t.y + t.h * (0.75 + rnd() * 0.25)) * 10) / 10;
120
- var y0 = Math.round((t.y + t.h * rnd() * 0.3) * 10) / 10;
121
- out.push({ tower: ti, x: x, y0: y0, y1: y1, glow: t.glow, dur: Math.round((2.2 + rnd() * 2.4) * 100) / 100, delay: Math.round(rnd() * 400) / 100 });
122
- }
123
- });
124
- return out;
125
- }
126
-
94
+ // ---------- the scene: the city in the clouds (task 1005308) ----------
95
+ // Home's own painters draw it (window.OTBHomeCityDraw + window.GovCity), from an empty week: the
96
+ // Cloud Bongos core and the museum, standing in cloud, in the hour's --sky-now-* tokens. It is a
97
+ // backdrop, so every door the city carries on Home (a link, a tooltip, a goal) becomes a plain
98
+ // group, nothing in it takes focus, and its ids are prefixed so they never meet Home's own city's.
127
99
  var NS = 'http://www.w3.org/2000/svg';
128
- function el(t, a, p) { var e = document.createElementNS(NS, t); for (var k in a) e.setAttribute(k, a[k]); if (p) p.appendChild(e); return e; }
129
-
130
- function drawMotion(host, when) {
131
- var night = when !== 'day';
132
- var rnd = seeded(11);
133
- var svg = el('svg', { viewBox: '0 0 ' + AW + ' ' + AH, preserveAspectRatio: 'none', 'class': 'wb-motion' }, host);
134
- if (night) {
135
- // the moon breathes; the stars stand still (task 1005087: no twinkle, no beacons)
136
- el('circle', { 'class': 'wb-moon', cx: MOON[0], cy: MOON[1], r: 26, fill: 'rgba(226,232,255,.25)' }, svg);
137
- for (var i = 0; i < 40; i++) {
138
- var sx = rnd() * AW, sy = rnd() * 140;
139
- if (Math.abs(sx - MOON[0]) < 50 && Math.abs(sy - MOON[1]) < 50) continue;
140
- el('circle', { 'class': 'wb-star', cx: sx.toFixed(1), cy: sy.toFixed(1), r: (0.6 + rnd() * 0.9).toFixed(2), fill: '#e0e7ff' }, svg);
141
- rnd(); rnd(); // the two draws the twinkle timing took, so every star keeps its place
142
- }
143
- }
144
- // data pulses climbing the circuit towers (the whole set brightens as the city powers up)
145
- var pulses = el('g', { 'class': 'wb-pulses' }, svg);
146
- pulseRuns(TOWERS, 3, 7).forEach(function (r) {
147
- el('line', { 'class': 'wb-pulse', x1: r.x, y1: r.y1, x2: r.x, y2: r.y0, pathLength: 100, stroke: night ? r.glow : '#ffffff',
148
- style: 'color:' + r.glow + ';animation-duration:' + r.dur + 's;animation-delay:' + (-r.delay) + 's' }, pulses);
149
- });
150
- // hover cars, each a light with a short trail, crossing on its lane
151
- LANES.forEach(function (l, i) {
152
- var g = el('g', { 'class': 'wb-car', style: '--wb-dx:' + (l.x2 - l.x1) + 'px;--wb-dy:' + (l.y2 - l.y1) + 'px;animation-duration:' + l.dur + 's;animation-delay:' + (-i * 1.7) + 's' }, svg);
153
- var dir = l.x2 > l.x1 ? -1 : 1;
154
- el('line', { x1: l.x1, y1: l.y1, x2: l.x1 + dir * 26, y2: l.y1, stroke: night ? 'rgba(125,211,252,.55)' : 'rgba(255,255,255,.7)', 'stroke-width': 2, 'stroke-linecap': 'round' }, g);
155
- el('circle', { cx: l.x1, cy: l.y1, r: 2.4, fill: night ? '#e0f2fe' : '#ffffff' }, g);
100
+ var SCENE_ID = 'cs-';
101
+ var DOOR_ATTRS = /^(href|tabindex|aria-label|data-tip|data-tip-body|data-goal|data-group|data-building|data-craft-door)$/;
102
+ var QUIET_RE = /\bhc-quiet\b/g, SPACES_RE = /\s+/g, URL_REF_RE = /url\(#/g;
103
+ // One shape node rewritten for the backdrop: { tag, attrs, kids, text } -> the same, door-free. Pure.
104
+ function sceneShape(n) {
105
+ var attrs = {};
106
+ Object.keys(n.attrs || {}).forEach(function (k) {
107
+ if (DOOR_ATTRS.test(k)) return;
108
+ var v = String(n.attrs[k]);
109
+ if (k === 'id') v = SCENE_ID + v;
110
+ else if (k === 'class') v = v.replace(QUIET_RE, '').replace(SPACES_RE, ' ').trim();
111
+ else v = v.replace(URL_REF_RE, 'url(#' + SCENE_ID);
112
+ if (k === 'class' && !v) return;
113
+ attrs[k] = v;
156
114
  });
157
- return svg;
115
+ return { tag: n.tag === 'a' ? 'g' : n.tag, attrs: attrs, kids: (n.kids || []).map(sceneShape), text: n.text == null ? null : n.text };
116
+ }
117
+ // The scene as shapes, or null when the painters are not on the page. Pure given the painters.
118
+ function sceneShapes(Draw, Gov) {
119
+ if (!Draw || typeof Draw.citySvg !== 'function' || typeof Draw.cityModel !== 'function') return null;
120
+ var out = Draw.citySvg(Draw.cityModel({ weeks: [{}] }), 0, { gov: Gov || null });
121
+ return { viewBox: out.viewBox, nodes: out.nodes.map(sceneShape) };
122
+ }
123
+ function build(n) {
124
+ var e = document.createElementNS(NS, n.tag);
125
+ Object.keys(n.attrs).forEach(function (k) { e.setAttribute(k, n.attrs[k]); });
126
+ if (n.text != null) e.textContent = n.text;
127
+ n.kids.forEach(function (c) { e.appendChild(build(c)); });
128
+ return e;
129
+ }
130
+ // Paint the city into host (it covers it; the host's class gives it the hour's sky). True when
131
+ // the city is drawn; false leaves the sky alone, which is still the right picture.
132
+ function paintScene(host) {
133
+ if (!host || typeof document === 'undefined') return false;
134
+ host.classList.add('cloud-scene');
135
+ var sky = window.OTBSkyTime;
136
+ if (sky && typeof sky.apply === 'function') { try { sky.apply(document); } catch (_) { /* the tokens' night default stands */ } }
137
+ var shapes;
138
+ try { shapes = sceneShapes(window.OTBHomeCityDraw, window.GovCity); } catch (_) { shapes = null; }
139
+ if (!shapes) return false;
140
+ var svg = document.createElementNS(NS, 'svg');
141
+ svg.setAttribute('class', 'cloud-scene__svg');
142
+ svg.setAttribute('viewBox', shapes.viewBox);
143
+ svg.setAttribute('preserveAspectRatio', 'xMidYMax slice');
144
+ svg.setAttribute('aria-hidden', 'true');
145
+ svg.setAttribute('focusable', 'false');
146
+ shapes.nodes.forEach(function (n) { svg.appendChild(build(n)); });
147
+ host.appendChild(svg);
148
+ return true;
158
149
  }
159
150
 
160
151
  // ---------- the overlay ----------
@@ -170,10 +161,7 @@
170
161
 
171
162
  function bannerTag(builder) { return builder ? 'div' : 'header'; }
172
163
  function overlayHtml(builder, now, when) {
173
- return '<div class="wb__art" aria-hidden="true"><div class="wb__pan">' +
174
- '<img class="wb__img" alt="" src="' + artFor(when) + '" decoding="async">' +
175
- '<div class="wb__fx" id="wb-fx"></div>' +
176
- '</div></div>' +
164
+ return '<div class="wb__art" aria-hidden="true"><div class="wb__pan" id="wb-clouds"></div></div>' +
177
165
  '<div class="wb__cloud wb__cloud--a" aria-hidden="true"></div><div class="wb__cloud wb__cloud--b" aria-hidden="true"></div>' +
178
166
  // For a visitor the welcome IS the page (the shell draws no header for them), so its logo
179
167
  // banner is the page's one <header>; over a signed-in hall the shell's header stands, and the
@@ -224,7 +212,7 @@
224
212
  if (first && first.focus) first.focus();
225
213
  }
226
214
 
227
- // The veil lifts once the painting is in (or after ART_WAIT), then the city powers up.
215
+ // The veil lifts once the scene is painted (or after ART_WAIT), then the city powers up.
228
216
  function begin() {
229
217
  if (!root || root.classList.contains('wb--lit')) return;
230
218
  root.classList.add('wb--lit');
@@ -324,13 +312,9 @@
324
312
  document.addEventListener('keydown', onKey, true);
325
313
  document.body.appendChild(root);
326
314
  document.documentElement.classList.add('wb-open');
327
- drawMotion(root.querySelector('#wb-fx'), when);
328
- var img = root.querySelector('.wb__img');
329
- // A painting that fails to load is hidden, leaving the sky gradient; the welcome goes on regardless.
330
- var go1 = function () { if (window.requestAnimationFrame) window.requestAnimationFrame(function () { window.requestAnimationFrame(begin); }); else begin(); };
331
- img.addEventListener('load', go1);
332
- img.addEventListener('error', function () { img.hidden = true; if (root) root.classList.add('wb--bare'); go1(); });
333
- if (img.complete && img.naturalWidth) go1();
315
+ // No city painters on the page: the hour's sky stands alone; the welcome goes on regardless.
316
+ if (!paintScene(root.querySelector('#wb-clouds'))) root.classList.add('wb--bare');
317
+ if (window.requestAnimationFrame) window.requestAnimationFrame(function () { window.requestAnimationFrame(begin); });
334
318
  at(ART_WAIT, begin);
335
319
  var skip = root.querySelector('[data-wb-skip]');
336
320
  if (skip && skip.focus) skip.focus();
@@ -378,7 +362,7 @@
378
362
  // The hall's front opens on Game or Classic (task 1005219): the pick is kept, the visitor
379
363
  // signs in, and comes back to that menu (shell-game.js). Without that file, the welcome.
380
364
  if (canChoose && game.start({
381
- name: projectName(window.__BRANDING__), art: artFor(timeOfDay(new Date())),
365
+ name: projectName(window.__BRANDING__), scene: paintScene,
382
366
  signIn: function () { markSigningIn(); go(SIGNIN_HREF); },
383
367
  })) return true;
384
368
  show(null);
@@ -398,11 +382,11 @@
398
382
  shown = true;
399
383
  var c = craft();
400
384
  // A fresh sign-in, or a new visit to Home, opens on the game's start screen instead (tasks
401
- // 1005143, 1005219; shell-game.js): the same still behind it, and the craft picker asked once
385
+ // 1005143, 1005219; shell-game.js): the same city behind it, and the craft picker asked once
402
386
  // the world is showing. Classic picked before signing in: no screen, straight to the hall.
403
387
  if ((fresh || ask) && canChoose) {
404
388
  if (game.start({
405
- name: projectName(window.__BRANDING__), art: artFor(timeOfDay(now)),
389
+ name: projectName(window.__BRANDING__), scene: paintScene,
406
390
  done: function () { if (c && typeof c.onAccess === 'function') c.onAccess(access, builder); },
407
391
  })) return true;
408
392
  if (fresh) return false;
@@ -413,9 +397,9 @@
413
397
  }
414
398
 
415
399
  var api = {
416
- DAY_KEY: DAY_KEY, SIGNIN_KEY: SIGNIN_KEY, ROLES: ROLES, ART: ART, TOWERS: TOWERS, LANES: LANES, AW: AW, AH: AH,
400
+ DAY_KEY: DAY_KEY, SIGNIN_KEY: SIGNIN_KEY, ROLES: ROLES, SCENE_ID: SCENE_ID,
417
401
  localDay: localDay, firstVisitToday: firstVisitToday, greetingFor: greetingFor, nameOf: nameOf, timeOfDay: timeOfDay,
418
- artFor: artFor, projectName: projectName, logoHtml: logoHtml, BUILT_ON: BUILT_ON, rolesFor: rolesFor, pulseRuns: pulseRuns,
402
+ projectName: projectName, logoHtml: logoHtml, BUILT_ON: BUILT_ON, rolesFor: rolesFor, sceneShape: sceneShape, sceneShapes: sceneShapes, paintScene: paintScene,
419
403
  onAccess: onAccess, finish: finish, close: close,
420
404
  };
421
405
  if (typeof window !== 'undefined') { window.OTBWelcome = api; watchSignInLinks(); }
@@ -4,23 +4,23 @@ Covers: Home (`shell-game.js` `start()`, `shell-game.css`, the hand-off in `welc
4
4
 
5
5
  ## What it settled
6
6
 
7
- - **A fresh sign-in opens on the start screen, on Home.** Home is where a sign-in lands (`postLoginLanding`), and `welcome-boot.js` already knows a fresh sign-in from a return visit (the flag the sign-in click leaves). On one, `OTBWelcome.onAccess` calls `OTBShellGame.start({ name, art, done })` in place of its own greeting. The day's first visit without a sign-in keeps the welcome; a signed-out visitor keeps the welcome. A sign-in that returns to a deep link or another page goes straight there, as the welcome always did. `shell.js` is untouched.
7
+ - **A fresh sign-in opens on the start screen, on Home.** Home is where a sign-in lands (`postLoginLanding`), and `welcome-boot.js` already knows a fresh sign-in from a return visit (the flag the sign-in click leaves). On one, `OTBWelcome.onAccess` calls `OTBShellGame.start({ name, scene, done })` in place of its own greeting. The day's first visit without a sign-in keeps the welcome; a signed-out visitor keeps the welcome. A sign-in that returns to a deep link or another page goes straight there, as the welcome always did. `shell.js` is untouched.
8
8
  - **Five choices, one column: Start, Options, Achievements, Extras, Quit.** Start is the one filled pill. Options is a link to `/settings`, which IS the one options menu since task 1005144 (`records/options-menu.md`); Achievements is `/profile`. Extras opens three links under the menu (the thinking sky, the atlas, the docs). Quit calls `window.close()`; a browser only closes a tab a script opened, so when the tab is still there 300ms later the screen says "Close it to quit" instead of doing nothing.
9
9
  - **Start turns game mode on** (`set(true)`), then shows the load screen. Nothing else in the hall's interface sets it.
10
- - **The load screen is the still, a tip, and the city building up.** The still is the welcome's painting for the time of day, handed in by `welcome-boot.js` (this file names no picture). In a card of the hall's own tokens, the project city is drawn by `home-city-draw.js` `citySvg` from the one bounded read Home already uses (`GET /home/city?weeks=10`), at 31 stops from the oldest week to now, about three seconds, with "The city, N weeks ago" under it. Each stop is a whole drawing set as state; nothing animates in JavaScript. With reduced motion it is the finished city at once. With no city to draw (the read failed, the scripts are missing) the still and the tip stand for 1.4s. Either way the world opens.
10
+ - **The load screen is the still, a tip, and the city building up.** The still is the city in the clouds in the hour's sky, painted by `welcome-boot.js` `paintScene`, which is handed in as `scene` (this file draws no city of its own for it; task 1005308). In a card of the hall's own tokens, the project city is drawn by `home-city-draw.js` `citySvg` from the one bounded read Home already uses (`GET /home/city?weeks=10`), at 31 stops from the oldest week to now, about three seconds, with "The city, N weeks ago" under it. Each stop is a whole drawing set as state; nothing animates in JavaScript. With reduced motion it is the finished city at once. With no city to draw (the read failed, the scripts are missing) the still and the tip stand for 1.4s. Either way the world opens.
11
11
  - **Tips rotate** (`localStorage 'hall-game-tip'` counts them), so each load shows the next one. A tip must be true of the hall today; the first one is the Escape rule from `game-mode.md` (rewritten by task 1005144, and again by task 1005250: Escape opens the game menu).
12
12
  - **The craft picker waits for the world.** While the start screen is up the shell asks no picker (`onAccess` returned true). `done` runs once the screen is gone and asks `OTBCraftMode.onAccess` then.
13
13
  - **Keys.** While either screen is up it keeps every key: the arrows and Tab walk the menu, Escape folds Extras away and does nothing else, so the shell's Escape cannot flip the bars underneath. Once the world shows, the last Escape opens the game menu (task 1005250; the options menu until then); the screen's capture listener calls `preventDefault`, so the menu never opens from under it.
14
- - **The look is Helm (task 1005192, [ADR 0402](../../../docs/adr/0402-helm-is-the-halls-look-and-the-hall-is-dark-only.md)).** The start screen is one full-height night panel over the painting (`--night-glass`, an ice hairline left and right at `--line-strong`), `--helm-panel-w` wide. On a desk it stands off the left edge by `--helm-inset`; that token is a `clamp`, so on a phone the panel is the whole width and the sheet still has no `@media`. In it: the mark (the project's logo when it has one) over the name in the name voice; Start as a full-width accent bar under `--accent-on` that names its key ("Enter", drawn by CSS so the choice's own text stays "Start"); the other four as quiet rows on `--rule-soft` hairlines; and at the foot, over the strong hairline, the tip the load screen will show next (`#gs-line`), with the Quit note under it when the browser keeps the tab. The rows are list rows in a panel, so they are bars, not cut buttons. A row's focus is the inset two-tone ring (`--press-focus`), which is what makes focus visible on the accent bar.
15
- - **The load screen is the same panel, centred** over the painting under the hall's `--scrim`: the week in the name voice (`#gs-when`), one segment per week of the history (`#gs-segs`, the shared `.segs` / `.seg`; filled up to the week being shown, counted the way the sentence words it, and hidden from a screen reader because the sentence says the same), the city, and the tip under a hairline. With no city there are no segments.
14
+ - **The look is Helm (task 1005192, [ADR 0402](../../../docs/adr/0402-helm-is-the-halls-look-and-the-hall-is-dark-only.md)).** The start screen is one full-height night panel over the city in the clouds (`--night-glass`, an ice hairline left and right at `--line-strong`), `--helm-panel-w` wide. On a desk it stands off the left edge by `--helm-inset`; that token is a `clamp`, so on a phone the panel is the whole width and the sheet still has no `@media`. In it: the mark (the project's logo when it has one) over the name in the name voice; Start as a full-width accent bar under `--accent-on` that names its key ("Enter", drawn by CSS so the choice's own text stays "Start"); the other four as quiet rows on `--rule-soft` hairlines; and at the foot, over the strong hairline, the tip the load screen will show next (`#gs-line`), with the Quit note under it when the browser keeps the tab. The rows are list rows in a panel, so they are bars, not cut buttons. A row's focus is the inset two-tone ring (`--press-focus`), which is what makes focus visible on the accent bar.
15
+ - **The load screen is the same panel, centred** over the city in the clouds under the hall's `--scrim`: the week in the name voice (`#gs-when`), one segment per week of the history (`#gs-segs`, the shared `.segs` / `.seg`; filled up to the week being shown, counted the way the sentence words it, and hidden from a screen reader because the sentence says the same), the city, and the tip under a hairline. With no city there are no segments.
16
16
  - **The literal floor is zero.** Every colour, size, corner, shadow, face and measure in `shell-game.css` is a hall-tier token (`style.css`); `tests/hall_start_screen.mjs` fails on a typed one. The fade into the world is one CSS opacity transition, stopped by the hall's reduced-motion rule.
17
17
 
18
18
  ## For the tasks after this one
19
19
 
20
20
  - R04 (task 1005144): point Options at the options menu and move Achievements/Extras targets if those pages lose their interfaces. Escape on the start screen is free for the menu to use.
21
- - `shell-game.js` is loaded on every hall page but only Home calls `start()`; `shell-game.css` is linked on Home only. A second page that wants the start screen links the sheet and calls `start()` with its own still.
21
+ - `shell-game.js` is loaded on every hall page but only Home calls `start()`; `shell-game.css` is linked on Home only. A second page that wants the start screen links the sheet and calls `start()` with its own `scene` painter.
22
22
 
23
- - New art for the start screen (the city in the clouds) is task 1005196; `welcome-boot.js` still hands in the welcome's painting.
23
+ - Since task 1005308 (owner, 2026-11-14) both screens stand on the city in the clouds from task 1005196; the circuit-city painting is retired everywhere.
24
24
 
25
25
  Tests: `tests/hall_start_screen.mjs` (the hand-off and pure parts in a vm; both screens, the panel's place at desk and phone widths, the segments, Quit, the no-city and reduced-motion paths in a real browser).
26
26
 
@@ -1,15 +1,21 @@
1
1
  # The welcome — Home's first visit of the day (tasks 1004785, 1004818)
2
2
 
3
3
  The owner's Builders' Hall idea 1001262, second half (the first is [`home-reshuffle.md`](home-reshuffle.md)).
4
- Files: `public/welcome-boot.js` + `public/welcome-boot.css`, and the owner's two paintings
5
- `public/welcome-day.webp` / `public/welcome-night.webp`, on Home only.
4
+ Files: `public/welcome-boot.js` + `public/welcome-boot.css`, on Home only. The scene is drawn by
5
+ Home's own city painters (`sky-time.js`, `city-draw.js`, `home-city-draw.js`).
6
6
 
7
7
  **How the look got here (2026-10-23, one day of owner rounds).** Task 1004785 built v5 "the tower"
8
8
  ([`docs/design/mocks/hall/welcome/v5.src.html`](../../../docs/design/mocks/hall/welcome/v5.src.html)),
9
9
  drawn live from the Repo Atlas. The owner then asked for day and night and a slow way out, then
10
10
  swapped the tower for a glowing module city with a lunch crew on a beam, dropped the crew, and
11
11
  finally had Gemini paint the city by day and by night and asked for those to be animated (task
12
- 1004818). The atlas-drawn scene is gone; the painting is the scene.
12
+ 1004818). The atlas-drawn scene is gone.
13
+
14
+ **The painting is retired too (task 1005308, owner 2026-11-14: "retire it everywhere").** The scene
15
+ is now the city in the clouds from task 1005196, the same city Home and the City page draw, in the
16
+ hour's sky. The two WebP paintings and every light that was pinned to their pixels (the circuit
17
+ towers' pulses, the hover cars, the moon and stars) are deleted. The game's start and load screens
18
+ stand on the same scene (`records/start-screen.md`).
13
19
 
14
20
  - **When.** A signed-in builder's first Home load of the LOCAL day, per builder id
15
21
  (`localStorage['hall-welcome-day:<id>']`). Storage that throws is never shown it, because it could not
@@ -20,15 +26,16 @@ finally had Gemini paint the city by day and by night and asked for those to be
20
26
  `welcome-boot.js` loads before the `shell-trail.js` / `craft-mode.js` / `shell.js` trio, which
21
27
  `tests/craft_mode.mjs` keeps adjacent.
22
28
  - **Day and night.** `timeOfDay` makes 06:00 to 17:59 local `day`, the rest `night`; the root wears
23
- `.wb--day` or `.wb--night` and shows the matching painting. It opens from a veil (`.wb__veil`),
24
- white by day and black by night, that lifts once the painting has loaded (or after 1.5s).
25
- - **The motion is laid on the painting, in its own pixels.** `.wb__art` covers the screen at the
26
- paintings' 1024x572 shape (both share one layout), so the SVG over it (`viewBox 0 0 1024 572`) keeps
27
- every light on its mark however the screen crops: `TOWERS` (the circuit towers, where `pulseRuns`
28
- climbs), `BEACONS` (mast lights, night), `LANES` (hover cars), the moon and stars (night), plus a
29
- slow drift of the whole picture and two clouds. **A new painting must keep that layout, or the
30
- coordinates in welcome-boot.js move with it** — `tests/welcome_boot.mjs` checks both files are
31
- 1024x572 WebP.
29
+ `.wb--day` or `.wb--night`, which sets the ink. It opens from a veil (`.wb__veil`), white by day
30
+ and black by night, that lifts two frames after the scene is painted (or after 1.5s). The scene's
31
+ own colours follow the hour more finely (`sky-time.js`: night, dawn, midday, dusk, blended).
32
+ - **The scene is the city, as a backdrop.** `paintScene(host)` asks `OTBHomeCityDraw.citySvg` for an
33
+ empty week (the Cloud Bongos core and the museum, standing in cloud) and rewrites it with
34
+ `sceneShape`: every link becomes a plain group, tooltips and goal hooks are dropped, the faded
35
+ `hc-quiet` look is lifted so the core leads, and every id is prefixed `cs-` so it never meets the
36
+ ids of Home's own city on the same page. It covers the screen, standing on the bottom edge
37
+ (`xMidYMax slice`), over `.cloud-scene`'s sky gradient, with a slow drift and two clouds. The same
38
+ `paintScene` is handed to the game's start screen as `scene`.
32
39
  - **Banner.** "Working on the core of <project>" from `__BRANDING__.identity` (never a hardcoded word),
33
40
  with `core <version>` under it when the public `GET /version` reports one.
34
41
  - **Sequence.** Veil lifts, the city powers up (pulses brighten), the greeting comes up at ~3.4s.
@@ -40,11 +47,13 @@ finally had Gemini paint the city by day and by night and asked for those to be
40
47
  Artisan → stays on Home (the Ideator door, once `/thinking`, folded into Artist). A pick also calls
41
48
  `OTBCraftMode.choose()` with patron / artist / artisan. **Patron shows only where
42
49
  `OTBCraftMode.offered('patron')`** — the patron craft is a seat — and the list is built when "Start with your role" is pressed, after the seats have settled.
43
- - **No painting** (a 404) hides the image and leaves the sky gradient; everything else works.
44
- - **Colours are the paintings'**, in both hall themes: `welcome-boot.css` and `welcome-boot.js` are on
45
- `tests/hall_audit.mjs`'s literal exemptions with that reason.
46
- - **Tests.** `tests/welcome_boot.mjs` (the day rule, the paintings' files and shape, every light inside
47
- the painting and every pulse inside its tower, the banner, the role map, the load order);
48
- `tests/welcome_boot_browser.mjs` runs Home's markup with the real shell, craft mode, welcome and
49
- paintings in a browser: once a day, Skip, Esc, no double picker, the seat rule, no painting, reduced
50
- motion, the veil by day and night, the slow exit, and that the old drawn scene is gone.
50
+ - **No city painters** on the page: the root wears `.wb--bare`, the hour's sky gradient stands alone,
51
+ and everything else works.
52
+ - **The scene's colours are the hour's sky tokens only** (`--sky-now-*`); the welcome's ink, veil and
53
+ panels keep their fixed day/night literals, on `tests/hall_audit.mjs`'s literal exemptions.
54
+ - **Tests.** `tests/welcome_boot.mjs` (the day rule, that the painting and its lights are gone, the
55
+ scene's shapes with no door, no unprefixed id and no typed colour, the banner, the role map, the
56
+ load order); `tests/welcome_boot_browser.mjs` runs Home's markup with the real shell, craft mode,
57
+ welcome and city painters in a browser: once a day, Skip, Esc, no double picker, the seat rule, no
58
+ painters, reduced motion, the city in the hour's sky by day and night, the slow exit, and that the
59
+ painting's lights are gone.
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.25.31",
3
+ "version": "1.25.32",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.25.31",
9
+ "version": "1.25.32",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.25.31",
3
+ "version": "1.25.32",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -11961,5 +11961,11 @@
11961
11961
  "id": "1005310",
11962
11962
  "text": "The governor's street asks the server for less and gets its answers sooner."
11963
11963
  }
11964
+ ],
11965
+ "1.25.32": [
11966
+ {
11967
+ "id": "1005308",
11968
+ "text": "The old circuit-city painting is gone. The welcome screen and the game's start screens now open on the city in the clouds, coloured for the time of day: deep night with lit windows, a warm dawn, a bright midday."
11969
+ }
11964
11970
  ]
11965
11971
  }