@bongos/core 1.24.30 → 1.24.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.
Files changed (101) hide show
  1. package/.bongos-core.json +153 -103
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +8 -0
  4. package/clients/bongos-client/index.cjs +8 -0
  5. package/clients/bongos-client/index.d.ts +8 -0
  6. package/clients/bongos-client/index.mjs +8 -0
  7. package/docs/api/openapi.json +139 -1
  8. package/docs/api-reference.md +7 -3
  9. package/docs/copy-inventory.md +192 -187
  10. package/docs/copy-registry.json +530 -458
  11. package/docs/module-api-changelog.md +4 -2
  12. package/docs/page-inventory.json +15 -35
  13. package/docs/page-readings.json +228 -265
  14. package/modules/copy-desk/module.json +1 -1
  15. package/modules/copy-desk/routes/copy-desk-studio.js +103 -0
  16. package/modules/copy-desk/tests/copy_no_cms.mjs +2 -2
  17. package/modules/hall-ui/public/ai-studio.html +1 -1
  18. package/modules/hall-ui/public/atlas.html +1 -1
  19. package/modules/hall-ui/public/blockers.html +1 -1
  20. package/modules/hall-ui/public/board-room.html +1 -1
  21. package/modules/hall-ui/public/city.html +1 -1
  22. package/modules/hall-ui/public/collab.html +1 -1
  23. package/modules/hall-ui/public/commands.html +1 -1
  24. package/modules/hall-ui/public/copy-desk.html +1 -1
  25. package/modules/hall-ui/public/craft-mode.js +1 -1
  26. package/modules/hall-ui/public/deploy.html +1 -1
  27. package/modules/hall-ui/public/diagrams.html +1 -1
  28. package/modules/hall-ui/public/drachmae.html +1 -1
  29. package/modules/hall-ui/public/fleet.html +1 -1
  30. package/modules/hall-ui/public/gate.html +1 -1
  31. package/modules/hall-ui/public/goal-map.html +1 -1
  32. package/modules/hall-ui/public/goals.html +1 -1
  33. package/modules/hall-ui/public/government.html +1 -1
  34. package/modules/hall-ui/public/hall-render.js +62 -86
  35. package/modules/hall-ui/public/home-city-draw.js +360 -52
  36. package/modules/hall-ui/public/home-city.css +47 -11
  37. package/modules/hall-ui/public/home-city.js +170 -40
  38. package/modules/hall-ui/public/home-gallery.css +129 -0
  39. package/modules/hall-ui/public/home-gallery.js +349 -0
  40. package/modules/hall-ui/public/home-studio.css +72 -0
  41. package/modules/hall-ui/public/home-studio.js +203 -0
  42. package/modules/hall-ui/public/idea.html +1 -1
  43. package/modules/hall-ui/public/ideas.html +1 -1
  44. package/modules/hall-ui/public/index.html +4 -1
  45. package/modules/hall-ui/public/market-watch-item.html +1 -1
  46. package/modules/hall-ui/public/market-watch-rules.html +1 -1
  47. package/modules/hall-ui/public/market-watch-settings.js +6 -1
  48. package/modules/hall-ui/public/market-watch.html +141 -54
  49. package/modules/hall-ui/public/market-watch.js +54 -0
  50. package/modules/hall-ui/public/market-watch.states.json +2 -1
  51. package/modules/hall-ui/public/modules.html +1 -1
  52. package/modules/hall-ui/public/my-agents.html +1 -1
  53. package/modules/hall-ui/public/primer.html +1 -1
  54. package/modules/hall-ui/public/profile.html +1 -1
  55. package/modules/hall-ui/public/project-settings.html +1 -1
  56. package/modules/hall-ui/public/ranks.html +1 -1
  57. package/modules/hall-ui/public/roadmap.html +1 -1
  58. package/modules/hall-ui/public/roster.html +1 -1
  59. package/modules/hall-ui/public/sessions.html +1 -1
  60. package/modules/hall-ui/public/settings.html +1 -1
  61. package/modules/hall-ui/public/shell.js +2 -3
  62. package/modules/hall-ui/public/street.html +1 -1
  63. package/modules/hall-ui/public/studio.html +9 -1
  64. package/modules/hall-ui/public/style.css +74 -27
  65. package/modules/hall-ui/public/task.html +1 -1
  66. package/modules/hall-ui/public/thinking.html +1 -1
  67. package/modules/hall-ui/public/tweak-editor.html +1 -1
  68. package/modules/hall-ui/public/watch.html +1 -1
  69. package/modules/hall-ui/public/work.html +1 -1
  70. package/modules/hall-ui/records/home-city.md +133 -0
  71. package/modules/hall-ui/records/loader.md +1 -1
  72. package/modules/hall-ui/records/market-watch-settings.md +16 -3
  73. package/modules/hall-ui/records/market-watch.md +3 -1
  74. package/modules/lifecycle/home-city.js +58 -10
  75. package/modules/lifecycle/home-gallery.js +320 -0
  76. package/modules/lifecycle/migrations/lifecycle_020_work_appreciations.sql +36 -0
  77. package/modules/lifecycle/module.json +1 -0
  78. package/modules/lifecycle/routes/home-gallery.js +37 -0
  79. package/modules/market-watch/routes/settings.js +1 -1
  80. package/package-lock.json +2 -2
  81. package/package.json +1 -1
  82. package/release-notes.json +35 -1
  83. package/scripts/bongos/leak-scan-allowlist.js +8 -0
  84. package/scripts/hall-preview/README.md +1 -0
  85. package/scripts/hall-preview/server.js +10 -0
  86. package/src/bongos/serve-internal.js +17 -3
  87. package/src/module-api.js +1 -1
  88. package/tests/hall_builders_page_pass.mjs +18 -27
  89. package/tests/hall_loading.mjs +1 -1
  90. package/tests/hall_nav.mjs +11 -4
  91. package/tests/hall_page_gate_map.mjs +7 -4
  92. package/tests/home_city.mjs +200 -5
  93. package/tests/home_city_widget.mjs +187 -27
  94. package/tests/home_gallery.mjs +255 -0
  95. package/tests/home_gallery_widget.mjs +220 -0
  96. package/tests/home_studio.mjs +169 -0
  97. package/tests/home_studio_widget.mjs +157 -0
  98. package/tests/market_watch_pages.mjs +162 -0
  99. package/tests/nav_permission_atoms.mjs +2 -2
  100. package/modules/hall-ui/public/market-watch-settings.html +0 -115
  101. package/modules/hall-ui/public/market-watch-settings.states.json +0 -14
@@ -1,4 +1,4 @@
1
- /* modules/hall-ui/public/home-city.css — Home's project city (task 1005054).
1
+ /* modules/hall-ui/public/home-city.css — Home's project city (task 1005054; v16, task 1005058).
2
2
  The scene card, the ladder of builders beside it and the time slider under it
3
3
  (home-city.js; record modules/hall-ui/records/home-city.md). Every colour is a
4
4
  hall token or a mix of one; the tower faces are drawing data in
@@ -12,7 +12,32 @@
12
12
  .hc-svg .hc-wait__n { font: 700 11px var(--font-display); fill: var(--accent-on); }
13
13
  .hc-svg .hc-av__i { font: 600 13px var(--font-display); }
14
14
  .hc-svg [data-tip] { cursor: default; }
15
+ .hc-svg a[href] { cursor: pointer; }
16
+ .hc-svg a:focus { outline: none; }
17
+ .hc-svg a:focus-visible .hc-face { filter: brightness(1.35); }
15
18
  .hc-svg .hc-tower:hover .hc-face { filter: brightness(1.3); }
19
+ /* the core and the museum stay quiet on the builder's Home so the new buildings lead;
20
+ each comes back to full colour under the pointer or the keyboard */
21
+ .hc-svg .hc-quiet { opacity: .3; filter: saturate(0) brightness(.85); transition: opacity .3s, filter .3s; }
22
+ .hc-svg .hc-quiet:hover, .hc-svg .hc-quiet:focus-visible { opacity: .95; filter: none; }
23
+ .hc-svg .hc-core:hover .hc-face, .hc-svg .hc-museum:hover .hc-face { filter: brightness(1.15); }
24
+ .hc-svg .hc-sky:hover circle, .hc-svg .hc-sky:focus-visible circle { opacity: 1; }
25
+
26
+ /* the ideator's sky (task 1005059): night falls over the same city, seen from the street.
27
+ The sky is the page's own ground tinted toward the sea, deeper at the horizon; the
28
+ skyline's outlines are hidden until one of the three groups is pointed at or focused,
29
+ then the whole group lights at once. */
30
+ .home-city.hc-is-sky { background: linear-gradient(color-mix(in srgb, var(--bg) 86%, var(--sea)), color-mix(in srgb, var(--bg-card) 86%, var(--accent))); }
31
+ html[data-theme="dark"] .home-city.hc-is-sky { background: linear-gradient(color-mix(in srgb, var(--bg) 92%, var(--sea)), var(--bg) 40%, color-mix(in srgb, var(--bg) 58%, var(--sea))); }
32
+ @media (prefers-color-scheme: dark) {
33
+ html:not([data-theme]) .home-city.hc-is-sky { background: linear-gradient(color-mix(in srgb, var(--bg) 92%, var(--sea)), var(--bg) 40%, color-mix(in srgb, var(--bg) 58%, var(--sea))); }
34
+ }
35
+ .hc-is-sky .hc-time { background: color-mix(in srgb, var(--bg-card) 70%, transparent); }
36
+ .hc-svg .hc-sky__label { font: 500 11px var(--font-display); letter-spacing: .06em; }
37
+ .hc-svg .hc-ol { stroke-opacity: 0; transition: stroke-opacity .25s; }
38
+ .hc-svg:has(.hc-hit[data-group="area"]:hover, .hc-hit[data-group="area"]:focus-visible) .hc-ol-area,
39
+ .hc-svg:has(.hc-hit[data-group="civic"]:hover, .hc-hit[data-group="civic"]:focus-visible) .hc-ol-civic,
40
+ .hc-svg:has(.hc-hit[data-group="museum"]:hover, .hc-hit[data-group="museum"]:focus-visible) .hc-ol-museum { stroke-opacity: .9; }
16
41
 
17
42
  /* the ladder: a small face and one thin line split by craft */
18
43
  .hc-ladder { position: relative; flex: none; width: 230px; margin: 24px 28px 24px 0; }
@@ -32,20 +57,30 @@
32
57
  .hc-seg:hover { color: var(--ink); }
33
58
  .hc-seg:hover i { background: var(--ink); }
34
59
 
35
- /* the time slider */
36
- .hc-time { display: flex; align-items: center; gap: 16px; padding: 6px 24px 18px; background: var(--bg-card); }
37
- .hc-play { flex: none; width: 46px; height: 46px; border-radius: 50%; border: 1px solid var(--rule); background: transparent; color: var(--ink); cursor: pointer; display: grid; place-items: center; }
38
- .hc-play svg { width: 16px; height: 16px; }
39
- .hc-play .hc-play__off, .hc-play.is-playing .hc-play__on { display: none; }
40
- .hc-play.is-playing .hc-play__off { display: block; }
41
- .hc-play:focus-visible, .hc-slider:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }
42
- .hc-track { flex: 1; min-width: 0; }
60
+ /* the time slider: live at its right end, a quiet player beside it */
61
+ .hc-time { display: flex; align-items: center; gap: 12px; padding: 18px 24px 16px; background: var(--bg-card); }
62
+ .hc-player { display: flex; gap: 2px; flex: none; }
63
+ .hc-p { width: 30px; height: 30px; padding: 0; border: 0; border-radius: 50%; background: transparent; color: var(--ink-faint); cursor: pointer; display: grid; place-items: center; }
64
+ .hc-p svg { width: 13px; height: 13px; }
65
+ .hc-p:hover { color: var(--ink); background: var(--rule-soft); }
66
+ .hc-p[aria-pressed="true"] { color: var(--ink); }
67
+ .hc-p:disabled { opacity: .3; cursor: default; background: transparent; }
68
+ .hc-p-play .hc-play__off, .hc-p-play.is-playing .hc-play__on { display: none; }
69
+ .hc-p-play.is-playing .hc-play__off { display: block; }
70
+ .hc-p:focus-visible, .hc-slider:focus-visible, .hc-live:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
71
+ .hc-track { position: relative; flex: 1; min-width: 0; }
72
+ .hc-when { position: absolute; top: -18px; transform: translateX(-50%); font: 600 11px/1 var(--font-display); padding: 3px 8px; border-radius: var(--radius-pill); background: var(--ink); color: var(--bg-card); opacity: 0; transition: opacity .25s; white-space: nowrap; pointer-events: none; }
73
+ .hc-when.is-on { opacity: 1; }
74
+ .hc-live { display: inline-flex; align-items: center; gap: 6px; min-height: 24px; padding: 0; border: 0; background: transparent; cursor: pointer; font: 600 11px var(--font-display); color: var(--ink-soft); }
75
+ .hc-live i { width: 7px; height: 7px; border-radius: 50%; background: var(--ink-faint); }
76
+ .hc-live.is-live { color: var(--ink); }
77
+ .hc-live.is-live i { background: var(--ok); animation: hc-live 2.4s ease-out infinite; }
43
78
  .hc-slider { width: 100%; margin: 0; appearance: none; -webkit-appearance: none; background: transparent; height: 24px; cursor: pointer; }
44
79
  .hc-slider::-webkit-slider-runnable-track { height: 4px; border-radius: 2px; background: var(--rule-soft); }
45
80
  .hc-slider::-moz-range-track { height: 4px; border-radius: 2px; background: var(--rule-soft); }
46
81
  .hc-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 20px; height: 20px; border-radius: 50%; background: var(--ink); margin-top: -8px; border: 3px solid var(--bg-card); }
47
82
  .hc-slider::-moz-range-thumb { width: 16px; height: 16px; border-radius: 50%; background: var(--ink); border: 3px solid var(--bg-card); }
48
- .hc-ticks { display: flex; justify-content: space-between; font-size: 11px; color: var(--ink-faint); margin-top: 2px; }
83
+ .hc-ticks { display: flex; justify-content: space-between; align-items: center; font-size: 11px; color: var(--ink-faint); margin-top: 2px; }
49
84
 
50
85
  .hc-tip { position: fixed; pointer-events: none; z-index: var(--z-modal); background: var(--ink); color: var(--bg-card); font-size: 12px; line-height: 1.35; padding: 8px 11px; border-radius: 9px; max-width: 260px; opacity: 0; transform: translate(-50%, calc(-100% - 12px)); }
51
86
  .hc-tip.is-on { opacity: 1; }
@@ -54,8 +89,9 @@
54
89
  /* live, and nothing more: a crane's slab climbs, this week's ideas glow (20s+) */
55
90
  @keyframes hc-lift { 0%, 12% { transform: translateY(0); } 55%, 70% { transform: translateY(-24px); } 100% { transform: translateY(0); } }
56
91
  @keyframes hc-glow { 0%, 100% { opacity: .55; } 50% { opacity: 1; } }
92
+ @keyframes hc-live { 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--ok) 55%, transparent); } 100% { box-shadow: 0 0 0 8px transparent; } }
57
93
  .hc-slab { animation: hc-lift 20s ease-in-out infinite; }
58
- .hc-orb { animation: hc-glow 24s ease-in-out infinite; }
94
+ .hc-orb, .hc-twinkle { animation: hc-glow 24s ease-in-out infinite; }
59
95
 
60
96
  /* a phone: the ladder runs full width under the city */
61
97
  @media (max-width: 700px) {
@@ -7,9 +7,25 @@
7
7
  // rule tests/home_bounded_reads.mjs holds Home to. The drawing is
8
8
  // home-city-draw.js (pure); this file owns the read, the DOM, the slider and hover.
9
9
  //
10
- // Motion stays CSS: play steps the slider a week at a time on a timer (state, not
11
- // animation); the ladder's rows slide and a crane's slab lifts in CSS, which the
12
- // hall's reduced-motion rule stops.
10
+ // v16 (task 1005058): the city is the one city of every craft (home-city-draw.js), so
11
+ // this file also hands the drawing the Governor City (window.GovCity, city-draw.js)
12
+ // and turns its district doors into craft switches. The time slider is LIVE at its
13
+ // right end; moving it, or the quiet player under it (rewind, play, fast forward),
14
+ // glides the city through every week in between — the owner asked for a rewind, not
15
+ // a jump. That glide is the one motion here driven from script (a requestAnimationFrame
16
+ // loop that redraws the city at a fractional week); under prefers-reduced-motion it
17
+ // does not run at all and every control jumps straight to where it was going. The
18
+ // ladder's rows and a crane's slab still move in CSS, which the hall's rule stops.
19
+ //
20
+ // The ideator's Home (task 1005059): while the craft is Ideating (OTBCraftMode, its
21
+ // otb:craft event) the same widget draws the sky over the city's skyline
22
+ // (home-city-draw.js skySvg) from the same read, and the ladder ranks the week's
23
+ // ideators — ideas filed, votes cast, ideas that became work. Every other craft keeps
24
+ // the builder's city exactly as it was.
25
+ //
26
+ // The governor's Home is the street (task 1005064; the v16 round's v16-governing.html):
27
+ // choosing Governing on Home, or opening Home with it chosen, goes to the street page
28
+ // (GovCity.STREET_HREF), which is never redrawn here.
13
29
  (function () {
14
30
  'use strict';
15
31
  if (!window.OTB || typeof window.OTB.contributeHallWidget !== 'function') return;
@@ -17,16 +33,31 @@
17
33
  if (!Draw) return;
18
34
  const API = '/api/bongos';
19
35
  const WEEKS = 10;
20
- const STEP_MS = 800;
21
36
  const RUNG = 40;
37
+ // the player's speeds, in weeks per frame at 60 fps: rewind and fast forward cross the
38
+ // whole slider in about five and four seconds; play is about a week every 1.6 s
39
+ const SPEED = { rw: -1 / 34, play: 1 / 96, ff: 1 / 26 };
40
+ const STILL = () => !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches);
22
41
  const CRAFTS = [
23
42
  ['art', 'artistry', 'Art', 'pages designed'],
24
43
  ['build', 'building', 'Build', 'shipped'],
25
44
  ['idea', 'ideating', 'Ideas', 'ideas'],
26
45
  ['gov', 'governing', 'Govern', 'votes and decisions'],
27
46
  ];
47
+ // the ideator's line: a lightbulb, a star, and an arrow into the work
48
+ const IDEATOR = [
49
+ ['idea', 'ideating', 'Ideas', 'ideas filed'],
50
+ ['vote', '#vote', 'Votes', 'votes cast'],
51
+ ['work', '#work', 'Became work', 'ideas became work'],
52
+ ];
53
+ const GLYPH = {
54
+ '#vote': '<path d="M8 2.2l1.7 3.6 3.9.5-2.9 2.7.8 3.9L8 11l-3.5 1.9.8-3.9L2.4 6.3l3.9-.5z"/>',
55
+ '#work': '<path d="M2.5 2.5l7 7M9.5 9.5l-1 4 5-5-4 1z"/>',
56
+ };
57
+ const SKY_LABEL = 'The project\'s ideas as a night sky over the city\'s real skyline, with this week\'s ideators ranked beside it';
58
+ const CITY_LABEL = 'The project as a city: one tower per working area, as tall as what has shipped there, with the builders of the week beside it';
28
59
 
29
- let host = null, model = null, viewerId = null, week = 0, timer = null;
60
+ let host = null, model = null, viewerId = null, t = 0, target = 0, raf = null, run = null, lastWeek = -1, shown = false, gallery = null;
30
61
  const rungs = new Map();
31
62
 
32
63
  function mode() {
@@ -35,6 +66,7 @@
35
66
  return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
36
67
  }
37
68
 
69
+ function atLive(v) { return !model || v >= model.W - 1 - 1e-3; }
38
70
  function weekName(i) {
39
71
  if (!model || i === model.W - 1) return 'this week';
40
72
  const w = model.weeks[i];
@@ -42,7 +74,15 @@
42
74
  return d && !isNaN(d) ? 'week of ' + d.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', timeZone: 'UTC' }) : 'that week';
43
75
  }
44
76
 
77
+ // Ideating draws the sky; every other craft (or none) the builder's city.
78
+ function craft() {
79
+ const CM = window.OTBCraftMode;
80
+ const m = CM && typeof CM.current === 'function' ? CM.current() : document.documentElement.dataset.craft;
81
+ return m === 'ideating' ? 'ideating' : null;
82
+ }
83
+
45
84
  function craftIcon(key) {
85
+ if (GLYPH[key]) return '<svg class="hc-ico" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' + GLYPH[key] + '</svg>';
46
86
  const CM = window.OTBCraftMode;
47
87
  return CM && typeof CM.iconHtml === 'function' ? CM.iconHtml(key, 'hc-ico') : '';
48
88
  }
@@ -59,7 +99,12 @@
59
99
  }
60
100
  function drawScene() {
61
101
  const svg = host.querySelector('.hc-svg');
62
- const out = Draw.citySvg(model, week, { mode: mode() });
102
+ const sky = craft() === 'ideating';
103
+ const draw = sky ? Draw.skySvg : Draw.citySvg;
104
+ const out = draw(model, atLive(t) ? model.W - 1 : t, { mode: mode(), gov: window.GovCity || null });
105
+ host.classList.toggle('hc-is-sky', sky);
106
+ svg.setAttribute('aria-label', sky ? SKY_LABEL : CITY_LABEL);
107
+ host.querySelector('#home-city-h').textContent = sky ? 'The project\'s ideas this week' : 'The project this week';
63
108
  svg.setAttribute('viewBox', out.viewBox);
64
109
  svg.replaceChildren(...out.nodes.map(build));
65
110
  }
@@ -91,8 +136,12 @@
91
136
  }
92
137
 
93
138
  function drawLadder() {
94
- const rows = Draw.ladderFor(model, week, viewerId);
139
+ const week = Math.floor(t + 1e-6);
140
+ const who = craft();
141
+ const rows = Draw.ladderFor(model, week, viewerId, who);
142
+ const kinds = who === 'ideating' ? IDEATOR : CRAFTS;
95
143
  const ladder = host.querySelector('.hc-ladder');
144
+ ladder.setAttribute('aria-label', who === 'ideating' ? 'This week\'s ideators, ranked by what they did' : 'This week\'s builders, ranked by what they did');
96
145
  ladder.style.height = (Math.max(1, rows.length) * RUNG) + 'px';
97
146
  const top = Math.max(1, ...rows.map((r) => r.total));
98
147
  const room = Math.max(60, ladder.clientWidth - 40);
@@ -107,7 +156,7 @@
107
156
  r.dataset.tipBody = weekName(week);
108
157
  const line = r.querySelector('.hc-line');
109
158
  line.textContent = '';
110
- const crafts = CRAFTS.filter((c) => row.v[c[0]] > 0);
159
+ const crafts = kinds.filter((c) => row.v[c[0]] > 0);
111
160
  line.style.width = Math.max(crafts.length * 29, room * row.total / top).toFixed(1) + 'px';
112
161
  crafts.forEach((c) => {
113
162
  const seg = document.createElement('span');
@@ -125,29 +174,73 @@
125
174
  rungs.forEach((r, id) => { if (!shown.has(id)) r.hidden = true; });
126
175
  }
127
176
 
128
- function setWeek(i) {
129
- week = Math.max(0, Math.min(model.W - 1, i));
177
+ // ---- time: live at the right end; the city glides to wherever the slider is sent ----
178
+ function when(v) {
179
+ if (atLive(v)) return 'Live';
180
+ const w0 = model.weeks[0] && model.weeks[0].start ? Date.parse(model.weeks[0].start + 'T00:00:00Z') : NaN;
181
+ return isNaN(w0) ? weekName(Math.floor(v)) : new Date(w0 + v * 7 * 864e5).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', timeZone: 'UTC' });
182
+ }
183
+ function setT(v) {
184
+ t = Math.max(0, Math.min(model.W - 1, v));
185
+ const live = atLive(t);
130
186
  const s = host.querySelector('.hc-slider');
131
- s.value = String(week);
132
- s.setAttribute('aria-valuetext', weekName(week));
187
+ s.value = String(t);
188
+ s.setAttribute('aria-valuetext', live ? 'Live' : when(t));
189
+ const pill = host.querySelector('.hc-when');
190
+ pill.textContent = when(t);
191
+ pill.style.left = (t / Math.max(1, model.W - 1) * 100) + '%';
192
+ pill.classList.toggle('is-on', !live);
193
+ const l = host.querySelector('.hc-live');
194
+ l.classList.toggle('is-live', live);
195
+ l.querySelector('span').textContent = live ? 'Live' : 'Back to live';
133
196
  drawScene();
134
- drawLadder();
197
+ const w = Math.floor(t + 1e-6);
198
+ if (w !== lastWeek) { lastWeek = w; drawLadder(); }
199
+ buttons();
135
200
  }
136
-
137
- // ---- play: a week at a time, then stop on now ----
138
- function stop() {
139
- if (timer) clearTimeout(timer);
140
- timer = null;
141
- const b = host && host.querySelector('.hc-play');
142
- if (b) { b.classList.remove('is-playing'); b.setAttribute('aria-label', 'Play the weeks from the start'); }
201
+ function buttons() {
202
+ const b = (k) => host.querySelector(`.hc-p-${k}`);
203
+ b('play').classList.toggle('is-playing', run === 'play');
204
+ b('play').setAttribute('aria-label', run === 'play' ? 'Pause' : atLive(t) ? 'Play from the start' : 'Play');
205
+ ['rw', 'play', 'ff'].forEach((k) => b(k).setAttribute('aria-pressed', String(run === k)));
206
+ b('rw').disabled = t <= 1e-3 && run !== 'rw';
207
+ b('ff').disabled = atLive(t) && run !== 'ff';
208
+ }
209
+ function halt() { if (raf) cancelAnimationFrame(raf); raf = null; run = null; target = t; if (host) buttons(); }
210
+ function tick() {
211
+ raf = requestAnimationFrame(() => {
212
+ let v;
213
+ if (run) { v = t + SPEED[run]; if (v <= 0 || v >= model.W - 1) { v = Math.max(0, Math.min(model.W - 1, v)); run = null; } target = v; }
214
+ else { v = t + (target - t) * 0.16; if (Math.abs(target - v) < 0.004) v = target; }
215
+ setT(v);
216
+ if (run || v !== target) tick(); else { raf = null; buttons(); }
217
+ });
218
+ }
219
+ function glideTo(v) {
220
+ run = null; target = Math.max(0, Math.min(model.W - 1, v));
221
+ if (STILL()) { const v = target; halt(); setT(v); return; }
222
+ if (!raf) tick();
223
+ }
224
+ function press(k) {
225
+ if (run === k) { halt(); return; }
226
+ if (k === 'play' && atLive(t)) setT(0);
227
+ if (STILL()) { halt(); setT(k === 'rw' ? 0 : model.W - 1); return; }
228
+ run = k; buttons();
229
+ if (!raf) tick();
143
230
  }
144
- function play() {
145
- if (timer) { stop(); return; }
146
- const b = host.querySelector('.hc-play');
147
- b.classList.add('is-playing'); b.setAttribute('aria-label', 'Pause');
148
- let i = 0;
149
- const step = () => { setWeek(i); if (i++ < model.W - 1) timer = setTimeout(step, STEP_MS); else stop(); };
150
- step();
231
+
232
+ // ---- the doors: the museum and the stars switch the craft (the craft's own Home); the
233
+ // core is a plain link to the street and a building a plain link to its goal ----
234
+ function wireDoors() {
235
+ host.addEventListener('click', (e) => {
236
+ const d = e.target.closest('[data-craft-door]');
237
+ if (!d || !host.contains(d)) return;
238
+ const CM = window.OTBCraftMode;
239
+ if (!CM || typeof CM.choose !== 'function') return;
240
+ e.preventDefault();
241
+ CM.choose(d.getAttribute('data-craft-door'));
242
+ window.scrollTo(0, 0);
243
+ });
151
244
  }
152
245
 
153
246
  // ---- one tooltip for the whole widget; labels are ledger data, so text only ----
@@ -169,12 +262,14 @@
169
262
  }
170
263
 
171
264
  function ticks() {
172
- const t = host.querySelector('.hc-ticks');
265
+ const tk = host.querySelector('.hc-ticks');
173
266
  const first = model.weeks[0] && model.weeks[0].start ? new Date(model.weeks[0].start + 'T00:00:00Z') : null;
174
267
  const mid = model.weeks[Math.floor(model.W / 2)] && model.weeks[Math.floor(model.W / 2)].start ? new Date(model.weeks[Math.floor(model.W / 2)].start + 'T00:00:00Z') : null;
175
268
  const mon = (d) => (d && !isNaN(d) ? d.toLocaleDateString('en-GB', { month: 'short', timeZone: 'UTC' }) : '');
176
- t.innerHTML = '';
177
- [mon(first), mon(mid), 'Now'].forEach((s) => { const sp = document.createElement('span'); sp.textContent = s; t.appendChild(sp); });
269
+ const live = host.querySelector('.hc-live');
270
+ tk.textContent = '';
271
+ [mon(first), mon(mid)].forEach((s) => { const sp = document.createElement('span'); sp.textContent = s; tk.appendChild(sp); });
272
+ tk.appendChild(live);
178
273
  }
179
274
 
180
275
  async function load() {
@@ -189,29 +284,64 @@
189
284
  const s = host.querySelector('.hc-slider');
190
285
  s.max = String(model.W - 1);
191
286
  ticks();
192
- host.hidden = false;
193
- setWeek(model.W - 1);
287
+ shown = true;
288
+ craftHome();
289
+ target = model.W - 1;
290
+ setT(model.W - 1);
291
+ }
292
+
293
+ // ---- the artist's Home (task 1005060): in Artistry the same section is the gallery,
294
+ // home-gallery.js, the bench in the museum the city's art district opens onto ----
295
+ function craftHome() {
296
+ const art = !!gallery && document.documentElement.dataset.craft === 'artistry';
297
+ host.classList.toggle('hc--gallery', art);
298
+ if (gallery) gallery.show(art);
299
+ host.hidden = !(art || shown);
300
+ }
301
+
302
+ // ---- the governor's Home is the street; tests swap the hop for a recorder ----
303
+ function streetHop() {
304
+ const href = window.GovCity && window.GovCity.STREET_HREF;
305
+ if (document.documentElement.dataset.craft !== 'governing' || !href) return false;
306
+ (window.OTBHomeStreetHop || ((h) => window.location.assign(h)))(href);
307
+ return true;
194
308
  }
195
309
 
196
310
  function mount(hostEl) {
311
+ document.addEventListener('otb:craft', streetHop);
312
+ if (streetHop()) return; // leaving for the street: build nothing, read nothing
197
313
  host = hostEl;
198
314
  host.innerHTML =
199
315
  '<h2 id="home-city-h" class="sr-only">The project this week</h2>' +
200
316
  '<div class="hc-stage">' +
201
- '<svg class="hc-svg" role="img" aria-label="The project as a city: one tower per working area, as tall as what has shipped there, with the builders of the week beside it"></svg>' +
317
+ '<svg class="hc-svg" role="img"></svg>' +
202
318
  '<div class="hc-ladder" aria-label="This week\'s builders, ranked by what they did"></div>' +
203
319
  '</div>' +
204
320
  '<div class="hc-time">' +
205
- '<button type="button" class="hc-play" aria-label="Play the weeks from the start">' +
206
- '<svg class="hc-play__on" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 2.5v11l9-5.5z" fill="currentColor"/></svg>' +
207
- '<svg class="hc-play__off" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 3h3v10H4zM9 3h3v10H9z" fill="currentColor"/></svg>' +
208
- '</button>' +
209
- '<div class="hc-track"><input type="range" class="hc-slider" min="0" max="0" step="1" value="0" aria-label="Week"><div class="hc-ticks"></div></div>' +
321
+ '<div class="hc-player" role="group" aria-label="Time">' +
322
+ '<button type="button" class="hc-p hc-p-rw" aria-label="Rewind"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 3.5v9L2.5 8zM14 3.5v9L8.5 8z" fill="currentColor"/></svg></button>' +
323
+ '<button type="button" class="hc-p hc-p-play" aria-label="Play">' +
324
+ '<svg class="hc-play__on" viewBox="0 0 16 16" aria-hidden="true"><path d="M4.5 3v10l8-5z" fill="currentColor"/></svg>' +
325
+ '<svg class="hc-play__off" viewBox="0 0 16 16" aria-hidden="true"><path d="M4.5 3h2.4v10H4.5zM9.1 3h2.4v10H9.1z" fill="currentColor"/></svg>' +
326
+ '</button>' +
327
+ '<button type="button" class="hc-p hc-p-ff" aria-label="Fast forward"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M2 3.5v9L7.5 8zM8 3.5v9L13.5 8z" fill="currentColor"/></svg></button>' +
328
+ '</div>' +
329
+ '<div class="hc-track"><div class="hc-when" aria-hidden="true"></div>' +
330
+ '<input type="range" class="hc-slider" min="0" max="0" step="any" value="0" aria-label="Time, from the start of the version to live">' +
331
+ '<div class="hc-ticks"></div>' +
332
+ '<button type="button" class="hc-live is-live"><i aria-hidden="true"></i><span>Live</span></button>' +
333
+ '</div>' +
210
334
  '</div>';
211
- host.querySelector('.hc-slider').addEventListener('input', (e) => { stop(); setWeek(Number(e.target.value)); });
212
- host.querySelector('.hc-play').addEventListener('click', play);
335
+ host.querySelector('.hc-slider').addEventListener('input', (e) => { if (model) glideTo(Number(e.target.value)); });
336
+ ['rw', 'play', 'ff'].forEach((k) => host.querySelector(`.hc-p-${k}`).addEventListener('click', () => { if (model) press(k); }));
337
+ host.querySelector('.hc-live').addEventListener('click', () => { if (model) glideTo(model.W - 1); });
213
338
  wireTip();
339
+ wireDoors();
340
+ if (window.OTBHomeGallery) gallery = window.OTBHomeGallery.attach(host);
341
+ document.addEventListener('otb:craft', craftHome);
342
+ craftHome();
214
343
  new MutationObserver(() => { if (model) drawScene(); }).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
344
+ document.addEventListener('otb:craft', () => { if (model) { drawScene(); drawLadder(); } });
215
345
  window.addEventListener('resize', () => { if (model) drawLadder(); });
216
346
  load();
217
347
  }
@@ -0,0 +1,129 @@
1
+ /* modules/hall-ui/public/home-gallery.css — the artist's Home: a bench in the museum
2
+ (task 1005060; mock docs/design/mocks/hall/home/v16-artistry.html, record
3
+ modules/hall-ui/records/home-city.md § "The gallery"). A quiet wall, a floor and a
4
+ bench barely there, and the art the largest thing in the room in a thin frame.
5
+ Every colour is a hall token or a mix of one; the only literal is black with alpha,
6
+ as a shadow. The tokens carry both modes, so no rule here is written per mode.
7
+ No :root; motion is CSS and the hall's reduced-motion rule stops it. */
8
+
9
+ /* home-city.js shows the gallery in Artistry and the city otherwise */
10
+ .home-city.hc--gallery > :not(.hg) { display: none; }
11
+ .home-city:not(.hc--gallery) > .hg { display: none; }
12
+
13
+ .hg-room {
14
+ --hg-wall: color-mix(in srgb, var(--bg-card) 94%, var(--accent));
15
+ --hg-wall-low: color-mix(in srgb, var(--bg) 90%, var(--accent-deep));
16
+ --hg-floor: color-mix(in srgb, var(--bg-deep) 86%, var(--accent-deep));
17
+ --hg-spot: color-mix(in srgb, var(--accent) 14%, transparent);
18
+ --hg-frame: rgba(0, 0, 0, .82);
19
+ --hg-shadow: rgba(0, 0, 0, .45);
20
+ position: relative; height: 760px; overflow: hidden; user-select: none; touch-action: pan-y; outline: none;
21
+ color: var(--ink);
22
+ background: linear-gradient(var(--hg-wall), var(--hg-wall-low) 88%, transparent 88%), var(--hg-floor);
23
+ }
24
+ .hg-room:focus-visible { box-shadow: inset 0 0 0 2px var(--focus, var(--accent)); }
25
+ .hg-room::before { content: ""; position: absolute; inset: 0 0 12% 0; background: radial-gradient(ellipse 40% 64% at 46% 44%, var(--hg-spot), transparent 70%); pointer-events: none; }
26
+ /* the skirting board and the floorboards */
27
+ .hg-room::after { content: ""; position: absolute; left: 0; right: 0; top: 88%; bottom: 0; border-top: 2px solid color-mix(in srgb, var(--hg-wall-low) 82%, var(--ink)); background: repeating-linear-gradient(90deg, transparent 0 160px, color-mix(in srgb, var(--ink) 5%, transparent) 160px 161px); pointer-events: none; }
28
+ .hg-bench { position: absolute; left: 50%; bottom: -30px; width: 46%; height: 44px; transform: translateX(-50%); border-radius: 14px 14px 0 0; background: linear-gradient(color-mix(in srgb, var(--ink) 70%, var(--accent-deep)), var(--ink)); opacity: .22; filter: blur(1.2px); z-index: 3; pointer-events: none; }
29
+
30
+ /* the wall title, in vinyl lettering */
31
+ .hg-vinyl { position: absolute; left: 44px; top: 34px; z-index: 4; }
32
+ .hg-h { margin: 0; font: 300 30px/1.2 var(--font-display); letter-spacing: .01em; color: var(--ink); }
33
+ .hg-sub { margin: 6px 0 0; font-size: 11px; letter-spacing: .16em; text-transform: uppercase; color: var(--ink-soft); }
34
+ .hg-sort { display: flex; gap: 18px; margin-top: 16px; }
35
+ .hg-sort button, .hg-way { all: unset; cursor: pointer; box-sizing: border-box; min-height: 24px; min-width: 24px; display: inline-flex; align-items: center; color: var(--ink-soft); }
36
+ .hg-sort button { font-size: 12.5px; letter-spacing: .04em; border-bottom: 1px solid transparent; }
37
+ .hg-sort button[aria-pressed="true"] { color: var(--ink); border-color: var(--ink); }
38
+ .hg-ways { display: flex; flex-wrap: wrap; gap: 4px 22px; margin-top: 22px; }
39
+ .hg-way { font-size: 13px; letter-spacing: .01em; text-decoration: none; gap: 6px; }
40
+ .hg-way:hover, .hg-way[aria-pressed="true"] { color: var(--ink); }
41
+ .hg-sort button:focus-visible, .hg-way:focus-visible { outline: 2px solid var(--focus, var(--accent)); outline-offset: 3px; }
42
+
43
+ /* the wall, one work at a time along it */
44
+ .hg-wall { position: absolute; inset: 0 0 12% 0; overflow: hidden; }
45
+ .hg-track { position: absolute; inset: 0; display: flex; transition: transform .7s cubic-bezier(.22, .7, .2, 1); }
46
+ .hg-track.is-drag { transition: none; }
47
+ .hg-hang { flex: 0 0 100%; position: relative; }
48
+ .hg-work { position: absolute; left: 49%; top: 188px; transform: translateX(-50%); width: min(700px, 56%); transition: opacity .3s; }
49
+ .hg-canvas { position: relative; aspect-ratio: 16 / 10; overflow: hidden; background: var(--bg-deep); border: 3px solid var(--hg-frame); box-shadow: 0 18px 34px -20px var(--hg-shadow); }
50
+ .hg-canvas img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: top center; pointer-events: none; -webkit-user-drag: none; }
51
+
52
+ /* the wall label: small, at the work's foot to its right; click to read it closer */
53
+ .hg-label { position: absolute; left: calc(100% + 28px); bottom: 0; width: 158px; font-size: 10px; line-height: 1.4; color: var(--ink); }
54
+ .hg-text { display: block; cursor: zoom-in; transform-origin: 100% 100%; transition: transform .35s cubic-bezier(.2, .7, .2, 1); border-radius: 4px; outline-offset: 6px; }
55
+ .hg-text:focus-visible { outline: 2px solid var(--focus, var(--accent)); }
56
+ .hg-who { display: block; font-weight: 700; font-size: 10.5px; }
57
+ .hg-bio { display: block; margin-bottom: 6px; color: var(--ink-soft); }
58
+ .hg-title { display: block; }
59
+ .hg-medium, .hg-place { display: block; color: var(--ink-soft); }
60
+ .hg-words, .hg-credit { display: none; margin: 8px 0 0; }
61
+ .hg-credit { color: var(--ink-soft); }
62
+ .hg-hang.is-reading .hg-text { transform: scale(1.85); cursor: zoom-out; }
63
+ .hg-hang.is-reading .hg-words, .hg-hang.is-reading .hg-credit { display: block; }
64
+ .hg-hang.is-reading .hg-work > .hg-canvas { opacity: .4; }
65
+ /* the signature, in the artist's hand, on the wall below the label; the heart never changes size */
66
+ .hg-sigline { display: flex; align-items: center; gap: 10px; margin-top: 8px; }
67
+ .hg-sig { font: 500 17px/1 "Caveat", "Segoe Script", "Bradley Hand", "Snell Roundhand", cursive; color: var(--ink); opacity: .82; transform: rotate(-2deg); white-space: nowrap; }
68
+ .hg-heart { all: unset; cursor: pointer; flex: none; width: 28px; height: 28px; display: grid; place-items: center; border-radius: 50%; color: var(--ink-soft); }
69
+ .hg-heart svg { width: 15px; height: 15px; }
70
+ .hg-heart:hover { color: var(--ink); }
71
+ .hg-heart[aria-pressed="true"] { color: var(--accent-deep); }
72
+ .hg-heart[aria-pressed="true"] path { fill: currentColor; }
73
+ .hg-heart:focus-visible { outline: 2px solid var(--focus, var(--accent)); }
74
+
75
+ /* moving along the wall */
76
+ .hg-edge { all: unset; position: absolute; top: 0; bottom: 12%; width: 9%; min-width: 44px; z-index: 4; cursor: pointer; display: grid; place-items: center; color: var(--ink-soft); opacity: .55; transition: opacity .2s; }
77
+ .hg-edge:hover, .hg-edge:focus-visible { opacity: 1; }
78
+ .hg-edge:focus-visible svg { outline: 2px solid var(--focus, var(--accent)); outline-offset: 6px; border-radius: 50%; }
79
+ .hg-edge svg { width: 28px; height: 28px; fill: none; stroke: currentColor; stroke-width: 1.3; stroke-linecap: round; stroke-linejoin: round; }
80
+ .hg-prev { left: 0; } .hg-next { right: 0; }
81
+ .hg-edge[disabled] { opacity: 0; pointer-events: none; }
82
+ .hg-count { position: absolute; left: 49%; top: calc(88% - 30px); transform: translateX(-50%); z-index: 4; font-size: 11px; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-soft); }
83
+ .hg-empty { position: absolute; left: 50%; top: 46%; transform: translate(-50%, -50%); width: min(440px, 80%); margin: 0; text-align: center; font-size: 14px; color: var(--ink-soft); }
84
+ .hg-room.is-empty .hg-edge, .hg-room.is-empty .hg-fsbtn { display: none; }
85
+
86
+ /* full screen: only the art, its caption, the signature and the heart */
87
+ .hg-fsbtn { all: unset; position: absolute; right: 16px; top: 16px; z-index: 5; width: 32px; height: 32px; display: grid; place-items: center; border-radius: 50%; color: var(--ink-soft); cursor: pointer; opacity: .6; }
88
+ .hg-fsbtn:hover, .hg-fsbtn:focus-visible { opacity: 1; }
89
+ .hg-fsbtn:focus-visible { outline: 2px solid var(--focus, var(--accent)); }
90
+ .hg-fsbtn svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round; }
91
+ .hg-cap { display: none; position: absolute; left: 50%; bottom: 22px; transform: translateX(-50%); font-size: 12px; color: var(--ink-soft); white-space: nowrap; max-width: 60vw; overflow: hidden; text-overflow: ellipsis; }
92
+ .hg-room:fullscreen, .hg-room.is-fs { --hg-spot: transparent; background: var(--hg-wall-low); }
93
+ .hg-room.is-fs { position: fixed; inset: 0; height: auto; z-index: 1000; }
94
+ .hg-room:fullscreen::after, .hg-room.is-fs::after, .hg-room:fullscreen .hg-bench, .hg-room.is-fs .hg-bench,
95
+ .hg-room:fullscreen .hg-vinyl, .hg-room.is-fs .hg-vinyl, .hg-room:fullscreen .hg-count, .hg-room.is-fs .hg-count { display: none; }
96
+ .hg-room:fullscreen .hg-wall, .hg-room.is-fs .hg-wall, .hg-room:fullscreen .hg-edge, .hg-room.is-fs .hg-edge { bottom: 0; }
97
+ .hg-room:fullscreen .hg-work, .hg-room.is-fs .hg-work { left: 50%; top: 50%; transform: translate(-50%, calc(-50% - 22px)); width: min(86vw, calc((100vh - 130px) * 1.6)); }
98
+ .hg-room:fullscreen .hg-canvas, .hg-room.is-fs .hg-canvas { border: 0; box-shadow: none; opacity: 1; }
99
+ .hg-room:fullscreen .hg-label, .hg-room.is-fs .hg-label { position: absolute; left: auto; right: 0; top: calc(100% + 10px); bottom: auto; width: auto; margin: 0; }
100
+ .hg-room:fullscreen .hg-text, .hg-room.is-fs .hg-text { display: none; }
101
+ .hg-room:fullscreen .hg-cap, .hg-room.is-fs .hg-cap { display: block; }
102
+
103
+ /* the full gallery: every work on the wall at once, hung salon-style */
104
+ .hg-salon { position: absolute; left: 44px; right: 44px; top: 210px; bottom: calc(12% + 20px); overflow: auto; display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 34px 30px; align-content: start; padding: 4px; z-index: 4; }
105
+ .hg-salon[hidden] { display: none; }
106
+ .hg-room.is-all .hg-wall, .hg-room.is-all .hg-edge, .hg-room.is-all .hg-count, .hg-room.is-all .hg-fsbtn { display: none; }
107
+ .hg-piece { all: unset; cursor: pointer; display: block; width: 100%; }
108
+ .hg-pc { position: relative; display: block; aspect-ratio: 16 / 10; overflow: hidden; background: var(--bg-deep); border: 2px solid var(--hg-frame); box-shadow: 0 12px 22px -16px var(--hg-shadow); transition: transform .25s; }
109
+ .hg-pc img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: top center; }
110
+ .hg-piece:hover .hg-pc, .hg-piece:focus-visible .hg-pc { transform: translateY(-3px); }
111
+ .hg-piece:focus-visible { outline: 2px solid var(--focus, var(--accent)); outline-offset: 6px; }
112
+ .hg-pt { display: block; margin-top: 9px; font-size: 10.5px; color: var(--ink); }
113
+ .hg-pl { display: flex; align-items: center; gap: 5px; margin-top: 2px; font-size: 10px; color: var(--ink-soft); }
114
+ .hg-pl svg { width: 11px; height: 11px; }
115
+
116
+ @media (max-width: 860px) {
117
+ .hg-room { height: 660px; }
118
+ .hg-vinyl { left: 20px; right: 20px; top: 20px; }
119
+ .hg-h { font-size: 24px; }
120
+ .hg-work { top: 196px; left: 50%; width: 86%; }
121
+ .hg-label { position: static; width: auto; margin-top: 14px; }
122
+ /* a phone has no room to scale the label: read closer, it opens over the dimmed picture */
123
+ .hg-hang.is-reading .hg-text { transform: none; }
124
+ .hg-hang.is-reading .hg-label { position: absolute; left: 0; right: 0; top: 0; margin: 0; font-size: 12px; }
125
+ .hg-hang.is-reading .hg-who { font-size: 12.5px; }
126
+ .hg-hang.is-reading .hg-work > .hg-canvas { opacity: 0; }
127
+ .hg-count { left: 50%; }
128
+ .hg-salon { left: 20px; right: 20px; top: 196px; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 22px 16px; }
129
+ }