@bongos/core 1.19.90 → 1.19.91
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.
- package/.bongos-core.json +30 -25
- package/config/branding.neutral.json +6 -6
- package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
- package/modules/dev-box/app/assets/tray/win-on.png +0 -0
- package/modules/dev-box/app/build-resources/icon.png +0 -0
- package/modules/dev-box/app/renderer/theme.generated.css +9 -9
- package/modules/dev-box/app/scripts/gen-brand.js +10 -10
- package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +3 -3
- package/modules/dev-box/app/scripts/gen-icons.js +3 -3
- package/modules/dev-box/app/src/brand.generated.json +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +112 -64
- package/public-landing/index.html +543 -88
- package/scripts/status-mirror-sync.js +12 -6
- package/src/bongos/serve-internal.js +53 -10
- package/src/module-api.js +3 -1
- package/src/platform-server.js +26 -2
- package/tests/cloud_bongos_pack.mjs +11 -7
- package/tests/landing_page.mjs +289 -0
|
@@ -3,119 +3,574 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
<!-- ── S0 · HEAD METADATA — SERVER-STAMPED ──────────────────────────────────
|
|
7
|
+
The {{…}} tokens below are replaced by landingPage() (src/platform-server.js)
|
|
8
|
+
from the server-side branding(), on every landing page it serves.
|
|
9
|
+
|
|
10
|
+
They CANNOT be filled by the page's own branding script: social tags are read
|
|
11
|
+
by crawlers before any JavaScript runs. They also cannot be baked, because
|
|
12
|
+
public-landing/ ships wholesale to every instance — a hardcoded apex would
|
|
13
|
+
make every child instance advertise the control plane (direction C5).
|
|
14
|
+
|
|
15
|
+
The baked values are the NEUTRAL pack's, so opening this file standalone
|
|
16
|
+
reads correctly and leaks nothing about any particular instance.
|
|
17
|
+
─────────────────────────────────────────────────────────────────────── -->
|
|
18
|
+
<title>{{productName}} — {{tagline}}</title>
|
|
19
|
+
<meta name="description" content="{{tagline}} {{productName}} is an open, AI-first build platform: agents claim tasks and ship the code, you set the direction and the gates.">
|
|
20
|
+
<link rel="canonical" href="{{publicOrigin}}/">
|
|
21
|
+
<meta property="og:type" content="website">
|
|
22
|
+
<meta property="og:site_name" content="{{productName}}">
|
|
23
|
+
<meta property="og:url" content="{{publicOrigin}}/">
|
|
24
|
+
<meta property="og:title" content="{{productName}} — {{tagline}}">
|
|
25
|
+
<meta property="og:description" content="{{tagline}} {{productName}} is an open, AI-first build platform: agents claim tasks and ship the code, you set the direction and the gates.">
|
|
26
|
+
<meta property="og:image" content="{{publicOrigin}}/buddha.png">
|
|
27
|
+
<meta name="twitter:card" content="summary_large_image">
|
|
28
|
+
<meta name="twitter:title" content="{{productName}} — {{tagline}}">
|
|
29
|
+
<meta name="twitter:description" content="{{tagline}} {{productName}} is an open, AI-first build platform: agents claim tasks and ship the code, you set the direction and the gates.">
|
|
30
|
+
<meta name="twitter:image" content="{{publicOrigin}}/buddha.png">
|
|
31
|
+
<!-- Fonts: production also injects these from the branding pack
|
|
9
32
|
(config/branding.json theme.ui.fonts.links); kept here so the page reads
|
|
10
33
|
correctly when viewed standalone. -->
|
|
11
34
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
12
35
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
13
|
-
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Baloo+2:wght@500;600;700&family=Quicksand:wght@400;500;600;700&display=swap">
|
|
36
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Baloo+2:wght@500;600;700&family=Quicksand:wght@400;500;600;700&family=Fira+Code:wght@400;500&display=swap">
|
|
37
|
+
<!-- The shared token + component layer. /create and /projects link the same
|
|
38
|
+
file; the landing page adds components, it does not fork the system. -->
|
|
39
|
+
<link rel="stylesheet" href="assets/cosmos.css">
|
|
40
|
+
<script>
|
|
41
|
+
/* Mode, resolved before first paint so an explicit choice never flashes.
|
|
42
|
+
Same contract as /create and /projects: data-mode on <html>, else the OS
|
|
43
|
+
preference decides. */
|
|
44
|
+
(function(){try{var m=localStorage.getItem('cb-color-mode');
|
|
45
|
+
if(m==='light'||m==='dark')document.documentElement.setAttribute('data-mode',m);}catch(e){}})();
|
|
46
|
+
</script>
|
|
14
47
|
<style>
|
|
15
|
-
/* ──
|
|
16
|
-
|
|
17
|
-
the
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
48
|
+
/* ── PAGE-LOCAL SHEET ────────────────────────────────────────────────────
|
|
49
|
+
Only components that are genuinely this page's: the hero split, the bongo
|
|
50
|
+
pair, the mechanism cards, the proof band, the doors. The container, type
|
|
51
|
+
scale, spacing scale, grid primitive and mid breakpoint all live in
|
|
52
|
+
cosmos.css so the landing page and the hub share them (direction §6).
|
|
53
|
+
|
|
54
|
+
There is deliberately NO :root block here. injectBrandingGlobal splices the
|
|
55
|
+
branding pack's theme <style> at the END of <head>; a page-local :root sits
|
|
56
|
+
at the same specificity and, being earlier, would be silently outranked in
|
|
57
|
+
one direction and would defeat re-branding in the other (direction C1).
|
|
58
|
+
Page-local custom properties are therefore scoped to their component, and
|
|
59
|
+
every dark-mode rule is written TWICE — the data-mode block and its
|
|
60
|
+
prefers-color-scheme twin — matching cosmos.css's own pattern (C2).
|
|
61
|
+
──────────────────────────────────────────────────────────────────────── */
|
|
62
|
+
|
|
63
|
+
/* ── S2 · hero ── */
|
|
64
|
+
.lead{display:grid;grid-template-columns:1.1fr .9fr;gap:clamp(18px,4vw,52px);align-items:center;padding:clamp(8px,2.5vw,26px) 0 clamp(30px,5vw,60px);}
|
|
65
|
+
.eyebrow{font-family:var(--font-mono);font-size:13.5px;font-weight:500;color:var(--chrome-ink-dim);margin-bottom:18px;text-shadow:var(--chrome-glow);}
|
|
66
|
+
.eyebrow .typed{color:var(--chrome-accent);font-weight:600;}
|
|
67
|
+
.eyebrow .caret{display:inline-block;width:8px;color:var(--chrome-accent);animation:blink 1.05s step-end infinite;}
|
|
68
|
+
@keyframes blink{50%{opacity:0;}}
|
|
69
|
+
h1.big{font-family:var(--font-display);font-weight:700;font-size:clamp(33px,5.4vw,58px);line-height:1.05;letter-spacing:-1.3px;color:var(--chrome-ink);margin-bottom:18px;text-shadow:var(--chrome-glow);}
|
|
70
|
+
.lead .lede{font-size:clamp(15.5px,1.85vw,18px);color:var(--hero-lede);max-width:50ch;margin-bottom:26px;line-height:1.58;text-shadow:var(--chrome-glow);font-weight:500;}
|
|
71
|
+
.cta-row{display:flex;gap:12px;flex-wrap:wrap;align-items:center;}
|
|
72
|
+
.btn.lg{font-size:16.5px;padding:14px 28px;}
|
|
73
|
+
|
|
74
|
+
/* ── the bongo pair · the hero mark ──
|
|
75
|
+
The brand's OWN mark made dimensional: the favicon is already two circles,
|
|
76
|
+
big and small, with light heads. Every fill is a token, so it follows a
|
|
77
|
+
re-theme instead of pinning one instance's art onto every instance's page.
|
|
78
|
+
|
|
79
|
+
The head is SKIN, not a card. It carries its own --skin-* tokens and must
|
|
80
|
+
NEVER inherit --bg-card — that turns it black in dark mode. These are
|
|
81
|
+
component-scoped (on .bongos, not :root) so they cannot collide with the
|
|
82
|
+
pack's contract tier. */
|
|
83
|
+
.bongos{display:block;width:100%;height:auto;overflow:visible;
|
|
84
|
+
--skin-hi:#fffdf8;--skin-mid:#fff6e9;--skin-lo:#e8d3b6;}
|
|
85
|
+
:root[data-mode="dark"] .bongos{--skin-hi:#fdf7ea;--skin-mid:#efe1c8;--skin-lo:#c3ae90;}
|
|
86
|
+
@media (prefers-color-scheme:dark){:root:not([data-mode="light"]) .bongos{--skin-hi:#fdf7ea;--skin-mid:#efe1c8;--skin-lo:#c3ae90;}}
|
|
87
|
+
.mascot{position:relative;}
|
|
88
|
+
.mascot::before{content:"";position:absolute;inset:-8% -12%;z-index:-1;border-radius:50%;
|
|
89
|
+
background:radial-gradient(ellipse at center,rgba(255,255,255,.95) 0%,rgba(255,255,255,.7) 45%,rgba(255,255,255,0) 72%);}
|
|
90
|
+
:root[data-mode="dark"] .mascot::before{background:radial-gradient(ellipse at center,rgba(255,217,138,.22) 0%,rgba(251,146,60,.10) 46%,rgba(255,255,255,0) 72%);}
|
|
91
|
+
@media (prefers-color-scheme:dark){:root:not([data-mode="light"]) .mascot::before{background:radial-gradient(ellipse at center,rgba(255,217,138,.22) 0%,rgba(251,146,60,.10) 46%,rgba(255,255,255,0) 72%);}}
|
|
92
|
+
/* the beat — a 1.6s loop: BIG on 0.0 and 0.8 · SMALL on 0.4, 1.0 and 1.25 */
|
|
93
|
+
.bongos .glow {transform-box:fill-box;transform-origin:center;animation:glowPulse 1.6s ease-out infinite;}
|
|
94
|
+
.bongos .bshadow{transform-box:fill-box;transform-origin:center;animation:shadowPulse 1.6s ease-out infinite;}
|
|
95
|
+
.bongos .drumBig{transform-origin:150px 270px;animation:bobBig 1.6s ease-out infinite;}
|
|
96
|
+
.bongos .drumSml{transform-origin:296px 270px;animation:bobSml 1.6s ease-out infinite;}
|
|
97
|
+
.bongos .headBig{transform-origin:150px 118px;animation:hitBig 1.6s ease-out infinite;}
|
|
98
|
+
.bongos .headSml{transform-origin:296px 134px;animation:hitSml 1.6s ease-out infinite;}
|
|
99
|
+
.bongos .ring {transform-box:fill-box;transform-origin:center;opacity:0;animation:burst 1.6s ease-out infinite;}
|
|
100
|
+
@keyframes hitBig{
|
|
101
|
+
0%{transform:translateY(5px) scaleY(.88);} 9%{transform:translateY(0) scaleY(1);}
|
|
102
|
+
50%{transform:translateY(0) scaleY(1);} 51%{transform:translateY(5px) scaleY(.88);}
|
|
103
|
+
60%,100%{transform:translateY(0) scaleY(1);}
|
|
26
104
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
background:linear-gradient(180deg,#fde8c8 0%,#fff4e6 45%,#fffdfa 100%);
|
|
33
|
-
background-attachment:fixed;
|
|
34
|
-
min-height:100vh;
|
|
35
|
-
-webkit-font-smoothing:antialiased;
|
|
36
|
-
overflow-x:hidden;
|
|
105
|
+
@keyframes hitSml{
|
|
106
|
+
0%,24%{transform:translateY(0) scaleY(1);} 25.5%{transform:translateY(3.5px) scaleY(.88);}
|
|
107
|
+
34%,62%{transform:translateY(0) scaleY(1);} 63.5%{transform:translateY(3.5px) scaleY(.88);}
|
|
108
|
+
71%,77%{transform:translateY(0) scaleY(1);} 78.5%{transform:translateY(2.5px) scaleY(.91);}
|
|
109
|
+
87%,100%{transform:translateY(0) scaleY(1);}
|
|
37
110
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
.cloud.b{width:280px;height:280px;top:6%;right:-50px;background:rgba(255,224,178,0.5);}
|
|
42
|
-
.cloud.c{width:440px;height:440px;bottom:-180px;left:28%;background:rgba(255,255,255,0.4);}
|
|
43
|
-
.cloud.d{width:220px;height:220px;top:40%;left:4%;background:rgba(56,189,248,0.15);}
|
|
44
|
-
|
|
45
|
-
.stage{
|
|
46
|
-
position:relative;z-index:1;min-height:100vh;
|
|
47
|
-
display:flex;flex-direction:column;align-items:center;justify-content:center;
|
|
48
|
-
text-align:center;padding:48px 24px 64px;gap:8px;
|
|
111
|
+
@keyframes bobBig{
|
|
112
|
+
0%{transform:translateY(2px);} 11%,50%{transform:translateY(0);}
|
|
113
|
+
52%{transform:translateY(2px);} 63%,100%{transform:translateY(0);}
|
|
49
114
|
}
|
|
50
|
-
|
|
51
|
-
.
|
|
52
|
-
|
|
115
|
+
@keyframes bobSml{
|
|
116
|
+
0%,24%{transform:translateY(0);} 26%{transform:translateY(1.5px);}
|
|
117
|
+
36%,64%{transform:translateY(0);} 65%{transform:translateY(1.5px);}
|
|
118
|
+
74%,79%{transform:translateY(0);} 80%{transform:translateY(1.2px);}
|
|
119
|
+
89%,100%{transform:translateY(0);}
|
|
53
120
|
}
|
|
54
|
-
.
|
|
55
|
-
|
|
56
|
-
|
|
121
|
+
@keyframes burst{0%{transform:scale(.3);opacity:.85;}26%{opacity:0;}100%{transform:scale(1.18);opacity:0;}}
|
|
122
|
+
@keyframes glowPulse{
|
|
123
|
+
0%{opacity:.85;transform:scale(1.035);} 16%{opacity:.42;transform:scale(1);}
|
|
124
|
+
50%{opacity:.72;transform:scale(1.025);} 66%,100%{opacity:.42;transform:scale(1);}
|
|
57
125
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
.
|
|
61
|
-
width:min(540px,86vw);aspect-ratio:1.6/1;border-radius:28px;
|
|
62
|
-
border:2px dashed var(--accent);background:rgba(255,255,255,0.6);
|
|
63
|
-
display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;
|
|
64
|
-
color:var(--ink-soft);font-size:15px;font-weight:500;margin-bottom:6px;padding:24px;
|
|
126
|
+
@keyframes shadowPulse{
|
|
127
|
+
0%{transform:scaleX(1.06);opacity:1;} 11%,50%{transform:scaleX(1);opacity:.78;}
|
|
128
|
+
53%{transform:scaleX(1.05);opacity:.95;} 64%,100%{transform:scaleX(1);opacity:.78;}
|
|
65
129
|
}
|
|
130
|
+
/* THE STATIC POSE (direction §7). The drums, heads, glow and shadow are stopped
|
|
131
|
+
by cosmos.css's own `*,*::before,*::after{animation:none !important}` under
|
|
132
|
+
reduce — an !important author declaration, so it beats these normal ones
|
|
133
|
+
whatever the order or specificity. Deliberately NOT duplicated here: one
|
|
134
|
+
global motion policy is the point of a shared layer. That cross-file
|
|
135
|
+
dependency is load-bearing, so tests/landing_page.mjs asserts BOTH halves —
|
|
136
|
+
that cosmos.css still carries the kill switch, and that nothing on this page
|
|
137
|
+
marks an animation !important and so escapes it.
|
|
138
|
+
`animation:none` renders the base style, not keyframe 0% — so the drums rest,
|
|
139
|
+
they do not freeze mid-hit. Only the ripples need hiding: they are opacity:0
|
|
140
|
+
at rest and would otherwise sit visible at full stroke. */
|
|
141
|
+
@media (prefers-reduced-motion:reduce){.bongos .ring{display:none;}}
|
|
66
142
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
143
|
+
/* ── S3 · the mechanism ── */
|
|
144
|
+
.beats{--col:238px;}
|
|
145
|
+
.beat{background:var(--card);border-radius:22px;box-shadow:var(--shadow-card);padding:clamp(19px,2.5vw,26px);}
|
|
146
|
+
.beatNum{font-family:var(--font-mono);font-size:12px;font-weight:500;color:var(--accent);letter-spacing:1.3px;text-transform:uppercase;margin-bottom:11px;}
|
|
147
|
+
.beat p{font-size:var(--fs-body);font-weight:500;color:var(--ink-soft);line-height:1.58;}
|
|
148
|
+
|
|
149
|
+
/* ── S4 · the lifecycle: cosmos.css's .strip, unmodified, doing marketing work.
|
|
150
|
+
The marketing page and the working page share the component rather than
|
|
151
|
+
merely resembling each other. ── */
|
|
152
|
+
.railWrap{background:var(--card);border-radius:24px;box-shadow:var(--shadow-card);padding:var(--pad-lg) var(--pad-lg) clamp(14px,2vw,20px);}
|
|
153
|
+
.railWrap .strip{margin-top:18px;}
|
|
154
|
+
|
|
155
|
+
/* ── S5 · living proof: the sky appears exactly ONCE in the warm page, as a
|
|
156
|
+
full-bleed band, so the catalog reads as a window into the hub rather
|
|
157
|
+
than as wallpaper. ── */
|
|
158
|
+
.band{position:relative;z-index:1;background:linear-gradient(180deg,#080a1c 0%,#0d0f28 52%,#0a0c20 100%);
|
|
159
|
+
padding:clamp(34px,5vw,58px) 0;margin-bottom:var(--pad-xl);overflow:hidden;}
|
|
160
|
+
.band::before{content:"";position:absolute;inset:0;pointer-events:none;
|
|
161
|
+
background:radial-gradient(60% 90% at 22% 12%,rgba(43,36,86,.75) 0%,rgba(43,36,86,0) 62%),
|
|
162
|
+
radial-gradient(50% 80% at 82% 88%,rgba(251,146,60,.16) 0%,rgba(251,146,60,0) 60%);}
|
|
163
|
+
.band .wrap{position:relative;z-index:1;}
|
|
164
|
+
.band h2.sec-h{color:#fff6e6;}
|
|
165
|
+
.bandSub{font-size:var(--fs-body);font-weight:600;color:rgba(255,246,230,.62);margin-bottom:22px;}
|
|
166
|
+
.bandHead{display:flex;align-items:flex-end;justify-content:space-between;gap:18px;flex-wrap:wrap;}
|
|
167
|
+
.bandHead .linklike{color:var(--gold);}
|
|
168
|
+
.conGrid{--col:268px;}
|
|
169
|
+
.conItem{display:flex;gap:13px;align-items:center;text-decoration:none;color:inherit;
|
|
170
|
+
border:1.5px solid rgba(255,217,138,.22);border-radius:16px;padding:13px 15px;
|
|
171
|
+
background:rgba(13,15,40,.55);backdrop-filter:blur(6px);transition:transform .14s ease,border-color .14s ease;}
|
|
172
|
+
.conItem:hover{transform:translateY(-2px);border-color:rgba(255,217,138,.5);}
|
|
173
|
+
.conItem .orb{flex:0 0 38px;width:38px;height:38px;cursor:inherit;}
|
|
174
|
+
.conName{font-family:var(--font-display);font-weight:700;font-size:15.5px;letter-spacing:-.2px;line-height:1.2;color:#fff6e6;display:block;}
|
|
175
|
+
.conTag{font-size:12.5px;font-weight:600;color:rgba(255,246,230,.58);margin-top:3px;line-height:1.35;display:block;}
|
|
176
|
+
.conItem .chip{margin-top:8px;white-space:nowrap;}
|
|
177
|
+
|
|
178
|
+
/* ── S6/S7 ── */
|
|
179
|
+
.own{background:var(--card);border-radius:24px;box-shadow:var(--shadow-card);padding:clamp(22px,3.2vw,34px);}
|
|
180
|
+
.door{background:var(--card);border-radius:22px;box-shadow:var(--shadow-card);padding:clamp(20px,2.8vw,28px);display:flex;flex-direction:column;gap:10px;align-items:flex-start;}
|
|
181
|
+
.door p{font-size:var(--fs-body);font-weight:500;color:var(--ink-soft);line-height:1.55;flex:1;}
|
|
182
|
+
|
|
183
|
+
@media (max-width:900px){
|
|
184
|
+
.lead{grid-template-columns:1fr;text-align:center;}
|
|
185
|
+
.lead .lede{margin-left:auto;margin-right:auto;}
|
|
186
|
+
.cta-row{justify-content:center;}
|
|
187
|
+
.mascot{max-width:330px;margin:0 auto;order:-1;}
|
|
86
188
|
}
|
|
87
189
|
</style>
|
|
88
190
|
</head>
|
|
89
191
|
<body>
|
|
192
|
+
|
|
193
|
+
<!-- C3: the pack's theme.backdrop emits a body{background:…} that overrides
|
|
194
|
+
--page-bg in BOTH modes. .sky is the opaque fixed layer that paints over
|
|
195
|
+
it, so this element is required, not decorative. Stars show only in dark
|
|
196
|
+
(cosmos.css drives them from --star-display). -->
|
|
90
197
|
<div class="sky" aria-hidden="true">
|
|
91
|
-
<
|
|
198
|
+
<span class="star" style="top:3%;left:11%;width:2px;height:2px;"></span>
|
|
199
|
+
<span class="star" style="top:6%;left:27%;width:1.5px;height:1.5px;animation-delay:.7s;"></span>
|
|
200
|
+
<span class="star" style="top:4%;left:44%;width:2px;height:2px;animation-delay:1.4s;"></span>
|
|
201
|
+
<span class="star" style="top:9%;left:61%;width:1.5px;height:1.5px;animation-delay:.3s;"></span>
|
|
202
|
+
<span class="star" style="top:5%;left:79%;width:2.5px;height:2.5px;animation-delay:2s;"></span>
|
|
203
|
+
<span class="star" style="top:12%;left:90%;width:1.5px;height:1.5px;animation-delay:1s;"></span>
|
|
204
|
+
<span class="star" style="top:16%;left:19%;width:1.5px;height:1.5px;animation-delay:1.8s;"></span>
|
|
205
|
+
<span class="star" style="top:24%;left:54%;width:2px;height:2px;animation-delay:.5s;"></span>
|
|
206
|
+
<span class="star" style="top:31%;left:35%;width:1.5px;height:1.5px;animation-delay:2.4s;"></span>
|
|
207
|
+
<span class="star" style="top:28%;left:77%;width:1.5px;height:1.5px;animation-delay:1.2s;"></span>
|
|
208
|
+
<span class="star" style="top:46%;left:7%;width:2px;height:2px;animation-delay:.9s;"></span>
|
|
209
|
+
<span class="star" style="top:53%;left:92%;width:1.5px;height:1.5px;animation-delay:1.6s;"></span>
|
|
210
|
+
<span class="star" style="top:66%;left:23%;width:1.5px;height:1.5px;animation-delay:2.1s;"></span>
|
|
211
|
+
<span class="star" style="top:74%;left:66%;width:2px;height:2px;animation-delay:.4s;"></span>
|
|
212
|
+
<span class="star" style="top:86%;left:41%;width:1.5px;height:1.5px;animation-delay:1.9s;"></span>
|
|
213
|
+
<span class="star" style="top:93%;left:81%;width:2px;height:2px;animation-delay:.8s;"></span>
|
|
92
214
|
</div>
|
|
93
215
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
216
|
+
<!-- ══ S1 · HEADER — THE DOOR ═══════════════════════════════════════════════
|
|
217
|
+
Its absence is what blocks the apex flip. The Hall href is a server-stamped
|
|
218
|
+
token, not a baked origin: clientBranding() does not project domains, so a
|
|
219
|
+
client fill can never re-point it and every child instance would send its
|
|
220
|
+
visitors to the platform's hall (direction C6).
|
|
221
|
+
═══════════════════════════════════════════════════════════════════════ -->
|
|
222
|
+
<header class="top">
|
|
223
|
+
<a class="mark" href="/">cloud <span>bongos</span></a>
|
|
224
|
+
<div class="top-right">
|
|
225
|
+
<a class="btn-space" href="/projects">Projects</a>
|
|
226
|
+
<a class="btn-space" data-link-hall href="{{buildersOrigin}}">Hall</a>
|
|
227
|
+
<button class="mode-toggle" id="mode" type="button" aria-label="Toggle light or dark">☾</button>
|
|
228
|
+
<div class="who" id="who"></div>
|
|
98
229
|
</div>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
230
|
+
</header>
|
|
231
|
+
|
|
232
|
+
<!-- ══ S2 · HERO ═══════════════════════════════════════════════════════════ -->
|
|
233
|
+
<div class="wrap">
|
|
234
|
+
<section class="lead">
|
|
235
|
+
<div>
|
|
236
|
+
<p class="eyebrow">type <span class="typed" id="typed">bongos</span><span class="caret" aria-hidden="true">▌</span> in any AI</p>
|
|
237
|
+
<h1 class="big" data-brand-tagline>Building software is laying down a beat.</h1>
|
|
238
|
+
<p class="lede">AI agents claim tasks and ship the code; you set the direction and the gates.</p>
|
|
239
|
+
<div class="cta-row">
|
|
240
|
+
<a class="btn lg" href="/projects?view=new">Start a project</a>
|
|
241
|
+
<!--apex-only:start--><a class="btn ghost lg" href="/projects">See what's building</a><!--apex-only:end-->
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
<div class="mascot">
|
|
245
|
+
<!-- Every COLOUR is a token so the mark follows a re-theme. The handful of
|
|
246
|
+
fill="#fff" below are not colour: they are the specular highlights and
|
|
247
|
+
rim-light on a curved shell, and a lit edge stays white under any
|
|
248
|
+
palette — tokenizing them would tint the light source itself. Same
|
|
249
|
+
reason fill="var(--ink)" carries the shading: shadow follows the ink. -->
|
|
250
|
+
<svg class="bongos" viewBox="0 0 440 320" role="img" aria-label="A pair of bongo drums being played">
|
|
251
|
+
<defs>
|
|
252
|
+
<linearGradient id="shellBig" x1="0" y1="0" x2="1" y2="0.35">
|
|
253
|
+
<stop offset="0%" stop-color="var(--accent-deep)"/><stop offset="16%" stop-color="var(--accent)"/>
|
|
254
|
+
<stop offset="48%" stop-color="var(--accent)"/><stop offset="86%" stop-color="var(--accent-deep)"/>
|
|
255
|
+
<stop offset="100%" stop-color="var(--accent-deep)"/>
|
|
256
|
+
</linearGradient>
|
|
257
|
+
<linearGradient id="shellSml" x1="0" y1="0" x2="1" y2="0.35">
|
|
258
|
+
<stop offset="0%" stop-color="var(--accent-deep)"/><stop offset="18%" stop-color="var(--terracotta)"/>
|
|
259
|
+
<stop offset="55%" stop-color="var(--terracotta)"/><stop offset="100%" stop-color="var(--accent-deep)"/>
|
|
260
|
+
</linearGradient>
|
|
261
|
+
<radialGradient id="skin" cx="0.36" cy="0.26" r="0.9">
|
|
262
|
+
<stop offset="0%" stop-color="var(--skin-hi)"/><stop offset="58%" stop-color="var(--skin-mid)"/>
|
|
263
|
+
<stop offset="100%" stop-color="var(--skin-lo)"/>
|
|
264
|
+
</radialGradient>
|
|
265
|
+
<radialGradient id="halo" cx="0.5" cy="0.5" r="0.5">
|
|
266
|
+
<stop offset="0%" stop-color="var(--accent)" stop-opacity="0.30"/>
|
|
267
|
+
<stop offset="48%" stop-color="var(--accent)" stop-opacity="0.13"/>
|
|
268
|
+
<stop offset="100%" stop-color="var(--accent)" stop-opacity="0"/>
|
|
269
|
+
</radialGradient>
|
|
270
|
+
<filter id="soft" x="-60%" y="-160%" width="220%" height="420%">
|
|
271
|
+
<feGaussianBlur stdDeviation="9"/>
|
|
272
|
+
</filter>
|
|
273
|
+
</defs>
|
|
274
|
+
|
|
275
|
+
<ellipse class="glow" cx="222" cy="170" rx="215" ry="140" fill="url(#halo)"/>
|
|
276
|
+
<ellipse class="bshadow" cx="220" cy="286" rx="142" ry="14"
|
|
277
|
+
fill="var(--ink)" opacity=".22" filter="url(#soft)"/>
|
|
103
278
|
|
|
104
|
-
|
|
279
|
+
<!-- the joining block, behind both shells -->
|
|
280
|
+
<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)"/>
|
|
281
|
+
<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"/>
|
|
282
|
+
|
|
283
|
+
<g class="drumBig">
|
|
284
|
+
<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)"/>
|
|
285
|
+
<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"/>
|
|
286
|
+
<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"/>
|
|
287
|
+
<ellipse cx="150" cy="121" rx="80" ry="29" fill="var(--accent-deep)"/>
|
|
288
|
+
<ellipse cx="150" cy="117" rx="80" ry="29" fill="var(--accent)"/>
|
|
289
|
+
<ellipse cx="150" cy="117" rx="80" ry="29" fill="#fff" opacity=".13"/>
|
|
290
|
+
<g class="headBig">
|
|
291
|
+
<ellipse cx="150" cy="118" rx="70" ry="24" fill="url(#skin)"/>
|
|
292
|
+
<ellipse cx="150" cy="118" rx="70" ry="24" fill="none" stroke="var(--ink)" stroke-opacity=".09" stroke-width="1.4"/>
|
|
293
|
+
<ellipse cx="150" cy="118" rx="55" ry="18" fill="none" stroke="var(--ink)" stroke-opacity=".045" stroke-width="1"/>
|
|
294
|
+
<ellipse class="ring" cx="150" cy="118" rx="56" ry="19" fill="none" stroke="var(--accent-deep)" stroke-width="2.6" style="animation-delay:0s"/>
|
|
295
|
+
<ellipse class="ring" cx="150" cy="118" rx="56" ry="19" fill="none" stroke="var(--accent-deep)" stroke-width="2.6" style="animation-delay:.8s"/>
|
|
296
|
+
</g>
|
|
297
|
+
<g fill="var(--accent-deep)">
|
|
298
|
+
<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"/>
|
|
299
|
+
<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"/>
|
|
300
|
+
</g>
|
|
301
|
+
</g>
|
|
302
|
+
|
|
303
|
+
<g class="drumSml">
|
|
304
|
+
<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)"/>
|
|
305
|
+
<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"/>
|
|
306
|
+
<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"/>
|
|
307
|
+
<ellipse cx="296" cy="137" rx="60" ry="22" fill="var(--accent-deep)"/>
|
|
308
|
+
<ellipse cx="296" cy="133" rx="60" ry="22" fill="var(--terracotta)"/>
|
|
309
|
+
<ellipse cx="296" cy="133" rx="60" ry="22" fill="#fff" opacity=".13"/>
|
|
310
|
+
<g class="headSml">
|
|
311
|
+
<ellipse cx="296" cy="134" rx="51" ry="18" fill="url(#skin)"/>
|
|
312
|
+
<ellipse cx="296" cy="134" rx="51" ry="18" fill="none" stroke="var(--ink)" stroke-opacity=".09" stroke-width="1.4"/>
|
|
313
|
+
<ellipse cx="296" cy="134" rx="39" ry="13" fill="none" stroke="var(--ink)" stroke-opacity=".045" stroke-width="1"/>
|
|
314
|
+
<ellipse class="ring" cx="296" cy="134" rx="41" ry="14" fill="none" stroke="var(--accent-deep)" stroke-width="2.3" style="animation-delay:.4s"/>
|
|
315
|
+
<ellipse class="ring" cx="296" cy="134" rx="41" ry="14" fill="none" stroke="var(--accent-deep)" stroke-width="2.3" style="animation-delay:1s"/>
|
|
316
|
+
<ellipse class="ring" cx="296" cy="134" rx="41" ry="14" fill="none" stroke="var(--accent-deep)" stroke-width="2.3" style="animation-delay:1.25s"/>
|
|
317
|
+
</g>
|
|
318
|
+
<g fill="var(--accent-deep)">
|
|
319
|
+
<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"/>
|
|
320
|
+
<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"/>
|
|
321
|
+
</g>
|
|
322
|
+
</g>
|
|
323
|
+
</svg>
|
|
324
|
+
</div>
|
|
325
|
+
</section>
|
|
326
|
+
</div>
|
|
327
|
+
|
|
328
|
+
<!-- ══ S3 · THE MECHANISM ══════════════════════════════════════════════════
|
|
329
|
+
"AI writes code" is a crowded claim and not the differentiator. What is
|
|
330
|
+
actually enforced — a complete ledger, a server-checked ladder, an
|
|
331
|
+
adversarial grade — is. All three are portable: a child instance inherits
|
|
332
|
+
them, so none of this section is apex-only.
|
|
333
|
+
═══════════════════════════════════════════════════════════════════════ -->
|
|
334
|
+
<div class="wrap">
|
|
335
|
+
<section class="sec grid beats">
|
|
336
|
+
<div class="beat">
|
|
337
|
+
<div class="beatNum">the ledger</div>
|
|
338
|
+
<h3 class="sec-h3">Every change is a claimed task</h3>
|
|
339
|
+
<p>No admin loophole, no tiny-fix loophole. If work happened, there is a row that says who did
|
|
340
|
+
it, what it cost, and why. The ledger is complete or it lies.</p>
|
|
341
|
+
</div>
|
|
342
|
+
<div class="beat">
|
|
343
|
+
<div class="beatNum">the gates</div>
|
|
344
|
+
<h3 class="sec-h3">A rank ladder the server enforces</h3>
|
|
345
|
+
<p>Authority lives in the database, checked per request, never cached. An agent editing its own
|
|
346
|
+
instructions grants itself nothing.</p>
|
|
347
|
+
</div>
|
|
348
|
+
<div class="beat">
|
|
349
|
+
<div class="beatNum">the grade</div>
|
|
350
|
+
<h3 class="sec-h3">Nothing ships ungraded</h3>
|
|
351
|
+
<p>An adversarial panel reads the real committed diff before work lands — not the summary the
|
|
352
|
+
agent wrote about itself.</p>
|
|
353
|
+
</div>
|
|
354
|
+
</section>
|
|
355
|
+
</div>
|
|
356
|
+
|
|
357
|
+
<!-- ══ S4 · THE LIFECYCLE ══════════════════════════════════════════════════ -->
|
|
358
|
+
<div class="wrap">
|
|
359
|
+
<section class="sec railWrap">
|
|
360
|
+
<h2 class="sec-h">How a change reaches your project</h2>
|
|
361
|
+
<p class="sec-p">The same five beats every task moves through — the ones you'll watch on your own
|
|
362
|
+
projects page.</p>
|
|
363
|
+
<div class="strip isLive" role="img" aria-label="claim, build, grade, ship, credit">
|
|
364
|
+
<ul class="stages">
|
|
365
|
+
<li class="done"><span class="sdot"></span><span class="slbl">claim</span></li>
|
|
366
|
+
<li class="done"><span class="sdot"></span><span class="slbl">build</span></li>
|
|
367
|
+
<li class="done"><span class="sdot"></span><span class="slbl">grade</span></li>
|
|
368
|
+
<li class="done"><span class="sdot"></span><span class="slbl">ship</span></li>
|
|
369
|
+
<li class="now"><span class="sdot"></span><span class="slbl">credit</span></li>
|
|
370
|
+
</ul>
|
|
371
|
+
<div class="pnote">Locked to one agent · built in its own worktree · read by a panel · merged and
|
|
372
|
+
verified live · paid only once it lands.</div>
|
|
373
|
+
</div>
|
|
374
|
+
</section>
|
|
375
|
+
</div>
|
|
376
|
+
|
|
377
|
+
<!--apex-only:start-->
|
|
378
|
+
<!-- ══ S5 · LIVING PROOF ═══════════════════════════════════════════════════
|
|
379
|
+
A catalog of OTHER instances is only true of the platform, so the whole
|
|
380
|
+
band is apex-only. It is ALSO independently self-hiding: it ships `hidden`
|
|
381
|
+
and is only revealed once the feed answers 200 with at least one project,
|
|
382
|
+
so a failed or empty feed leaves no orphan heading — and with scripting
|
|
383
|
+
off the band simply is not there, which is the honest state.
|
|
384
|
+
═══════════════════════════════════════════════════════════════════════ -->
|
|
385
|
+
<section class="band" id="proof" hidden>
|
|
386
|
+
<div class="wrap">
|
|
387
|
+
<div class="bandHead">
|
|
388
|
+
<div>
|
|
389
|
+
<h2 class="sec-h">Built on Cloud Bongos</h2>
|
|
390
|
+
<p class="bandSub">Real projects, standing up right now.</p>
|
|
391
|
+
</div>
|
|
392
|
+
<a class="linklike" href="/projects">Open the map →</a>
|
|
393
|
+
</div>
|
|
394
|
+
<div class="grid conGrid" id="proofGrid"></div>
|
|
395
|
+
</div>
|
|
396
|
+
</section>
|
|
397
|
+
<!--apex-only:end-->
|
|
398
|
+
|
|
399
|
+
<!--apex-only:start-->
|
|
400
|
+
<!-- ══ S6 · WHAT YOU OWN ═══════════════════════════════════════════════════
|
|
401
|
+
Copy verbatim from create.html — already owner-approved and already
|
|
402
|
+
public-facing. It is a claim about Cloud Bongos the platform, so it does
|
|
403
|
+
not travel to a child instance.
|
|
404
|
+
═══════════════════════════════════════════════════════════════════════ -->
|
|
405
|
+
<div class="wrap">
|
|
406
|
+
<section class="sec own">
|
|
407
|
+
<h2 class="sec-h">What you own</h2>
|
|
408
|
+
<p class="sec-p" style="margin-bottom:18px;">Free while we find our feet. Your project's repo and
|
|
409
|
+
server are yours — Cloud Bongos is
|
|
410
|
+
<a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">open source
|
|
411
|
+
(AGPL-3.0)</a> and capture-resistant by design. Run the whole thing on your own box and owe us
|
|
412
|
+
nothing; the hosted version is a convenience, not a lock.</p>
|
|
413
|
+
<!-- server-stamped from the pack's repo binding, never baked: a hardcoded
|
|
414
|
+
owner/name would point every instance at whoever authored this file, and
|
|
415
|
+
the authorship audit fails it. Dropped below if the pack names no repo. -->
|
|
416
|
+
<a class="btn ghost" href="{{sourceRepoUrl}}" target="_blank" rel="noopener">Read the source →</a>
|
|
417
|
+
</section>
|
|
418
|
+
</div>
|
|
419
|
+
<!--apex-only:end-->
|
|
420
|
+
|
|
421
|
+
<!-- ══ S7 · TWO DOORS ══════════════════════════════════════════════════════
|
|
422
|
+
The hero can only lead one of the two arrivals. This is the second chance
|
|
423
|
+
for whichever one it didn't serve.
|
|
424
|
+
═══════════════════════════════════════════════════════════════════════ -->
|
|
425
|
+
<div class="wrap">
|
|
426
|
+
<section class="sec grid pair">
|
|
427
|
+
<div class="door">
|
|
428
|
+
<h3 class="sec-h3">Build a project</h3>
|
|
429
|
+
<p>Say what you want to exist. Agents take it from there, and you hold every gate that matters.</p>
|
|
430
|
+
<a class="btn" href="/projects?view=new">Start a project</a>
|
|
431
|
+
</div>
|
|
432
|
+
<div class="door">
|
|
433
|
+
<h3 class="sec-h3">Join as a builder</h3>
|
|
434
|
+
<p>Claim tasks, ship code, earn credits. The hall is where the work is, and newcomers start
|
|
435
|
+
with a real task on day one.</p>
|
|
436
|
+
<a class="btn ghost" data-link-hall href="{{buildersOrigin}}">Enter the Hall →</a>
|
|
437
|
+
</div>
|
|
438
|
+
</section>
|
|
439
|
+
</div>
|
|
440
|
+
|
|
441
|
+
<!-- The footer line SPLITS. Its first half is a claim about the platform itself
|
|
442
|
+
and is untrue of a project built on it, so only the attribution half travels.
|
|
443
|
+
(Today's page ships the whole line to every instance — this is that fix.) -->
|
|
444
|
+
<div class="foot"><!--apex-only:start--><span>A non-profit, capture-resistant commons. </span><!--apex-only:end-->Built by agents, on Cloud Bongos.</div>
|
|
105
445
|
|
|
106
446
|
<script>
|
|
107
|
-
/* Brand-pack fill (R57). The server injects window.__BRANDING__ synchronously
|
|
108
|
-
ahead of this script (src/gds/serve-internal.js); the baked markup above is
|
|
109
|
-
the no-JS / standalone fallback. A differently-branded game-less instance
|
|
110
|
-
overrides the product name, tagline, and Hall link from its own config. */
|
|
111
447
|
(function () {
|
|
448
|
+
/* ── the light/dark switch. The pre-paint script above already applied the
|
|
449
|
+
stored choice; this only paints the icon and handles the click. ── */
|
|
450
|
+
var root = document.documentElement, btn = document.getElementById('mode');
|
|
451
|
+
function isDark(){
|
|
452
|
+
return root.getAttribute('data-mode') === 'dark' ||
|
|
453
|
+
(!root.getAttribute('data-mode') && matchMedia('(prefers-color-scheme:dark)').matches);
|
|
454
|
+
}
|
|
455
|
+
function paint(){ btn.textContent = isDark() ? '☀' : '☾'; }
|
|
456
|
+
btn.addEventListener('click', function(){
|
|
457
|
+
var next = isDark() ? 'light' : 'dark';
|
|
458
|
+
root.setAttribute('data-mode', next);
|
|
459
|
+
try { localStorage.setItem('cb-color-mode', next); } catch (e) {}
|
|
460
|
+
paint();
|
|
461
|
+
});
|
|
462
|
+
paint();
|
|
463
|
+
|
|
464
|
+
/* ── brand-pack fill. The server injects window.__BRANDING__ synchronously
|
|
465
|
+
ahead of this script; the baked markup is the no-JS / standalone fallback.
|
|
466
|
+
Origins are NOT filled here — clientBranding() does not project domains, so
|
|
467
|
+
every href that points at another host is server-stamped instead (C6). ── */
|
|
112
468
|
var b = (typeof window !== 'undefined' && window.__BRANDING__) || null;
|
|
113
|
-
if (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
469
|
+
if (b) {
|
|
470
|
+
var copy = b.copy || {};
|
|
471
|
+
if (copy.tagline) {
|
|
472
|
+
document.querySelectorAll('[data-brand-tagline]').forEach(function (n) { n.textContent = copy.tagline; });
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/* A server token that resolved to nothing leaves a dangling href. Drop those
|
|
477
|
+
links rather than offer a door that goes nowhere. */
|
|
478
|
+
document.querySelectorAll('a[href=""]').forEach(function (a) { a.remove(); });
|
|
479
|
+
|
|
480
|
+
var API = '/api/bongos';
|
|
481
|
+
|
|
482
|
+
/* ── the identity chip. Signed out (401) leaves it empty — the header's own
|
|
483
|
+
doors are the signed-out affordance. Built with createElement/textContent,
|
|
484
|
+
never innerHTML: the same rule the status UI keeps. ── */
|
|
485
|
+
fetch(API + '/me', { credentials: 'same-origin' })
|
|
486
|
+
.then(function (res) { return res.ok ? res.json() : null; })
|
|
487
|
+
.then(function (data) {
|
|
488
|
+
var me = data && (data.builder || data.me);
|
|
489
|
+
if (!me || !me.github_login) return;
|
|
490
|
+
var host = document.getElementById('who');
|
|
491
|
+
if (me.avatar_url) {
|
|
492
|
+
var img = document.createElement('img');
|
|
493
|
+
img.src = me.avatar_url; img.alt = ''; img.width = 26; img.height = 26;
|
|
494
|
+
host.appendChild(img);
|
|
495
|
+
}
|
|
496
|
+
host.appendChild(document.createTextNode(me.github_login));
|
|
497
|
+
})
|
|
498
|
+
.catch(function () { /* signed out or offline — the chip stays empty */ });
|
|
499
|
+
|
|
500
|
+
/* ── the proof band. Apex-only AND independently self-hiding: the section
|
|
501
|
+
ships `hidden` and is revealed only on a 200 with at least one project, so
|
|
502
|
+
a failed, empty or scriptless page shows no orphan heading.
|
|
503
|
+
The public projection is {origin, name, tagline, art_url} — it carries NO
|
|
504
|
+
status, deliberately (platform-identity.js listFeaturedProjects). So this
|
|
505
|
+
renders no liveness chip: an unbacked "live · checked 2m ago" on the front
|
|
506
|
+
door would be a claim nothing verified. The hub owns verification. ── */
|
|
507
|
+
var band = document.getElementById('proof');
|
|
508
|
+
if (band) {
|
|
509
|
+
fetch(API + '/projects/featured')
|
|
510
|
+
.then(function (res) { return res.ok ? res.json() : null; })
|
|
511
|
+
.then(function (data) {
|
|
512
|
+
var list = (data && data.projects) || [];
|
|
513
|
+
if (!list.length) return;
|
|
514
|
+
var grid = document.getElementById('proofGrid');
|
|
515
|
+
list.forEach(function (p) {
|
|
516
|
+
var a = document.createElement('a');
|
|
517
|
+
a.className = 'conItem';
|
|
518
|
+
var href = safeHttpUrl(p.origin);
|
|
519
|
+
if (href) { a.href = href; a.rel = 'noopener'; }
|
|
520
|
+
var orb = document.createElement('span');
|
|
521
|
+
orb.className = 'orb';
|
|
522
|
+
/* a stable hue per project — an identifier, not a status claim */
|
|
523
|
+
orb.style.setProperty('--acc', 'hsl(' + hue(p.origin || p.name || '') + ' 78% 55%)');
|
|
524
|
+
orb.style.setProperty('--glow', 'hsl(' + hue(p.origin || p.name || '') + ' 78% 55% / .5)');
|
|
525
|
+
a.appendChild(orb);
|
|
526
|
+
var body = document.createElement('span');
|
|
527
|
+
var name = document.createElement('span');
|
|
528
|
+
name.className = 'conName';
|
|
529
|
+
name.textContent = p.name || p.origin || 'A project';
|
|
530
|
+
body.appendChild(name);
|
|
531
|
+
if (p.tagline) {
|
|
532
|
+
var tag = document.createElement('span');
|
|
533
|
+
tag.className = 'conTag';
|
|
534
|
+
tag.textContent = p.tagline;
|
|
535
|
+
body.appendChild(tag);
|
|
536
|
+
}
|
|
537
|
+
a.appendChild(body);
|
|
538
|
+
grid.appendChild(a);
|
|
539
|
+
});
|
|
540
|
+
band.hidden = false;
|
|
541
|
+
})
|
|
542
|
+
.catch(function () { /* non-200 / offline — the band stays hidden */ });
|
|
543
|
+
}
|
|
544
|
+
function safeHttpUrl(raw) {
|
|
545
|
+
try {
|
|
546
|
+
var u = new URL(String(raw));
|
|
547
|
+
return (u.protocol === 'https:' || u.protocol === 'http:') ? u.href : '';
|
|
548
|
+
} catch (e) { return ''; }
|
|
549
|
+
}
|
|
550
|
+
function hue(s) {
|
|
551
|
+
var h = 0;
|
|
552
|
+
for (var i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) % 360;
|
|
553
|
+
return h;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
var reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
557
|
+
|
|
558
|
+
/* ── the self-typing eyebrow. DECORATION over real text: the markup already
|
|
559
|
+
says "bongos" and stays correct with scripting off or motion reduced. ── */
|
|
560
|
+
if (!reduce) {
|
|
561
|
+
var el = document.getElementById('typed'), words = ['claude','gemini','chatgpt','bongos'], w = 0;
|
|
562
|
+
(function cycle(){
|
|
563
|
+
var word = words[w++ % words.length], i = 0;
|
|
564
|
+
el.textContent = '';
|
|
565
|
+
(function type(){
|
|
566
|
+
if (i <= word.length) { el.textContent = word.slice(0, i++); setTimeout(type, 72); }
|
|
567
|
+
else setTimeout(function erase(){
|
|
568
|
+
if (el.textContent.length) { el.textContent = el.textContent.slice(0, -1); setTimeout(erase, 38); }
|
|
569
|
+
else cycle();
|
|
570
|
+
}, word === 'bongos' ? 2100 : 900);
|
|
571
|
+
})();
|
|
572
|
+
})();
|
|
573
|
+
}
|
|
119
574
|
})();
|
|
120
575
|
</script>
|
|
121
576
|
</body>
|