@cyanheads/mcp-ts-core 0.6.0 → 0.6.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.
@@ -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: #a1a1aa;
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: #6b6b73;
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,728 @@ 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(--border-strong);
345
+ }
346
+ .status-signin:hover { color: var(--accent); border-color: var(--accent); }
347
+
348
+ /* -------------------- Connect card -------------------- */
349
+
350
+ .connect {
351
+ border: 1px solid var(--border);
352
+ border-radius: var(--radius-lg);
353
+ background: var(--bg-elevated);
354
+ overflow: hidden;
355
+ box-shadow: var(--shadow-md);
356
+ position: relative;
357
+ }
358
+
359
+ /* Chrome header — three dots + endpoint path */
360
+ .connect-chrome {
361
+ display: flex;
362
+ align-items: center;
363
+ gap: var(--space-3);
181
364
  padding: var(--space-3) var(--space-4);
182
- border-radius: var(--radius-md);
183
- border: 1px solid var(--border-subtle);
365
+ border-bottom: 1px solid var(--border-subtle);
184
366
  background: var(--bg-subtle);
367
+ }
368
+ .connect-chrome-dots {
369
+ display: inline-flex;
370
+ gap: 6px;
371
+ flex-shrink: 0;
372
+ }
373
+ .connect-chrome-dot {
374
+ width: 10px; height: 10px;
375
+ border-radius: 50%;
376
+ background: color-mix(in oklab, var(--fg-subtle), transparent 60%);
377
+ display: inline-block;
378
+ }
379
+ .connect-chrome-endpoint {
380
+ margin-left: auto;
381
+ font-family: var(--font-mono);
382
+ font-size: 0.6875rem;
185
383
  color: var(--fg-muted);
384
+ white-space: nowrap;
385
+ overflow: hidden;
386
+ text-overflow: ellipsis;
387
+ min-width: 0;
388
+ }
389
+
390
+ /* Radio-hack tabs */
391
+ .connect-tab-input { position: absolute; opacity: 0; pointer-events: none; }
392
+ .connect-tabs {
393
+ display: flex;
394
+ gap: 0;
395
+ padding: 0 var(--space-4);
396
+ border-bottom: 1px solid var(--border-subtle);
397
+ overflow-x: auto;
398
+ scrollbar-width: none;
399
+ }
400
+ .connect-tabs::-webkit-scrollbar { display: none; }
401
+ .connect-tab-label {
402
+ padding: var(--space-3) var(--space-4);
186
403
  font-size: var(--text-sm);
187
- display: flex; align-items: center; gap: var(--space-2);
404
+ font-weight: 500;
405
+ color: var(--fg-muted);
406
+ cursor: pointer;
407
+ border-bottom: 2px solid transparent;
408
+ margin-bottom: -1px;
409
+ white-space: nowrap;
410
+ transition: color var(--duration-fast) var(--ease-out), border-color var(--duration-fast) var(--ease-out);
411
+ }
412
+ .connect-tab-label:hover { color: var(--fg); }
413
+ .connect-tab-input:checked + .connect-tab-label {
414
+ color: var(--fg);
415
+ border-bottom-color: var(--accent);
416
+ }
417
+ .connect-tab-input:focus-visible + .connect-tab-label {
418
+ outline: 2px solid var(--accent);
419
+ outline-offset: -6px;
420
+ border-radius: var(--radius-sm);
421
+ }
422
+
423
+ .connect-panels { position: relative; padding: var(--space-5) var(--space-4); }
424
+ .connect-panel { display: none; }
425
+ .connect:has(#connect-tab-stdio:checked) .panel-stdio,
426
+ .connect:has(#connect-tab-http:checked) .panel-http,
427
+ .connect:has(#connect-tab-claude:checked) .panel-claude,
428
+ .connect:has(#connect-tab-curl:checked) .panel-curl { display: block; }
429
+ /* Fallback when :has() unsupported — show first visible panel */
430
+ @supports not selector(:has(*)) {
431
+ .connect-panel:first-of-type { display: block; }
188
432
  }
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); }
433
+ .connect-panel pre {
434
+ padding: var(--space-4);
435
+ padding-right: var(--space-12);
436
+ background: var(--bg-code);
437
+ border: 1px solid var(--border-subtle);
438
+ border-radius: var(--radius-sm);
439
+ font-size: 0.8125rem;
440
+ line-height: 1.6;
441
+ }
442
+ .connect-copy {
443
+ position: absolute;
444
+ top: calc(var(--space-5) + 8px);
445
+ right: calc(var(--space-4) + 8px);
446
+ font-family: var(--font-mono);
447
+ font-size: 0.6875rem;
448
+ font-weight: 500;
449
+ padding: 4px 10px;
450
+ border-radius: var(--radius-sm);
451
+ border: 1px solid var(--border);
452
+ background: var(--bg);
453
+ color: var(--fg-muted);
454
+ cursor: pointer;
455
+ transition: all var(--duration-fast) var(--ease-out);
456
+ letter-spacing: 0.02em;
457
+ }
458
+ .connect-copy:hover {
459
+ color: var(--accent);
460
+ border-color: var(--accent-edge);
461
+ background: var(--accent-softer);
462
+ }
463
+ .connect-copy:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
464
+ .connect-copy[data-copied="true"] {
465
+ color: #16a34a;
466
+ border-color: color-mix(in oklab, #16a34a, transparent 60%);
467
+ background: color-mix(in oklab, #16a34a, transparent 92%);
468
+ }
469
+
470
+ /* Framework attribution pill */
471
+ .hero-badges {
472
+ display: inline-flex;
473
+ align-items: center;
474
+ gap: var(--space-2);
475
+ }
476
+ .badge-shield {
477
+ display: inline-flex;
478
+ align-items: center;
479
+ gap: 6px;
480
+ padding: 4px 10px;
481
+ border-radius: var(--radius-pill);
482
+ font-family: var(--font-mono);
483
+ font-size: 0.6875rem;
484
+ font-weight: 500;
485
+ letter-spacing: 0.01em;
486
+ color: var(--fg-muted);
487
+ background: var(--bg-subtle);
488
+ border: 1px solid var(--border-subtle);
489
+ text-decoration: none;
490
+ transition: all var(--duration-fast) var(--ease-out);
491
+ }
492
+ .badge-shield:hover {
493
+ color: var(--accent);
494
+ border-color: var(--accent-edge);
495
+ background: var(--accent-softer);
496
+ text-decoration: none;
497
+ transform: translateY(-1px);
498
+ }
499
+ .badge-shield-label { color: var(--fg-subtle); transition: color var(--duration-fast); }
500
+ .badge-shield-value { color: var(--fg-muted); transition: color var(--duration-fast); }
501
+ .badge-shield:hover .badge-shield-label,
502
+ .badge-shield:hover .badge-shield-value { color: var(--accent); }
503
+
504
+ /* -------------------- Sections -------------------- */
505
+
506
+ section { padding: var(--space-12) 0 0; }
192
507
 
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; }
508
+ .section-heading {
509
+ display: flex;
510
+ align-items: baseline;
511
+ gap: var(--space-3);
512
+ margin: 0 0 var(--space-6);
513
+ padding-bottom: var(--space-3);
514
+ border-bottom: 1px solid var(--border-subtle);
515
+ }
516
+ .section-heading h2 {
517
+ margin: 0;
518
+ font-size: var(--text-2xl);
519
+ font-weight: 600;
520
+ letter-spacing: -0.025em;
521
+ color: var(--fg);
522
+ text-transform: lowercase;
523
+ }
524
+ .section-count {
525
+ font-family: var(--font-mono);
526
+ font-size: var(--text-2xl);
527
+ font-weight: 600;
528
+ color: var(--accent);
529
+ font-variant-numeric: tabular-nums;
530
+ letter-spacing: -0.02em;
531
+ line-height: 1;
532
+ }
197
533
 
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; }
534
+ .group-heading {
535
+ margin: var(--space-6) 0 var(--space-3);
536
+ color: var(--fg-muted);
537
+ font-family: var(--font-mono);
538
+ font-size: 0.6875rem;
539
+ font-weight: 600;
540
+ text-transform: uppercase;
541
+ letter-spacing: 0.12em;
542
+ }
199
543
  .group-heading:first-child { margin-top: 0; }
200
544
 
545
+ /* -------------------- Cards -------------------- */
546
+
547
+ .card-grid {
548
+ display: grid;
549
+ grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
550
+ gap: var(--space-3);
551
+ }
201
552
  .card {
202
553
  border: 1px solid var(--border-subtle);
203
- border-radius: var(--radius-lg);
204
- padding: var(--space-5);
205
- margin-bottom: var(--space-3);
554
+ border-radius: var(--radius-md);
555
+ padding: var(--space-4) var(--space-5);
206
556
  background: var(--bg-elevated);
207
- transition: border-color var(--duration-fast) var(--ease-out), box-shadow var(--duration-fast) var(--ease-out);
557
+ display: flex;
558
+ flex-direction: column;
559
+ gap: var(--space-2);
560
+ transition: border-color var(--duration-fast) var(--ease-out),
561
+ transform var(--duration-fast) var(--ease-out),
562
+ box-shadow var(--duration-fast) var(--ease-out);
563
+ position: relative;
564
+ }
565
+ .card:hover {
566
+ border-color: var(--accent-edge);
567
+ transform: translateY(-1px);
568
+ box-shadow: var(--shadow-sm);
569
+ }
570
+ .card-head {
571
+ display: flex;
572
+ align-items: center;
573
+ gap: var(--space-2);
574
+ flex-wrap: wrap;
575
+ }
576
+ .card-title {
577
+ margin: 0;
578
+ font-size: 0.9375rem;
579
+ font-weight: 600;
580
+ font-family: var(--font-mono);
581
+ color: var(--fg);
582
+ letter-spacing: -0.015em;
208
583
  }
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
584
  .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; }
585
+ .card-title a:hover { color: var(--accent); text-decoration: none; }
586
+ .card-desc {
587
+ margin: 0;
588
+ color: var(--fg-muted);
589
+ font-size: var(--text-sm);
590
+ line-height: 1.5;
591
+ }
592
+ .card-meta {
593
+ display: flex;
594
+ flex-wrap: wrap;
595
+ gap: var(--space-1) var(--space-3);
596
+ font-size: var(--text-xs);
597
+ color: var(--fg-muted);
598
+ font-family: var(--font-mono);
599
+ align-items: center;
600
+ }
215
601
  .card-meta-label { color: var(--fg-subtle); }
216
- .card-meta code { font-size: 0.9em; }
602
+ .card-meta code {
603
+ font-size: 1em;
604
+ color: var(--fg);
605
+ background: transparent;
606
+ border: 0;
607
+ padding: 0;
608
+ }
217
609
 
218
- .pill-row { display: inline-flex; flex-wrap: wrap; gap: var(--space-1); }
610
+ /* Annotation pills dot-chip style */
611
+ .pill-row { display: inline-flex; flex-wrap: wrap; gap: 5px; align-items: center; }
219
612
  .pill {
220
- display: inline-flex; align-items: center;
221
- padding: 1px 8px;
613
+ display: inline-flex;
614
+ align-items: center;
615
+ gap: 5px;
616
+ padding: 2px 8px;
222
617
  border-radius: var(--radius-pill);
223
- font-size: 0.7rem;
618
+ font-family: var(--font-mono);
619
+ font-size: 0.6875rem;
224
620
  font-weight: 500;
225
- line-height: 1.5;
226
- border: 1px solid var(--border-subtle);
621
+ line-height: 1.4;
227
622
  color: var(--fg-muted);
228
623
  background: var(--bg-subtle);
624
+ border: 1px solid var(--border-subtle);
625
+ letter-spacing: 0.01em;
229
626
  }
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; }
627
+ .pill::before {
628
+ content: "";
629
+ width: 4px; height: 4px;
630
+ border-radius: 50%;
631
+ background: currentColor;
632
+ flex-shrink: 0;
633
+ }
634
+ .pill-readonly { color: #16a34a; background: color-mix(in oklab, #16a34a, transparent 92%); border-color: color-mix(in oklab, #16a34a, transparent 72%); }
635
+ .pill-destructive { color: #dc2626; background: color-mix(in oklab, #dc2626, transparent 92%); border-color: color-mix(in oklab, #dc2626, transparent 72%); }
636
+ .pill-openworld { color: #2563eb; background: color-mix(in oklab, #2563eb, transparent 92%); border-color: color-mix(in oklab, #2563eb, transparent 72%); }
637
+ .pill-task { color: var(--accent); background: var(--accent-softer); border-color: var(--accent-edge); }
638
+ .pill-app { color: #9333ea; background: color-mix(in oklab, #9333ea, transparent 92%); border-color: color-mix(in oklab, #9333ea, transparent 72%); }
639
+ .pill-auth { color: var(--fg-subtle); font-size: 0.65rem; }
640
+ .pill-auth::before { display: none; }
236
641
 
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);
642
+ @media (prefers-color-scheme: dark) {
643
+ .pill-readonly { color: #4ade80; }
644
+ .pill-destructive { color: #f87171; }
645
+ .pill-openworld { color: #60a5fa; }
646
+ .pill-app { color: #c084fc; }
647
+ }
648
+
649
+ .source-link {
242
650
  font-size: var(--text-xs);
243
- padding: 2px var(--space-2);
651
+ color: var(--fg-subtle);
652
+ margin-left: auto;
653
+ font-family: var(--font-mono);
654
+ transition: color var(--duration-fast);
655
+ }
656
+ .source-link:hover { color: var(--accent); text-decoration: none; }
657
+
658
+ /* Inline snippet (tool invocation) */
659
+ .snippet {
660
+ position: relative;
661
+ margin-top: var(--space-2);
662
+ }
663
+ .snippet pre {
664
+ padding: var(--space-3);
665
+ padding-right: var(--space-12);
666
+ background: var(--bg-code);
667
+ border: 1px solid var(--border-subtle);
244
668
  border-radius: var(--radius-sm);
669
+ font-size: 0.75rem;
670
+ line-height: 1.55;
671
+ }
672
+ .snippet-copy {
673
+ position: absolute;
674
+ top: 6px;
675
+ right: 6px;
676
+ font-family: var(--font-mono);
677
+ font-size: 0.65rem;
678
+ font-weight: 500;
679
+ padding: 3px 8px;
680
+ border-radius: var(--radius-xs);
245
681
  border: 1px solid var(--border);
246
682
  background: var(--bg);
247
683
  color: var(--fg-muted);
248
684
  cursor: pointer;
249
- transition: color var(--duration-fast), border-color var(--duration-fast);
685
+ transition: all var(--duration-fast) var(--ease-out);
686
+ letter-spacing: 0.02em;
250
687
  }
251
- .snippet-copy:hover { color: var(--accent); border-color: var(--accent); }
688
+ .snippet-copy:hover { color: var(--accent); border-color: var(--accent-edge); }
252
689
  .snippet-copy:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
253
- .snippet-copy[data-copied="true"] { color: #16a34a; border-color: #16a34a; }
690
+ .snippet-copy[data-copied="true"] {
691
+ color: #16a34a;
692
+ border-color: color-mix(in oklab, #16a34a, transparent 60%);
693
+ }
254
694
 
255
- details { margin-top: var(--space-3); border: 1px solid var(--border-subtle); border-radius: var(--radius-md); }
695
+ /* Collapsible details (schema preview) */
696
+ details {
697
+ margin-top: var(--space-1);
698
+ border: 0;
699
+ border-radius: 0;
700
+ }
256
701
  details > summary {
257
702
  cursor: pointer;
258
- padding: var(--space-2) var(--space-3);
703
+ padding: 4px 0;
704
+ font-family: var(--font-mono);
259
705
  font-size: var(--text-xs);
260
- color: var(--fg-muted);
706
+ color: var(--fg-subtle);
261
707
  list-style: none;
262
708
  user-select: none;
263
- transition: background var(--duration-fast);
709
+ display: inline-flex;
710
+ align-items: center;
711
+ gap: var(--space-2);
712
+ transition: color var(--duration-fast);
264
713
  }
265
714
  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; }
715
+ details > summary::before {
716
+ content: "+";
717
+ display: inline-block;
718
+ width: 10px;
719
+ text-align: center;
720
+ color: var(--fg-subtle);
721
+ font-weight: 600;
722
+ transition: transform var(--duration-fast), color var(--duration-fast);
723
+ }
724
+ details[open] > summary::before { content: "−"; color: var(--accent); }
725
+ details > summary:hover { color: var(--accent); }
726
+ details > pre {
727
+ margin-top: var(--space-2);
728
+ font-size: 0.7rem;
729
+ line-height: 1.55;
730
+ }
271
731
 
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);
732
+ /* Prompt args */
733
+ .args-list {
734
+ list-style: none;
735
+ padding: 0;
736
+ margin: var(--space-1) 0 0;
737
+ display: flex;
738
+ flex-direction: column;
739
+ gap: 4px;
740
+ font-size: var(--text-xs);
277
741
  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);
742
+ font-family: var(--font-mono);
743
+ }
744
+ .args-list li { line-height: 1.6; }
745
+ .args-list code {
746
+ font-size: 1em;
747
+ background: transparent;
748
+ border: 0;
749
+ padding: 0;
750
+ color: var(--fg);
751
+ }
752
+ .args-required {
753
+ color: var(--accent);
754
+ font-size: 0.625rem;
755
+ font-weight: 600;
756
+ margin-left: 5px;
757
+ letter-spacing: 0.08em;
758
+ text-transform: uppercase;
282
759
  }
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
760
 
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);
297
- color: var(--fg-muted);
761
+ /* Extensions */
762
+ .ext-card { background: var(--bg-subtle); border-color: var(--border-subtle); }
763
+ .ext-key { font-family: var(--font-mono); font-size: var(--text-sm); color: var(--fg); }
764
+ .ext-preview {
765
+ margin: var(--space-2) 0 0;
766
+ font-size: 0.7rem;
767
+ line-height: 1.55;
298
768
  }
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
769
 
306
- .source-link { font-size: var(--text-xs); color: var(--fg-muted); margin-left: auto; }
307
- .source-link:hover { color: var(--accent); }
770
+ /* Empty / degraded state */
771
+ .empty-state {
772
+ padding: var(--space-10) var(--space-6);
773
+ text-align: center;
774
+ color: var(--fg-muted);
775
+ border: 1px dashed var(--border);
776
+ border-radius: var(--radius-md);
777
+ background: var(--bg-subtle);
778
+ font-size: var(--text-sm);
779
+ font-family: var(--font-mono);
780
+ }
308
781
 
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); }
782
+ /* -------------------- Footer -------------------- */
312
783
 
313
- .ext-card { background: var(--bg-subtle); border-color: var(--border-subtle); }
314
- .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); }
784
+ footer {
785
+ margin-top: var(--space-20);
786
+ padding: var(--space-6) 0;
787
+ border-top: 1px solid var(--border-subtle);
788
+ display: flex;
789
+ flex-wrap: wrap;
790
+ align-items: center;
791
+ gap: var(--space-2) var(--space-4);
792
+ font-family: var(--font-mono);
793
+ font-size: var(--text-xs);
794
+ color: var(--fg-subtle);
795
+ letter-spacing: 0.01em;
796
+ }
797
+ footer a {
798
+ color: var(--fg-muted);
799
+ text-decoration: none;
800
+ transition: color var(--duration-fast) var(--ease-out);
801
+ }
802
+ footer a:hover { color: var(--accent); text-decoration: none; }
803
+ .footer-sep { color: var(--fg-subtle); opacity: 0.5; user-select: none; }
804
+ .footer-spacer { flex: 1 0 var(--space-4); }
805
+ .footer-attrib { color: var(--fg-subtle); }
806
+ .footer-attrib a { color: var(--fg-muted); }
316
807
 
317
- .empty-state { padding: var(--space-8); text-align: center; color: var(--fg-muted); border: 1px dashed var(--border); border-radius: var(--radius-md); }
808
+ /* -------------------- Responsive -------------------- */
318
809
 
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; }
810
+ @media (max-width: 760px) {
811
+ main { padding: var(--space-6) var(--space-4) var(--space-16); }
812
+ .hero { padding: var(--space-8) 0 var(--space-6); gap: var(--space-5); }
813
+ .hero-heading { font-size: clamp(1.875rem, 7vw + 0.5rem, 2.5rem); letter-spacing: -0.035em; }
814
+ .hero-title-row { gap: var(--space-3); }
815
+ .hero-logo { width: 40px; height: 40px; }
816
+ .hero-tagline { font-size: var(--text-base); }
817
+ .status-strip { gap: var(--space-2); font-size: 0.6875rem; }
818
+ .connect-chrome-endpoint { display: none; }
819
+ .card-grid { grid-template-columns: 1fr; }
820
+ section { padding: var(--space-8) 0 0; }
821
+ .section-heading h2 { font-size: var(--text-xl); }
822
+ .section-count { font-size: var(--text-xl); }
324
823
  }
325
824
 
326
825
  @media (prefers-reduced-motion: reduce) {
327
826
  *, *::before, *::after {
328
827
  transition-duration: 0.01ms !important;
329
828
  animation-duration: 0.01ms !important;
829
+ animation-iteration-count: 1 !important;
330
830
  }
331
831
  }
332
832
  `;
@@ -336,8 +836,6 @@ footer {
336
836
  // Copy-to-clipboard — single inlined script, < 1KB
337
837
  // ---------------------------------------------------------------------------
338
838
  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
839
  const js = `
342
840
  document.addEventListener('click', function(e) {
343
841
  var btn = e.target.closest('[data-copy]');
@@ -366,17 +864,10 @@ document.addEventListener('click', function(e) {
366
864
  // ---------------------------------------------------------------------------
367
865
  // Primitives
368
866
  // ---------------------------------------------------------------------------
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
867
  /**
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.
868
+ * shields.io-style bi-part badge: "Built on" label + framework name. Links to
869
+ * the framework's npm page. Lives in the hero when `landing.attribution` is
870
+ * enabled.
380
871
  */
381
872
  function renderFrameworkBadge(framework) {
382
873
  const npmUrl = `https://www.npmjs.com/package/${encodeURIComponent(framework.name)}`;
@@ -385,45 +876,104 @@ function renderFrameworkBadge(framework) {
385
876
  function renderPill(text, variant) {
386
877
  return html `<span class="pill pill-${variant}">${text}</span>`;
387
878
  }
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
879
  function renderSectionHeading(id, label, count) {
400
880
  return html `
401
881
  <div class="section-heading">
402
882
  <h2 id="${id}">${label}</h2>
403
- <span class="section-count" aria-label="${String(count)} ${label.toLowerCase()}">(${count})</span>
883
+ <span class="section-count" aria-label="${String(count)} ${label}">${String(count)}</span>
404
884
  </div>
405
885
  `;
406
886
  }
407
- function renderSnippet(id, text, variant = 'code') {
887
+ function renderSnippet(id, text) {
408
888
  const targetId = `snippet-${id}`;
409
889
  return html `
410
890
  <div class="snippet">
411
- <pre id="${targetId}" class="snippet-${variant}"><code>${text}</code></pre>
891
+ <pre id="${targetId}"><code>${text}</code></pre>
412
892
  <button type="button" class="snippet-copy" data-copy data-copy-target="#${targetId}" aria-label="Copy">Copy</button>
413
893
  </div>
414
894
  `;
415
895
  }
416
896
  // ---------------------------------------------------------------------------
897
+ // Status strip — replaces the old auth banner
898
+ // ---------------------------------------------------------------------------
899
+ /**
900
+ * Single-line status strip under the hero. Communicates auth mode, capability
901
+ * counts, and protocol version in one mono-spaced, dot-separated row.
902
+ *
903
+ * Accessibility:
904
+ * - `role="status"` so changes are announced live
905
+ * - `aria-label` carries the long-form auth phrase for screen readers
906
+ * ("Public access", "Requires OAuth", etc.) even when the visible label is
907
+ * compact ("public", "oauth")
908
+ */
909
+ function renderStatusStrip(manifest, degraded) {
910
+ const { auth, definitionCounts, protocol } = manifest;
911
+ const authMeta = describeAuth(auth);
912
+ // Counts hidden in degraded mode to avoid leaking inventory shape.
913
+ const counts = degraded
914
+ ? []
915
+ : [
916
+ { n: definitionCounts.tools, label: 'tools' },
917
+ { n: definitionCounts.resources, label: 'resources' },
918
+ { n: definitionCounts.prompts, label: 'prompts' },
919
+ ].filter((c) => c.n > 0);
920
+ const signin = auth.mode === 'oauth' && auth.oauthIssuer
921
+ ? html ` <a class="status-signin" href="${auth.oauthIssuer}" rel="noopener">sign in ↗</a>`
922
+ : html ``;
923
+ return html `
924
+ <div class="status-strip" role="status" aria-label="${authMeta.ariaLabel}">
925
+ <span class="status-item">
926
+ <span class="status-dot ${authMeta.dotClass}" aria-hidden="true"></span>
927
+ <span class="status-value">${authMeta.label}</span>${signin}
928
+ </span>
929
+ ${counts.map((c) => html `
930
+ <span class="status-item">
931
+ <span class="status-value">${String(c.n)}</span>
932
+ <span>${c.label}</span>
933
+ </span>
934
+ `)}
935
+ <span class="status-item">
936
+ <span>MCP</span>
937
+ <span class="status-value">${protocol.latestVersion}</span>
938
+ </span>
939
+ </div>
940
+ `;
941
+ }
942
+ /** Visible label, dot class, and long-form aria phrase for the auth strip item. */
943
+ function describeAuth(auth) {
944
+ if (auth.mode === 'none') {
945
+ return {
946
+ label: 'public',
947
+ dotClass: 'status-dot-public',
948
+ ariaLabel: 'Public access — no authentication required',
949
+ };
950
+ }
951
+ if (auth.mode === 'jwt') {
952
+ return {
953
+ label: 'bearer',
954
+ dotClass: 'status-dot-gated',
955
+ ariaLabel: 'Requires a bearer token',
956
+ };
957
+ }
958
+ return {
959
+ label: 'oauth',
960
+ dotClass: 'status-dot-gated',
961
+ ariaLabel: 'Requires OAuth',
962
+ };
963
+ }
964
+ // ---------------------------------------------------------------------------
417
965
  // Hero
418
966
  // ---------------------------------------------------------------------------
419
- function renderHero(manifest, baseUrl) {
967
+ function renderHero(manifest, baseUrl, degraded) {
420
968
  const { server, landing } = manifest;
421
969
  const releaseUrl = landing.repoRoot
422
970
  ? `${landing.repoRoot.url}/releases/tag/v${server.version}`
423
971
  : undefined;
424
- const versionBadge = renderBadge(`v${server.version}`, 'version', releaseUrl);
972
+ const versionBadge = releaseUrl
973
+ ? html `<a class="badge-version" href="${releaseUrl}" aria-label="v${server.version} release notes">v${server.version}</a>`
974
+ : html `<span class="badge-version">v${server.version}</span>`;
425
975
  const preReleaseBadge = landing.preRelease.isPreRelease
426
- ? renderBadge(landing.preRelease.label ?? 'pre-release', 'pre')
976
+ ? html `<span class="badge-pre">${landing.preRelease.label ?? 'pre-release'}</span>`
427
977
  : html ``;
428
978
  const tagline = landing.tagline ?? server.description ?? '';
429
979
  const logo = landing.logo
@@ -432,73 +982,142 @@ function renderHero(manifest, baseUrl) {
432
982
  const frameworkBadge = landing.attribution
433
983
  ? html `<div class="hero-badges">${renderFrameworkBadge(manifest.framework)}</div>`
434
984
  : html ``;
985
+ const connect = degraded ? html `` : renderConnectSnippets(manifest, baseUrl);
435
986
  return html `
436
987
  <header class="hero">
437
- <div class="hero-top">
988
+ <span class="hero-eyebrow" aria-hidden="true">MCP Server</span>
989
+ <div class="hero-title-row">
438
990
  ${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>
991
+ <h1 class="hero-heading">${server.name}</h1>
992
+ ${versionBadge}
993
+ ${preReleaseBadge}
448
994
  </div>
449
- ${renderAuthBanner(manifest.auth)}
450
- ${renderConnectSnippets(manifest, baseUrl)}
995
+ ${tagline ? html `<p class="hero-tagline">${tagline}</p>` : html ``}
996
+ ${renderStatusStrip(manifest, degraded)}
997
+ ${connect}
998
+ ${frameworkBadge}
451
999
  </header>
452
1000
  `;
453
1001
  }
1002
+ /**
1003
+ * `@scope/pkg-name` → `pkg-name`. Fall through for bare names.
1004
+ * Used as the `mcpServers` key and the Claude CLI server alias.
1005
+ */
1006
+ function deriveShortName(serverName) {
1007
+ const slash = serverName.lastIndexOf('/');
1008
+ return slash >= 0 ? serverName.slice(slash + 1) : serverName;
1009
+ }
1010
+ /** Convert ordered env entries to the `{ KEY: value }` shape MCP clients expect. */
1011
+ function envFromEntries(entries) {
1012
+ return Object.fromEntries(entries.map(({ key, value }) => [key, value]));
1013
+ }
1014
+ /** `claude mcp add --transport stdio <name> [--env K=V]* -- bunx <pkg>@latest` */
1015
+ function buildClaudeStdioCmd(shortName, npmPackage, envExample) {
1016
+ const envFlags = envExample.map(({ key, value }) => `--env ${key}=${value}`).join(' ');
1017
+ const envSegment = envFlags.length > 0 ? ` ${envFlags}` : '';
1018
+ return `claude mcp add --transport stdio ${shortName}${envSegment} -- bunx ${npmPackage}@latest`;
1019
+ }
1020
+ /** `claude mcp add --transport http <name> <url>` */
1021
+ function buildClaudeHttpCmd(shortName, endpoint) {
1022
+ return `claude mcp add --transport http ${shortName} ${endpoint}`;
1023
+ }
454
1024
  function renderConnectSnippets(manifest, baseUrl) {
455
1025
  const endpoint = `${baseUrl.replace(/\/$/, '')}${manifest.transport.endpointPath}`;
456
- const claudeDesktopConfig = JSON.stringify({
1026
+ const npmPackage = manifest.landing.npmPackage?.name;
1027
+ // `@cyanheads/mcp-ts-core` → `mcp-ts-core`. Short aliases match the convention
1028
+ // used in real Claude Desktop / Cursor configs and make the `claude mcp add`
1029
+ // command more ergonomic.
1030
+ const shortName = deriveShortName(manifest.server.name);
1031
+ const envExample = manifest.landing.envExample;
1032
+ const envObject = envExample.length > 0 ? envFromEntries(envExample) : undefined;
1033
+ // STDIO: prefer native `bunx <pkg>@latest` when the server is published;
1034
+ // fall back to `mcp-remote` as a stdio → HTTP bridge so the tab is always
1035
+ // useful even for unpublished servers.
1036
+ const stdioConfig = JSON.stringify({
457
1037
  mcpServers: {
458
- [manifest.server.name]: {
459
- type: 'http',
460
- url: endpoint,
1038
+ [shortName]: {
1039
+ command: 'bunx',
1040
+ args: npmPackage ? [`${npmPackage}@latest`] : ['mcp-remote', endpoint],
1041
+ ...(envObject && { env: envObject }),
461
1042
  },
462
1043
  },
463
1044
  }, null, 2);
464
- const mcpRemoteConfig = JSON.stringify({
1045
+ const httpConfig = JSON.stringify({
465
1046
  mcpServers: {
466
- [manifest.server.name]: {
467
- command: 'npx',
468
- args: ['-y', 'mcp-remote', endpoint],
1047
+ [shortName]: {
1048
+ type: 'http',
1049
+ url: endpoint,
1050
+ ...(envObject && { env: envObject }),
469
1051
  },
470
1052
  },
471
1053
  }, null, 2);
1054
+ // `claude mcp add` — published package routes through stdio; otherwise
1055
+ // point it straight at the HTTP endpoint.
1056
+ const claudeCmd = npmPackage
1057
+ ? buildClaudeStdioCmd(shortName, npmPackage, envExample)
1058
+ : buildClaudeHttpCmd(shortName, endpoint);
472
1059
  const curl = [
473
1060
  `curl -X POST ${endpoint} \\`,
474
1061
  ` -H "Content-Type: application/json" \\`,
475
1062
  ` -H "MCP-Protocol-Version: ${manifest.protocol.latestVersion}" \\`,
476
1063
  ` -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"${manifest.protocol.latestVersion}","capabilities":{},"clientInfo":{"name":"curl","version":"1.0.0"}}}'`,
477
1064
  ].join('\n');
478
- // Radio-hack tabsno JS required for tab switching.
1065
+ // Chrome labelnpm package when published, else the HTTP endpoint (trimmed).
1066
+ const chromeLabel = npmPackage ?? endpoint.replace(/^https?:\/\//, '');
1067
+ const panels = [
1068
+ { id: 'stdio', label: 'STDIO', content: stdioConfig, copyAriaLabel: 'Copy stdio config' },
1069
+ {
1070
+ id: 'http',
1071
+ label: 'Streamable HTTP',
1072
+ content: httpConfig,
1073
+ copyAriaLabel: 'Copy HTTP config',
1074
+ },
1075
+ {
1076
+ id: 'claude',
1077
+ label: 'Claude',
1078
+ content: claudeCmd,
1079
+ copyAriaLabel: 'Copy claude mcp add command',
1080
+ },
1081
+ { id: 'curl', label: 'curl', content: curl, copyAriaLabel: 'Copy curl command' },
1082
+ ];
479
1083
  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>
1084
+ <div class="connect" aria-label="Connection snippets">
1085
+ <div class="connect-chrome">
1086
+ <span class="connect-chrome-dots" aria-hidden="true">
1087
+ <span class="connect-chrome-dot"></span>
1088
+ <span class="connect-chrome-dot"></span>
1089
+ <span class="connect-chrome-dot"></span>
1090
+ </span>
1091
+ <span class="connect-chrome-endpoint" title="${endpoint}">${chromeLabel}</span>
1092
+ </div>
1093
+ ${panels.map((p, i) => i === 0
1094
+ ? html `<input type="radio" class="connect-tab-input" name="connect" id="connect-tab-${p.id}" checked />`
1095
+ : html `<input type="radio" class="connect-tab-input" name="connect" id="connect-tab-${p.id}" />`)}
1096
+ <div class="connect-tabs" role="tablist">
1097
+ ${panels.map((p) => html `<label for="connect-tab-${p.id}" class="connect-tab-label" role="tab">${p.label}</label>`)}
1098
+ </div>
487
1099
  <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>
1100
+ ${panels.map((p) => renderConnectPanel(p.id, p.content, p.copyAriaLabel))}
491
1101
  </div>
492
1102
  </div>
493
1103
  `;
494
1104
  }
1105
+ /** Single panel inside the connect card — pre/code + copy button. */
1106
+ function renderConnectPanel(id, content, copyAriaLabel) {
1107
+ const snippetId = `connect-snippet-${id}`;
1108
+ return html `
1109
+ <div class="connect-panel panel-${id}" role="tabpanel">
1110
+ <pre id="${snippetId}"><code>${content}</code></pre>
1111
+ <button type="button" class="connect-copy" data-copy data-copy-target="#${snippetId}" aria-label="${copyAriaLabel}">Copy</button>
1112
+ </div>
1113
+ `;
1114
+ }
495
1115
  // ---------------------------------------------------------------------------
496
1116
  // Tools section
497
1117
  // ---------------------------------------------------------------------------
498
1118
  function groupToolsByPrefix(tools) {
499
1119
  if (tools.length < 3)
500
1120
  return [{ label: null, tools }];
501
- // Count first-segment prefixes. A prefix earns a group when >= 2 tools share it.
502
1121
  const prefixCounts = new Map();
503
1122
  for (const tool of tools) {
504
1123
  const prefix = tool.name.split('_', 1)[0];
@@ -509,7 +1128,6 @@ function groupToolsByPrefix(tools) {
509
1128
  const groupablePrefixes = new Set([...prefixCounts.entries()].filter(([, count]) => count >= 2).map(([p]) => p));
510
1129
  if (groupablePrefixes.size === 0)
511
1130
  return [{ label: null, tools }];
512
- // Preserve encounter order; create one group per qualifying prefix plus "Other".
513
1131
  const groups = new Map();
514
1132
  const other = [];
515
1133
  for (const tool of tools) {
@@ -574,9 +1192,14 @@ function renderToolCard(tool) {
574
1192
  </details>
575
1193
  `
576
1194
  : html ``;
577
- const invocation = buildInvocationSnippet(tool);
1195
+ const invocation = html `
1196
+ <details>
1197
+ <summary>Invocation</summary>
1198
+ ${renderSnippet(`tool-${tool.name}`, buildInvocationSnippet(tool))}
1199
+ </details>
1200
+ `;
578
1201
  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>`
1202
+ ? 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
1203
  : html ``;
581
1204
  return html `
582
1205
  <article class="card" id="${anchor}">
@@ -587,7 +1210,7 @@ function renderToolCard(tool) {
587
1210
  </div>
588
1211
  <p class="card-desc">${tool.description}</p>
589
1212
  ${authBadges}
590
- ${renderSnippet(`tool-${tool.name}`, invocation)}
1213
+ ${invocation}
591
1214
  ${schemaPreview}
592
1215
  </article>
593
1216
  `;
@@ -598,7 +1221,7 @@ function renderToolsSection(tools) {
598
1221
  const groups = groupToolsByPrefix(tools);
599
1222
  const body = groups.map((group) => {
600
1223
  const heading = group.label ? html `<h4 class="group-heading">${group.label}</h4>` : html ``;
601
- return html `${heading}${group.tools.map(renderToolCard)}`;
1224
+ return html `${heading}<div class="card-grid">${group.tools.map(renderToolCard)}</div>`;
602
1225
  });
603
1226
  return html `
604
1227
  <section aria-labelledby="section-tools">
@@ -629,8 +1252,8 @@ function renderResourceCard(resource) {
629
1252
  </div>
630
1253
  <p class="card-desc">${resource.description}</p>
631
1254
  <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 ``}
1255
+ <span><span class="card-meta-label">uri</span> <code>${resource.uriTemplate}</code></span>
1256
+ ${resource.mimeType ? html `<span><span class="card-meta-label">mime</span> <code>${resource.mimeType}</code></span>` : html ``}
634
1257
  </div>
635
1258
  </article>
636
1259
  `;
@@ -641,7 +1264,7 @@ function renderResourcesSection(resources) {
641
1264
  return html `
642
1265
  <section aria-labelledby="section-resources">
643
1266
  ${renderSectionHeading('section-resources', 'Resources', resources.length)}
644
- ${resources.map(renderResourceCard)}
1267
+ <div class="card-grid">${resources.map(renderResourceCard)}</div>
645
1268
  </section>
646
1269
  `;
647
1270
  }
@@ -682,7 +1305,7 @@ function renderPromptsSection(prompts) {
682
1305
  return html `
683
1306
  <section aria-labelledby="section-prompts">
684
1307
  ${renderSectionHeading('section-prompts', 'Prompts', prompts.length)}
685
- ${prompts.map(renderPromptCard)}
1308
+ <div class="card-grid">${prompts.map(renderPromptCard)}</div>
686
1309
  </section>
687
1310
  `;
688
1311
  }
@@ -696,14 +1319,16 @@ function renderExtensionsSection(extensions) {
696
1319
  return html `
697
1320
  <section aria-labelledby="section-extensions">
698
1321
  ${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
- `)}
1322
+ <div class="card-grid">
1323
+ ${entries.map(([key, value]) => html `
1324
+ <article class="card ext-card">
1325
+ <div class="card-head">
1326
+ <h3 class="card-title ext-key">${key}</h3>
1327
+ </div>
1328
+ <pre class="ext-preview"><code>${JSON.stringify(value, null, 2)}</code></pre>
1329
+ </article>
1330
+ `)}
1331
+ </div>
707
1332
  </section>
708
1333
  `;
709
1334
  }
@@ -712,48 +1337,39 @@ function renderExtensionsSection(extensions) {
712
1337
  // ---------------------------------------------------------------------------
713
1338
  function renderFooter(manifest) {
714
1339
  const { landing, framework } = manifest;
715
- const groups = [];
1340
+ const links = [];
716
1341
  // 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
- });
1342
+ for (const link of landing.links) {
1343
+ links.push({ href: link.href, label: link.label });
722
1344
  }
723
1345
  // Auto-derived GitHub cluster
724
1346
  if (landing.repoRoot) {
725
1347
  const repo = landing.repoRoot;
726
1348
  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
- ],
1349
+ links.push({
1350
+ href: landing.changelogUrl ?? `${repo.url}/blob/main/CHANGELOG.md`,
1351
+ label: 'Changelog',
1352
+ });
1353
+ links.push({
1354
+ href: `${repo.url}/releases/tag/v${version}`,
1355
+ label: `v${version}`,
735
1356
  });
1357
+ links.push({ href: `${repo.url}/issues`, label: 'Issues' });
1358
+ links.push({ href: repo.url, label: 'Source' });
736
1359
  }
737
1360
  // Package / registry
738
1361
  if (landing.npmPackage) {
739
- groups.push({
740
- label: 'Registry',
741
- links: [{ href: landing.npmPackage.url, label: `npm: ${landing.npmPackage.name}` }],
742
- });
1362
+ links.push({ href: landing.npmPackage.url, label: 'npm' });
743
1363
  }
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
1364
  const frameworkNpm = `https://www.npmjs.com/package/${encodeURIComponent(framework.name)}`;
1365
+ 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
1366
  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>`
1367
+ ? 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
1368
  : html ``;
754
1369
  return html `
755
1370
  <footer>
756
- ${groups.length > 0 ? html `<div class="footer-links">${groupsHtml}</div>` : html ``}
1371
+ ${linkEls}
1372
+ <span class="footer-spacer"></span>
757
1373
  ${attribution}
758
1374
  </footer>
759
1375
  `;
@@ -771,6 +1387,7 @@ function renderHead(manifest, pageUrl) {
771
1387
  const favicon = landing.logo && isImageDataUri(landing.logo)
772
1388
  ? html `<link rel="icon" href="${landing.logo}" />`
773
1389
  : html ``;
1390
+ const themeColor = html `<meta name="theme-color" content="${landing.theme.accent}" />`;
774
1391
  const jsonLd = JSON.stringify({
775
1392
  '@context': 'https://schema.org',
776
1393
  '@type': 'SoftwareApplication',
@@ -785,6 +1402,7 @@ function renderHead(manifest, pageUrl) {
785
1402
  <meta name="viewport" content="width=device-width, initial-scale=1" />
786
1403
  <title>${title}</title>
787
1404
  <meta name="description" content="${description}" />
1405
+ ${themeColor}
788
1406
  <meta property="og:title" content="${title}" />
789
1407
  <meta property="og:description" content="${description}" />
790
1408
  <meta property="og:type" content="website" />
@@ -828,10 +1446,9 @@ function escapeLdJson(json) {
828
1446
  */
829
1447
  export function renderLandingPage(manifest, baseUrl, degraded = false) {
830
1448
  const pageUrl = `${baseUrl.replace(/\/$/, '')}/`;
831
- const landing = manifest.landing;
832
1449
  const body = degraded
833
1450
  ? html `
834
- ${renderHero(manifest, baseUrl)}
1451
+ ${renderHero(manifest, baseUrl, true)}
835
1452
  <section>
836
1453
  <p class="empty-state">
837
1454
  Full server inventory is available to authenticated callers.
@@ -840,7 +1457,7 @@ export function renderLandingPage(manifest, baseUrl, degraded = false) {
840
1457
  ${renderFooter(manifest)}
841
1458
  `
842
1459
  : html `
843
- ${renderHero(manifest, baseUrl)}
1460
+ ${renderHero(manifest, baseUrl, false)}
844
1461
  ${renderToolsSection(manifest.definitions.tools)}
845
1462
  ${renderResourcesSection(manifest.definitions.resources)}
846
1463
  ${renderPromptsSection(manifest.definitions.prompts)}
@@ -851,7 +1468,7 @@ export function renderLandingPage(manifest, baseUrl, degraded = false) {
851
1468
  <html lang="en">
852
1469
  <head>
853
1470
  ${renderHead(manifest, pageUrl)}
854
- ${renderTokens(landing.theme.accent)}
1471
+ ${renderTokens(manifest.landing.theme.accent)}
855
1472
  </head>
856
1473
  <body>
857
1474
  <main>${body}</main>