@cyanheads/mcp-ts-core 0.6.0 → 0.6.2

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.
@@ -9,15 +9,15 @@
9
9
  *
10
10
  * ## Surfaces
11
11
  *
12
- * - Hero — name, clickable version badge, pre-release pill, tagline, logo,
13
- * auth-status banner, copy-to-clipboard connect snippets
14
- * - Tools section — counts in header; auto-grouped by shared prefix; per-card
15
- * annotations, invocation snippet, view-source link, schema preview
12
+ * - Hero — eyebrow, display-size server name, version + pre-release chips,
13
+ * tagline, single-line status strip, terminal-chrome connection card,
14
+ * framework attribution pill
15
+ * - Tools section — responsive 2-column card grid; prefix-grouped; per-card
16
+ * annotation chips, invocation snippet, view-source link, schema preview
16
17
  * - Resources section — URI template, mime type, description, view-source link
17
18
  * - Prompts section — args list, view-source link
18
19
  * - Extensions section — rendered when SEP-2133 extensions are present
19
- * - Footer — configured links + auto-derived GitHub cluster + npm/registry +
20
- * attribution
20
+ * - Footer — single-row, dim, separator-dot delimited
21
21
  *
22
22
  * @module src/mcp-server/transports/http/landing-page
23
23
  */
@@ -34,59 +34,69 @@ function renderTokens(accent) {
34
34
  :root {
35
35
  --space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px;
36
36
  --space-5: 20px; --space-6: 24px; --space-8: 32px; --space-10: 40px;
37
- --space-12: 48px; --space-16: 64px;
37
+ --space-12: 48px; --space-16: 64px; --space-20: 80px;
38
38
 
39
39
  --text-xs: 0.75rem; --text-sm: 0.875rem; --text-base: 1rem;
40
40
  --text-lg: 1.125rem; --text-xl: 1.25rem; --text-2xl: 1.5rem;
41
- --text-3xl: 1.875rem; --text-4xl: 2.25rem;
41
+ --text-3xl: 1.875rem;
42
+ --text-display: clamp(2rem, 4.5vw + 0.5rem, 3.5rem);
42
43
 
43
- --radius-sm: 6px; --radius-md: 8px; --radius-lg: 12px; --radius-pill: 999px;
44
+ --radius-xs: 4px; --radius-sm: 6px; --radius-md: 10px;
45
+ --radius-lg: 14px; --radius-pill: 999px;
44
46
 
45
- --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif;
46
- --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
47
+ --font-sans: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif;
48
+ --font-mono: ui-monospace, "JetBrains Mono", SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
47
49
 
48
- --duration-fast: 120ms; --duration-base: 200ms;
50
+ --duration-fast: 120ms; --duration-base: 200ms; --duration-slow: 320ms;
49
51
  --ease-out: cubic-bezier(0.2, 0.8, 0.2, 1);
50
52
 
51
53
  --accent: ${safeAccent};
52
- --accent-fg: color-mix(in oklab, ${safeAccent}, white 85%);
53
- --accent-hover: color-mix(in oklab, ${safeAccent}, black 12%);
54
- --accent-soft: color-mix(in oklab, ${safeAccent}, transparent 86%);
54
+ --accent-hover: color-mix(in oklab, ${safeAccent}, black 10%);
55
+ --accent-edge: color-mix(in oklab, ${safeAccent}, transparent 65%);
56
+ --accent-soft: color-mix(in oklab, ${safeAccent}, transparent 82%);
57
+ --accent-softer: color-mix(in oklab, ${safeAccent}, transparent 92%);
55
58
 
56
- --bg: #ffffff;
57
- --bg-subtle: #f6f8fa;
59
+ --bg: #fcfcfd;
60
+ --bg-subtle: #f5f5f7;
58
61
  --bg-elevated: #ffffff;
59
- --fg: #1f2328;
60
- --fg-muted: #656d76;
61
- --fg-subtle: #8c959f;
62
- --border: #d0d7de;
63
- --border-subtle: #eaeef2;
62
+ --bg-code: #f7f7f9;
63
+ --fg: #09090b;
64
+ --fg-muted: #52525b;
65
+ --fg-subtle: #71717a;
66
+ --border: #e4e4e7;
67
+ --border-subtle: #ececf0;
68
+ --border-strong: #d4d4d8;
64
69
  --shadow-sm: 0 1px 2px rgb(0 0 0 / 0.04);
65
- --shadow-md: 0 4px 16px -4px rgb(0 0 0 / 0.08), 0 1px 2px rgb(0 0 0 / 0.04);
70
+ --shadow-md: 0 4px 20px -8px rgb(0 0 0 / 0.08), 0 1px 2px rgb(0 0 0 / 0.04);
71
+ --grid-dot: rgb(0 0 0 / 0.045);
72
+ --glow-strength: 0.08;
66
73
  }
67
74
 
68
75
  @media (prefers-color-scheme: dark) {
69
76
  :root {
70
- --bg: #0d1117;
71
- --bg-subtle: #161b22;
72
- --bg-elevated: #161b22;
73
- --fg: #e6edf3;
74
- --fg-muted: #8d96a0;
75
- --fg-subtle: #6e7681;
76
- --border: #30363d;
77
- --border-subtle: #21262d;
78
- --shadow-sm: 0 1px 2px rgb(0 0 0 / 0.3);
79
- --shadow-md: 0 4px 16px -4px rgb(0 0 0 / 0.4), 0 1px 2px rgb(0 0 0 / 0.2);
80
- --accent-fg: color-mix(in oklab, ${safeAccent}, white 10%);
77
+ --bg: #08090d;
78
+ --bg-subtle: #0f1015;
79
+ --bg-elevated: #0d0e13;
80
+ --bg-code: #0a0b10;
81
+ --fg: #ededef;
82
+ --fg-muted: #a1a1a8;
83
+ --fg-subtle: #8a8a93;
84
+ --border: #1f2027;
85
+ --border-subtle: #16171c;
86
+ --border-strong: #2a2b33;
87
+ --shadow-sm: 0 1px 2px rgb(0 0 0 / 0.35);
88
+ --shadow-md: 0 4px 24px -8px rgb(0 0 0 / 0.55), 0 1px 2px rgb(0 0 0 / 0.3);
89
+ --grid-dot: rgb(255 255 255 / 0.028);
90
+ --glow-strength: 0.12;
81
91
  }
82
92
  }
83
93
 
84
94
  *, *::before, *::after { box-sizing: border-box; }
85
-
86
95
  html { color-scheme: light dark; }
87
96
 
88
97
  body {
89
98
  margin: 0;
99
+ min-height: 100vh;
90
100
  font-family: var(--font-sans);
91
101
  font-size: var(--text-base);
92
102
  line-height: 1.5;
@@ -95,238 +105,746 @@ body {
95
105
  -webkit-font-smoothing: antialiased;
96
106
  -moz-osx-font-smoothing: grayscale;
97
107
  text-rendering: optimizeLegibility;
108
+ position: relative;
109
+ overflow-x: hidden;
110
+ }
111
+
112
+ /* Top accent hairline — spans viewport, brand-variable gradient. */
113
+ body::before {
114
+ content: "";
115
+ position: fixed;
116
+ top: 0; left: 0; right: 0;
117
+ height: 1px;
118
+ background: linear-gradient(90deg,
119
+ transparent 0%,
120
+ color-mix(in oklab, var(--accent), transparent 60%) 15%,
121
+ var(--accent) 50%,
122
+ color-mix(in oklab, var(--accent), transparent 60%) 85%,
123
+ transparent 100%);
124
+ z-index: 100;
125
+ pointer-events: none;
126
+ }
127
+
128
+ /* Ambient background — radial accent glow top-left + fine dot grid. */
129
+ body::after {
130
+ content: "";
131
+ position: fixed;
132
+ inset: 0;
133
+ background-image:
134
+ radial-gradient(ellipse 70% 55% at 12% -5%, color-mix(in oklab, var(--accent), transparent calc((1 - var(--glow-strength)) * 100%)), transparent 60%),
135
+ radial-gradient(circle at center, var(--grid-dot) 1px, transparent 1.5px);
136
+ background-size: 100% 100%, 24px 24px;
137
+ pointer-events: none;
138
+ z-index: -1;
98
139
  }
99
140
 
100
141
  ::selection { background: var(--accent-soft); color: var(--fg); }
101
142
 
102
- main { max-width: 960px; margin: 0 auto; padding: var(--space-8) var(--space-6) var(--space-16); }
143
+ main {
144
+ max-width: 1120px;
145
+ margin: 0 auto;
146
+ padding: var(--space-10) var(--space-6) var(--space-20);
147
+ position: relative;
148
+ }
103
149
 
104
150
  a {
105
151
  color: var(--accent);
106
152
  text-decoration: none;
107
153
  transition: color var(--duration-fast) var(--ease-out);
108
154
  }
109
- a:hover { color: var(--accent-hover); text-decoration: underline; }
155
+ a:hover { color: var(--accent-hover); }
110
156
  a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }
111
157
 
112
158
  code, pre {
113
159
  font-family: var(--font-mono);
160
+ font-size: 0.875em;
161
+ font-feature-settings: "liga" 0, "calt" 0;
162
+ }
163
+ code {
164
+ background: var(--bg-subtle);
165
+ padding: 0.1em 0.35em;
166
+ border-radius: var(--radius-xs);
167
+ border: 1px solid var(--border-subtle);
114
168
  font-size: 0.85em;
115
169
  }
116
- code { background: var(--bg-subtle); padding: 0.15em 0.35em; border-radius: var(--radius-sm); border: 1px solid var(--border-subtle); }
117
170
  pre {
118
- background: var(--bg-subtle);
171
+ background: var(--bg-code);
119
172
  border: 1px solid var(--border-subtle);
120
- border-radius: var(--radius-md);
173
+ border-radius: var(--radius-sm);
121
174
  padding: var(--space-4);
122
175
  margin: 0;
123
176
  overflow-x: auto;
124
- line-height: 1.5;
177
+ line-height: 1.55;
125
178
  white-space: pre;
179
+ color: var(--fg);
126
180
  }
127
- pre code { background: transparent; padding: 0; border: 0; }
181
+ pre code { background: transparent; padding: 0; border: 0; font-size: inherit; }
128
182
 
129
- .hero { padding: var(--space-10) 0 var(--space-8); border-bottom: 1px solid var(--border-subtle); }
130
- .hero-top { display: flex; align-items: flex-start; gap: var(--space-4); margin-bottom: var(--space-4); }
131
- .hero-logo { width: 56px; height: 56px; border-radius: var(--radius-md); object-fit: contain; background: var(--bg-subtle); border: 1px solid var(--border-subtle); flex-shrink: 0; }
132
- .hero-identity { flex: 1; min-width: 0; }
133
- .hero-heading { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-3); margin: 0; font-size: var(--text-3xl); font-weight: 700; letter-spacing: -0.02em; color: var(--fg); }
134
- .hero-tagline { margin: var(--space-3) 0 0; color: var(--fg-muted); font-size: var(--text-lg); max-width: 60ch; }
183
+ /* -------------------- Hero -------------------- */
135
184
 
136
- .badge {
137
- display: inline-flex; align-items: center; gap: var(--space-1);
138
- padding: 2px var(--space-2);
139
- border-radius: var(--radius-pill);
140
- font-size: var(--text-xs);
185
+ .hero {
186
+ padding: var(--space-12) 0 var(--space-10);
187
+ display: flex;
188
+ flex-direction: column;
189
+ gap: var(--space-6);
190
+ }
191
+
192
+ .hero-eyebrow {
193
+ display: inline-flex;
194
+ align-items: center;
195
+ gap: var(--space-2);
196
+ font-family: var(--font-mono);
197
+ font-size: 0.6875rem;
141
198
  font-weight: 500;
142
- line-height: 1.4;
199
+ letter-spacing: 0.14em;
200
+ text-transform: uppercase;
201
+ color: var(--accent);
202
+ }
203
+ .hero-eyebrow::before {
204
+ content: "";
205
+ width: 5px; height: 5px;
206
+ background: var(--accent);
207
+ border-radius: 50%;
208
+ box-shadow: 0 0 10px var(--accent);
209
+ }
210
+
211
+ .hero-title-row {
212
+ display: flex;
213
+ align-items: center;
214
+ gap: var(--space-4);
215
+ flex-wrap: wrap;
216
+ }
217
+ .hero-logo {
218
+ width: 44px; height: 44px;
219
+ border-radius: var(--radius-md);
220
+ object-fit: contain;
143
221
  border: 1px solid var(--border);
222
+ background: var(--bg-elevated);
223
+ flex-shrink: 0;
224
+ }
225
+ .hero-heading {
226
+ margin: 0;
227
+ font-size: var(--text-display);
228
+ font-weight: 700;
229
+ letter-spacing: -0.04em;
230
+ line-height: 1.02;
231
+ color: var(--fg);
232
+ word-break: break-word;
233
+ flex: 1 1 auto;
234
+ min-width: 0;
235
+ }
236
+ @supports ((-webkit-background-clip: text) or (background-clip: text)) {
237
+ .hero-heading {
238
+ background: linear-gradient(180deg, var(--fg) 0%, color-mix(in oklab, var(--fg), transparent 25%) 100%);
239
+ -webkit-background-clip: text;
240
+ background-clip: text;
241
+ -webkit-text-fill-color: transparent;
242
+ color: transparent;
243
+ }
244
+ }
245
+ .hero-tagline {
246
+ margin: 0;
144
247
  color: var(--fg-muted);
145
- background: var(--bg-subtle);
146
- text-decoration: none;
147
- white-space: nowrap;
248
+ font-size: var(--text-lg);
249
+ line-height: 1.5;
250
+ max-width: 62ch;
148
251
  }
149
- .badge-version { color: var(--accent); border-color: var(--accent-soft); background: var(--accent-soft); font-weight: 600; }
150
- .badge-version:hover { background: color-mix(in oklab, var(--accent), transparent 75%); text-decoration: none; }
151
- .badge-pre { background: color-mix(in oklab, #f59e0b, transparent 85%); border-color: color-mix(in oklab, #f59e0b, transparent 60%); color: #b45309; }
152
- @media (prefers-color-scheme: dark) { .badge-pre { color: #fbbf24; } }
153
252
 
154
- .hero-badges { margin-top: var(--space-4); display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; }
155
-
156
- .badge-shield {
253
+ /* Version + pre-release chips */
254
+ .badge-version {
157
255
  display: inline-flex;
158
- align-items: stretch;
159
- border-radius: var(--radius-sm);
160
- overflow: hidden;
161
- font-size: 0.7rem;
256
+ align-items: center;
257
+ padding: 4px 10px;
258
+ border-radius: var(--radius-pill);
259
+ font-family: var(--font-mono);
260
+ font-size: 0.75rem;
162
261
  font-weight: 600;
163
- line-height: 1.4;
164
- letter-spacing: 0.01em;
262
+ letter-spacing: -0.01em;
263
+ color: var(--accent);
264
+ background: var(--accent-softer);
265
+ border: 1px solid var(--accent-edge);
165
266
  text-decoration: none;
166
- font-family: var(--font-sans);
167
- box-shadow: 0 0 0 1px rgb(0 0 0 / 0.04), 0 1px 1px rgb(0 0 0 / 0.04);
168
- transition: transform var(--duration-fast) var(--ease-out), box-shadow var(--duration-fast) var(--ease-out);
267
+ transition: background var(--duration-fast) var(--ease-out), transform var(--duration-fast) var(--ease-out);
268
+ }
269
+ .badge-version:hover {
270
+ background: var(--accent-soft);
271
+ color: var(--accent);
272
+ text-decoration: none;
273
+ transform: translateY(-1px);
274
+ }
275
+ .badge-pre {
276
+ display: inline-flex;
277
+ align-items: center;
278
+ padding: 4px 10px;
279
+ border-radius: var(--radius-pill);
280
+ font-family: var(--font-mono);
281
+ font-size: 0.75rem;
282
+ font-weight: 600;
283
+ letter-spacing: -0.01em;
284
+ color: #b45309;
285
+ background: color-mix(in oklab, #f59e0b, transparent 88%);
286
+ border: 1px solid color-mix(in oklab, #f59e0b, transparent 65%);
169
287
  }
170
- .badge-shield:hover { text-decoration: none; transform: translateY(-1px); box-shadow: 0 0 0 1px rgb(0 0 0 / 0.08), 0 2px 4px rgb(0 0 0 / 0.08); }
171
- .badge-shield-label, .badge-shield-value { padding: 3px var(--space-2); white-space: nowrap; }
172
- .badge-shield-label { background: #555; color: #fff; }
173
- .badge-shield-value { background: #2259c9; color: #fff; }
174
288
  @media (prefers-color-scheme: dark) {
175
- .badge-shield-label { background: #3a3a3a; }
176
- .badge-shield-value { background: #3b6fd4; }
289
+ .badge-pre { color: #fbbf24; }
177
290
  }
178
291
 
179
- .auth-banner {
180
- margin: var(--space-5) 0 0;
292
+ /* Status strip — single line under the tagline. */
293
+ .status-strip {
294
+ display: flex;
295
+ flex-wrap: wrap;
296
+ align-items: center;
297
+ gap: var(--space-4);
298
+ padding: var(--space-3) 0;
299
+ border-top: 1px solid var(--border-subtle);
300
+ border-bottom: 1px solid var(--border-subtle);
301
+ font-family: var(--font-mono);
302
+ font-size: var(--text-xs);
303
+ color: var(--fg-muted);
304
+ letter-spacing: 0.01em;
305
+ }
306
+ .status-item {
307
+ display: inline-flex;
308
+ align-items: center;
309
+ gap: 6px;
310
+ position: relative;
311
+ }
312
+ .status-item + .status-item::before {
313
+ content: "·";
314
+ color: var(--fg-subtle);
315
+ margin-right: var(--space-2);
316
+ opacity: 0.7;
317
+ }
318
+ .status-dot {
319
+ width: 6px; height: 6px;
320
+ border-radius: 50%;
321
+ display: inline-block;
322
+ flex-shrink: 0;
323
+ }
324
+ .status-dot-public {
325
+ background: #22c55e;
326
+ box-shadow: 0 0 0 3px color-mix(in oklab, #22c55e, transparent 80%);
327
+ animation: status-pulse 2.4s ease-in-out infinite;
328
+ }
329
+ .status-dot-gated {
330
+ background: var(--accent);
331
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent), transparent 75%);
332
+ }
333
+ @keyframes status-pulse {
334
+ 0%, 100% { box-shadow: 0 0 0 3px color-mix(in oklab, #22c55e, transparent 80%); }
335
+ 50% { box-shadow: 0 0 0 6px color-mix(in oklab, #22c55e, transparent 92%); }
336
+ }
337
+ .status-value {
338
+ color: var(--fg);
339
+ font-weight: 500;
340
+ }
341
+ .status-signin {
342
+ color: var(--fg-muted);
343
+ text-decoration: none;
344
+ border-bottom: 1px dotted var(--fg-subtle);
345
+ }
346
+ .status-signin:hover { color: var(--accent); border-color: var(--accent); }
347
+ .status-link {
348
+ color: inherit;
349
+ text-decoration: none;
350
+ transition: color var(--duration-fast) var(--ease-out);
351
+ }
352
+ .status-link .status-value { transition: color var(--duration-fast) var(--ease-out); }
353
+ .status-link:hover,
354
+ .status-link:hover .status-value { color: var(--accent); }
355
+ .status-link:focus-visible {
356
+ outline: 2px solid var(--accent);
357
+ outline-offset: 3px;
358
+ border-radius: 2px;
359
+ }
360
+
361
+ /* -------------------- Connect card -------------------- */
362
+
363
+ .connect {
364
+ border: 1px solid var(--border);
365
+ border-radius: var(--radius-lg);
366
+ background: var(--bg-elevated);
367
+ overflow: hidden;
368
+ box-shadow: var(--shadow-md);
369
+ position: relative;
370
+ }
371
+
372
+ /* Chrome header — three dots + endpoint path */
373
+ .connect-chrome {
374
+ display: flex;
375
+ align-items: center;
376
+ gap: var(--space-3);
181
377
  padding: var(--space-3) var(--space-4);
182
- border-radius: var(--radius-md);
183
- border: 1px solid var(--border-subtle);
378
+ border-bottom: 1px solid var(--border-subtle);
184
379
  background: var(--bg-subtle);
380
+ }
381
+ .connect-chrome-dots {
382
+ display: inline-flex;
383
+ gap: 6px;
384
+ flex-shrink: 0;
385
+ }
386
+ .connect-chrome-dot {
387
+ width: 10px; height: 10px;
388
+ border-radius: 50%;
389
+ background: color-mix(in oklab, var(--fg-subtle), transparent 60%);
390
+ display: inline-block;
391
+ }
392
+ .connect-chrome-endpoint {
393
+ margin-left: auto;
394
+ font-family: var(--font-mono);
395
+ font-size: 0.6875rem;
185
396
  color: var(--fg-muted);
397
+ white-space: nowrap;
398
+ overflow: hidden;
399
+ text-overflow: ellipsis;
400
+ min-width: 0;
401
+ }
402
+
403
+ /* Radio-hack tabs */
404
+ .connect-tab-input { position: absolute; opacity: 0; pointer-events: none; }
405
+ .connect-tabs {
406
+ display: flex;
407
+ gap: 0;
408
+ padding: 0 var(--space-4);
409
+ border-bottom: 1px solid var(--border-subtle);
410
+ overflow-x: auto;
411
+ scrollbar-width: none;
412
+ }
413
+ .connect-tabs::-webkit-scrollbar { display: none; }
414
+ .connect-tab-label {
415
+ padding: var(--space-3) var(--space-4);
186
416
  font-size: var(--text-sm);
187
- display: flex; align-items: center; gap: var(--space-2);
417
+ font-weight: 500;
418
+ color: var(--fg-muted);
419
+ cursor: pointer;
420
+ border-bottom: 3px solid transparent;
421
+ margin-bottom: -1px;
422
+ white-space: nowrap;
423
+ transition: color var(--duration-fast) var(--ease-out),
424
+ border-color var(--duration-fast) var(--ease-out),
425
+ background var(--duration-fast) var(--ease-out);
426
+ }
427
+ .connect-tab-label:hover { color: var(--fg); }
428
+ .connect-tab-input:checked + .connect-tab-label {
429
+ color: var(--fg);
430
+ font-weight: 600;
431
+ border-bottom-color: var(--accent);
432
+ background: linear-gradient(to top, var(--accent-softer), transparent 70%);
433
+ }
434
+ .connect-tab-input:focus-visible + .connect-tab-label {
435
+ outline: 2px solid var(--accent);
436
+ outline-offset: -6px;
437
+ border-radius: var(--radius-sm);
188
438
  }
189
- .auth-banner-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
190
- .auth-banner-public .auth-banner-dot { background: #22c55e; }
191
- .auth-banner-gated .auth-banner-dot { background: var(--accent); }
192
439
 
193
- section { padding: var(--space-10) 0 0; }
194
- .section-heading { display: flex; align-items: baseline; gap: var(--space-3); margin: 0 0 var(--space-6); }
195
- .section-heading h2 { margin: 0; font-size: var(--text-2xl); font-weight: 600; letter-spacing: -0.01em; }
196
- .section-count { color: var(--fg-subtle); font-size: var(--text-sm); font-variant-numeric: tabular-nums; font-weight: 500; }
440
+ .connect-panels { position: relative; padding: var(--space-5) var(--space-4); }
441
+ .connect-panel { display: none; }
442
+ .connect:has(#connect-tab-stdio:checked) .panel-stdio,
443
+ .connect:has(#connect-tab-http:checked) .panel-http,
444
+ .connect:has(#connect-tab-claude:checked) .panel-claude,
445
+ .connect:has(#connect-tab-curl:checked) .panel-curl { display: block; }
446
+ /* Fallback when :has() unsupported — show first visible panel */
447
+ @supports not selector(:has(*)) {
448
+ .connect-panel:first-of-type { display: block; }
449
+ }
450
+ .connect-panel pre {
451
+ padding: var(--space-4);
452
+ padding-right: var(--space-12);
453
+ background: var(--bg-code);
454
+ border: 1px solid var(--border-subtle);
455
+ border-radius: var(--radius-sm);
456
+ font-size: 0.8125rem;
457
+ line-height: 1.6;
458
+ }
459
+ .connect-copy {
460
+ position: absolute;
461
+ top: calc(var(--space-5) + 8px);
462
+ right: calc(var(--space-4) + 8px);
463
+ font-family: var(--font-mono);
464
+ font-size: 0.6875rem;
465
+ font-weight: 500;
466
+ padding: 4px 10px;
467
+ border-radius: var(--radius-sm);
468
+ border: 1px solid var(--border);
469
+ background: var(--bg);
470
+ color: var(--fg-muted);
471
+ cursor: pointer;
472
+ transition: all var(--duration-fast) var(--ease-out);
473
+ letter-spacing: 0.02em;
474
+ }
475
+ .connect-copy:hover {
476
+ color: var(--accent);
477
+ border-color: var(--accent-edge);
478
+ background: var(--accent-softer);
479
+ }
480
+ .connect-copy:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
481
+ .connect-copy[data-copied="true"] {
482
+ color: #16a34a;
483
+ border-color: color-mix(in oklab, #16a34a, transparent 60%);
484
+ background: color-mix(in oklab, #16a34a, transparent 92%);
485
+ }
486
+
487
+ /* Framework attribution pill */
488
+ .hero-badges {
489
+ display: inline-flex;
490
+ align-items: center;
491
+ gap: var(--space-2);
492
+ }
493
+ .badge-shield {
494
+ display: inline-flex;
495
+ align-items: center;
496
+ gap: 6px;
497
+ padding: 4px 10px;
498
+ border-radius: var(--radius-pill);
499
+ font-family: var(--font-mono);
500
+ font-size: 0.6875rem;
501
+ font-weight: 500;
502
+ letter-spacing: 0.01em;
503
+ color: var(--fg-muted);
504
+ background: var(--bg-subtle);
505
+ border: 1px solid var(--border-subtle);
506
+ text-decoration: none;
507
+ transition: all var(--duration-fast) var(--ease-out);
508
+ }
509
+ .badge-shield:hover {
510
+ color: var(--accent);
511
+ border-color: var(--accent-edge);
512
+ background: var(--accent-softer);
513
+ text-decoration: none;
514
+ transform: translateY(-1px);
515
+ }
516
+ .badge-shield-label { color: var(--fg-subtle); transition: color var(--duration-fast); }
517
+ .badge-shield-value { color: var(--fg-muted); transition: color var(--duration-fast); }
518
+ .badge-shield:hover .badge-shield-label,
519
+ .badge-shield:hover .badge-shield-value { color: var(--accent); }
520
+
521
+ /* -------------------- Sections -------------------- */
522
+
523
+ section { padding: var(--space-12) 0 0; }
524
+
525
+ .section-heading {
526
+ display: flex;
527
+ align-items: baseline;
528
+ gap: var(--space-3);
529
+ margin: 0 0 var(--space-6);
530
+ padding-bottom: var(--space-3);
531
+ border-bottom: 1px solid var(--border-subtle);
532
+ }
533
+ .section-heading h2 {
534
+ margin: 0;
535
+ font-size: var(--text-2xl);
536
+ font-weight: 600;
537
+ letter-spacing: -0.025em;
538
+ color: var(--fg);
539
+ text-transform: lowercase;
540
+ }
541
+ .section-count {
542
+ font-family: var(--font-mono);
543
+ font-size: var(--text-2xl);
544
+ font-weight: 600;
545
+ color: var(--accent);
546
+ font-variant-numeric: tabular-nums;
547
+ letter-spacing: -0.02em;
548
+ line-height: 1;
549
+ }
197
550
 
198
- .group-heading { margin: var(--space-6) 0 var(--space-3); color: var(--fg-muted); font-size: var(--text-xs); font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; }
551
+ .group-heading {
552
+ margin: var(--space-6) 0 var(--space-3);
553
+ color: var(--fg-muted);
554
+ font-family: var(--font-mono);
555
+ font-size: 0.6875rem;
556
+ font-weight: 600;
557
+ text-transform: uppercase;
558
+ letter-spacing: 0.12em;
559
+ }
199
560
  .group-heading:first-child { margin-top: 0; }
200
561
 
562
+ /* -------------------- Cards -------------------- */
563
+
564
+ .card-grid {
565
+ display: grid;
566
+ grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
567
+ gap: var(--space-3);
568
+ align-items: start;
569
+ }
201
570
  .card {
202
571
  border: 1px solid var(--border-subtle);
203
- border-radius: var(--radius-lg);
204
- padding: var(--space-5);
205
- margin-bottom: var(--space-3);
572
+ border-radius: var(--radius-md);
573
+ padding: var(--space-4) var(--space-5);
206
574
  background: var(--bg-elevated);
207
- transition: border-color var(--duration-fast) var(--ease-out), box-shadow var(--duration-fast) var(--ease-out);
575
+ display: flex;
576
+ flex-direction: column;
577
+ gap: var(--space-2);
578
+ transition: border-color var(--duration-fast) var(--ease-out),
579
+ transform var(--duration-fast) var(--ease-out),
580
+ box-shadow var(--duration-fast) var(--ease-out);
581
+ position: relative;
582
+ }
583
+ .card:hover {
584
+ border-color: var(--accent-edge);
585
+ transform: translateY(-1px);
586
+ box-shadow: var(--shadow-md);
587
+ }
588
+ .card-head {
589
+ display: flex;
590
+ align-items: center;
591
+ gap: var(--space-2);
592
+ flex-wrap: wrap;
593
+ }
594
+ .card-title {
595
+ margin: 0;
596
+ font-size: 0.9375rem;
597
+ font-weight: 600;
598
+ font-family: var(--font-mono);
599
+ color: var(--fg);
600
+ letter-spacing: -0.015em;
208
601
  }
209
- .card:hover { border-color: var(--border); box-shadow: var(--shadow-sm); }
210
- .card-head { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; margin-bottom: var(--space-2); }
211
- .card-title { margin: 0; font-size: var(--text-base); font-weight: 600; font-family: var(--font-mono); color: var(--fg); }
212
602
  .card-title a { color: inherit; }
213
- .card-desc { margin: 0; color: var(--fg-muted); font-size: var(--text-sm); }
214
- .card-meta { margin-top: var(--space-3); display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); font-size: var(--text-xs); color: var(--fg-muted); align-items: center; }
603
+ .card-title a:hover { color: var(--accent); text-decoration: none; }
604
+ .card-desc {
605
+ margin: 0;
606
+ color: var(--fg-muted);
607
+ font-size: var(--text-sm);
608
+ line-height: 1.5;
609
+ }
610
+ .card-meta {
611
+ display: flex;
612
+ flex-wrap: wrap;
613
+ gap: var(--space-1) var(--space-3);
614
+ font-size: var(--text-xs);
615
+ color: var(--fg-muted);
616
+ font-family: var(--font-mono);
617
+ align-items: center;
618
+ }
215
619
  .card-meta-label { color: var(--fg-subtle); }
216
- .card-meta code { font-size: 0.9em; }
620
+ .card-meta code {
621
+ font-size: 1em;
622
+ color: var(--fg);
623
+ background: transparent;
624
+ border: 0;
625
+ padding: 0;
626
+ }
217
627
 
218
- .pill-row { display: inline-flex; flex-wrap: wrap; gap: var(--space-1); }
628
+ /* Annotation pills dot-chip style */
629
+ .pill-row { display: inline-flex; flex-wrap: wrap; gap: 5px; align-items: center; }
219
630
  .pill {
220
- display: inline-flex; align-items: center;
221
- padding: 1px 8px;
631
+ display: inline-flex;
632
+ align-items: center;
633
+ gap: 5px;
634
+ padding: 2px 8px;
222
635
  border-radius: var(--radius-pill);
223
- font-size: 0.7rem;
636
+ font-family: var(--font-mono);
637
+ font-size: 0.6875rem;
224
638
  font-weight: 500;
225
- line-height: 1.5;
226
- border: 1px solid var(--border-subtle);
639
+ line-height: 1.4;
227
640
  color: var(--fg-muted);
228
641
  background: var(--bg-subtle);
642
+ border: 1px solid var(--border-subtle);
643
+ letter-spacing: 0.01em;
644
+ }
645
+ .pill::before {
646
+ content: "";
647
+ width: 4px; height: 4px;
648
+ border-radius: 50%;
649
+ background: currentColor;
650
+ flex-shrink: 0;
229
651
  }
230
- .pill-readonly { color: #16a34a; border-color: color-mix(in oklab, #16a34a, transparent 70%); background: color-mix(in oklab, #16a34a, transparent 90%); }
231
- .pill-destructive { color: #dc2626; border-color: color-mix(in oklab, #dc2626, transparent 70%); background: color-mix(in oklab, #dc2626, transparent 90%); }
232
- .pill-openworld { color: #2563eb; border-color: color-mix(in oklab, #2563eb, transparent 70%); background: color-mix(in oklab, #2563eb, transparent 90%); }
233
- .pill-task { color: var(--accent); border-color: var(--accent-soft); background: var(--accent-soft); }
234
- .pill-app { color: #9333ea; border-color: color-mix(in oklab, #9333ea, transparent 70%); background: color-mix(in oklab, #9333ea, transparent 90%); }
235
- .pill-auth { color: var(--fg-muted); font-family: var(--font-mono); font-size: 0.65rem; }
652
+ .pill-readonly { color: #16a34a; background: color-mix(in oklab, #16a34a, transparent 92%); border-color: color-mix(in oklab, #16a34a, transparent 72%); }
653
+ .pill-destructive { color: #dc2626; background: color-mix(in oklab, #dc2626, transparent 92%); border-color: color-mix(in oklab, #dc2626, transparent 72%); }
654
+ .pill-openworld { color: #2563eb; background: color-mix(in oklab, #2563eb, transparent 92%); border-color: color-mix(in oklab, #2563eb, transparent 72%); }
655
+ .pill-task { color: var(--accent); background: var(--accent-softer); border-color: var(--accent-edge); }
656
+ .pill-app { color: #9333ea; background: color-mix(in oklab, #9333ea, transparent 92%); border-color: color-mix(in oklab, #9333ea, transparent 72%); }
657
+ .pill-auth { color: var(--fg-subtle); font-size: 0.65rem; }
658
+ .pill-auth::before { display: none; }
236
659
 
237
- .snippet { position: relative; margin-top: var(--space-3); }
238
- .snippet pre { padding-right: var(--space-12); font-size: 0.8rem; }
239
- .snippet-copy {
240
- position: absolute; top: var(--space-2); right: var(--space-2);
241
- font-family: var(--font-sans);
660
+ @media (prefers-color-scheme: dark) {
661
+ .pill-readonly { color: #4ade80; }
662
+ .pill-destructive { color: #f87171; }
663
+ .pill-openworld { color: #60a5fa; }
664
+ .pill-app { color: #c084fc; }
665
+ }
666
+
667
+ .source-link {
242
668
  font-size: var(--text-xs);
243
- padding: 2px var(--space-2);
669
+ color: var(--fg-muted);
670
+ margin-left: auto;
671
+ font-family: var(--font-mono);
672
+ transition: color var(--duration-fast);
673
+ }
674
+ .source-link:hover { color: var(--accent); text-decoration: none; }
675
+
676
+ /* Inline snippet (tool invocation) */
677
+ .snippet {
678
+ position: relative;
679
+ margin-top: var(--space-2);
680
+ }
681
+ .snippet pre {
682
+ padding: var(--space-3);
683
+ padding-right: var(--space-12);
684
+ background: var(--bg-code);
685
+ border: 1px solid var(--border-subtle);
244
686
  border-radius: var(--radius-sm);
687
+ font-size: 0.75rem;
688
+ line-height: 1.55;
689
+ }
690
+ .snippet-copy {
691
+ position: absolute;
692
+ top: 6px;
693
+ right: 6px;
694
+ font-family: var(--font-mono);
695
+ font-size: 0.65rem;
696
+ font-weight: 500;
697
+ padding: 3px 8px;
698
+ border-radius: var(--radius-xs);
245
699
  border: 1px solid var(--border);
246
700
  background: var(--bg);
247
701
  color: var(--fg-muted);
248
702
  cursor: pointer;
249
- transition: color var(--duration-fast), border-color var(--duration-fast);
703
+ transition: all var(--duration-fast) var(--ease-out);
704
+ letter-spacing: 0.02em;
250
705
  }
251
- .snippet-copy:hover { color: var(--accent); border-color: var(--accent); }
706
+ .snippet-copy:hover { color: var(--accent); border-color: var(--accent-edge); }
252
707
  .snippet-copy:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
253
- .snippet-copy[data-copied="true"] { color: #16a34a; border-color: #16a34a; }
708
+ .snippet-copy[data-copied="true"] {
709
+ color: #16a34a;
710
+ border-color: color-mix(in oklab, #16a34a, transparent 60%);
711
+ }
254
712
 
255
- details { margin-top: var(--space-3); border: 1px solid var(--border-subtle); border-radius: var(--radius-md); }
713
+ /* Collapsible details (schema preview) */
714
+ details {
715
+ margin-top: var(--space-1);
716
+ border: 0;
717
+ border-radius: 0;
718
+ }
256
719
  details > summary {
257
720
  cursor: pointer;
258
- padding: var(--space-2) var(--space-3);
721
+ padding: 4px 0;
722
+ font-family: var(--font-mono);
259
723
  font-size: var(--text-xs);
260
724
  color: var(--fg-muted);
261
725
  list-style: none;
262
726
  user-select: none;
263
- transition: background var(--duration-fast);
727
+ display: inline-flex;
728
+ align-items: center;
729
+ gap: var(--space-2);
730
+ transition: color var(--duration-fast);
264
731
  }
265
732
  details > summary::-webkit-details-marker { display: none; }
266
- details > summary:hover { background: var(--bg-subtle); color: var(--fg); }
267
- details > summary::before { content: ""; margin-right: 4px; transition: transform var(--duration-fast); display: inline-block; color: var(--fg-subtle); }
268
- details[open] > summary::before { transform: rotate(90deg); }
269
- details[open] > summary { border-bottom: 1px solid var(--border-subtle); }
270
- details > pre { border: 0; border-radius: 0 0 var(--radius-md) var(--radius-md); margin: 0; }
271
-
272
- .connect-tabs { display: flex; gap: var(--space-1); margin-top: var(--space-4); border-bottom: 1px solid var(--border-subtle); }
273
- .connect-tab-input { position: absolute; opacity: 0; pointer-events: none; }
274
- .connect-tab-label {
275
- padding: var(--space-2) var(--space-4);
276
- font-size: var(--text-sm);
733
+ details > summary::before {
734
+ content: "+";
735
+ display: inline-block;
736
+ width: 10px;
737
+ text-align: center;
277
738
  color: var(--fg-muted);
278
- cursor: pointer;
279
- border-bottom: 2px solid transparent;
280
- margin-bottom: -1px;
281
- transition: color var(--duration-fast), border-color var(--duration-fast);
739
+ font-weight: 600;
740
+ transition: transform var(--duration-fast), color var(--duration-fast);
741
+ }
742
+ details[open] > summary::before { content: "−"; color: var(--accent); }
743
+ details > summary:hover { color: var(--accent); }
744
+ details > pre {
745
+ margin-top: var(--space-2);
746
+ font-size: 0.7rem;
747
+ line-height: 1.55;
282
748
  }
283
- .connect-tab-label:hover { color: var(--fg); }
284
- .connect-tab-input:focus-visible + .connect-tab-label { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--radius-sm); }
285
- .connect-panels { position: relative; margin-top: var(--space-3); }
286
- .connect-panel { display: none; }
287
- .connect-tab-input:checked + .connect-tab-label { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
288
- ${[0, 1, 2]
289
- .map((i) => `.connect-tab-input:nth-of-type(${i + 1}):checked ~ .connect-panels .connect-panel:nth-child(${i + 1}) { display: block; }`)
290
- .join('\n')}
291
749
 
292
- footer {
293
- margin-top: var(--space-16);
294
- padding: var(--space-8) 0 var(--space-6);
295
- border-top: 1px solid var(--border-subtle);
296
- font-size: var(--text-sm);
750
+ /* Prompt args */
751
+ .args-list {
752
+ list-style: none;
753
+ padding: 0;
754
+ margin: var(--space-1) 0 0;
755
+ display: flex;
756
+ flex-direction: column;
757
+ gap: 4px;
758
+ font-size: var(--text-xs);
297
759
  color: var(--fg-muted);
760
+ font-family: var(--font-mono);
761
+ }
762
+ .args-list li { line-height: 1.6; }
763
+ .args-list code {
764
+ font-size: 1em;
765
+ background: transparent;
766
+ border: 0;
767
+ padding: 0;
768
+ color: var(--fg);
769
+ }
770
+ .args-required {
771
+ color: var(--accent);
772
+ font-size: 0.625rem;
773
+ font-weight: 600;
774
+ margin-left: 5px;
775
+ letter-spacing: 0.08em;
776
+ text-transform: uppercase;
298
777
  }
299
- .footer-links { display: flex; flex-wrap: wrap; gap: var(--space-4) var(--space-6); margin-bottom: var(--space-4); }
300
- .footer-group { display: flex; flex-direction: column; gap: var(--space-2); min-width: 140px; }
301
- .footer-group-label { color: var(--fg-subtle); font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.08em; font-weight: 600; }
302
- .footer-attrib { font-size: var(--text-xs); color: var(--fg-subtle); }
303
- .footer-attrib a { color: inherit; text-decoration: underline; text-decoration-color: var(--border); }
304
- .footer-attrib a:hover { color: var(--accent); text-decoration-color: var(--accent); }
305
-
306
- .source-link { font-size: var(--text-xs); color: var(--fg-muted); margin-left: auto; }
307
- .source-link:hover { color: var(--accent); }
308
-
309
- .args-list { list-style: none; padding: 0; margin: var(--space-3) 0 0; display: flex; flex-direction: column; gap: var(--space-1); font-size: var(--text-sm); }
310
- .args-list code { font-size: 0.85em; }
311
- .args-required { color: var(--accent); font-size: 0.7rem; font-weight: 600; margin-left: var(--space-1); }
312
778
 
779
+ /* Extensions */
313
780
  .ext-card { background: var(--bg-subtle); border-color: var(--border-subtle); }
314
781
  .ext-key { font-family: var(--font-mono); font-size: var(--text-sm); color: var(--fg); }
315
- .ext-preview { margin-top: var(--space-2); font-size: var(--text-xs); color: var(--fg-muted); }
782
+ .ext-preview {
783
+ margin: var(--space-2) 0 0;
784
+ font-size: 0.7rem;
785
+ line-height: 1.55;
786
+ }
787
+
788
+ /* Empty / degraded state */
789
+ .empty-state {
790
+ padding: var(--space-10) var(--space-6);
791
+ text-align: center;
792
+ color: var(--fg-muted);
793
+ border: 1px dashed var(--border);
794
+ border-radius: var(--radius-md);
795
+ background: var(--bg-subtle);
796
+ font-size: var(--text-sm);
797
+ font-family: var(--font-mono);
798
+ }
316
799
 
317
- .empty-state { padding: var(--space-8); text-align: center; color: var(--fg-muted); border: 1px dashed var(--border); border-radius: var(--radius-md); }
800
+ /* -------------------- Footer -------------------- */
318
801
 
319
- @media (max-width: 640px) {
320
- main { padding: var(--space-5) var(--space-4) var(--space-12); }
321
- .hero-heading { font-size: var(--text-2xl); }
322
- .hero-top { flex-direction: column; gap: var(--space-3); }
323
- .hero-logo { width: 48px; height: 48px; }
802
+ footer {
803
+ margin-top: var(--space-20);
804
+ padding: var(--space-6) 0;
805
+ border-top: 1px solid var(--border-subtle);
806
+ display: flex;
807
+ flex-wrap: wrap;
808
+ align-items: center;
809
+ gap: var(--space-2) var(--space-4);
810
+ font-family: var(--font-mono);
811
+ font-size: var(--text-xs);
812
+ color: var(--fg-subtle);
813
+ letter-spacing: 0.01em;
814
+ }
815
+ footer a {
816
+ color: var(--fg-muted);
817
+ text-decoration: none;
818
+ transition: color var(--duration-fast) var(--ease-out);
819
+ }
820
+ footer a:hover { color: var(--accent); text-decoration: none; }
821
+ .footer-sep { color: var(--fg-subtle); opacity: 0.5; user-select: none; }
822
+ .footer-spacer { flex: 1 0 var(--space-4); }
823
+ .footer-attrib { color: var(--fg-subtle); }
824
+ .footer-attrib a { color: var(--fg-muted); }
825
+
826
+ /* -------------------- Responsive -------------------- */
827
+
828
+ @media (max-width: 760px) {
829
+ main { padding: var(--space-6) var(--space-4) var(--space-16); }
830
+ .hero { padding: var(--space-8) 0 var(--space-6); gap: var(--space-5); }
831
+ .hero-heading { font-size: clamp(1.875rem, 7vw + 0.5rem, 2.5rem); letter-spacing: -0.035em; }
832
+ .hero-title-row { gap: var(--space-3); }
833
+ .hero-logo { width: 40px; height: 40px; }
834
+ .hero-tagline { font-size: var(--text-base); }
835
+ .status-strip { gap: var(--space-2); font-size: 0.6875rem; }
836
+ .connect-chrome-endpoint { display: none; }
837
+ .card-grid { grid-template-columns: 1fr; }
838
+ section { padding: var(--space-8) 0 0; }
839
+ .section-heading h2 { font-size: var(--text-xl); }
840
+ .section-count { font-size: var(--text-xl); }
324
841
  }
325
842
 
326
843
  @media (prefers-reduced-motion: reduce) {
327
844
  *, *::before, *::after {
328
845
  transition-duration: 0.01ms !important;
329
846
  animation-duration: 0.01ms !important;
847
+ animation-iteration-count: 1 !important;
330
848
  }
331
849
  }
332
850
  `;
@@ -336,8 +854,6 @@ footer {
336
854
  // Copy-to-clipboard — single inlined script, < 1KB
337
855
  // ---------------------------------------------------------------------------
338
856
  function renderCopyScript() {
339
- // Works without JS too — the button is inert but the pre/code is still selectable.
340
- // `data-copy-target` holds a CSS selector or inline text content.
341
857
  const js = `
342
858
  document.addEventListener('click', function(e) {
343
859
  var btn = e.target.closest('[data-copy]');
@@ -366,17 +882,10 @@ document.addEventListener('click', function(e) {
366
882
  // ---------------------------------------------------------------------------
367
883
  // Primitives
368
884
  // ---------------------------------------------------------------------------
369
- function renderBadge(label, variant, href) {
370
- const cls = variant === 'version' ? 'badge badge-version' : variant === 'pre' ? 'badge badge-pre' : 'badge';
371
- if (href) {
372
- return html `<a class="${cls}" href="${href}">${label}</a>`;
373
- }
374
- return html `<span class="${cls}">${label}</span>`;
375
- }
376
885
  /**
377
- * shields.io-style bi-part badge: grey "Built on" label + accent framework name.
378
- * Links to the framework's npm page for discoverability. Lives in the hero
379
- * badge strip when `landing.attribution` is enabled.
886
+ * shields.io-style bi-part badge: "Built on" label + framework name. Links to
887
+ * the framework's npm page. Lives in the hero when `landing.attribution` is
888
+ * enabled.
380
889
  */
381
890
  function renderFrameworkBadge(framework) {
382
891
  const npmUrl = `https://www.npmjs.com/package/${encodeURIComponent(framework.name)}`;
@@ -385,45 +894,104 @@ function renderFrameworkBadge(framework) {
385
894
  function renderPill(text, variant) {
386
895
  return html `<span class="pill pill-${variant}">${text}</span>`;
387
896
  }
388
- function renderAuthBanner(auth) {
389
- if (auth.mode === 'none') {
390
- return html `<div class="auth-banner auth-banner-public" role="status"><span class="auth-banner-dot" aria-hidden="true"></span><span>Public access — no authentication required.</span></div>`;
391
- }
392
- if (auth.mode === 'jwt') {
393
- return html `<div class="auth-banner auth-banner-gated" role="status"><span class="auth-banner-dot" aria-hidden="true"></span><span>Requires a bearer token.</span></div>`;
394
- }
395
- // oauth
396
- const issuer = auth.oauthIssuer ? html ` <a href="${auth.oauthIssuer}">Sign in ↗</a>` : html ``;
397
- return html `<div class="auth-banner auth-banner-gated" role="status"><span class="auth-banner-dot" aria-hidden="true"></span><span>Requires OAuth.${issuer}</span></div>`;
398
- }
399
897
  function renderSectionHeading(id, label, count) {
400
898
  return html `
401
899
  <div class="section-heading">
402
900
  <h2 id="${id}">${label}</h2>
403
- <span class="section-count" aria-label="${String(count)} ${label.toLowerCase()}">(${count})</span>
901
+ <span class="section-count" aria-label="${String(count)} ${label}">${String(count)}</span>
404
902
  </div>
405
903
  `;
406
904
  }
407
- function renderSnippet(id, text, variant = 'code') {
905
+ function renderSnippet(id, text) {
408
906
  const targetId = `snippet-${id}`;
409
907
  return html `
410
908
  <div class="snippet">
411
- <pre id="${targetId}" class="snippet-${variant}"><code>${text}</code></pre>
909
+ <pre id="${targetId}"><code>${text}</code></pre>
412
910
  <button type="button" class="snippet-copy" data-copy data-copy-target="#${targetId}" aria-label="Copy">Copy</button>
413
911
  </div>
414
912
  `;
415
913
  }
416
914
  // ---------------------------------------------------------------------------
915
+ // Status strip — replaces the old auth banner
916
+ // ---------------------------------------------------------------------------
917
+ /**
918
+ * Single-line status strip under the hero. Communicates auth mode, capability
919
+ * counts, and protocol version in one mono-spaced, dot-separated row.
920
+ *
921
+ * Accessibility:
922
+ * - `role="status"` so changes are announced live
923
+ * - `aria-label` carries the long-form auth phrase for screen readers
924
+ * ("Public access", "Requires OAuth", etc.) even when the visible label is
925
+ * compact ("public", "oauth")
926
+ */
927
+ function renderStatusStrip(manifest, degraded) {
928
+ const { auth, definitionCounts, protocol } = manifest;
929
+ const authMeta = describeAuth(auth);
930
+ // Counts hidden in degraded mode to avoid leaking inventory shape.
931
+ const counts = degraded
932
+ ? []
933
+ : [
934
+ { n: definitionCounts.tools, label: 'tools' },
935
+ { n: definitionCounts.resources, label: 'resources' },
936
+ { n: definitionCounts.prompts, label: 'prompts' },
937
+ ].filter((c) => c.n > 0);
938
+ const signin = auth.mode === 'oauth' && auth.oauthIssuer
939
+ ? html ` <a class="status-signin" href="${auth.oauthIssuer}" rel="noopener">sign in ↗</a>`
940
+ : html ``;
941
+ return html `
942
+ <div class="status-strip" role="status" aria-label="${authMeta.ariaLabel}">
943
+ <span class="status-item" title="${authMeta.ariaLabel}">
944
+ <span class="status-dot ${authMeta.dotClass}" aria-hidden="true"></span>
945
+ <span class="status-value">${authMeta.label}</span>${signin}
946
+ </span>
947
+ ${counts.map((c) => html `
948
+ <a class="status-item status-link" href="#section-${c.label}">
949
+ <span class="status-value">${String(c.n)}</span>
950
+ <span>${c.label}</span>
951
+ </a>
952
+ `)}
953
+ <span class="status-item" title="MCP protocol version ${protocol.latestVersion}">
954
+ <span>protocol</span>
955
+ <span class="status-value">${protocol.latestVersion}</span>
956
+ </span>
957
+ </div>
958
+ `;
959
+ }
960
+ /** Visible label, dot class, and long-form aria phrase for the auth strip item. */
961
+ function describeAuth(auth) {
962
+ if (auth.mode === 'none') {
963
+ return {
964
+ label: 'public',
965
+ dotClass: 'status-dot-public',
966
+ ariaLabel: 'Public access — no authentication required',
967
+ };
968
+ }
969
+ if (auth.mode === 'jwt') {
970
+ return {
971
+ label: 'bearer',
972
+ dotClass: 'status-dot-gated',
973
+ ariaLabel: 'Requires a bearer token',
974
+ };
975
+ }
976
+ return {
977
+ label: 'oauth',
978
+ dotClass: 'status-dot-gated',
979
+ ariaLabel: 'Requires OAuth',
980
+ };
981
+ }
982
+ // ---------------------------------------------------------------------------
417
983
  // Hero
418
984
  // ---------------------------------------------------------------------------
419
- function renderHero(manifest, baseUrl) {
985
+ function renderHero(manifest, baseUrl, degraded) {
420
986
  const { server, landing } = manifest;
421
987
  const releaseUrl = landing.repoRoot
422
988
  ? `${landing.repoRoot.url}/releases/tag/v${server.version}`
423
989
  : undefined;
424
- const versionBadge = renderBadge(`v${server.version}`, 'version', releaseUrl);
990
+ const versionBadge = releaseUrl
991
+ ? html `<a class="badge-version" href="${releaseUrl}" aria-label="v${server.version} release notes">v${server.version}</a>`
992
+ : html `<span class="badge-version">v${server.version}</span>`;
425
993
  const preReleaseBadge = landing.preRelease.isPreRelease
426
- ? renderBadge(landing.preRelease.label ?? 'pre-release', 'pre')
994
+ ? html `<span class="badge-pre">${landing.preRelease.label ?? 'pre-release'}</span>`
427
995
  : html ``;
428
996
  const tagline = landing.tagline ?? server.description ?? '';
429
997
  const logo = landing.logo
@@ -432,73 +1000,142 @@ function renderHero(manifest, baseUrl) {
432
1000
  const frameworkBadge = landing.attribution
433
1001
  ? html `<div class="hero-badges">${renderFrameworkBadge(manifest.framework)}</div>`
434
1002
  : html ``;
1003
+ const connect = degraded ? html `` : renderConnectSnippets(manifest, baseUrl);
435
1004
  return html `
436
1005
  <header class="hero">
437
- <div class="hero-top">
1006
+ <span class="hero-eyebrow" aria-hidden="true">MCP Server</span>
1007
+ <div class="hero-title-row">
438
1008
  ${logo}
439
- <div class="hero-identity">
440
- <h1 class="hero-heading">
441
- <span>${server.name}</span>
442
- ${versionBadge}
443
- ${preReleaseBadge}
444
- </h1>
445
- ${tagline ? html `<p class="hero-tagline">${tagline}</p>` : html ``}
446
- ${frameworkBadge}
447
- </div>
1009
+ <h1 class="hero-heading">${server.name}</h1>
1010
+ ${versionBadge}
1011
+ ${preReleaseBadge}
448
1012
  </div>
449
- ${renderAuthBanner(manifest.auth)}
450
- ${renderConnectSnippets(manifest, baseUrl)}
1013
+ ${tagline ? html `<p class="hero-tagline">${tagline}</p>` : html ``}
1014
+ ${renderStatusStrip(manifest, degraded)}
1015
+ ${connect}
1016
+ ${frameworkBadge}
451
1017
  </header>
452
1018
  `;
453
1019
  }
1020
+ /**
1021
+ * `@scope/pkg-name` → `pkg-name`. Fall through for bare names.
1022
+ * Used as the `mcpServers` key and the Claude CLI server alias.
1023
+ */
1024
+ function deriveShortName(serverName) {
1025
+ const slash = serverName.lastIndexOf('/');
1026
+ return slash >= 0 ? serverName.slice(slash + 1) : serverName;
1027
+ }
1028
+ /** Convert ordered env entries to the `{ KEY: value }` shape MCP clients expect. */
1029
+ function envFromEntries(entries) {
1030
+ return Object.fromEntries(entries.map(({ key, value }) => [key, value]));
1031
+ }
1032
+ /** `claude mcp add --transport stdio <name> [--env K=V]* -- bunx <pkg>@latest` */
1033
+ function buildClaudeStdioCmd(shortName, npmPackage, envExample) {
1034
+ const envFlags = envExample.map(({ key, value }) => `--env ${key}=${value}`).join(' ');
1035
+ const envSegment = envFlags.length > 0 ? ` ${envFlags}` : '';
1036
+ return `claude mcp add --transport stdio ${shortName}${envSegment} -- bunx ${npmPackage}@latest`;
1037
+ }
1038
+ /** `claude mcp add --transport http <name> <url>` */
1039
+ function buildClaudeHttpCmd(shortName, endpoint) {
1040
+ return `claude mcp add --transport http ${shortName} ${endpoint}`;
1041
+ }
454
1042
  function renderConnectSnippets(manifest, baseUrl) {
455
1043
  const endpoint = `${baseUrl.replace(/\/$/, '')}${manifest.transport.endpointPath}`;
456
- const claudeDesktopConfig = JSON.stringify({
1044
+ const npmPackage = manifest.landing.npmPackage?.name;
1045
+ // `@cyanheads/mcp-ts-core` → `mcp-ts-core`. Short aliases match the convention
1046
+ // used in real Claude Desktop / Cursor configs and make the `claude mcp add`
1047
+ // command more ergonomic.
1048
+ const shortName = deriveShortName(manifest.server.name);
1049
+ const envExample = manifest.landing.envExample;
1050
+ const envObject = envExample.length > 0 ? envFromEntries(envExample) : undefined;
1051
+ // STDIO: prefer native `bunx <pkg>@latest` when the server is published;
1052
+ // fall back to `mcp-remote` as a stdio → HTTP bridge so the tab is always
1053
+ // useful even for unpublished servers.
1054
+ const stdioConfig = JSON.stringify({
457
1055
  mcpServers: {
458
- [manifest.server.name]: {
459
- type: 'http',
460
- url: endpoint,
1056
+ [shortName]: {
1057
+ command: 'bunx',
1058
+ args: npmPackage ? [`${npmPackage}@latest`] : ['mcp-remote', endpoint],
1059
+ ...(envObject && { env: envObject }),
461
1060
  },
462
1061
  },
463
1062
  }, null, 2);
464
- const mcpRemoteConfig = JSON.stringify({
1063
+ const httpConfig = JSON.stringify({
465
1064
  mcpServers: {
466
- [manifest.server.name]: {
467
- command: 'npx',
468
- args: ['-y', 'mcp-remote', endpoint],
1065
+ [shortName]: {
1066
+ type: 'http',
1067
+ url: endpoint,
1068
+ ...(envObject && { env: envObject }),
469
1069
  },
470
1070
  },
471
1071
  }, null, 2);
1072
+ // `claude mcp add` — published package routes through stdio; otherwise
1073
+ // point it straight at the HTTP endpoint.
1074
+ const claudeCmd = npmPackage
1075
+ ? buildClaudeStdioCmd(shortName, npmPackage, envExample)
1076
+ : buildClaudeHttpCmd(shortName, endpoint);
472
1077
  const curl = [
473
1078
  `curl -X POST ${endpoint} \\`,
474
1079
  ` -H "Content-Type: application/json" \\`,
475
1080
  ` -H "MCP-Protocol-Version: ${manifest.protocol.latestVersion}" \\`,
476
1081
  ` -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"${manifest.protocol.latestVersion}","capabilities":{},"clientInfo":{"name":"curl","version":"1.0.0"}}}'`,
477
1082
  ].join('\n');
478
- // Radio-hack tabsno JS required for tab switching.
1083
+ // Chrome labelnpm package when published, else the HTTP endpoint (trimmed).
1084
+ const chromeLabel = npmPackage ?? endpoint.replace(/^https?:\/\//, '');
1085
+ const panels = [
1086
+ { id: 'stdio', label: 'STDIO', content: stdioConfig, copyAriaLabel: 'Copy stdio config' },
1087
+ {
1088
+ id: 'http',
1089
+ label: 'Streamable HTTP',
1090
+ content: httpConfig,
1091
+ copyAriaLabel: 'Copy HTTP config',
1092
+ },
1093
+ {
1094
+ id: 'claude',
1095
+ label: 'Claude',
1096
+ content: claudeCmd,
1097
+ copyAriaLabel: 'Copy claude mcp add command',
1098
+ },
1099
+ { id: 'curl', label: 'curl', content: curl, copyAriaLabel: 'Copy curl command' },
1100
+ ];
479
1101
  return html `
480
- <div class="connect-tabs" role="tablist" aria-label="Connection snippets">
481
- <input type="radio" class="connect-tab-input" name="connect" id="connect-tab-http" checked />
482
- <label for="connect-tab-http" class="connect-tab-label" role="tab">HTTP client</label>
483
- <input type="radio" class="connect-tab-input" name="connect" id="connect-tab-remote" />
484
- <label for="connect-tab-remote" class="connect-tab-label" role="tab">mcp-remote (stdio)</label>
485
- <input type="radio" class="connect-tab-input" name="connect" id="connect-tab-curl" />
486
- <label for="connect-tab-curl" class="connect-tab-label" role="tab">curl</label>
1102
+ <div class="connect" aria-label="Connection snippets">
1103
+ <div class="connect-chrome">
1104
+ <span class="connect-chrome-dots" aria-hidden="true">
1105
+ <span class="connect-chrome-dot"></span>
1106
+ <span class="connect-chrome-dot"></span>
1107
+ <span class="connect-chrome-dot"></span>
1108
+ </span>
1109
+ <span class="connect-chrome-endpoint" title="${endpoint}">${chromeLabel}</span>
1110
+ </div>
1111
+ ${panels.map((p, i) => i === 0
1112
+ ? html `<input type="radio" class="connect-tab-input" name="connect" id="connect-tab-${p.id}" checked />`
1113
+ : html `<input type="radio" class="connect-tab-input" name="connect" id="connect-tab-${p.id}" />`)}
1114
+ <div class="connect-tabs" role="tablist">
1115
+ ${panels.map((p) => html `<label for="connect-tab-${p.id}" class="connect-tab-label" role="tab">${p.label}</label>`)}
1116
+ </div>
487
1117
  <div class="connect-panels">
488
- <div class="connect-panel" role="tabpanel">${renderSnippet('http', claudeDesktopConfig)}</div>
489
- <div class="connect-panel" role="tabpanel">${renderSnippet('remote', mcpRemoteConfig)}</div>
490
- <div class="connect-panel" role="tabpanel">${renderSnippet('curl', curl)}</div>
1118
+ ${panels.map((p) => renderConnectPanel(p.id, p.content, p.copyAriaLabel))}
491
1119
  </div>
492
1120
  </div>
493
1121
  `;
494
1122
  }
1123
+ /** Single panel inside the connect card — pre/code + copy button. */
1124
+ function renderConnectPanel(id, content, copyAriaLabel) {
1125
+ const snippetId = `connect-snippet-${id}`;
1126
+ return html `
1127
+ <div class="connect-panel panel-${id}" role="tabpanel">
1128
+ <pre id="${snippetId}"><code>${content}</code></pre>
1129
+ <button type="button" class="connect-copy" data-copy data-copy-target="#${snippetId}" aria-label="${copyAriaLabel}">Copy</button>
1130
+ </div>
1131
+ `;
1132
+ }
495
1133
  // ---------------------------------------------------------------------------
496
1134
  // Tools section
497
1135
  // ---------------------------------------------------------------------------
498
1136
  function groupToolsByPrefix(tools) {
499
1137
  if (tools.length < 3)
500
1138
  return [{ label: null, tools }];
501
- // Count first-segment prefixes. A prefix earns a group when >= 2 tools share it.
502
1139
  const prefixCounts = new Map();
503
1140
  for (const tool of tools) {
504
1141
  const prefix = tool.name.split('_', 1)[0];
@@ -509,7 +1146,6 @@ function groupToolsByPrefix(tools) {
509
1146
  const groupablePrefixes = new Set([...prefixCounts.entries()].filter(([, count]) => count >= 2).map(([p]) => p));
510
1147
  if (groupablePrefixes.size === 0)
511
1148
  return [{ label: null, tools }];
512
- // Preserve encounter order; create one group per qualifying prefix plus "Other".
513
1149
  const groups = new Map();
514
1150
  const other = [];
515
1151
  for (const tool of tools) {
@@ -574,9 +1210,14 @@ function renderToolCard(tool) {
574
1210
  </details>
575
1211
  `
576
1212
  : html ``;
577
- const invocation = buildInvocationSnippet(tool);
1213
+ const invocation = html `
1214
+ <details>
1215
+ <summary>Invocation</summary>
1216
+ ${renderSnippet(`tool-${tool.name}`, buildInvocationSnippet(tool))}
1217
+ </details>
1218
+ `;
578
1219
  const authBadges = tool.auth && tool.auth.length > 0
579
- ? html `<div class="card-meta"><span class="card-meta-label">scopes:</span>${tool.auth.map((scope) => html ` <span class="pill pill-auth">${scope}</span>`)}</div>`
1220
+ ? html `<div class="card-meta"><span class="card-meta-label">scopes</span>${tool.auth.map((scope) => html ` <span class="pill pill-auth">${scope}</span>`)}</div>`
580
1221
  : html ``;
581
1222
  return html `
582
1223
  <article class="card" id="${anchor}">
@@ -587,7 +1228,7 @@ function renderToolCard(tool) {
587
1228
  </div>
588
1229
  <p class="card-desc">${tool.description}</p>
589
1230
  ${authBadges}
590
- ${renderSnippet(`tool-${tool.name}`, invocation)}
1231
+ ${invocation}
591
1232
  ${schemaPreview}
592
1233
  </article>
593
1234
  `;
@@ -596,9 +1237,12 @@ function renderToolsSection(tools) {
596
1237
  if (tools.length === 0)
597
1238
  return html ``;
598
1239
  const groups = groupToolsByPrefix(tools);
1240
+ // A single group — whether labeled or not — would render as redundant with
1241
+ // the section header. Skip the sub-heading; render a flat grid.
1242
+ const showHeadings = groups.length > 1;
599
1243
  const body = groups.map((group) => {
600
- const heading = group.label ? html `<h4 class="group-heading">${group.label}</h4>` : html ``;
601
- return html `${heading}${group.tools.map(renderToolCard)}`;
1244
+ const heading = showHeadings && group.label ? html `<h4 class="group-heading">${group.label}</h4>` : html ``;
1245
+ return html `${heading}<div class="card-grid">${group.tools.map(renderToolCard)}</div>`;
602
1246
  });
603
1247
  return html `
604
1248
  <section aria-labelledby="section-tools">
@@ -629,8 +1273,8 @@ function renderResourceCard(resource) {
629
1273
  </div>
630
1274
  <p class="card-desc">${resource.description}</p>
631
1275
  <div class="card-meta">
632
- <span><span class="card-meta-label">uri:</span> <code>${resource.uriTemplate}</code></span>
633
- ${resource.mimeType ? html `<span><span class="card-meta-label">mime:</span> <code>${resource.mimeType}</code></span>` : html ``}
1276
+ <span><span class="card-meta-label">uri</span> <code>${resource.uriTemplate}</code></span>
1277
+ ${resource.mimeType ? html `<span><span class="card-meta-label">mime</span> <code>${resource.mimeType}</code></span>` : html ``}
634
1278
  </div>
635
1279
  </article>
636
1280
  `;
@@ -641,7 +1285,7 @@ function renderResourcesSection(resources) {
641
1285
  return html `
642
1286
  <section aria-labelledby="section-resources">
643
1287
  ${renderSectionHeading('section-resources', 'Resources', resources.length)}
644
- ${resources.map(renderResourceCard)}
1288
+ <div class="card-grid">${resources.map(renderResourceCard)}</div>
645
1289
  </section>
646
1290
  `;
647
1291
  }
@@ -682,7 +1326,7 @@ function renderPromptsSection(prompts) {
682
1326
  return html `
683
1327
  <section aria-labelledby="section-prompts">
684
1328
  ${renderSectionHeading('section-prompts', 'Prompts', prompts.length)}
685
- ${prompts.map(renderPromptCard)}
1329
+ <div class="card-grid">${prompts.map(renderPromptCard)}</div>
686
1330
  </section>
687
1331
  `;
688
1332
  }
@@ -696,14 +1340,16 @@ function renderExtensionsSection(extensions) {
696
1340
  return html `
697
1341
  <section aria-labelledby="section-extensions">
698
1342
  ${renderSectionHeading('section-extensions', 'Extensions', entries.length)}
699
- ${entries.map(([key, value]) => html `
700
- <article class="card ext-card">
701
- <div class="card-head">
702
- <h3 class="card-title ext-key">${key}</h3>
703
- </div>
704
- <pre class="ext-preview"><code>${JSON.stringify(value, null, 2)}</code></pre>
705
- </article>
706
- `)}
1343
+ <div class="card-grid">
1344
+ ${entries.map(([key, value]) => html `
1345
+ <article class="card ext-card">
1346
+ <div class="card-head">
1347
+ <h3 class="card-title ext-key">${key}</h3>
1348
+ </div>
1349
+ <pre class="ext-preview"><code>${JSON.stringify(value, null, 2)}</code></pre>
1350
+ </article>
1351
+ `)}
1352
+ </div>
707
1353
  </section>
708
1354
  `;
709
1355
  }
@@ -712,48 +1358,39 @@ function renderExtensionsSection(extensions) {
712
1358
  // ---------------------------------------------------------------------------
713
1359
  function renderFooter(manifest) {
714
1360
  const { landing, framework } = manifest;
715
- const groups = [];
1361
+ const links = [];
716
1362
  // User-supplied links
717
- if (landing.links.length > 0) {
718
- groups.push({
719
- label: 'Links',
720
- links: landing.links.map((l) => ({ href: l.href, label: l.label })),
721
- });
1363
+ for (const link of landing.links) {
1364
+ links.push({ href: link.href, label: link.label });
722
1365
  }
723
1366
  // Auto-derived GitHub cluster
724
1367
  if (landing.repoRoot) {
725
1368
  const repo = landing.repoRoot;
726
1369
  const version = manifest.server.version;
727
- groups.push({
728
- label: 'Repository',
729
- links: [
730
- { href: landing.changelogUrl ?? `${repo.url}/blob/main/CHANGELOG.md`, label: 'Changelog' },
731
- { href: `${repo.url}/releases/tag/v${version}`, label: `Release v${version}` },
732
- { href: `${repo.url}/issues`, label: 'Issues' },
733
- { href: repo.url, label: 'Source' },
734
- ],
1370
+ links.push({
1371
+ href: landing.changelogUrl ?? `${repo.url}/blob/main/CHANGELOG.md`,
1372
+ label: 'Changelog',
1373
+ });
1374
+ links.push({
1375
+ href: `${repo.url}/releases/tag/v${version}`,
1376
+ label: `v${version}`,
735
1377
  });
1378
+ links.push({ href: `${repo.url}/issues`, label: 'Issues' });
1379
+ links.push({ href: repo.url, label: 'Source' });
736
1380
  }
737
1381
  // Package / registry
738
1382
  if (landing.npmPackage) {
739
- groups.push({
740
- label: 'Registry',
741
- links: [{ href: landing.npmPackage.url, label: `npm: ${landing.npmPackage.name}` }],
742
- });
1383
+ links.push({ href: landing.npmPackage.url, label: 'npm' });
743
1384
  }
744
- const groupsHtml = groups.map((g) => html `
745
- <div class="footer-group">
746
- <span class="footer-group-label">${g.label}</span>
747
- ${g.links.map((l) => html `<a href="${l.href}" rel="noopener">${l.label}</a>`)}
748
- </div>
749
- `);
750
1385
  const frameworkNpm = `https://www.npmjs.com/package/${encodeURIComponent(framework.name)}`;
1386
+ const linkEls = links.map((l, i) => html `${i > 0 ? html `<span class="footer-sep">·</span>` : html ``}<a href="${l.href}" rel="noopener">${l.label}</a>`);
751
1387
  const attribution = landing.attribution
752
- ? html `<p class="footer-attrib">Built on <a href="${framework.homepage}">${framework.name}</a> v${framework.version} · <a href="${frameworkNpm}" rel="noopener">npm</a></p>`
1388
+ ? html `<span class="footer-attrib">built on <a href="${framework.homepage}">${framework.name}</a> v${framework.version} · <a href="${frameworkNpm}" rel="noopener">npm</a></span>`
753
1389
  : html ``;
754
1390
  return html `
755
1391
  <footer>
756
- ${groups.length > 0 ? html `<div class="footer-links">${groupsHtml}</div>` : html ``}
1392
+ ${linkEls}
1393
+ <span class="footer-spacer"></span>
757
1394
  ${attribution}
758
1395
  </footer>
759
1396
  `;
@@ -771,6 +1408,7 @@ function renderHead(manifest, pageUrl) {
771
1408
  const favicon = landing.logo && isImageDataUri(landing.logo)
772
1409
  ? html `<link rel="icon" href="${landing.logo}" />`
773
1410
  : html ``;
1411
+ const themeColor = html `<meta name="theme-color" content="${landing.theme.accent}" />`;
774
1412
  const jsonLd = JSON.stringify({
775
1413
  '@context': 'https://schema.org',
776
1414
  '@type': 'SoftwareApplication',
@@ -785,6 +1423,7 @@ function renderHead(manifest, pageUrl) {
785
1423
  <meta name="viewport" content="width=device-width, initial-scale=1" />
786
1424
  <title>${title}</title>
787
1425
  <meta name="description" content="${description}" />
1426
+ ${themeColor}
788
1427
  <meta property="og:title" content="${title}" />
789
1428
  <meta property="og:description" content="${description}" />
790
1429
  <meta property="og:type" content="website" />
@@ -828,10 +1467,9 @@ function escapeLdJson(json) {
828
1467
  */
829
1468
  export function renderLandingPage(manifest, baseUrl, degraded = false) {
830
1469
  const pageUrl = `${baseUrl.replace(/\/$/, '')}/`;
831
- const landing = manifest.landing;
832
1470
  const body = degraded
833
1471
  ? html `
834
- ${renderHero(manifest, baseUrl)}
1472
+ ${renderHero(manifest, baseUrl, true)}
835
1473
  <section>
836
1474
  <p class="empty-state">
837
1475
  Full server inventory is available to authenticated callers.
@@ -840,7 +1478,7 @@ export function renderLandingPage(manifest, baseUrl, degraded = false) {
840
1478
  ${renderFooter(manifest)}
841
1479
  `
842
1480
  : html `
843
- ${renderHero(manifest, baseUrl)}
1481
+ ${renderHero(manifest, baseUrl, false)}
844
1482
  ${renderToolsSection(manifest.definitions.tools)}
845
1483
  ${renderResourcesSection(manifest.definitions.resources)}
846
1484
  ${renderPromptsSection(manifest.definitions.prompts)}
@@ -851,7 +1489,7 @@ export function renderLandingPage(manifest, baseUrl, degraded = false) {
851
1489
  <html lang="en">
852
1490
  <head>
853
1491
  ${renderHead(manifest, pageUrl)}
854
- ${renderTokens(landing.theme.accent)}
1492
+ ${renderTokens(manifest.landing.theme.accent)}
855
1493
  </head>
856
1494
  <body>
857
1495
  <main>${body}</main>