4bnode 4.1.9 → 4.2.1

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.
@@ -2,291 +2,399 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>4bnode App</title>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
6
+ <meta name="color-scheme" content="dark light">
7
+ <title>App is running</title>
8
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%235b8cff'/%3E%3Ctext x='16' y='21' font-family='system-ui,sans-serif' font-size='13' font-weight='700' fill='white' text-anchor='middle'%3E4b%3C/text%3E%3C/svg%3E">
7
9
  <style>
10
+ /* Self-contained on purpose: no external fonts, scripts or images, so the
11
+ page renders the same in production and under helmet's CSP. */
12
+ :root {
13
+ --bg: #07070a;
14
+ --surface: rgba(20, 20, 27, 0.66);
15
+ --border: rgba(255, 255, 255, 0.09);
16
+ --text: #f1f1f4;
17
+ --muted: #9b9ba8;
18
+ --faint: #5e5e6a;
19
+ --a1: #8fb0ff;
20
+ --a2: #5b8cff;
21
+ --a3: #9d7bff;
22
+ --ok: #3ecf8e;
23
+ --warn: #f5a524;
24
+ --net: 143, 176, 255;
25
+ }
26
+ @media (prefers-color-scheme: light) {
27
+ :root {
28
+ --bg: #f6f7fb;
29
+ --surface: rgba(255, 255, 255, 0.75);
30
+ --border: rgba(0, 0, 0, 0.08);
31
+ --text: #15151a;
32
+ --muted: #5b5b66;
33
+ --faint: #9a9aa4;
34
+ --a1: #5b8cff;
35
+ --a2: #3a6ff0;
36
+ --a3: #7b5cf0;
37
+ --ok: #0f9d63;
38
+ --warn: #b7791f;
39
+ --net: 58, 111, 240;
40
+ }
41
+ }
42
+
8
43
  * { margin: 0; padding: 0; box-sizing: border-box; }
44
+ [hidden] { display: none !important; }
45
+ html, body { height: 100%; overflow: hidden; }
46
+ body {
47
+ font-family: "Inter", ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
48
+ background: var(--bg);
49
+ color: var(--text);
50
+ -webkit-font-smoothing: antialiased;
51
+ }
52
+ .mono { font-family: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace; }
9
53
 
10
- @font-face { font-family: "Noto Sans"; src: url("/_dev/assets/fonts/NotoSans-Light.woff2") format("woff2"); font-weight: 300; font-style: normal; font-display: swap; }
11
- @font-face { font-family: "Noto Sans"; src: url("/_dev/assets/fonts/NotoSans-Regular.woff2") format("woff2"); font-weight: 400; font-style: normal; font-display: swap; }
12
- @font-face { font-family: "Noto Sans"; src: url("/_dev/assets/fonts/NotoSans-Medium.woff2") format("woff2"); font-weight: 500; font-style: normal; font-display: swap; }
13
- @font-face { font-family: "Noto Sans"; src: url("/_dev/assets/fonts/NotoSans-SemiBold.woff2") format("woff2"); font-weight: 600; font-style: normal; font-display: swap; }
14
- @font-face { font-family: "Noto Sans"; src: url("/_dev/assets/fonts/NotoSans-Bold.woff2") format("woff2"); font-weight: 700; font-style: normal; font-display: swap; }
54
+ /* ── Background ── */
55
+ #net { position: fixed; inset: 0; width: 100%; height: 100%; }
56
+ .aurora { position: fixed; inset: 0; pointer-events: none; filter: blur(70px); }
57
+ .aurora span { position: absolute; border-radius: 50%; animation: drift 16s ease-in-out infinite alternate; }
58
+ .aurora span:nth-child(1) { width: 50vmax; height: 50vmax; left: -8vmax; top: -12vmax; background: radial-gradient(circle, rgba(91, 140, 255, 0.30), transparent 62%); }
59
+ .aurora span:nth-child(2) { width: 46vmax; height: 46vmax; right: -10vmax; bottom: -14vmax; background: radial-gradient(circle, rgba(157, 123, 255, 0.26), transparent 62%); animation-duration: 20s; animation-delay: -7s; }
60
+ .aurora span:nth-child(3) { width: 30vmax; height: 30vmax; left: 50%; top: 50%; margin: -15vmax 0 0 -15vmax; background: radial-gradient(circle, rgba(62, 207, 142, 0.12), transparent 62%); animation-duration: 24s; animation-delay: -12s; }
61
+ @media (prefers-color-scheme: light) { .aurora span { opacity: 0.55; } }
62
+ .vignette { position: fixed; inset: 0; pointer-events: none; background: radial-gradient(ellipse 75% 70% at 50% 50%, transparent 35%, var(--bg) 100%); }
15
63
 
16
- body {
17
- font-family: "Noto Sans", -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
18
- background: #0a0b10;
19
- color: #e2e8f0;
20
- min-height: 100vh;
64
+ /* ── Stage: one screen, centered ── */
65
+ .stage {
66
+ position: relative;
67
+ z-index: 1;
68
+ height: 100vh;
69
+ height: 100dvh;
70
+ display: grid;
71
+ grid-template-rows: 1fr auto;
72
+ padding: env(safe-area-inset-top) 16px env(safe-area-inset-bottom);
73
+ }
74
+ .center {
21
75
  display: flex;
76
+ flex-direction: column;
22
77
  align-items: center;
23
78
  justify-content: center;
24
- overflow: hidden;
79
+ text-align: center;
80
+ min-height: 0;
25
81
  }
26
82
 
27
- .scene {
28
- position: fixed;
29
- inset: 0;
30
- z-index: 0;
83
+ /* Core: logo, spinning gradient rings, orbiting dots, heartbeat ripples */
84
+ .core {
85
+ --size: clamp(150px, 26vmin, 230px);
86
+ position: relative;
87
+ width: var(--size);
88
+ height: var(--size);
89
+ display: grid;
90
+ place-items: center;
91
+ margin-bottom: clamp(20px, 4.5vmin, 40px);
92
+ animation: pop 1s cubic-bezier(0.2, 0.9, 0.2, 1.2) both;
31
93
  }
32
-
33
- /* Animated grid floor */
34
- .grid {
94
+ .ring {
35
95
  position: absolute;
36
- bottom: 0;
37
- left: 50%;
38
- transform: translateX(-50%) perspective(600px) rotateX(55deg);
39
- width: 200%;
40
- height: 70vh;
41
- background-image:
42
- linear-gradient(rgba(0,114,255,0.08) 1px, transparent 1px),
43
- linear-gradient(90deg, rgba(0,114,255,0.08) 1px, transparent 1px);
44
- background-size: 60px 60px;
45
- animation: gridScroll 12s linear infinite;
46
- mask-image: linear-gradient(to top, rgba(0,0,0,0.4) 0%, transparent 80%);
47
- -webkit-mask-image: linear-gradient(to top, rgba(0,0,0,0.4) 0%, transparent 80%);
96
+ inset: 0;
97
+ border-radius: 50%;
98
+ background: conic-gradient(from 0deg, transparent 0deg, var(--a2) 80deg, var(--a3) 160deg, transparent 230deg, transparent 360deg);
99
+ -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));
100
+ mask: radial-gradient(farthest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));
101
+ animation: spin 3.2s linear infinite;
48
102
  }
49
-
50
- @keyframes gridScroll {
51
- 0% { background-position: 0 0; }
52
- 100% { background-position: 0 60px; }
103
+ .ring.r2 {
104
+ inset: 14%;
105
+ background: conic-gradient(from 180deg, transparent 0deg, var(--a1) 70deg, transparent 150deg, transparent 360deg);
106
+ animation-duration: 5s;
107
+ animation-direction: reverse;
108
+ opacity: 0.8;
53
109
  }
54
-
55
- /* Floating orbs */
56
- .orb {
110
+ .track { position: absolute; inset: 0; border-radius: 50%; border: 1px solid var(--border); }
111
+ .track.t2 { inset: 14%; }
112
+ .orbit { position: absolute; inset: 0; animation: spin 9s linear infinite; }
113
+ .orbit.o2 { inset: 14%; animation-duration: 6s; animation-direction: reverse; }
114
+ .orbit i {
57
115
  position: absolute;
116
+ left: 50%;
117
+ top: -4px;
118
+ width: 8px;
119
+ height: 8px;
120
+ margin-left: -4px;
58
121
  border-radius: 50%;
59
- filter: blur(80px);
60
- opacity: 0.35;
61
- animation: float 8s ease-in-out infinite;
62
- }
63
- .orb-1 {
64
- width: 400px; height: 400px;
65
- background: radial-gradient(circle, #0072ff, transparent 70%);
66
- top: 10%; left: 15%;
67
- animation-delay: 0s;
68
- }
69
- .orb-2 {
70
- width: 350px; height: 350px;
71
- background: radial-gradient(circle, #7b2ff7, transparent 70%);
72
- top: 30%; right: 10%;
73
- animation-delay: -3s;
74
- animation-duration: 10s;
75
- }
76
- .orb-3 {
77
- width: 300px; height: 300px;
78
- background: radial-gradient(circle, #00c6ff, transparent 70%);
79
- bottom: 15%; left: 40%;
80
- animation-delay: -5s;
81
- animation-duration: 12s;
122
+ background: var(--a1);
123
+ box-shadow: 0 0 12px 2px var(--a2);
82
124
  }
83
-
84
- @keyframes float {
85
- 0%, 100% { transform: translate(0, 0) scale(1); }
86
- 33% { transform: translate(30px, -20px) scale(1.05); }
87
- 66% { transform: translate(-20px, 15px) scale(0.95); }
125
+ .orbit i:nth-child(2) { top: auto; bottom: -4px; background: var(--a3); box-shadow: 0 0 12px 2px var(--a3); }
126
+ .orbit.o2 i { width: 6px; height: 6px; margin-left: -3px; top: -3px; }
127
+ .glow {
128
+ position: absolute;
129
+ inset: 26%;
130
+ border-radius: 50%;
131
+ background: radial-gradient(circle, rgba(91, 140, 255, 0.55), transparent 70%);
132
+ filter: blur(18px);
133
+ animation: breathe 3.2s ease-in-out infinite;
88
134
  }
89
-
90
- /* Content */
91
- .container {
135
+ .logo {
92
136
  position: relative;
93
- z-index: 1;
94
- text-align: center;
95
- padding: 40px;
96
- animation: fadeUp 1s ease-out;
97
- }
98
-
99
- @keyframes fadeUp {
100
- from { opacity: 0; transform: translateY(30px); }
101
- to { opacity: 1; transform: translateY(0); }
102
- }
103
-
104
- /* Animated node SVG */
105
- .node-graphic {
106
- width: 120px;
107
- height: 120px;
108
- margin: 0 auto 32px;
109
- }
110
-
111
- .node-ring {
112
- fill: none;
113
- stroke: url(#ringGrad);
114
- stroke-width: 1.5;
115
- stroke-dasharray: 8 6;
116
- transform-origin: 60px 60px;
117
- animation: spin 20s linear infinite;
118
- }
119
- .node-ring-2 {
120
- animation: spin 15s linear infinite reverse;
121
- stroke-dasharray: 12 8;
137
+ width: 34%;
138
+ aspect-ratio: 1;
139
+ border-radius: 26%;
140
+ display: grid;
141
+ place-items: center;
142
+ font-size: calc(var(--size) * 0.13);
143
+ font-weight: 750;
144
+ letter-spacing: -0.04em;
145
+ color: #fff;
146
+ background: linear-gradient(140deg, #9fbcff 0%, #5b8cff 45%, #3a5fd9 100%);
147
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.35), 0 14px 40px -10px rgba(91, 140, 255, 0.9);
122
148
  }
123
- .node-ring-3 {
124
- animation: spin 25s linear infinite;
125
- stroke-dasharray: 4 10;
126
- }
127
-
128
- @keyframes spin {
129
- to { transform: rotate(360deg); }
130
- }
131
-
132
- .node-dot {
133
- fill: #0072ff;
134
- animation: pulse 2s ease-in-out infinite;
135
- }
136
- .node-dot:nth-child(2) { animation-delay: 0.3s; fill: #7b2ff7; }
137
- .node-dot:nth-child(3) { animation-delay: 0.6s; fill: #00c6ff; }
138
- .node-dot:nth-child(4) { animation-delay: 0.9s; fill: #0072ff; }
139
- .node-dot:nth-child(5) { animation-delay: 1.2s; fill: #7b2ff7; }
140
- .node-dot:nth-child(6) { animation-delay: 1.5s; fill: #00c6ff; }
141
-
142
- @keyframes pulse {
143
- 0%, 100% { opacity: 0.5; r: 3; }
144
- 50% { opacity: 1; r: 5; }
145
- }
146
-
147
- .node-line {
148
- stroke: url(#lineGrad);
149
- stroke-width: 0.8;
150
- opacity: 0.4;
151
- }
152
-
153
- .node-center {
154
- fill: url(#centerGrad);
155
- filter: drop-shadow(0 0 8px rgba(0,114,255,0.4));
149
+ .ripple {
150
+ position: absolute;
151
+ inset: 33%;
152
+ border-radius: 50%;
153
+ border: 2px solid var(--ok);
154
+ pointer-events: none;
155
+ animation: ripple 1.6s cubic-bezier(0.2, 0, 0, 1) forwards;
156
156
  }
157
+ .ripple.bad { border-color: var(--warn); }
157
158
 
158
159
  h1 {
159
- font-size: 36px;
160
- font-weight: 800;
161
- letter-spacing: -0.5px;
162
- margin-bottom: 12px;
163
- background: linear-gradient(135deg, #ffffff 0%, #94a3b8 100%);
160
+ font-size: clamp(34px, 7.4vmin, 76px);
161
+ line-height: 1.04;
162
+ font-weight: 720;
163
+ letter-spacing: -0.045em;
164
+ }
165
+ h1 .w { display: inline-block; animation: word 0.9s cubic-bezier(0.2, 0, 0, 1) both; }
166
+ h1 .grad {
167
+ background: linear-gradient(100deg, var(--a1) 0%, var(--a2) 35%, var(--a3) 65%, var(--a1) 100%);
168
+ background-size: 220% auto;
164
169
  -webkit-background-clip: text;
170
+ background-clip: text;
165
171
  -webkit-text-fill-color: transparent;
172
+ animation: word 0.9s cubic-bezier(0.2, 0, 0, 1) both, shine 5s linear 1s infinite;
166
173
  }
167
-
168
- .tagline {
169
- font-size: 15px;
170
- color: #64748b;
171
- font-weight: 400;
172
- letter-spacing: 0.3px;
173
- margin-bottom: 32px;
174
+ .sub {
175
+ margin-top: clamp(10px, 2vmin, 16px);
176
+ font-size: clamp(14px, 2.2vmin, 17px);
177
+ color: var(--muted);
178
+ animation: rise 0.9s cubic-bezier(0.2, 0, 0, 1) 0.55s both;
174
179
  }
175
180
 
176
- .status {
177
- display: inline-flex;
181
+ .row {
182
+ margin-top: clamp(20px, 4vmin, 32px);
183
+ display: flex;
184
+ flex-wrap: wrap;
178
185
  align-items: center;
179
- gap: 8px;
180
- padding: 8px 20px;
181
- border-radius: 100px;
182
- background: rgba(16, 185, 129, 0.08);
183
- border: 1px solid rgba(16, 185, 129, 0.2);
184
- font-size: 13px;
185
- color: #10b981;
186
- font-weight: 500;
187
- animation: fadeUp 1s ease-out 0.3s both;
186
+ justify-content: center;
187
+ gap: 10px;
188
+ animation: rise 0.9s cubic-bezier(0.2, 0, 0, 1) 0.75s both;
188
189
  }
189
-
190
- .status-dot {
191
- width: 8px;
192
- height: 8px;
193
- border-radius: 50%;
194
- background: #10b981;
195
- animation: blink 2s ease-in-out infinite;
196
- box-shadow: 0 0 8px rgba(16, 185, 129, 0.5);
190
+ .pill {
191
+ display: inline-flex;
192
+ align-items: center;
193
+ gap: 10px;
194
+ height: 38px;
195
+ padding: 0 16px 0 14px;
196
+ border-radius: 999px;
197
+ border: 1px solid var(--border);
198
+ background: var(--surface);
199
+ backdrop-filter: blur(12px);
200
+ -webkit-backdrop-filter: blur(12px);
201
+ font-size: 13.5px;
202
+ font-weight: 550;
197
203
  }
198
-
199
- @keyframes blink {
200
- 0%, 100% { opacity: 1; }
201
- 50% { opacity: 0.4; }
204
+ .dot { position: relative; width: 8px; height: 8px; border-radius: 50%; background: var(--faint); }
205
+ .ok .dot { background: var(--ok); }
206
+ .warn .dot { background: var(--warn); }
207
+ .ok .dot::after { content: ""; position: absolute; inset: -4px; border-radius: 50%; border: 2px solid var(--ok); animation: ping 2s cubic-bezier(0, 0, 0.2, 1) infinite; }
208
+ .ms { color: var(--muted); font-size: 12.5px; padding-left: 10px; border-left: 1px solid var(--border); font-variant-numeric: tabular-nums; }
209
+ .ms:empty { display: none; }
210
+ .link {
211
+ display: inline-flex;
212
+ align-items: center;
213
+ gap: 7px;
214
+ height: 38px;
215
+ padding: 0 15px;
216
+ border-radius: 999px;
217
+ border: 1px solid var(--border);
218
+ background: var(--surface);
219
+ backdrop-filter: blur(12px);
220
+ -webkit-backdrop-filter: blur(12px);
221
+ color: var(--text);
222
+ text-decoration: none;
223
+ font-size: 13.5px;
224
+ font-weight: 550;
225
+ transition: transform 0.15s, border-color 0.15s;
202
226
  }
227
+ .link:hover { transform: translateY(-1px); border-color: var(--a2); }
228
+ .link:focus-visible { outline: 2px solid var(--a2); outline-offset: 2px; }
229
+ .link svg { width: 15px; height: 15px; color: var(--muted); }
203
230
 
204
- .powered {
205
- position: fixed;
206
- bottom: 28px;
207
- left: 0;
208
- right: 0;
231
+ footer {
232
+ padding: 0 0 clamp(16px, 3vmin, 28px);
209
233
  text-align: center;
210
234
  font-size: 12px;
211
- color: #334155;
212
- letter-spacing: 0.3px;
213
- animation: fadeUp 1s ease-out 0.6s both;
235
+ color: var(--faint);
236
+ animation: rise 0.9s cubic-bezier(0.2, 0, 0, 1) 1s both;
214
237
  }
215
- .powered a {
216
- text-decoration: none;
217
- }
218
- .powered span {
219
- font-weight: 600;
220
- background: linear-gradient(135deg, #00c6ff, #7b2ff7);
221
- -webkit-background-clip: text;
222
- -webkit-text-fill-color: transparent;
223
- transition: opacity 0.2s;
238
+ footer a { color: var(--muted); font-weight: 600; text-decoration: none; }
239
+ footer a:hover { color: var(--text); }
240
+
241
+ @keyframes spin { to { transform: rotate(360deg); } }
242
+ @keyframes breathe { 0%, 100% { opacity: 0.6; transform: scale(0.92); } 50% { opacity: 1; transform: scale(1.08); } }
243
+ @keyframes ripple { from { opacity: 0.85; transform: scale(1); } to { opacity: 0; transform: scale(3.4); } }
244
+ @keyframes ping { 0% { transform: scale(0.6); opacity: 0.8; } 80%, 100% { transform: scale(1.9); opacity: 0; } }
245
+ @keyframes pop { from { opacity: 0; transform: scale(0.6) rotate(-20deg); } to { opacity: 1; transform: none; } }
246
+ @keyframes word { from { opacity: 0; transform: translateY(0.45em); filter: blur(8px); } to { opacity: 1; transform: none; filter: blur(0); } }
247
+ @keyframes rise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
248
+ @keyframes shine { to { background-position: 220% center; } }
249
+ @keyframes drift { to { transform: translate(5vmax, 4vmax) scale(1.12); } }
250
+
251
+ @media (max-height: 520px) {
252
+ .core { --size: 110px; margin-bottom: 14px; }
253
+ .row { margin-top: 14px; }
224
254
  }
225
- .powered a:hover span {
226
- opacity: 0.75;
255
+ @media (prefers-reduced-motion: reduce) {
256
+ *, *::before, *::after { animation: none !important; transition: none !important; }
257
+ .ripple { display: none; }
227
258
  }
228
259
  </style>
229
260
  </head>
230
261
  <body>
262
+ <canvas id="net" aria-hidden="true"></canvas>
263
+ <div class="aurora" aria-hidden="true"><span></span><span></span><span></span></div>
264
+ <div class="vignette" aria-hidden="true"></div>
231
265
 
232
- <div class="scene">
233
- <div class="orb orb-1"></div>
234
- <div class="orb orb-2"></div>
235
- <div class="orb orb-3"></div>
236
- <div class="grid"></div>
237
- </div>
266
+ <div class="stage">
267
+ <main class="center">
268
+ <div class="core" id="core" aria-hidden="true">
269
+ <div class="glow"></div>
270
+ <div class="track"></div>
271
+ <div class="track t2"></div>
272
+ <div class="ring"></div>
273
+ <div class="ring r2"></div>
274
+ <div class="orbit"><i></i><i></i></div>
275
+ <div class="orbit o2"><i></i></div>
276
+ <div class="logo">4b</div>
277
+ </div>
238
278
 
239
- <div class="container">
240
- <svg class="node-graphic" viewBox="0 0 120 120">
241
- <defs>
242
- <linearGradient id="ringGrad" x1="0%" y1="0%" x2="100%" y2="100%">
243
- <stop offset="0%" stop-color="#0072ff" />
244
- <stop offset="100%" stop-color="#7b2ff7" />
245
- </linearGradient>
246
- <linearGradient id="lineGrad" x1="0%" y1="0%" x2="100%" y2="100%">
247
- <stop offset="0%" stop-color="#0072ff" stop-opacity="0.6" />
248
- <stop offset="100%" stop-color="#7b2ff7" stop-opacity="0.2" />
249
- </linearGradient>
250
- <radialGradient id="centerGrad" cx="50%" cy="50%" r="50%">
251
- <stop offset="0%" stop-color="#0072ff" />
252
- <stop offset="100%" stop-color="#7b2ff7" />
253
- </radialGradient>
254
- </defs>
279
+ <h1>
280
+ <span class="w" style="animation-delay:.15s">Your</span>
281
+ <span class="w" style="animation-delay:.25s">app</span>
282
+ <span class="w" style="animation-delay:.35s">is</span>
283
+ <span class="w grad" style="animation-delay:.45s">running</span>
284
+ </h1>
285
+ <p class="sub">Live and ready to accept requests.</p>
255
286
 
256
- <!-- Connection lines -->
257
- <line class="node-line" x1="60" y1="60" x2="60" y2="14" />
258
- <line class="node-line" x1="60" y1="60" x2="100" y2="37" />
259
- <line class="node-line" x1="60" y1="60" x2="100" y2="83" />
260
- <line class="node-line" x1="60" y1="60" x2="60" y2="106" />
261
- <line class="node-line" x1="60" y1="60" x2="20" y2="83" />
262
- <line class="node-line" x1="60" y1="60" x2="20" y2="37" />
287
+ <div class="row">
288
+ <span class="pill" id="pill" role="status" aria-live="polite">
289
+ <span class="dot"></span><span id="pill-text">Checking…</span><span class="ms mono" id="ms"></span>
290
+ </span>
291
+ <a class="link" href="/docs" data-probe="/docs" hidden>
292
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>
293
+ API docs
294
+ </a>
295
+ <a class="link" href="/_dev/" data-probe="/_dev/" hidden>
296
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="7" height="9" rx="1"/><rect x="14" y="3" width="7" height="5" rx="1"/><rect x="14" y="12" width="7" height="9" rx="1"/><rect x="3" y="16" width="7" height="5" rx="1"/></svg>
297
+ Dashboard
298
+ </a>
299
+ </div>
300
+ </main>
263
301
 
264
- <!-- Rotating rings -->
265
- <circle class="node-ring" cx="60" cy="60" r="50" />
266
- <circle class="node-ring node-ring-2" cx="60" cy="60" r="38" />
267
- <circle class="node-ring node-ring-3" cx="60" cy="60" r="26" />
302
+ <footer>Powered by <a href="https://www.npmjs.com/package/4bnode" target="_blank" rel="noopener noreferrer">4bnode</a></footer>
303
+ </div>
268
304
 
269
- <!-- Orbiting dots -->
270
- <circle class="node-dot" cx="60" cy="14" r="4" />
271
- <circle class="node-dot" cx="100" cy="37" r="4" />
272
- <circle class="node-dot" cx="100" cy="83" r="4" />
273
- <circle class="node-dot" cx="60" cy="106" r="4" />
274
- <circle class="node-dot" cx="20" cy="83" r="4" />
275
- <circle class="node-dot" cx="20" cy="37" r="4" />
305
+ <script>
306
+ (function () {
307
+ var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
308
+ var $ = function (id) { return document.getElementById(id); };
276
309
 
277
- <!-- Center node -->
278
- <circle class="node-center" cx="60" cy="60" r="8" />
279
- </svg>
310
+ // ── Background: drifting node network, pulled toward the cursor ──
311
+ var canvas = $("net"), ctx = canvas.getContext("2d");
312
+ var nodes = [], W = 0, H = 0, DPR = Math.min(window.devicePixelRatio || 1, 2);
313
+ var mouse = { x: -9999, y: -9999 };
314
+ function netColor() { return getComputedStyle(document.documentElement).getPropertyValue("--net").trim() || "143,176,255"; }
315
+ var color = netColor();
316
+ function resize() {
317
+ W = window.innerWidth; H = window.innerHeight;
318
+ canvas.width = W * DPR; canvas.height = H * DPR;
319
+ ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
320
+ var n = Math.round(Math.min(110, (W * H) / 13000));
321
+ nodes = [];
322
+ for (var i = 0; i < n; i++) {
323
+ nodes.push({ x: Math.random() * W, y: Math.random() * H, vx: (Math.random() - 0.5) * 0.3, vy: (Math.random() - 0.5) * 0.3, r: Math.random() * 1.4 + 0.6 });
324
+ }
325
+ }
326
+ function draw() {
327
+ ctx.clearRect(0, 0, W, H);
328
+ var max = 140;
329
+ for (var i = 0; i < nodes.length; i++) {
330
+ var a = nodes[i];
331
+ if (!reduce) {
332
+ a.x += a.vx; a.y += a.vy;
333
+ if (a.x < -20) a.x = W + 20; else if (a.x > W + 20) a.x = -20;
334
+ if (a.y < -20) a.y = H + 20; else if (a.y > H + 20) a.y = -20;
335
+ var mx = mouse.x - a.x, my = mouse.y - a.y, md = Math.sqrt(mx * mx + my * my);
336
+ if (md < 190 && md > 1) { a.x += (mx / md) * 0.4; a.y += (my / md) * 0.4; }
337
+ }
338
+ for (var j = i + 1; j < nodes.length; j++) {
339
+ var b = nodes[j], dx = a.x - b.x, dy = a.y - b.y, d = dx * dx + dy * dy;
340
+ if (d < max * max) {
341
+ ctx.strokeStyle = "rgba(" + color + "," + (0.17 * (1 - Math.sqrt(d) / max)).toFixed(3) + ")";
342
+ ctx.lineWidth = 1;
343
+ ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke();
344
+ }
345
+ }
346
+ var near = (mouse.x - a.x) * (mouse.x - a.x) + (mouse.y - a.y) * (mouse.y - a.y) < 190 * 190;
347
+ ctx.fillStyle = "rgba(" + color + "," + (near ? 0.95 : 0.45) + ")";
348
+ ctx.beginPath(); ctx.arc(a.x, a.y, near ? a.r + 0.9 : a.r, 0, Math.PI * 2); ctx.fill();
349
+ }
350
+ if (!reduce) requestAnimationFrame(draw);
351
+ }
352
+ resize(); draw();
353
+ window.addEventListener("resize", function () { resize(); if (reduce) draw(); });
354
+ window.addEventListener("pointermove", function (e) { mouse.x = e.clientX; mouse.y = e.clientY; }, { passive: true });
355
+ document.addEventListener("pointerleave", function () { mouse.x = mouse.y = -9999; });
356
+ window.matchMedia("(prefers-color-scheme: light)").addEventListener("change", function () { color = netColor(); if (reduce) draw(); });
280
357
 
281
- <h1>Your app is running</h1>
282
- <p class="tagline">Server is live and ready to accept requests</p>
283
- <div class="status">
284
- <span class="status-dot"></span>
285
- All systems operational
286
- </div>
287
- </div>
358
+ // ── Heartbeat: a ripple from the core on every real ping ──
359
+ var core = $("core");
360
+ function beat(ok) {
361
+ if (reduce) return;
362
+ var r = document.createElement("span");
363
+ r.className = "ripple" + (ok ? "" : " bad");
364
+ core.appendChild(r);
365
+ setTimeout(function () { r.remove(); }, 1700);
366
+ }
288
367
 
289
- <div class="powered">Powered by <a href="https://www.npmjs.com/package/4bnode" target="_blank" rel="noopener noreferrer"><span>4bnode</span></a></div>
368
+ // ── Live status (HEAD to this page) ──
369
+ function check() {
370
+ if (document.visibilityState === "hidden") return;
371
+ var t0 = performance.now();
372
+ fetch(location.pathname, { method: "HEAD", cache: "no-store" })
373
+ .then(function (r) {
374
+ var ms = Math.max(1, Math.round(performance.now() - t0));
375
+ $("pill").className = "pill " + (r.ok ? "ok" : "warn");
376
+ $("pill-text").textContent = r.ok ? "All systems operational" : "Server answered " + r.status;
377
+ $("ms").textContent = r.ok ? ms + " ms" : "";
378
+ beat(r.ok);
379
+ })
380
+ .catch(function () {
381
+ $("pill").className = "pill warn";
382
+ $("pill-text").textContent = "Server unreachable";
383
+ $("ms").textContent = "";
384
+ beat(false);
385
+ });
386
+ }
387
+ setTimeout(check, 900); // after the intro animation
388
+ setInterval(check, 3000);
389
+ document.addEventListener("visibilitychange", check);
290
390
 
391
+ // ── Links appear only if they answer (docs once set up; /_dev only in dev) ──
392
+ document.querySelectorAll("[data-probe]").forEach(function (el) {
393
+ fetch(el.getAttribute("data-probe"), { method: "HEAD", cache: "no-store" })
394
+ .then(function (r) { if (r.ok) el.hidden = false; })
395
+ .catch(function () {});
396
+ });
397
+ })();
398
+ </script>
291
399
  </body>
292
400
  </html>