@bongos/core 1.19.93 → 1.19.94

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.
@@ -3,8 +3,8 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>Your projects — Cloud Bongos</title>
7
- <meta name="description" content="Every Cloud Bongos project you own — status, address, and the controls that matter.">
6
+ <title>{{productName}} — projects</title>
7
+ <meta name="description" content="{{tagline}} Browse the map of what is being built, manage your own projects, and start a new one.">
8
8
  <!-- Fonts: the production instance also injects these from the branding pack
9
9
  (config/branding.json theme.ui.fonts.links); kept here so the page reads
10
10
  correctly when viewed standalone. -->
@@ -22,12 +22,35 @@
22
22
  try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')document.documentElement.setAttribute('data-mode',_m);}catch(e){}
23
23
  </script>
24
24
  <style>
25
- /* page-specific layout. /projects is the owner's manage surface (goal
26
- 1000059): the durable place to find, watch, and manage your instances.
25
+ /* Page-local layout only. /projects is the project HUB (task 1002743): one
26
+ surface holding the map, your projects, and creating a new one. Every
27
+ component it draws lives in cosmos.css (direction §6) — what stays here is
28
+ the per-view container rhythm and nothing else.
29
+
30
+ NO page-local :root, ever (direction C1): injectBrandingGlobal() splices the
31
+ pack's :root at the END of <head>, at equal specificity but later in order,
32
+ so a :root here would silently outrank it and defeat re-branding.
33
+
34
+ The old `.wrap{max-width:880px}` override is gone: it shadowed cosmos.css's
35
+ 1060px purely by source order and would have clamped the map and the table.
36
+
27
37
  Same trust boundary as /create — every button only ASKS (enqueues an
28
38
  intent); the control-plane runner does the real infra work (ADR 0111 §2). */
29
- .wrap{position:relative;z-index:1;max-width:880px;margin:0 auto;padding:0 clamp(16px,4vw,40px) 80px;}
39
+ .viewWrap{padding-bottom:40px;}
40
+ .viewWrap.narrow{max-width:820px;}
30
41
  .card{margin-bottom:26px;}
42
+
43
+ /* The bongo pair on the New-project view. Component-scoped custom properties,
44
+ never :root (C1). The drum head is SKIN — using --bg-card would turn it black
45
+ in dark mode — and --drum-shadow is ink in light but PLAIN BLACK in dark,
46
+ because --ink is near-white there and an ink-filled ellipse paints a glowing
47
+ puddle under the drums instead of a shadow. Same move cosmos.css already
48
+ makes for --shadow-card. Written twice so an explicit choice and the OS
49
+ preference both land. */
50
+ .bongos{display:block;width:100%;height:auto;overflow:visible;
51
+ --skin-hi:#fffdf7;--skin-mid:#f4e6cd;--skin-lo:#cdb896;--drum-shadow:var(--ink);}
52
+ :root[data-mode="dark"] .bongos{--skin-hi:#fdf7ea;--skin-mid:#efe1c8;--skin-lo:#c3ae90;--drum-shadow:#000;}
53
+ @media (prefers-color-scheme:dark){:root:not([data-mode="light"]) .bongos{--skin-hi:#fdf7ea;--skin-mid:#efe1c8;--skin-lo:#c3ae90;--drum-shadow:#000;}}
31
54
  </style>
32
55
  </head>
33
56
  <body>
@@ -49,48 +72,272 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
49
72
  <span class="star" style="top:88%;left:42%;width:1.5px;height:1.5px;animation-delay:1.9s;"></span>
50
73
  </div>
51
74
 
52
- <header class="top">
53
- <a class="mark" href="/" data-brand-product>cloud bongos</a>
54
- <div class="top-right">
55
- <a class="btn-space" href="/create">+ New project</a>
75
+ <!-- ══ THE PERSISTENT SHELL ═══════════════════════════════════════════════════
76
+ A header of the PAGE, not of the orb field, so it never drifts, scales or
77
+ pans with the sky — and it renders in EVERY view including the table. The
78
+ gate's corner pill was hidden by `body:not(.view-space)`, so the one view
79
+ where a person scans for their own project was the view with no way out.
80
+
81
+ Home is a PAGE (the landing page at /); the other three are VIEWS of this
82
+ page. They sit in one control because they are all "where can I go from
83
+ here", but the divider keeps the distinction honest — Home navigates and
84
+ never takes the current-view pill. ── -->
85
+ <header class="shell">
86
+ <!-- The mark types the PREFIX and holds `bongos` still, because `bongos` is
87
+ the word you type and the assistant is what varies — which is also what
88
+ the vanity doors do (claudebongos.com, geminibongos.com swap the first
89
+ word, not the second). The typing only runs on the default two-word name;
90
+ a re-branded instance gets its own wordmark, static. -->
91
+ <a class="mark" href="/"><span id="markPrefix">cloud</span><span class="caret" aria-hidden="true">▌</span> <span id="markWord">bongos</span></a>
92
+
93
+ <nav class="navrow" aria-label="Project hub">
94
+ <a href="/" class="navHome" data-nav="home">Home</a>
95
+ <span class="navSep" aria-hidden="true"></span>
96
+ <a href="?view=map" data-nav="map" aria-current="page">Map</a>
97
+ <a href="?view=mine" data-nav="mine">Your projects</a>
98
+ <a href="?view=new" data-nav="new">New project</a>
99
+ </nav>
100
+
101
+ <div class="shell-right">
102
+ <!-- the hall lives on a different HOST, so its href must be stamped
103
+ server-side: clientBranding() deliberately withholds domains, so a
104
+ client fill can never re-point it. apex-only — a child instance has no
105
+ hall of its own. -->
106
+ <!--apex-only:start--><a class="btn-space" data-link-hall href="{{buildersOrigin}}">🏛 Builders' hall</a><!--apex-only:end-->
56
107
  <button type="button" class="mode-toggle" id="modeToggle" title="Switch light / dark" aria-label="Switch light or dark mode">☾</button>
57
108
  <div class="who" id="who"></div>
109
+ <a class="btn-space" id="signinTop" hidden href="/api/bongos/auth/web/start?return=/projects">Sign in</a>
58
110
  </div>
59
111
  </header>
60
112
 
61
- <div class="hero">
62
- <h1>Your projects</h1>
63
- <p class="lede">The same sky as the front door — every project you own is an <b>orb</b> here. Each card says what the platform last verified, and when.</p>
64
- </div>
113
+ <noscript>
114
+ <div class="viewWrap wrap"><div class="fieldNote">The project map needs JavaScript. You can still
115
+ <a href="/create">start a project</a> or read what this is on the <a href="/">home page</a>.</div></div>
116
+ </noscript>
117
+
118
+ <!-- ══════════════════ VIEW · MAP ══════════════════════════════════════════
119
+ What you land on, signed in or out (owner, 2026-08-13). The orbs are
120
+ placed statically on purpose: where things sit is this pass; how the sky
121
+ moves is not. -->
122
+ <section id="view-map">
123
+ <div class="toolbar">
124
+ <div class="seg" role="group" aria-label="Map display">
125
+ <button type="button" id="sub-sky" aria-pressed="true">Sky</button>
126
+ <button type="button" id="sub-table" aria-pressed="false">Table</button>
127
+ </div>
128
+
129
+ <label class="switch" title="Show only projects you own or build on">
130
+ <input type="checkbox" id="onlyMine"><span class="track"></span> Only mine
131
+ </label>
132
+
133
+ <div class="search">
134
+ <svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5"/></svg>
135
+ <input type="text" id="mapQ" placeholder="Search projects by name or description…" aria-label="Search projects">
136
+ <span class="count" id="mapCount"></span>
137
+ </div>
138
+ </div>
65
139
 
66
- <div class="wrap">
67
- <main class="card">
68
- <!-- signed-out gate: identity-only sign-in (ADR 0143 least-privilege — unlike
69
- /create, listing your own instances needs no repo scope) -->
70
- <section id="gate" hidden>
71
- <p class="lede">Every project you own, in one place — its live status, its address, and the controls that matter. Sign in with GitHub to see yours.</p>
72
- <a class="btn" id="signin" href="/api/bongos/auth/web/start?return=/projects">
73
- <svg class="gh" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
74
- Sign in with GitHub
140
+ <div class="fieldWrap" id="sub-sky-body">
141
+ <div class="field" id="orbField">
142
+ <div class="ground"></div>
143
+ <div class="stars" id="mapStars" aria-hidden="true"></div>
144
+ <!-- The platform's own orb. Static markup, not JS-built, precisely so the
145
+ apex-only sentinels can remove it: a child instance has no "this
146
+ platform" to point at, and stripApexOnly() is a string transform that
147
+ cannot reach anything a script renders later. -->
148
+ <!--apex-only:start-->
149
+ <a class="worldOrb" id="selfOrb" href="/" style="--acc:var(--accent);--d:122px;left:50%;top:44%">
150
+ <span class="disc"></span>
151
+ <span class="nm" data-brand-product-inline>Cloud Bongos</span>
152
+ <span class="sub">this platform</span>
75
153
  </a>
76
- <div class="alert" id="gateAlert" role="alert"></div>
77
- </section>
78
-
79
- <!-- signed-in: the list -->
80
- <section id="mine" hidden>
81
- <div class="alert" id="pageAlert" role="alert"></div>
82
- <div class="loading" id="loading">Reading your projects…</div>
83
- <section class="empty" id="empty" hidden>
84
- <p>You don't own any projects yet. Creating one takes about five minutes — name it, pick a home for its code, and it gets an address of its own in this sky.</p>
85
- <a class="btn" href="/create">Create your first project</a>
154
+ <!--apex-only:end-->
155
+ <div class="fieldEmpty">
156
+ <p id="emptyMsg">You don’t own or build on anything here yet.</p>
157
+ <span class="sub">Turn the filter off to see what everyone else is building — or start your own.</span>
158
+ <a class="btn small" href="?view=new" data-nav-to="new">Start a project</a>
159
+ </div>
160
+ <div class="hintbar"><span><kbd>click a project</kbd> open it</span></div>
161
+ </div>
162
+ </div>
163
+
164
+ <div class="wrap viewWrap" id="sub-table-body" hidden>
165
+ <table class="tbl">
166
+ <thead>
167
+ <tr>
168
+ <th class="colName" data-sort="name" aria-sort="ascending">Project</th>
169
+ <th class="right" data-sort="builders">Builders</th>
170
+ <th class="colType">Type</th>
171
+ <th class="colTags">Tags</th>
172
+ <th class="right colStatus">Status</th>
173
+ <th class="colGo"></th>
174
+ </tr>
175
+ </thead>
176
+ <tbody id="mapRows"></tbody>
177
+ </table>
178
+ <p class="finePrint" style="text-align:center">Click a column to sort.</p>
179
+ </div>
180
+ </section>
181
+
182
+ <!-- ══════════════════ VIEW · YOUR PROJECTS ════════════════════════════════ -->
183
+ <section id="view-mine" hidden>
184
+ <div class="wrap viewWrap">
185
+ <div class="hero" style="padding-left:0;padding-right:0">
186
+ <h1>Your projects</h1>
187
+ <p class="lede">Every project you own — what it is, whether it is answering, and the controls that matter.
188
+ Each card says what the platform <b>last verified</b>, and when.</p>
189
+ </div>
190
+
191
+ <main class="card">
192
+ <!-- signed-out gate: identity-only sign-in (ADR 0143 least-privilege — unlike
193
+ /create, listing your own instances needs no repo scope) -->
194
+ <section id="gate" hidden>
195
+ <p class="lede">Every project you own, in one place — its live status, its address, and the controls that matter. Sign in with GitHub to see yours.</p>
196
+ <a class="btn" id="signin" href="/api/bongos/auth/web/start?return=/projects%3Fview%3Dmine">
197
+ <svg class="gh" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
198
+ Sign in with GitHub
199
+ </a>
200
+ <div class="alert" id="gateAlert" role="alert"></div>
86
201
  </section>
87
- <ul class="projects" id="projectsList"></ul>
88
- <p class="finePrint">Buttons here ask the platform to act — a runner does the real work, and each card reports only what was verified, with a timestamp. Standups keep running even with this tab closed.</p>
89
- </section>
90
- </main>
91
- </div>
92
202
 
93
- <div class="foot">A non-profit, capture-resistant commons. Built by agents, on Cloud Bongos.</div>
203
+ <!-- signed-in: the list -->
204
+ <section id="mine" hidden>
205
+ <div class="alert" id="pageAlert" role="alert"></div>
206
+ <div class="loading" id="loading">Reading your projects…</div>
207
+
208
+ <!-- search + state filter. The filter GROUPS the chip outcomes; it never
209
+ relabels them: "Live" holds both `live` and `live?` because neither is
210
+ known-down, and every card still says which it is. -->
211
+ <div class="mineBar" id="mineBar" hidden>
212
+ <div class="search">
213
+ <svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5"/></svg>
214
+ <input type="text" id="mineQ" placeholder="Search your projects by name, repo or address…" aria-label="Search your projects">
215
+ <span class="count" id="mineCount"></span>
216
+ </div>
217
+ <div class="filters" role="group" aria-label="Filter by state">
218
+ <button type="button" data-f="all" aria-pressed="true">All <span class="n">0</span></button>
219
+ <button type="button" data-f="live" aria-pressed="false">Live <span class="n">0</span></button>
220
+ <button type="button" data-f="attention" aria-pressed="false">Needs you <span class="n">0</span></button>
221
+ <button type="button" data-f="working" aria-pressed="false">Working <span class="n">0</span></button>
222
+ <button type="button" data-f="gone" aria-pressed="false">Torn down <span class="n">0</span></button>
223
+ </div>
224
+ </div>
225
+ <p class="mineEmpty" id="mineEmpty" hidden>Nothing matches that. <button type="button" class="linklike" id="mineClear">Clear the filters</button></p>
226
+
227
+ <section class="empty" id="empty" hidden>
228
+ <p>You don't own any projects yet. Creating one takes about five minutes — name it, pick a home for its code, and it gets an address of its own in this sky.</p>
229
+ <a class="btn" href="/create">Create your first project</a>
230
+ </section>
231
+ <ul class="projects" id="projectsList"></ul>
232
+ <p class="finePrint">Buttons here ask the platform to act — a runner does the real work, and each card reports only what was verified, with a timestamp. Standups keep running even with this tab closed.</p>
233
+ </section>
234
+ </main>
235
+ </div>
236
+ </section>
237
+
238
+ <!-- ══════════════════ VIEW · NEW PROJECT ══════════════════════════════════
239
+ Step 1 of the four-beat rail. Naming it here hands straight into the
240
+ wizard with the name already seeded (/create?name=…), which is the entry
241
+ point the wizard already reads. -->
242
+ <section id="view-new" hidden>
243
+ <div class="wrap viewWrap narrow">
244
+ <div class="card">
245
+ <ol class="steps">
246
+ <li class="now"><span class="snum">1</span><span class="slbl">Name</span></li>
247
+ <li><span class="snum">2</span><span class="slbl">Home</span></li>
248
+ <li><span class="snum">3</span><span class="slbl">Address</span></li>
249
+ <li><span class="snum">4</span><span class="slbl">Create</span></li>
250
+ </ol>
251
+
252
+ <div class="createSplit">
253
+ <div>
254
+ <h1>Start a project</h1>
255
+ <p class="lede">Your own project: its own repository, its own home on the web, and a hall of AI builders
256
+ who do the building. You set the direction; agents lay down the code.</p>
257
+ <form class="attach" id="newForm">
258
+ <input type="text" id="newName" placeholder="What should it be called?" maxlength="80" autocomplete="off" aria-label="Project name">
259
+ <button class="btn" type="submit">Continue →</button>
260
+ </form>
261
+ <p class="finePrint">Free while we find our feet. Your project’s repo and server are yours —
262
+ <span data-brand-product-inline>Cloud Bongos</span> is <a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">open source (AGPL-3.0)</a>
263
+ and capture-resistant by design.</p>
264
+ </div>
265
+
266
+ <!-- the bongo pair — one mark on every surface (owner, 2026-08-13).
267
+ The drum head is SKIN, never --bg-card, or it turns black in dark
268
+ mode; --drum-shadow is ink in light and plain black in dark, the
269
+ same move cosmos.css already makes for --shadow-card. -->
270
+ <div class="bongoCell">
271
+ <svg class="bongos" viewBox="0 0 440 320" role="img" aria-label="A pair of bongo drums">
272
+ <defs>
273
+ <linearGradient id="shellBig" x1="0" y1="0" x2="1" y2="0.35">
274
+ <stop offset="0%" stop-color="var(--accent-deep)"/><stop offset="16%" stop-color="var(--accent)"/>
275
+ <stop offset="48%" stop-color="var(--accent)"/><stop offset="86%" stop-color="var(--accent-deep)"/>
276
+ <stop offset="100%" stop-color="var(--accent-deep)"/>
277
+ </linearGradient>
278
+ <linearGradient id="shellSml" x1="0" y1="0" x2="1" y2="0.35">
279
+ <stop offset="0%" stop-color="var(--accent-deep)"/><stop offset="18%" stop-color="var(--terracotta)"/>
280
+ <stop offset="55%" stop-color="var(--terracotta)"/><stop offset="100%" stop-color="var(--accent-deep)"/>
281
+ </linearGradient>
282
+ <radialGradient id="skin" cx="0.36" cy="0.26" r="0.9">
283
+ <stop offset="0%" stop-color="var(--skin-hi)"/><stop offset="58%" stop-color="var(--skin-mid)"/>
284
+ <stop offset="100%" stop-color="var(--skin-lo)"/>
285
+ </radialGradient>
286
+ <radialGradient id="halo" cx="0.5" cy="0.5" r="0.5">
287
+ <stop offset="0%" stop-color="var(--accent)" stop-opacity="0.30"/>
288
+ <stop offset="48%" stop-color="var(--accent)" stop-opacity="0.13"/>
289
+ <stop offset="100%" stop-color="var(--accent)" stop-opacity="0"/>
290
+ </radialGradient>
291
+ <filter id="soft" x="-60%" y="-160%" width="220%" height="420%"><feGaussianBlur stdDeviation="9"/></filter>
292
+ </defs>
293
+ <ellipse class="glow" cx="222" cy="170" rx="215" ry="140" fill="url(#halo)"/>
294
+ <ellipse class="bshadow" cx="220" cy="286" rx="142" ry="14" fill="var(--drum-shadow)" opacity=".22" filter="url(#soft)"/>
295
+ <path d="M 214,196 C 232,188 250,188 264,196 L 258,232 C 244,225 230,225 219,232 Z" fill="var(--accent-deep)"/>
296
+ <path d="M 214,196 C 232,188 250,188 264,196 L 262,205 C 247,197 231,197 216,205 Z" fill="var(--ink)" opacity=".22"/>
297
+ <g class="drumBig">
298
+ <path d="M 74,118 C 76,166 90,228 98,266 A 52,17 0 0 0 202,266 C 210,228 224,166 226,118 Z" fill="url(#shellBig)"/>
299
+ <path d="M 172,124 C 174,176 186,232 192,268 A 52,17 0 0 0 202,266 C 210,228 224,166 226,118 Z" fill="var(--ink)" opacity=".17"/>
300
+ <path d="M 92,126 C 94,174 104,230 110,266 A 52,17 0 0 1 100,267 C 92,230 78,172 76,124 Z" fill="#fff" opacity=".20"/>
301
+ <ellipse cx="150" cy="121" rx="80" ry="29" fill="var(--accent-deep)"/>
302
+ <ellipse cx="150" cy="117" rx="80" ry="29" fill="var(--accent)"/>
303
+ <ellipse cx="150" cy="117" rx="80" ry="29" fill="#fff" opacity=".13"/>
304
+ <ellipse cx="150" cy="118" rx="70" ry="24" fill="url(#skin)"/>
305
+ <ellipse cx="150" cy="118" rx="70" ry="24" fill="none" stroke="var(--ink)" stroke-opacity=".09" stroke-width="1.4"/>
306
+ <ellipse cx="150" cy="118" rx="55" ry="18" fill="none" stroke="var(--ink)" stroke-opacity=".045" stroke-width="1"/>
307
+ <g fill="var(--accent-deep)">
308
+ <path d="M 78,138 l 9,0 -1.5,20 -6,0 z"/><path d="M 120,148 l 9,0 -1,21 -7,0 z"/>
309
+ <path d="M 172,148 l 9,0 -1,21 -7,0 z"/><path d="M 214,138 l 9,0 -1.5,20 -6,0 z"/>
310
+ </g>
311
+ </g>
312
+ <g class="drumSml">
313
+ <path d="M 240,134 C 242,178 252,230 258,266 A 40,13 0 0 0 338,266 C 344,230 354,178 356,134 Z" fill="url(#shellSml)"/>
314
+ <path d="M 314,140 C 316,186 324,234 328,268 A 40,13 0 0 0 338,266 C 344,230 354,178 356,134 Z" fill="var(--ink)" opacity=".19"/>
315
+ <path d="M 254,140 C 256,182 264,232 268,266 A 40,13 0 0 1 260,267 C 254,232 244,182 242,138 Z" fill="#fff" opacity=".17"/>
316
+ <ellipse cx="296" cy="137" rx="60" ry="22" fill="var(--accent-deep)"/>
317
+ <ellipse cx="296" cy="133" rx="60" ry="22" fill="var(--terracotta)"/>
318
+ <ellipse cx="296" cy="133" rx="60" ry="22" fill="#fff" opacity=".13"/>
319
+ <ellipse cx="296" cy="134" rx="51" ry="18" fill="url(#skin)"/>
320
+ <ellipse cx="296" cy="134" rx="51" ry="18" fill="none" stroke="var(--ink)" stroke-opacity=".09" stroke-width="1.4"/>
321
+ <ellipse cx="296" cy="134" rx="39" ry="13" fill="none" stroke="var(--ink)" stroke-opacity=".045" stroke-width="1"/>
322
+ <g fill="var(--accent-deep)">
323
+ <path d="M 243,152 l 8,0 -1,17 -6,0 z"/><path d="M 276,160 l 8,0 -1,18 -6,0 z"/>
324
+ <path d="M 312,160 l 8,0 -1,18 -6,0 z"/><path d="M 345,152 l 8,0 -1,17 -6,0 z"/>
325
+ </g>
326
+ </g>
327
+ </svg>
328
+ </div>
329
+ </div>
330
+ </div>
331
+
332
+ <!-- a child instance does not provision: say so plainly rather than fall
333
+ into the generic "couldn't read your projects" alert -->
334
+ <div class="fieldNote" id="childNote" style="margin-top:22px" hidden>
335
+ Projects aren’t created from this instance — this is a project, not the platform that hosts them.
336
+ </div>
337
+ </div>
338
+ </section>
339
+
340
+ <div class="foot"><!--apex-only:start--><span>A non-profit, capture-resistant commons. </span><!--apex-only:end-->Built by agents, on <span data-brand-product-inline>Cloud&nbsp;Bongos</span>.</div>
94
341
 
95
342
  <script>
96
343
  /* Brand-pack fill (R57): the server injects window.__BRANDING__ ahead of this
@@ -109,6 +356,7 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
109
356
  var b = (typeof window !== 'undefined' && window.__BRANDING__) || {};
110
357
  var productName = (b.identity && b.identity.productName) || 'Cloud Bongos';
111
358
  document.querySelectorAll('[data-brand-product]').forEach(function (n) { n.textContent = productName.toLowerCase(); });
359
+ document.querySelectorAll('[data-brand-product-inline]').forEach(function (n) { n.textContent = productName; });
112
360
 
113
361
  function $(id) { return document.getElementById(id); }
114
362
 
@@ -133,6 +381,344 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
133
381
  });
134
382
  }
135
383
 
384
+ var REDUCED = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
385
+
386
+ /* ══════════════════════════════════════════════════════════════════════════
387
+ THE HUB SHELL — one surface, three views, one real URL.
388
+
389
+ `?view=map|mine|new` is the deep-link contract. It is written with
390
+ history.replaceState so a view is linkable and shareable, and it is what
391
+ makes the landing page's two "Start a project" CTAs (which have always
392
+ pointed at /projects?view=new) actually arrive somewhere.
393
+ ══════════════════════════════════════════════════════════════════════════ */
394
+
395
+ var VIEWS = { map: 1, mine: 1, new: 1 };
396
+ var currentView = 'map';
397
+
398
+ function viewFromUrl() {
399
+ var v;
400
+ try { v = new URLSearchParams(location.search).get('view'); } catch (e) { v = null; }
401
+ return VIEWS[v] ? v : 'map';
402
+ }
403
+
404
+ function setView(v, focus) {
405
+ if (!VIEWS[v]) v = 'map';
406
+ currentView = v;
407
+ Object.keys(VIEWS).forEach(function (k) { $('view-' + k).hidden = (k !== v); });
408
+ document.querySelectorAll('[data-nav]').forEach(function (a) {
409
+ /* Home is a PAGE, not a view — it never takes the current-view pill */
410
+ if (a.dataset.nav !== 'home' && a.dataset.nav === v) a.setAttribute('aria-current', 'page');
411
+ else a.removeAttribute('aria-current');
412
+ });
413
+ try { history.replaceState(null, '', '?view=' + v); } catch (e) {}
414
+ /* the mine view is the only one that polls; it re-reads on entry so a
415
+ card that changed while you were on the map is not stale */
416
+ if (v === 'mine' && me) refreshList();
417
+ /* focus the name field only on a deliberate move into the view — never on
418
+ boot, where it would steal focus from whatever the visitor arrived at */
419
+ if (v === 'new' && focus) $('newName').focus();
420
+ }
421
+
422
+ document.querySelectorAll('[data-nav]').forEach(function (a) {
423
+ if (a.dataset.nav === 'home') return; /* a real navigation — leave it alone */
424
+ a.addEventListener('click', function (ev) { ev.preventDefault(); setView(a.dataset.nav, true); });
425
+ });
426
+ document.querySelectorAll('[data-nav-to]').forEach(function (a) {
427
+ a.addEventListener('click', function (ev) { ev.preventDefault(); setView(a.dataset.navTo, true); });
428
+ });
429
+
430
+ /* ── the self-typing mark. `bongos` is the CONSTANT — it is the word you
431
+ type — and the prefix is what varies, which is exactly what the vanity
432
+ doors do (claudebongos.com swaps the first word, not the second). Only
433
+ runs on the default two-word name so a re-branded instance keeps its own
434
+ wordmark, and never when the visitor asked for reduced motion. ── */
435
+ (function typeMark() {
436
+ var parts = productName.toLowerCase().split(/\s+/);
437
+ var word = parts.pop();
438
+ var prefix = parts.join(' ');
439
+ $('markWord').textContent = word;
440
+ $('markPrefix').textContent = prefix;
441
+ if (REDUCED || prefix !== 'cloud' || word !== 'bongos') return;
442
+ var PREFIXES = ['cloud', 'claude', 'chatgpt', 'gemini'];
443
+ var wi = 0, ci = PREFIXES[0].length, dir = -1;
444
+ setInterval(function () {
445
+ ci += dir;
446
+ if (ci <= 0) { dir = 1; wi = (wi + 1) % PREFIXES.length; ci = 0; }
447
+ else if (ci >= PREFIXES[wi].length) { dir = -1; ci = PREFIXES[wi].length; }
448
+ $('markPrefix').textContent = PREFIXES[wi].slice(0, ci);
449
+ }, 190);
450
+ })();
451
+
452
+ /* ══ THE MAP ═══════════════════════════════════════════════════════════════
453
+ The public catalog: GET /projects/featured is the PUBLIC projection
454
+ (featured + active only, no auth), which is what lets a signed-out visitor
455
+ land on the map at all. The richer GET /projects needs project.curate and
456
+ would 401 them. It carries origin/name/tagline/art_url and nothing else —
457
+ so builders, type and tags render as "—" rather than as invented values.
458
+ The orbs are placed on fixed slots: where things sit is this pass; how the
459
+ sky moves is goal 1000031. ══ */
460
+
461
+ var ORB_SLOTS = [
462
+ [22, 30, 74], [78, 66, 60], [33, 74, 56], [70, 26, 64],
463
+ [13, 58, 52], [88, 42, 50], [50, 80, 54], [62, 48, 58]
464
+ ];
465
+ /* ONE accent palette for every orb on the page — the map's spheres and the
466
+ manage cards' — so a project reads the same colour in both views.
467
+ Token references, never literals: the five hardcoded hexes this replaced
468
+ followed no re-theme (two happened to equal --sun-lit/--gold-soft, three
469
+ were orphans). Keeping the per-project variety is what makes a sky legible;
470
+ deriving it from contract tokens is what makes it survive a re-brand. */
471
+ var ORB_ACCENTS = ['var(--sea)', 'var(--terracotta)', 'var(--gold-soft)', 'var(--sun-lit)', 'var(--accent)'];
472
+ var ORB_GLOWS = ORB_ACCENTS.map(function (t) { return 'color-mix(in srgb,' + t + ' 45%,transparent)'; });
473
+ var catalog = [];
474
+ var mineOrigins = {}; /* normalized host → 'owner' | 'builder' */
475
+ var mapSort = { key: 'name', dir: 1 };
476
+
477
+ /* Catalog origins become hrefs, so they are scheme-checked before they are
478
+ rendered — esc() neutralizes HTML metacharacters but not a `javascript:`
479
+ URI, and this page also drives teardown / revive / domain-attach for the
480
+ signed-in owner, so script running in its origin is not a small thing.
481
+ Same predicate the landing page's catalog band already uses. */
482
+ function safeHttpUrl(raw) {
483
+ try {
484
+ var u = new URL(String(raw));
485
+ return (u.protocol === 'https:' || u.protocol === 'http:') ? u.href : '';
486
+ } catch (e) { return ''; }
487
+ }
488
+ function hostOf(origin) {
489
+ return String(origin || '').replace(/^https?:\/\//, '').replace(/^www\./, '').replace(/\/.*$/, '').toLowerCase();
490
+ }
491
+ /* Accent by POSITION in a canonical (origin-sorted) order, not by hashing the
492
+ name: a hash collides — with five tokens and four projects it put three of
493
+ them on the same ember and the sky stopped being readable. Position over
494
+ the FULL catalog, so filtering or sorting the view never re-colours an orb
495
+ the visitor has already learned. */
496
+ function assignAccents() {
497
+ catalog.slice()
498
+ .sort(function (a, b) { return String(a.origin).localeCompare(String(b.origin)); })
499
+ .forEach(function (p, i) { p._acc = ORB_ACCENTS[i % ORB_ACCENTS.length]; });
500
+ }
501
+ function accentOf(p) { return p._acc || ORB_ACCENTS[0]; }
502
+ function relOf(p) { return mineOrigins[hostOf(p.origin)] || 'none'; }
503
+ function matchesMapQuery(p, q) {
504
+ if (!q) return true;
505
+ return (String(p.name || '') + ' ' + String(p.tagline || '')).toLowerCase().indexOf(q) !== -1;
506
+ }
507
+
508
+ function renderOrbs(list) {
509
+ var field = $('orbField');
510
+ /* replace only the generated orbs — the platform's own orb is static
511
+ apex-only markup and must survive every re-render */
512
+ field.querySelectorAll('.worldOrb[data-generated]').forEach(function (n) { n.remove(); });
513
+ var hint = field.querySelector('.hintbar');
514
+ list.forEach(function (p, i) {
515
+ var slot = ORB_SLOTS[i % ORB_SLOTS.length];
516
+ var a = document.createElement('a');
517
+ a.className = 'worldOrb';
518
+ a.setAttribute('data-generated', '');
519
+ a.setAttribute('data-rel', relOf(p));
520
+ var href = safeHttpUrl(p.origin);
521
+ a.href = href || '#';
522
+ if (href) { a.target = '_blank'; a.rel = 'noopener'; }
523
+ a.title = p.tagline || p.name || '';
524
+ a.style.cssText = '--acc:' + accentOf(p) +
525
+ ';--d:' + slot[2] + 'px;left:' + slot[0] + '%;top:' + slot[1] + '%';
526
+ var rel = relOf(p);
527
+ a.innerHTML =
528
+ (rel === 'owner' ? '<span class="badge">yours</span>' :
529
+ rel === 'builder' ? '<span class="badge">you build here</span>' : '') +
530
+ '<span class="disc"></span>' +
531
+ '<span class="nm">' + esc(p.name) + '</span>';
532
+ field.insertBefore(a, hint);
533
+ });
534
+ }
535
+
536
+ function renderRows(list) {
537
+ $('mapRows').innerHTML = list.map(function (p) {
538
+ var rel = relOf(p);
539
+ var pill = rel === 'owner' ? ' <span class="pill">yours</span>'
540
+ : rel === 'builder' ? ' <span class="pill">you build here</span>' : '';
541
+ return '<tr>' +
542
+ '<td class="colName"><span class="pname"><span class="pdot" style="--acc:' +
543
+ accentOf(p) + '"></span>' +
544
+ '<span><span class="ptitle">' + esc(p.name) + '</span>' + pill + '<br>' +
545
+ '<span class="ptag">' + esc(p.tagline || '') + '</span></span></span></td>' +
546
+ /* the public projection carries no builder count, type or tags — an
547
+ em dash is the truthful cell, an invented number is not */
548
+ '<td class="right num">—</td>' +
549
+ '<td class="colType">—</td>' +
550
+ '<td class="colTags">—</td>' +
551
+ /* liveness on a catalog row is NOT probed by this surface. A chip that
552
+ said "live" here would claim more than anything verified (C6); the
553
+ verification overlay on owned orbs is its own task. */
554
+ '<td class="right colStatus"><span class="chip calm"><span class="cdot"></span>—</span></td>' +
555
+ '<td class="colGo">' + (safeHttpUrl(p.origin)
556
+ ? '<a class="btn small ghost" href="' + esc(safeHttpUrl(p.origin)) + '" target="_blank" rel="noopener">Open →</a>'
557
+ : '') + '</td>' +
558
+ '</tr>';
559
+ }).join('');
560
+ }
561
+
562
+ function applyMapFilter() {
563
+ var onlyMine = $('onlyMine').checked;
564
+ var q = $('mapQ').value.trim().toLowerCase();
565
+ var list = catalog.filter(function (p) {
566
+ return matchesMapQuery(p, q) && (!onlyMine || relOf(p) !== 'none');
567
+ });
568
+ list.sort(function (x, y) {
569
+ if (mapSort.key === 'name') return String(x.name || '').localeCompare(String(y.name || '')) * mapSort.dir;
570
+ return 0;
571
+ });
572
+ renderOrbs(list);
573
+ renderRows(list);
574
+ var self = $('selfOrb');
575
+ if (self) self.hidden = onlyMine; /* the platform is nobody's project */
576
+ $('orbField').classList.toggle('isEmpty', list.length === 0 && (onlyMine || !!q));
577
+ $('emptyMsg').textContent = onlyMine
578
+ ? 'You don’t own or build on anything here yet.'
579
+ : 'Nothing here matches that.';
580
+ $('mapCount').textContent = list.length + (list.length === 1 ? ' project' : ' projects');
581
+ }
582
+
583
+ function loadCatalog() {
584
+ fetch(API + '/projects/featured', { credentials: 'same-origin' })
585
+ .then(function (res) { if (!res.ok) throw new Error('catalog'); return res.json(); })
586
+ .then(function (data) { catalog = (data && data.projects) || []; assignAccents(); applyMapFilter(); })
587
+ .catch(function () {
588
+ /* a child instance has no catalog module at all — say so plainly
589
+ rather than leave an empty sky with no explanation */
590
+ catalog = [];
591
+ applyMapFilter();
592
+ $('mapCount').textContent = '';
593
+ });
594
+ }
595
+
596
+ /* "Mine" is a MEMBERSHIP, not the catalog's advisory owner column: the
597
+ authority is platform_identity_project_memberships, whose role is
598
+ owner|builder|visitor, and GET /my-projects already returns the
599
+ { owned, building } split. 401 (signed out) is not an error here. */
600
+ function loadMemberships() {
601
+ return fetch(API + '/my-projects', { credentials: 'same-origin' })
602
+ .then(function (res) { return res.ok ? res.json() : { owned: [], building: [] }; })
603
+ .then(function (d) {
604
+ mineOrigins = {};
605
+ (d.owned || []).forEach(function (m) { if (m.origin) mineOrigins[hostOf(m.origin)] = 'owner'; });
606
+ (d.building || []).forEach(function (m) { if (m.origin) mineOrigins[hostOf(m.origin)] = 'builder'; });
607
+ })
608
+ .catch(function () { mineOrigins = {}; });
609
+ }
610
+
611
+ $('onlyMine').addEventListener('change', applyMapFilter);
612
+ $('mapQ').addEventListener('input', applyMapFilter);
613
+ document.querySelectorAll('.tbl th[data-sort]').forEach(function (th) {
614
+ th.style.cursor = 'pointer';
615
+ th.addEventListener('click', function () {
616
+ var k = th.getAttribute('data-sort');
617
+ mapSort.dir = (mapSort.key === k) ? -mapSort.dir : 1;
618
+ mapSort.key = k;
619
+ document.querySelectorAll('.tbl th[data-sort]').forEach(function (o) { o.removeAttribute('aria-sort'); });
620
+ th.setAttribute('aria-sort', mapSort.dir === 1 ? 'ascending' : 'descending');
621
+ applyMapFilter();
622
+ });
623
+ });
624
+
625
+ function setSub(which) {
626
+ $('sub-sky').setAttribute('aria-pressed', String(which === 'sky'));
627
+ $('sub-table').setAttribute('aria-pressed', String(which === 'table'));
628
+ $('sub-sky-body').hidden = which !== 'sky';
629
+ $('sub-table-body').hidden = which !== 'table';
630
+ }
631
+ $('sub-sky').addEventListener('click', function () { setSub('sky'); });
632
+ $('sub-table').addEventListener('click', function () { setSub('table'); });
633
+
634
+ /* the starfield only paints where --star-display is block (dark mode) */
635
+ (function stars() {
636
+ var frag = document.createDocumentFragment();
637
+ for (var i = 0; i < 90; i++) {
638
+ var s = document.createElement('i');
639
+ s.style.left = ((i * 37 + 11) % 100) + '%';
640
+ s.style.top = ((i * 61 + 7) % 100) + '%';
641
+ s.style.opacity = (0.25 + ((i * 13) % 50) / 100).toFixed(2);
642
+ frag.appendChild(s);
643
+ }
644
+ $('mapStars').appendChild(frag);
645
+ })();
646
+
647
+ /* ══ NEW PROJECT — step 1 of the four-beat rail. Naming it here hands into
648
+ the wizard with the name already seeded; /create?name= is the entry point
649
+ the wizard already reads, so nothing about the flow changes. ══ */
650
+ $('newForm').addEventListener('submit', function (ev) {
651
+ ev.preventDefault();
652
+ var name = String($('newName').value || '').trim();
653
+ location.assign('/create' + (name ? '?name=' + encodeURIComponent(name) : ''));
654
+ });
655
+
656
+ /* ══ YOUR PROJECTS — search + the five state pills. The filter GROUPS the
657
+ chip outcomes; it never relabels them, so "Live" holding both `live` and
658
+ `live?` is a question about what is known-down, not a claim that they are
659
+ the same thing. Counts recompute against the search so a "Needs you 0" is
660
+ a real answer rather than a stale one. ══ */
661
+
662
+ var mineFilter = 'all';
663
+ var STATE_GROUP = {
664
+ active: 'live', requested: 'working', provisioning: 'working',
665
+ tearing_down: 'working', error: 'attention', torn_down: 'gone'
666
+ };
667
+ function groupFor(inst) {
668
+ if (inst.status === 'active' && inst.last_verified_up === false) return 'attention';
669
+ return STATE_GROUP[inst.status] || 'all';
670
+ }
671
+ function findText(inst) {
672
+ return [inst.slug, inst.target_ref, inst.domain].join(' ').toLowerCase();
673
+ }
674
+ function applyMine() {
675
+ var q = $('mineQ').value.trim().toLowerCase();
676
+ var shown = 0;
677
+ Object.keys(byId).forEach(function (id) {
678
+ var inst = byId[id];
679
+ var li = document.querySelector('[data-inst="' + esc(String(id)) + '"]');
680
+ if (!li) return;
681
+ var keep = (mineFilter === 'all' || groupFor(inst) === mineFilter) &&
682
+ (!q || findText(inst).indexOf(q) !== -1);
683
+ li.hidden = !keep;
684
+ if (keep) shown++;
685
+ });
686
+ $('mineCount').textContent = shown + (shown === 1 ? ' project' : ' projects');
687
+ $('mineEmpty').hidden = shown !== 0 || !Object.keys(byId).length;
688
+ }
689
+ function recountMine() {
690
+ var q = $('mineQ').value.trim().toLowerCase();
691
+ var tally = { all: 0 };
692
+ Object.keys(byId).forEach(function (id) {
693
+ var inst = byId[id];
694
+ if (q && findText(inst).indexOf(q) === -1) return;
695
+ tally.all++;
696
+ var g = groupFor(inst);
697
+ tally[g] = (tally[g] || 0) + 1;
698
+ });
699
+ document.querySelectorAll('.filters button').forEach(function (btn) {
700
+ btn.querySelector('.n').textContent = tally[btn.dataset.f] || 0;
701
+ });
702
+ }
703
+ document.querySelectorAll('.filters button').forEach(function (btn) {
704
+ btn.addEventListener('click', function () {
705
+ mineFilter = btn.dataset.f;
706
+ document.querySelectorAll('.filters button').forEach(function (o) {
707
+ o.setAttribute('aria-pressed', String(o === btn));
708
+ });
709
+ applyMine();
710
+ });
711
+ });
712
+ $('mineQ').addEventListener('input', function () { recountMine(); applyMine(); });
713
+ $('mineClear').addEventListener('click', function () {
714
+ $('mineQ').value = '';
715
+ mineFilter = 'all';
716
+ document.querySelectorAll('.filters button').forEach(function (o) {
717
+ o.setAttribute('aria-pressed', String(o.dataset.f === 'all'));
718
+ });
719
+ recountMine(); applyMine();
720
+ });
721
+
136
722
  var me = null;
137
723
  var byId = {}; /* id → last-seen instance (+open_intent) */
138
724
  var timers = {}; /* id → watch timer */
@@ -218,8 +804,6 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
218
804
 
219
805
  /* ── the orb: the gate's project sphere, glow keyed to verification ────── */
220
806
 
221
- var ORB_ACCENTS = ['#fb923c', '#38bdf8', '#fb7185', '#f6c976', '#8b7ec8'];
222
- var ORB_GLOWS = ['rgba(251,146,60,.5)', 'rgba(56,189,248,.5)', 'rgba(251,113,133,.45)', 'rgba(246,201,118,.45)', 'rgba(139,126,200,.45)'];
223
807
  function orbFor(inst) {
224
808
  var h = 0, slug = String(inst.slug || '');
225
809
  for (var i = 0; i < slug.length; i++) h = (h * 31 + slug.charCodeAt(i)) >>> 0;
@@ -374,10 +958,15 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
374
958
  byId = {};
375
959
  if (!instances.length) {
376
960
  $('empty').hidden = false;
961
+ $('mineBar').hidden = true;
962
+ $('mineEmpty').hidden = true;
377
963
  list.innerHTML = '';
378
964
  return;
379
965
  }
380
966
  $('empty').hidden = true;
967
+ /* the search + state filter only earn their space once there is a list to
968
+ narrow — one project does not need a filter bar above it */
969
+ $('mineBar').hidden = instances.length < 2;
381
970
  list.innerHTML = instances.map(function (inst) {
382
971
  return '<li class="proj" data-inst="' + esc(String(inst.id)) + '"></li>';
383
972
  }).join('');
@@ -386,6 +975,8 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
386
975
  renderCard(inst);
387
976
  if (shouldWatch(inst)) watch(inst.id);
388
977
  });
978
+ recountMine();
979
+ applyMine();
389
980
  }
390
981
 
391
982
  function msg(id, text, cls) {
@@ -421,7 +1012,7 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
421
1012
  lastMsg[id] = null;
422
1013
  }
423
1014
  byId[id] = inst;
424
- if (changed) renderCard(inst);
1015
+ if (changed) { renderCard(inst); recountMine(); applyMine(); }
425
1016
  }
426
1017
  function watch(id) {
427
1018
  stopWatch(id);
@@ -478,8 +1069,12 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
478
1069
  })
479
1070
  .catch(function () { /* transient — next tick will try again */ });
480
1071
  }
481
- setInterval(function () { if (!document.hidden && me) refreshList(); }, REFRESH_MS);
482
- document.addEventListener('visibilitychange', function () { if (!document.hidden && me) refreshList(); });
1072
+ /* view-guarded: the map and the create view have no cards to refresh, so the
1073
+ poll must not run there — a hub that kept re-reading the owner's instances
1074
+ while you browsed the sky would be spending requests on nothing */
1075
+ function shouldPoll() { return !document.hidden && me && currentView === 'mine'; }
1076
+ setInterval(function () { if (shouldPoll()) refreshList(); }, REFRESH_MS);
1077
+ document.addEventListener('visibilitychange', function () { if (shouldPoll()) refreshList(); });
483
1078
 
484
1079
  /* ── teardown: ask, confirm (the truth, itemized — direction §5.4), then
485
1080
  SAY exactly what the server said ── */
@@ -650,17 +1245,38 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
650
1245
  });
651
1246
  }
652
1247
 
1248
+ /* the shell never renders a door it has not earned: signed out, there is no
1249
+ identity chip, no "your projects" tab and no only-mine filter — but the
1250
+ view itself stays reachable by URL and answers with the sign-in gate */
1251
+ function paintShellIdentity() {
1252
+ var signedIn = !!me;
1253
+ $('signinTop').hidden = signedIn;
1254
+ $('who').hidden = !signedIn;
1255
+ document.querySelector('[data-nav="mine"]').hidden = !signedIn;
1256
+ $('onlyMine').closest('.switch').hidden = !signedIn;
1257
+ if (!signedIn && $('onlyMine').checked) { $('onlyMine').checked = false; applyMapFilter(); }
1258
+ }
1259
+
1260
+ /* Boot. The view is set FIRST, from the URL, so the page is navigable before
1261
+ any request resolves — /projects?view=new must land on the create view even
1262
+ if the identity call is slow or fails. */
1263
+ setView(viewFromUrl());
1264
+
653
1265
  fetch(API + '/me', { credentials: 'same-origin' })
654
1266
  .then(function (res) { if (!res.ok) throw new Error(String(res.status)); return res.json(); })
655
1267
  .then(function (data) {
656
1268
  me = data.builder || data.me || data;
657
1269
  renderWho();
1270
+ paintShellIdentity();
658
1271
  $('gate').hidden = true;
659
1272
  $('mine').hidden = false;
660
1273
  load();
1274
+ return loadMemberships();
661
1275
  })
662
1276
  .catch(function (err) {
1277
+ me = null;
663
1278
  renderWho();
1279
+ paintShellIdentity();
664
1280
  $('gate').hidden = false;
665
1281
  if (String(err && err.message) !== '401') {
666
1282
  var g = $('gateAlert');
@@ -669,7 +1285,10 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
669
1285
  g.classList.add('on');
670
1286
  }
671
1287
  }
672
- });
1288
+ })
1289
+ /* the catalog is PUBLIC, so it loads either way — the map is the view a
1290
+ signed-out stranger lands on and it must not depend on identity */
1291
+ .then(loadCatalog, loadCatalog);
673
1292
  })();
674
1293
  </script>
675
1294
  </body>