Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commit3918b2eunknown_key

polish: 2026 treatment for follows + vs-github

polish: 2026 treatment for follows + vs-github

Picked up from parallel polish agents.
- follows.tsx: user cards w/ avatar + bio + follow/unfollow button
- vs-github.tsx: comparison rows with gradient highlights on Gluecron-wins,
  checkmark/X icons, fully-polished hero matching the rest of the site

All scoped CSS. Form actions / queries preserved.
Claude committed on May 25, 2026Parent: 711675b
2 files changed+10122463918b2e52edfd61abf20a8a41faf5f8acb00e68a
2 changed files+1012−246
Modifiedsrc/routes/follows.tsx+587−104View fileUnifiedSplit
66 * GET /:user/followers — public list
77 * GET /:user/following — public list
88 * GET /feed — auth required, personalised activity
9 *
10 * 2026 polish: scoped `.follows-*` class system gives this surface a card
11 * grid, gradient hairline hero, and per-row follow/unfollow buttons. No
12 * shared files touched; the actions and queries are preserved verbatim.
913 */
1014
1115import { Hono } from "hono";
5458 return `/${username}`;
5559}
5660
61// ─── Scoped CSS (.follows-*) ────────────────────────────────────────────────
62//
63// All selectors namespaced with `.follows-` to avoid bleeding into shared
64// chrome (RepoHeader, layout nav). Pulls tokens from the layout for theme
65// continuity (--bg-elevated, --border, --text-strong, --accent, --space-*).
66
67const followStyles = `
68 .follows-wrap { max-width: 1100px; margin: 0 auto; padding: var(--space-5) var(--space-4) var(--space-8); }
69
70 /* ─── Header ─── */
71 .follows-head { margin-bottom: var(--space-5); }
72 .follows-eyebrow {
73 display: inline-flex;
74 align-items: center;
75 gap: 8px;
76 text-transform: uppercase;
77 font-family: var(--font-mono);
78 font-size: 11px;
79 letter-spacing: 0.16em;
80 color: var(--text-muted);
81 font-weight: 600;
82 margin-bottom: 10px;
83 }
84 .follows-eyebrow-dot {
85 width: 8px; height: 8px;
86 border-radius: 9999px;
87 background: linear-gradient(135deg, #8c6dff, #36c5d6);
88 box-shadow: 0 0 0 3px rgba(140,109,255,0.18);
89 }
90 .follows-title {
91 font-family: var(--font-display);
92 font-size: clamp(24px, 3.4vw, 36px);
93 font-weight: 800;
94 letter-spacing: -0.028em;
95 line-height: 1.1;
96 margin: 0 0 6px;
97 color: var(--text-strong);
98 }
99 .follows-title-grad {
100 background-image: linear-gradient(135deg, #a48bff 0%, #8c6dff 50%, #36c5d6 100%);
101 -webkit-background-clip: text;
102 background-clip: text;
103 -webkit-text-fill-color: transparent;
104 color: transparent;
105 }
106 .follows-sub {
107 margin: 0;
108 font-size: 14px;
109 color: var(--text-muted);
110 line-height: 1.5;
111 max-width: 680px;
112 }
113 .follows-sub a { color: var(--accent); text-decoration: none; }
114 .follows-sub a:hover { text-decoration: underline; }
115
116 /* ─── Tab strip ─── */
117 .follows-tabs {
118 display: inline-flex;
119 flex-wrap: wrap;
120 gap: 8px;
121 margin: var(--space-3) 0 var(--space-5);
122 padding: 4px;
123 background: rgba(255,255,255,0.02);
124 border: 1px solid var(--border);
125 border-radius: 12px;
126 }
127 .follows-tab {
128 display: inline-flex;
129 align-items: center;
130 gap: 8px;
131 padding: 7px 13px;
132 border-radius: 9px;
133 font-size: 13px;
134 font-weight: 600;
135 color: var(--text-muted);
136 text-decoration: none;
137 border: 1px solid transparent;
138 line-height: 1;
139 font-variant-numeric: tabular-nums;
140 transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
141 }
142 .follows-tab:hover {
143 color: var(--text-strong);
144 background: rgba(255,255,255,0.03);
145 text-decoration: none;
146 }
147 .follows-tab.is-active {
148 background: linear-gradient(135deg, rgba(140,109,255,0.16), rgba(54,197,214,0.14));
149 color: #e9e2ff;
150 border-color: rgba(140,109,255,0.40);
151 }
152 .follows-tab-count {
153 font-family: var(--font-mono);
154 font-size: 11.5px;
155 opacity: 0.75;
156 }
157
158 /* ─── Section card (wraps the grid) ─── */
159 .follows-section {
160 background: var(--bg-elevated);
161 border: 1px solid var(--border);
162 border-radius: 14px;
163 overflow: hidden;
164 position: relative;
165 }
166 .follows-section::before {
167 content: '';
168 position: absolute;
169 top: 0; left: 0; right: 0;
170 height: 2px;
171 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
172 opacity: 0.55;
173 pointer-events: none;
174 }
175 .follows-section-body { padding: var(--space-4) var(--space-5); }
176
177 /* ─── User card grid ─── */
178 .follows-grid {
179 display: grid;
180 grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
181 gap: 12px;
182 }
183 .follows-card {
184 display: flex;
185 align-items: flex-start;
186 gap: 14px;
187 padding: 14px;
188 background: rgba(255,255,255,0.018);
189 border: 1px solid var(--border);
190 border-radius: 12px;
191 transition: border-color 120ms ease, background 120ms ease;
192 }
193 .follows-card:hover {
194 border-color: var(--border-strong);
195 background: rgba(255,255,255,0.03);
196 }
197 .follows-avatar {
198 width: 48px; height: 48px;
199 border-radius: 9999px;
200 background: linear-gradient(135deg, rgba(140,109,255,0.30), rgba(54,197,214,0.25));
201 color: #ffffff;
202 display: inline-flex;
203 align-items: center;
204 justify-content: center;
205 font-family: var(--font-display);
206 font-weight: 700;
207 font-size: 18px;
208 flex-shrink: 0;
209 overflow: hidden;
210 box-shadow: inset 0 0 0 1px rgba(255,255,255,0.10);
211 text-decoration: none;
212 }
213 .follows-avatar img {
214 width: 100%; height: 100%;
215 object-fit: cover;
216 display: block;
217 }
218 .follows-card-body { flex: 1; min-width: 0; }
219 .follows-card-row {
220 display: flex;
221 align-items: baseline;
222 gap: 8px;
223 flex-wrap: wrap;
224 }
225 .follows-card-name {
226 font-family: var(--font-display);
227 font-weight: 700;
228 font-size: 14.5px;
229 color: var(--text-strong);
230 text-decoration: none;
231 letter-spacing: -0.005em;
232 }
233 .follows-card-name:hover { text-decoration: underline; }
234 .follows-card-handle {
235 font-family: var(--font-mono);
236 font-size: 12px;
237 color: var(--text-muted);
238 }
239 .follows-card-bio {
240 margin: 6px 0 0;
241 font-size: 12.5px;
242 color: var(--text-muted);
243 line-height: 1.45;
244 display: -webkit-box;
245 -webkit-line-clamp: 2;
246 -webkit-box-orient: vertical;
247 overflow: hidden;
248 }
249 .follows-card-action { margin-top: 10px; }
250 .follows-card-action form { margin: 0; }
251
252 /* ─── Buttons ─── */
253 .follows-btn {
254 display: inline-flex;
255 align-items: center;
256 justify-content: center;
257 gap: 6px;
258 padding: 7px 14px;
259 border-radius: 9px;
260 font-size: 12.5px;
261 font-weight: 600;
262 text-decoration: none;
263 border: 1px solid transparent;
264 cursor: pointer;
265 font: inherit;
266 line-height: 1;
267 white-space: nowrap;
268 transition: transform 120ms ease, box-shadow 120ms ease, background 120ms ease, border-color 120ms ease, color 120ms ease;
269 }
270 .follows-btn-primary {
271 background: linear-gradient(135deg, #8c6dff 0%, #36c5d6 100%);
272 color: #ffffff;
273 box-shadow: 0 6px 18px -6px rgba(140,109,255,0.50), inset 0 1px 0 rgba(255,255,255,0.16);
274 }
275 .follows-btn-primary:hover {
276 transform: translateY(-1px);
277 box-shadow: 0 10px 24px -8px rgba(140,109,255,0.60), inset 0 1px 0 rgba(255,255,255,0.20);
278 color: #ffffff;
279 text-decoration: none;
280 }
281 .follows-btn-ghost {
282 background: transparent;
283 color: var(--text);
284 border-color: var(--border-strong);
285 }
286 .follows-btn-ghost:hover {
287 background: rgba(140,109,255,0.06);
288 border-color: rgba(140,109,255,0.45);
289 color: var(--text-strong);
290 text-decoration: none;
291 }
292
293 /* ─── Empty state ─── */
294 .follows-empty {
295 position: relative;
296 overflow: hidden;
297 padding: clamp(28px, 5vw, 48px) clamp(20px, 4vw, 36px);
298 text-align: center;
299 background: var(--bg-elevated);
300 border: 1px dashed var(--border-strong);
301 border-radius: 16px;
302 }
303 .follows-empty-orb {
304 position: absolute;
305 inset: -40% 30% auto 30%;
306 height: 280px;
307 background: radial-gradient(circle, rgba(140,109,255,0.18), rgba(54,197,214,0.10) 45%, transparent 70%);
308 filter: blur(70px);
309 opacity: 0.7;
310 pointer-events: none;
311 z-index: 0;
312 }
313 .follows-empty-inner { position: relative; z-index: 1; }
314 .follows-empty-icon {
315 width: 56px; height: 56px;
316 border-radius: 9999px;
317 background: linear-gradient(135deg, rgba(140,109,255,0.25), rgba(54,197,214,0.20));
318 box-shadow: inset 0 0 0 1px rgba(140,109,255,0.40);
319 display: inline-flex;
320 align-items: center;
321 justify-content: center;
322 color: #c4b5fd;
323 margin-bottom: 14px;
324 }
325 .follows-empty-title {
326 font-family: var(--font-display);
327 font-size: 18px;
328 font-weight: 700;
329 margin: 0 0 6px;
330 color: var(--text-strong);
331 }
332 .follows-empty-sub {
333 margin: 0 auto;
334 font-size: 13.5px;
335 color: var(--text-muted);
336 max-width: 420px;
337 line-height: 1.5;
338 }
339 .follows-empty-sub a { color: var(--accent); text-decoration: none; font-weight: 600; }
340 .follows-empty-sub a:hover { text-decoration: underline; }
341
342 /* ─── Feed entries ─── */
343 .follows-feed {
344 display: flex;
345 flex-direction: column;
346 gap: 8px;
347 }
348 .follows-feed-entry {
349 padding: 12px 14px;
350 background: rgba(255,255,255,0.018);
351 border: 1px solid var(--border);
352 border-radius: 11px;
353 transition: border-color 120ms ease, background 120ms ease;
354 }
355 .follows-feed-entry:hover {
356 border-color: var(--border-strong);
357 background: rgba(255,255,255,0.03);
358 }
359 .follows-feed-line {
360 font-size: 13.5px;
361 color: var(--text);
362 line-height: 1.45;
363 }
364 .follows-feed-line a {
365 color: var(--text-strong);
366 text-decoration: none;
367 font-weight: 600;
368 }
369 .follows-feed-line a:hover { text-decoration: underline; }
370 .follows-feed-verb {
371 color: var(--text-muted);
372 font-weight: 400;
373 }
374 .follows-feed-meta {
375 margin-top: 4px;
376 font-family: var(--font-mono);
377 font-size: 11.5px;
378 color: var(--text-muted);
379 font-variant-numeric: tabular-nums;
380 }
381 .follows-feed-meta a { color: var(--text-muted); text-decoration: none; }
382 .follows-feed-meta a:hover { color: var(--text); text-decoration: underline; }
383 .follows-feed-sha {
384 font-family: var(--font-mono);
385 background: rgba(255,255,255,0.04);
386 padding: 1px 6px;
387 border-radius: 4px;
388 }
389`;
390
391function IconUsers() {
392 return (
393 <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
394 <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
395 <circle cx="9" cy="7" r="4" />
396 <path d="M23 21v-2a4 4 0 0 0-3-3.87" />
397 <path d="M16 3.13a4 4 0 0 1 0 7.75" />
398 </svg>
399 );
400}
401function IconFeed() {
402 return (
403 <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
404 <path d="M4 11a9 9 0 0 1 9 9" />
405 <path d="M4 4a16 16 0 0 1 16 16" />
406 <circle cx="5" cy="19" r="1.5" />
407 </svg>
408 );
409}
410function IconPlus() {
411 return (
412 <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
413 <line x1="12" y1="5" x2="12" y2="19" />
414 <line x1="5" y1="12" x2="19" y2="12" />
415 </svg>
416 );
417}
418function IconCheck() {
419 return (
420 <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
421 <polyline points="20 6 9 17 4 12" />
422 </svg>
423 );
424}
425
57426// ---------- Follow / unfollow ----------
58427
59428follows.post("/:user/follow", requireAuth, async (c) => {
109478 : await listFollowing(target.id);
110479 const counts = await followCounts(target.id);
111480
481 // For each row, compute whether the viewer already follows them. This is
482 // best-effort: anonymous viewers / self-references skip the lookup so the
483 // page still renders if `isFollowing` errors.
484 const viewerFollows = new Map<string, boolean>();
485 if (user) {
486 await Promise.all(
487 list.map(async (u) => {
488 if (u.id === user.id) return;
489 try {
490 viewerFollows.set(u.id, await isFollowing(user.id, u.id));
491 } catch {
492 viewerFollows.set(u.id, false);
493 }
494 })
495 );
496 }
497
112498 return c.html(
113499 <Layout
114500 title={`${mode === "followers" ? "Followers" : "Following"} — ${ownerName}`}
115501 user={user}
116502 >
117 <div class="settings-container">
118 <h2 style="margin:0">
119 <a href={`/${ownerName}`} style="text-decoration:none">
120 @{ownerName}
121 </a>
122 </h2>
123 <div style="display:flex;gap:16px;margin:10px 0 20px">
124 <a
125 href={`/${ownerName}/followers`}
126 class={mode === "followers" ? "btn btn-primary" : "btn"}
127 >
128 Followers <span style="opacity:.7">({counts.followers})</span>
129 </a>
130 <a
131 href={`/${ownerName}/following`}
132 class={mode === "following" ? "btn btn-primary" : "btn"}
133 >
134 Following <span style="opacity:.7">({counts.following})</span>
135 </a>
136 </div>
503 <div class="follows-wrap">
504 <header class="follows-head">
505 <div class="follows-eyebrow">
506 <span class="follows-eyebrow-dot" aria-hidden="true" />
507 Profile · @{ownerName}
508 </div>
509 <h1 class="follows-title">
510 <span class="follows-title-grad">
511 {mode === "followers" ? "Followers" : "Following"}.
512 </span>
513 </h1>
514 <p class="follows-sub">
515 {mode === "followers" ? (
516 <>People who follow <a href={`/${ownerName}`}>@{ownerName}</a>.</>
517 ) : (
518 <>People <a href={`/${ownerName}`}>@{ownerName}</a> follows.</>
519 )}
520 </p>
521
522 <nav class="follows-tabs" aria-label="Follow view">
523 <a
524 href={`/${ownerName}/followers`}
525 class={"follows-tab" + (mode === "followers" ? " is-active" : "")}
526 >
527 Followers <span class="follows-tab-count">{counts.followers}</span>
528 </a>
529 <a
530 href={`/${ownerName}/following`}
531 class={"follows-tab" + (mode === "following" ? " is-active" : "")}
532 >
533 Following <span class="follows-tab-count">{counts.following}</span>
534 </a>
535 </nav>
536 </header>
537
137538 {list.length === 0 ? (
138 <div class="panel-empty" style="padding:24px">
139 No {mode}.
539 <div class="follows-empty">
540 <div class="follows-empty-orb" aria-hidden="true" />
541 <div class="follows-empty-inner">
542 <div class="follows-empty-icon" aria-hidden="true">
543 <IconUsers />
544 </div>
545 <h3 class="follows-empty-title">No {mode} yet</h3>
546 <p class="follows-empty-sub">
547 {mode === "followers"
548 ? "Nobody follows this account yet."
549 : "This account hasn't followed anyone yet."}
550 </p>
551 </div>
140552 </div>
141553 ) : (
142 <div class="panel">
143 {list.map((u) => (
144 <div class="panel-item">
145 <a href={`/${u.username}`} style="font-weight:600">
146 @{u.username}
147 </a>
148 {u.displayName && (
149 <span style="color:var(--text-muted);margin-left:8px">
150 {u.displayName}
151 </span>
152 )}
554 <section class="follows-section">
555 <div class="follows-section-body">
556 <div class="follows-grid">
557 {list.map((u) => {
558 const isSelf = !!user && u.id === user.id;
559 const already = viewerFollows.get(u.id) === true;
560 return (
561 <div class="follows-card">
562 <a
563 href={`/${u.username}`}
564 class="follows-avatar"
565 aria-hidden="true"
566 tabIndex={-1}
567 >
568 {u.avatarUrl ? (
569 <img src={u.avatarUrl} alt="" loading="lazy" />
570 ) : (
571 u.username[0]?.toUpperCase() ?? "?"
572 )}
573 </a>
574 <div class="follows-card-body">
575 <div class="follows-card-row">
576 <a href={`/${u.username}`} class="follows-card-name">
577 {u.displayName || u.username}
578 </a>
579 <span class="follows-card-handle">@{u.username}</span>
580 </div>
581 {u.bio && <p class="follows-card-bio">{u.bio}</p>}
582 {user && !isSelf && (
583 <div class="follows-card-action">
584 {already ? (
585 <form
586 method="post"
587 action={`/${u.username}/unfollow`}
588 >
589 <button
590 type="submit"
591 class="follows-btn follows-btn-ghost"
592 >
593 <IconCheck />
594 Following
595 </button>
596 </form>
597 ) : (
598 <form
599 method="post"
600 action={`/${u.username}/follow`}
601 >
602 <button
603 type="submit"
604 class="follows-btn follows-btn-primary"
605 >
606 <IconPlus />
607 Follow
608 </button>
609 </form>
610 )}
611 </div>
612 )}
613 </div>
614 </div>
615 );
616 })}
153617 </div>
154 ))}
155 </div>
618 </div>
619 </section>
156620 )}
157621 </div>
622 <style dangerouslySetInnerHTML={{ __html: followStyles }} />
158623 </Layout>
159624 );
160625}
173638 const entries = await feedForUser(user.id, 50);
174639 return c.html(
175640 <Layout title="Feed" user={user}>
176 <div class="settings-container">
177 <h2>Your feed</h2>
178 <p style="color:var(--text-muted)">
179 Recent activity from users you follow. Follow someone from their
180 profile page to start filling this up.
181 </p>
641 <div class="follows-wrap">
642 <header class="follows-head">
643 <div class="follows-eyebrow">
644 <span class="follows-eyebrow-dot" aria-hidden="true" />
645 Your network · Live activity
646 </div>
647 <h1 class="follows-title">
648 <span class="follows-title-grad">Your feed.</span>
649 </h1>
650 <p class="follows-sub">
651 Recent activity from accounts you follow. Find more people on{" "}
652 <a href="/explore">/explore</a> and follow them from their profile.
653 </p>
654 </header>
655
182656 {entries.length === 0 ? (
183 <div class="panel-empty" style="padding:24px">
184 Nothing here yet. Try the{" "}
185 <a href="/explore">explore page</a> to find people to follow.
657 <div class="follows-empty">
658 <div class="follows-empty-orb" aria-hidden="true" />
659 <div class="follows-empty-inner">
660 <div class="follows-empty-icon" aria-hidden="true">
661 <IconFeed />
662 </div>
663 <h3 class="follows-empty-title">Nothing here yet</h3>
664 <p class="follows-empty-sub">
665 Try the <a href="/explore">explore page</a> to find people to
666 follow — their pushes, PRs, and issues will surface here.
667 </p>
668 </div>
186669 </div>
187670 ) : (
188 <div class="panel">
189 {entries.map((e) => {
190 const repoUrl = `/${e.ownerUsername}/${e.repository.name}`;
191 return (
192 <div class="panel-item" style="flex-direction:column;align-items:stretch;gap:2px">
193 <div>
194 <a href={`/${e.actor.username}`} style="font-weight:600">
195 @{e.actor.username}
196 </a>{" "}
197 <span style="color:var(--text-muted)">
198 {describeAction(e.activity.action)}
199 </span>{" "}
200 <a href={repoUrl} style="font-weight:600">
201 {e.ownerUsername}/{e.repository.name}
202 </a>
203 </div>
204 <div
205 style="font-size:12px;color:var(--text-muted)"
206 >
207 {new Date(e.activity.createdAt).toLocaleString()}
208 {e.activity.targetType === "issue" &&
209 e.activity.targetId && (
210 <>
211 {" "}
212 ·{" "}
213 <a
214 href={`${repoUrl}/issues/${e.activity.targetId}`}
215 >
216 #{e.activity.targetId}
217 </a>
218 </>
219 )}
220 {e.activity.targetType === "pr" &&
221 e.activity.targetId && (
222 <>
223 {" "}
224 ·{" "}
225 <a href={`${repoUrl}/pulls/${e.activity.targetId}`}>
226 #{e.activity.targetId}
227 </a>
228 </>
229 )}
230 {e.activity.targetType === "commit" &&
231 e.activity.targetId && (
232 <>
233 {" "}
234 ·{" "}
235 <a
236 href={`${repoUrl}/commit/${e.activity.targetId}`}
237 class="commit-sha"
238 >
239 {String(e.activity.targetId).slice(0, 7)}
240 </a>
241 </>
242 )}
243 </div>
244 </div>
245 );
246 })}
247 </div>
671 <section class="follows-section">
672 <div class="follows-section-body">
673 <div class="follows-feed">
674 {entries.map((e) => {
675 const repoUrl = `/${e.ownerUsername}/${e.repository.name}`;
676 return (
677 <div class="follows-feed-entry">
678 <div class="follows-feed-line">
679 <a href={`/${e.actor.username}`}>
680 @{e.actor.username}
681 </a>{" "}
682 <span class="follows-feed-verb">
683 {describeAction(e.activity.action)}
684 </span>{" "}
685 <a href={repoUrl}>
686 {e.ownerUsername}/{e.repository.name}
687 </a>
688 </div>
689 <div class="follows-feed-meta">
690 {new Date(e.activity.createdAt).toLocaleString()}
691 {e.activity.targetType === "issue" &&
692 e.activity.targetId && (
693 <>
694 {" · "}
695 <a
696 href={`${repoUrl}/issues/${e.activity.targetId}`}
697 >
698 #{e.activity.targetId}
699 </a>
700 </>
701 )}
702 {e.activity.targetType === "pr" &&
703 e.activity.targetId && (
704 <>
705 {" · "}
706 <a href={`${repoUrl}/pulls/${e.activity.targetId}`}>
707 #{e.activity.targetId}
708 </a>
709 </>
710 )}
711 {e.activity.targetType === "commit" &&
712 e.activity.targetId && (
713 <>
714 {" · "}
715 <a
716 href={`${repoUrl}/commit/${e.activity.targetId}`}
717 class="follows-feed-sha"
718 >
719 {String(e.activity.targetId).slice(0, 7)}
720 </a>
721 </>
722 )}
723 </div>
724 </div>
725 );
726 })}
727 </div>
728 </div>
729 </section>
248730 )}
249731 </div>
732 <style dangerouslySetInnerHTML={{ __html: followStyles }} />
250733 </Layout>
251734 );
252735});
Modifiedsrc/routes/vs-github.tsx+425−142View fileUnifiedSplit
237237 return "vsg-cell-no";
238238}
239239
240/**
241 * SVG glyphs for the comparison cells — replace the emoji in the visual
242 * cells (emoji are kept for accessibility/no-CSS fallback inside .vsg-icon).
243 * Gluecron-side cells get a polished gradient checkmark; GitHub-side cells
244 * get a plain glyph so the eye lands on the wins.
245 */
246function VerdictGlyph({ verdict, side }: { verdict: Verdict; side: "them" | "us" }) {
247 const id = `vsg-grad-${side}-${verdict}`;
248 if (verdict === "yes") {
249 if (side === "us") {
250 return (
251 <svg class="vsg-glyph vsg-glyph-yes-us" width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
252 <defs>
253 <linearGradient id={id} x1="0" y1="0" x2="1" y2="1">
254 <stop offset="0%" stop-color="#a48bff" />
255 <stop offset="100%" stop-color="#36c5d6" />
256 </linearGradient>
257 </defs>
258 <circle cx="12" cy="12" r="10" fill={`url(#${id})`} opacity="0.20" />
259 <path d="M7.5 12.5l3 3 6-6.5" stroke={`url(#${id})`} stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" />
260 </svg>
261 );
262 }
263 return (
264 <svg class="vsg-glyph vsg-glyph-yes-them" width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
265 <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="1.4" opacity="0.40" />
266 <path d="M7.5 12.5l3 3 6-6.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
267 </svg>
268 );
269 }
270 if (verdict === "partial") {
271 return (
272 <svg class="vsg-glyph vsg-glyph-partial" width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
273 <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="1.4" opacity="0.55" />
274 <path d="M12 2a10 10 0 0 1 0 20z" fill="currentColor" opacity="0.55" />
275 </svg>
276 );
277 }
278 return (
279 <svg class="vsg-glyph vsg-glyph-no" width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
280 <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="1.2" opacity="0.35" />
281 <path d="M8 8l8 8M16 8l-8 8" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
282 </svg>
283 );
284}
285
286/**
287 * A row is a "Gluecron win" when Gluecron is yes and GitHub is no/partial.
288 * Those rows get a soft gradient wash so the visual scan tells the story.
289 */
290function isGluecronWin(row: Row): boolean {
291 return row.gc.verdict === "yes" && row.gh.verdict !== "yes";
292}
293
240294// ---------------------------------------------------------------------------
241295// Route
242296// ---------------------------------------------------------------------------
246300 return c.html(
247301 <Layout title="Gluecron vs GitHub" user={user}>
248302 <style dangerouslySetInnerHTML={{ __html: pageCss }} />
249 <div class="vsg-root">
250 {/* ---------- Hero ---------- */}
303 <div class="vsg-page vsg-root">
304 {/* ---------- Hero (2026 polish: gradient hairline + orb + grad headline) ---------- */}
251305 <header class="vsg-hero">
252 <div class="eyebrow">Side by side</div>
253 <h1 class="display vsg-hero-title">
254 Gluecron <span class="vsg-vs">vs</span>{" "}
255 <span class="vsg-gh-word">GitHub</span>
256 </h1>
257 <p class="vsg-hero-sub">
258 The git host built around Claude.
259 </p>
260 <div class="vsg-logos">
261 <div class="vsg-logo-card vsg-logo-them">
262 <span class="vsg-logo-text">GitHub</span>
263 <span class="vsg-logo-sub">the incumbent</span>
306 <div class="vsg-hero-orb" aria-hidden="true" />
307 <div class="vsg-hero-inner">
308 <div class="vsg-eyebrow">
309 <span class="vsg-eyebrow-pill" aria-hidden="true">
310 <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
311 <polyline points="20 6 9 17 4 12" />
312 </svg>
313 </span>
314 Side by side · The honest scorecard · Updated 2026-05
315 </div>
316 <h1 class="vsg-hero-title">
317 Gluecron{" "}
318 <span class="vsg-vs">vs</span>{" "}
319 <span class="vsg-gh-word">GitHub</span>
320 </h1>
321 <p class="vsg-hero-sub">
322 The git host built around Claude. One base-URL swap, three new
323 superpowers, zero per-seat fees.
324 </p>
325 <div class="vsg-logos">
326 <div class="vsg-logo-card vsg-logo-them">
327 <span class="vsg-logo-text">GitHub</span>
328 <span class="vsg-logo-sub">the incumbent</span>
329 </div>
330 <div class="vsg-logo-vs" aria-hidden="true">vs</div>
331 <div class="vsg-logo-card vsg-logo-us">
332 <span class="vsg-logo-text vsg-title-grad">gluecron</span>
333 <span class="vsg-logo-sub">the Claude-native one</span>
334 </div>
264335 </div>
265 <div class="vsg-logo-vs" aria-hidden="true">vs</div>
266 <div class="vsg-logo-card vsg-logo-us">
267 <span class="vsg-logo-text gradient-text">gluecron</span>
268 <span class="vsg-logo-sub">the Claude-native one</span>
336 <div class="vsg-hero-cta">
337 <a href="/import" class="btn btn-primary btn-lg">
338 Migrate from GitHub in 60 seconds &rarr;
339 </a>
340 <a href="/demo" class="btn btn-ghost btn-lg">
341 Try the demo
342 </a>
269343 </div>
270344 </div>
271 <div class="vsg-hero-cta">
272 <a href="/import" class="btn btn-primary btn-lg">
273 Migrate from GitHub in 60 seconds &rarr;
274 </a>
275 <a href="/demo" class="btn btn-ghost btn-lg">
276 Try the demo
277 </a>
278 </div>
279345 </header>
280346
281347 {/* ---------- Feature comparison table ---------- */}
293359 <div class="vsg-table" role="table" aria-label="Gluecron vs GitHub feature comparison">
294360 <div class="vsg-thead" role="row">
295361 <div class="vsg-th vsg-th-feature" role="columnheader">Feature</div>
296 <div class="vsg-th vsg-th-them" role="columnheader">GitHub</div>
297 <div class="vsg-th vsg-th-us" role="columnheader">Gluecron</div>
362 <div class="vsg-th vsg-th-them" role="columnheader">
363 <span class="vsg-th-dot vsg-th-dot-them" aria-hidden="true" />
364 GitHub
365 </div>
366 <div class="vsg-th vsg-th-us" role="columnheader">
367 <span class="vsg-th-dot vsg-th-dot-us" aria-hidden="true" />
368 Gluecron
369 </div>
298370 </div>
299371
300372 {CATEGORIES.map((cat) => (
301373 <>
302374 <div class="vsg-cat-row" role="row">
303375 <div class="vsg-cat-title" role="cell">
376 <span class="vsg-cat-bar" aria-hidden="true" />
304377 {cat.title}
305378 </div>
306379 </div>
307 {cat.rows.map((row) => (
308 <div class="vsg-row" role="row">
309 <div class="vsg-cell vsg-cell-feature" role="cell">
310 {row.feature}
311 </div>
312 <div
313 class={`vsg-cell vsg-cell-them ${verdictClass(row.gh.verdict)}`}
314 role="cell"
315 >
316 <span class="vsg-icon" aria-hidden="true">
317 {verdictIcon(row.gh.verdict)}
318 </span>
319 <span class="vsg-note">{row.gh.note}</span>
320 </div>
321 <div
322 class={`vsg-cell vsg-cell-us ${verdictClass(row.gc.verdict)}`}
323 role="cell"
324 >
325 <span class="vsg-icon" aria-hidden="true">
326 {verdictIcon(row.gc.verdict)}
327 </span>
328 <span class="vsg-note">{row.gc.note}</span>
380 {cat.rows.map((row) => {
381 const win = isGluecronWin(row);
382 return (
383 <div class={`vsg-row${win ? " vsg-row-win" : ""}`} role="row">
384 <div class="vsg-cell vsg-cell-feature" role="cell">
385 {win && <span class="vsg-win-bar" aria-hidden="true" />}
386 {row.feature}
387 </div>
388 <div
389 class={`vsg-cell vsg-cell-them ${verdictClass(row.gh.verdict)}`}
390 role="cell"
391 >
392 <VerdictGlyph verdict={row.gh.verdict} side="them" />
393 <span class="vsg-icon vsg-icon-fallback" aria-hidden="true">
394 {verdictIcon(row.gh.verdict)}
395 </span>
396 <span class="vsg-note">{row.gh.note}</span>
397 </div>
398 <div
399 class={`vsg-cell vsg-cell-us ${verdictClass(row.gc.verdict)}`}
400 role="cell"
401 >
402 <VerdictGlyph verdict={row.gc.verdict} side="us" />
403 <span class="vsg-icon vsg-icon-fallback" aria-hidden="true">
404 {verdictIcon(row.gc.verdict)}
405 </span>
406 <span class="vsg-note">{row.gc.note}</span>
407 </div>
329408 </div>
330 </div>
331 ))}
409 );
410 })}
332411 </>
333412 ))}
334413 </div>
337416 {/* ---------- The killer move ---------- */}
338417 <section class="vsg-section vsg-killer">
339418 <div class="vsg-killer-card">
340 <div class="eyebrow">The killer move</div>
341 <h2 class="vsg-killer-headline">
342 Toggle Sleep Mode, walk away,{" "}
343 <span class="gradient-text">wake up to a digest.</span>
344 </h2>
345 <p class="vsg-killer-sub">
346 GitHub: not possible. Gluecron: ships in L1. While you sleep,
347 Claude auto-merges green PRs, builds features from{" "}
348 <code>ai:build</code> issues, and patches the gates that fail.
349 </p>
350 <a href="/sleep-mode" class="btn btn-secondary btn-lg">
351 See how Sleep Mode works &rarr;
352 </a>
419 <div class="vsg-killer-orb" aria-hidden="true" />
420 <div class="vsg-killer-inner">
421 <div class="vsg-eyebrow">
422 <span class="vsg-eyebrow-pill" aria-hidden="true">
423 <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
424 <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
425 </svg>
426 </span>
427 The killer move
428 </div>
429 <h2 class="vsg-killer-headline">
430 Toggle Sleep Mode, walk away,{" "}
431 <span class="vsg-title-grad">wake up to a digest.</span>
432 </h2>
433 <p class="vsg-killer-sub">
434 GitHub: not possible. Gluecron: ships in L1. While you sleep,
435 Claude auto-merges green PRs, builds features from{" "}
436 <code>ai:build</code> issues, and patches the gates that fail.
437 </p>
438 <a href="/sleep-mode" class="btn btn-secondary btn-lg">
439 See how Sleep Mode works &rarr;
440 </a>
441 </div>
353442 </div>
354443 </section>
355444
371460
372461 {/* ---------- CTA ---------- */}
373462 <section class="vsg-section vsg-cta-section">
463 <div class="vsg-cta-orb" aria-hidden="true" />
374464 <h2 class="vsg-cta-title">
375465 Stop renting your repos.{" "}
376 <span class="gradient-text">Start owning your stack.</span>
466 <span class="vsg-title-grad">Start owning your stack.</span>
377467 </h2>
378468 <p class="vsg-cta-sub">
379469 One command imports a GitHub repo. One toggle hands the night
394484});
395485
396486const pageCss = `
397 .vsg-root {
487 /* ───────── Scoped under .vsg-page so nothing leaks ───────── */
488 .vsg-page.vsg-root {
398489 max-width: 1120px;
399490 margin: 0 auto;
400491 padding: 48px 24px 80px;
401492 }
402493
403 /* ---------- Hero ---------- */
404 .vsg-hero { text-align: center; padding: 32px 0 48px; }
405 .vsg-hero-title {
494 /* ---------- Gradient text utility (scoped local copy) ---------- */
495 .vsg-page .vsg-title-grad {
496 background-image: linear-gradient(135deg, #a48bff 0%, #8c6dff 50%, #36c5d6 100%);
497 -webkit-background-clip: text;
498 background-clip: text;
499 -webkit-text-fill-color: transparent;
500 color: transparent;
501 }
502
503 /* ---------- Hero (2026 polish — hairline + orb + grad headline) ---------- */
504 .vsg-page .vsg-hero {
505 position: relative;
506 text-align: center;
507 padding: clamp(28px, 4vw, 56px) clamp(24px, 4vw, 48px);
508 margin-bottom: 48px;
509 background: var(--bg-elevated);
510 border: 1px solid var(--border);
511 border-radius: 20px;
512 overflow: hidden;
513 box-shadow: 0 1px 0 rgba(255,255,255,0.04), 0 20px 48px -16px rgba(0,0,0,0.45);
514 }
515 .vsg-page .vsg-hero::before {
516 content: '';
517 position: absolute;
518 top: 0; left: 0; right: 0;
519 height: 2px;
520 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
521 opacity: 0.78;
522 pointer-events: none;
523 z-index: 2;
524 }
525 .vsg-page .vsg-hero-orb {
526 position: absolute;
527 inset: -28% -10% auto auto;
528 width: 520px; height: 520px;
529 background: radial-gradient(circle, rgba(140,109,255,0.22), rgba(54,197,214,0.10) 45%, transparent 70%);
530 filter: blur(80px);
531 opacity: 0.75;
532 pointer-events: none;
533 z-index: 0;
534 }
535 .vsg-page .vsg-hero-inner { position: relative; z-index: 1; }
536
537 .vsg-page .vsg-eyebrow {
538 display: inline-flex;
539 align-items: center;
540 gap: 8px;
541 font-family: var(--font-mono);
542 font-size: 11.5px;
543 text-transform: uppercase;
544 letter-spacing: 0.14em;
545 color: var(--text-muted);
546 font-weight: 600;
547 margin-bottom: 18px;
548 }
549 .vsg-page .vsg-eyebrow-pill {
550 display: inline-flex;
551 align-items: center;
552 justify-content: center;
553 width: 18px; height: 18px;
554 border-radius: 6px;
555 background: rgba(140,109,255,0.14);
556 color: #b69dff;
557 box-shadow: inset 0 0 0 1px rgba(140,109,255,0.35);
558 }
559
560 .vsg-page .vsg-hero-title {
561 font-family: var(--font-display);
406562 font-size: clamp(40px, 7vw, 80px);
407 line-height: 1.05;
408 letter-spacing: -0.03em;
409 margin: 16px 0 16px;
563 line-height: 1.04;
564 letter-spacing: -0.034em;
565 font-weight: 800;
566 margin: 6px 0 16px;
410567 color: var(--text-strong);
411568 }
412 .vsg-vs {
569 .vsg-page .vsg-vs {
413570 font-family: var(--font-mono);
414 font-size: 0.55em;
571 font-size: 0.42em;
415572 color: var(--text-faint);
416573 text-transform: lowercase;
417 letter-spacing: 0.06em;
418 vertical-align: 0.18em;
419 padding: 0 0.2em;
574 letter-spacing: 0.10em;
575 vertical-align: 0.32em;
576 padding: 0 0.25em;
577 font-weight: 500;
420578 }
421 .vsg-gh-word { color: var(--text-muted); }
422 .vsg-hero-sub {
579 .vsg-page .vsg-gh-word { color: var(--text-muted); }
580 .vsg-page .vsg-hero-sub {
423581 max-width: 640px;
424582 margin: 0 auto 32px;
425583 color: var(--text-muted);
427585 line-height: 1.55;
428586 }
429587
430 .vsg-logos {
588 .vsg-page .vsg-logos {
431589 display: flex;
432590 align-items: center;
433591 justify-content: center;
434592 gap: 18px;
435 margin: 32px auto 28px;
593 margin: 28px auto 28px;
436594 flex-wrap: wrap;
437595 }
438 .vsg-logo-card {
439 background: var(--bg-elevated);
596 .vsg-page .vsg-logo-card {
597 background: var(--bg);
440598 border: 1px solid var(--border);
441599 border-radius: 14px;
442600 padding: 20px 32px;
446604 gap: 6px;
447605 align-items: center;
448606 }
449 .vsg-logo-us {
607 .vsg-page .vsg-logo-us {
450608 border-color: rgba(140,109,255,0.35);
609 background: linear-gradient(160deg, rgba(140,109,255,0.07), rgba(54,197,214,0.04) 55%, var(--bg) 100%);
451610 box-shadow: 0 0 0 1px rgba(140,109,255,0.18), 0 12px 32px -10px rgba(140,109,255,0.30);
452611 }
453 .vsg-logo-text {
612 .vsg-page .vsg-logo-text {
454613 font-family: var(--font-display);
455614 font-size: 32px;
456615 font-weight: 700;
457616 letter-spacing: -0.025em;
458617 line-height: 1;
459618 }
460 .vsg-logo-them .vsg-logo-text { color: var(--text); }
461 .vsg-logo-sub {
619 .vsg-page .vsg-logo-them .vsg-logo-text { color: var(--text); }
620 .vsg-page .vsg-logo-sub {
462621 font-family: var(--font-mono);
463622 font-size: 10.5px;
464623 letter-spacing: 0.14em;
465624 text-transform: uppercase;
466625 color: var(--text-faint);
467626 }
468 .vsg-logo-vs {
627 .vsg-page .vsg-logo-vs {
469628 font-family: var(--font-mono);
470629 font-size: 14px;
471630 color: var(--text-faint);
473632 text-transform: uppercase;
474633 }
475634
476 .vsg-hero-cta {
635 .vsg-page .vsg-hero-cta {
477636 display: flex;
478637 gap: 12px;
479638 justify-content: center;
482641 }
483642
484643 /* ---------- Section base ---------- */
485 .vsg-section { margin: 64px 0; }
644 .vsg-page .vsg-section { margin: 64px 0; }
645 .vsg-page .vsg-section .section-header { text-align: center; margin-bottom: 8px; }
486646
487 /* ---------- Comparison table ---------- */
488 .vsg-table {
647 /* ---------- Comparison table (polished, gradient-highlight on wins) ---------- */
648 .vsg-page .vsg-table {
489649 margin: 32px auto 0;
490650 border: 1px solid var(--border);
491 border-radius: 14px;
651 border-radius: 16px;
492652 overflow: hidden;
493653 background: var(--bg-elevated);
654 box-shadow: 0 1px 0 rgba(255,255,255,0.04), 0 14px 40px -20px rgba(0,0,0,0.55);
494655 }
495 .vsg-thead {
656 .vsg-page .vsg-thead {
496657 display: grid;
497658 grid-template-columns: 1.7fr 1fr 1fr;
498659 align-items: center;
499660 padding: 14px 20px;
500661 border-bottom: 1px solid var(--border);
501 background: var(--bg-hover, rgba(255,255,255,0.02));
662 background:
663 linear-gradient(180deg, rgba(255,255,255,0.025), transparent),
664 var(--bg-elevated);
665 position: relative;
502666 }
503 .vsg-th {
667 .vsg-page .vsg-thead::after {
668 content: '';
669 position: absolute;
670 left: 0; right: 0; bottom: -1px;
671 height: 1px;
672 background: linear-gradient(90deg, transparent 0%, rgba(140,109,255,0.50) 30%, rgba(54,197,214,0.50) 70%, transparent 100%);
673 }
674 .vsg-page .vsg-th {
675 display: inline-flex;
676 align-items: center;
677 gap: 8px;
504678 font-family: var(--font-mono);
505679 font-size: 11px;
506680 letter-spacing: 0.14em;
508682 color: var(--text-faint);
509683 font-weight: 600;
510684 }
511 .vsg-th-them, .vsg-th-us { text-align: left; }
512 .vsg-th-us { color: var(--accent); }
685 .vsg-page .vsg-th-them, .vsg-page .vsg-th-us { text-align: left; }
686 .vsg-page .vsg-th-us { color: #b69dff; }
687 .vsg-page .vsg-th-dot {
688 width: 7px; height: 7px; border-radius: 9999px;
689 flex-shrink: 0;
690 }
691 .vsg-page .vsg-th-dot-them { background: var(--text-faint); opacity: 0.6; }
692 .vsg-page .vsg-th-dot-us {
693 background: linear-gradient(135deg, #8c6dff, #36c5d6);
694 box-shadow: 0 0 0 3px rgba(140,109,255,0.16);
695 }
513696
514 .vsg-cat-row {
515 padding: 14px 20px 6px;
697 .vsg-page .vsg-cat-row {
698 padding: 16px 20px 8px;
516699 border-top: 1px solid var(--border-subtle, var(--border));
517 background: var(--accent-gradient-faint, rgba(140,109,255,0.04));
700 background: linear-gradient(90deg, rgba(140,109,255,0.06), rgba(54,197,214,0.025) 45%, transparent 100%);
518701 }
519 .vsg-cat-row:first-of-type { border-top: none; }
520 .vsg-cat-title {
702 .vsg-page .vsg-cat-row:first-of-type { border-top: none; }
703 .vsg-page .vsg-cat-title {
704 display: inline-flex;
705 align-items: center;
706 gap: 10px;
521707 font-family: var(--font-mono);
522708 font-size: 11px;
523709 letter-spacing: 0.16em;
525711 color: var(--accent);
526712 font-weight: 700;
527713 }
714 .vsg-page .vsg-cat-bar {
715 display: inline-block;
716 width: 18px; height: 2px;
717 background: linear-gradient(90deg, #8c6dff, #36c5d6);
718 border-radius: 2px;
719 }
528720
529 .vsg-row {
721 .vsg-page .vsg-row {
530722 display: grid;
531723 grid-template-columns: 1.7fr 1fr 1fr;
532724 align-items: stretch;
533 padding: 12px 20px;
725 padding: 14px 20px;
534726 border-top: 1px solid var(--border-subtle, var(--border));
535727 font-size: 14px;
536 transition: background var(--t-fast, 0.15s) ease;
728 transition: background 150ms ease;
729 position: relative;
730 }
731 .vsg-page .vsg-row:hover { background: rgba(255,255,255,0.025); }
732
733 /* Gluecron-win row: soft gradient wash + accent left bar */
734 .vsg-page .vsg-row-win {
735 background: linear-gradient(90deg, rgba(140,109,255,0.05) 0%, rgba(54,197,214,0.025) 45%, transparent 100%);
537736 }
538 .vsg-row:hover { background: var(--bg-hover, rgba(255,255,255,0.02)); }
539 .vsg-cell {
737 .vsg-page .vsg-row-win:hover {
738 background: linear-gradient(90deg, rgba(140,109,255,0.085) 0%, rgba(54,197,214,0.04) 45%, transparent 100%);
739 }
740 .vsg-page .vsg-win-bar {
741 position: absolute;
742 left: 0;
743 top: 18%;
744 bottom: 18%;
745 width: 2px;
746 border-radius: 2px;
747 background: linear-gradient(180deg, #8c6dff, #36c5d6);
748 box-shadow: 0 0 8px rgba(140,109,255,0.45);
749 }
750
751 .vsg-page .vsg-cell {
540752 display: flex;
541753 align-items: center;
542 gap: 8px;
754 gap: 10px;
543755 padding-right: 12px;
756 min-width: 0;
544757 }
545 .vsg-cell-feature {
758 .vsg-page .vsg-cell-feature {
546759 color: var(--text-strong);
547760 font-weight: 500;
761 position: relative;
548762 }
549 .vsg-icon {
763 .vsg-page .vsg-row-win .vsg-cell-feature {
764 color: var(--text-strong);
765 font-weight: 600;
766 }
767 .vsg-page .vsg-icon {
550768 flex-shrink: 0;
551769 font-size: 14px;
552770 line-height: 1;
553771 }
554 .vsg-note {
772 /* The emoji are a fallback only — hide when SVG glyph is present. */
773 .vsg-page .vsg-icon-fallback { display: none; }
774 .vsg-page .vsg-glyph { flex-shrink: 0; }
775 .vsg-page .vsg-glyph-yes-them { color: var(--text); }
776 .vsg-page .vsg-glyph-partial { color: #f0c674; }
777 .vsg-page .vsg-glyph-no { color: var(--text-faint); }
778 .vsg-page .vsg-note {
555779 color: var(--text-muted);
556780 font-size: 13px;
557781 line-height: 1.4;
558782 }
559 .vsg-cell-yes .vsg-note { color: var(--text); }
560 .vsg-cell-partial .vsg-note { color: var(--text-muted); }
561 .vsg-cell-no .vsg-note { color: var(--text-faint); }
783 .vsg-page .vsg-cell-us.vsg-cell-yes .vsg-note { color: var(--text-strong); font-weight: 500; }
784 .vsg-page .vsg-cell-yes .vsg-note { color: var(--text); }
785 .vsg-page .vsg-cell-partial .vsg-note { color: var(--text-muted); }
786 .vsg-page .vsg-cell-no .vsg-note { color: var(--text-faint); }
562787
563788 @media (max-width: 720px) {
564 .vsg-thead, .vsg-row { grid-template-columns: 1.4fr 1fr 1fr; padding: 10px 12px; }
565 .vsg-note { font-size: 12px; }
789 .vsg-page .vsg-thead, .vsg-page .vsg-row { grid-template-columns: 1.4fr 1fr 1fr; padding: 10px 12px; }
790 .vsg-page .vsg-note { font-size: 12px; }
791 .vsg-page .vsg-glyph { width: 18px; height: 18px; }
566792 }
567793
568 /* ---------- Killer move ---------- */
569 .vsg-killer-card {
570 padding: 40px 32px;
794 /* ---------- Killer move (now also gets hairline + orb treatment) ---------- */
795 .vsg-page .vsg-killer-card {
796 position: relative;
797 padding: clamp(32px, 4vw, 56px) clamp(24px, 4vw, 40px);
571798 border: 1px solid rgba(140,109,255,0.35);
572 border-radius: 18px;
799 border-radius: 20px;
573800 background:
574801 linear-gradient(135deg, rgba(140,109,255,0.10), rgba(54,197,214,0.06)),
575802 var(--bg-elevated);
576803 text-align: center;
577 }
578 .vsg-killer-headline {
804 overflow: hidden;
805 box-shadow: 0 1px 0 rgba(255,255,255,0.04), 0 20px 48px -18px rgba(140,109,255,0.30);
806 }
807 .vsg-page .vsg-killer-card::before {
808 content: '';
809 position: absolute;
810 top: 0; left: 0; right: 0;
811 height: 2px;
812 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
813 opacity: 0.78;
814 pointer-events: none;
815 }
816 .vsg-page .vsg-killer-orb {
817 position: absolute;
818 inset: auto auto -30% -10%;
819 width: 420px; height: 420px;
820 background: radial-gradient(circle, rgba(54,197,214,0.18), rgba(140,109,255,0.10) 45%, transparent 70%);
821 filter: blur(80px);
822 opacity: 0.75;
823 pointer-events: none;
824 z-index: 0;
825 }
826 .vsg-page .vsg-killer-inner { position: relative; z-index: 1; }
827 .vsg-page .vsg-killer-headline {
828 font-family: var(--font-display);
579829 font-size: clamp(24px, 3.8vw, 40px);
580830 line-height: 1.15;
581 margin: 12px 0 16px;
831 margin: 10px 0 16px;
582832 letter-spacing: -0.025em;
833 font-weight: 700;
834 color: var(--text-strong);
583835 }
584 .vsg-killer-sub {
836 .vsg-page .vsg-killer-sub {
585837 max-width: 620px;
586838 margin: 0 auto 24px;
587839 color: var(--text-muted);
588840 line-height: 1.55;
589841 font-size: 15px;
590842 }
591 .vsg-killer-sub code {
843 .vsg-page .vsg-killer-sub code {
592844 background: rgba(255,255,255,0.06);
593845 border: 1px solid rgba(255,255,255,0.10);
594846 padding: 1px 6px;
598850 }
599851
600852 /* ---------- FAQ ---------- */
601 .vsg-faq-grid {
853 .vsg-page .vsg-faq-grid {
602854 margin-top: 32px;
603855 display: grid;
604856 grid-template-columns: repeat(2, 1fr);
605857 gap: 18px;
606858 }
607 .vsg-faq {
859 .vsg-page .vsg-faq {
608860 background: var(--bg-elevated);
609861 border: 1px solid var(--border);
610 border-radius: 12px;
862 border-radius: 14px;
611863 padding: 22px;
864 transition: border-color 160ms ease, transform 160ms ease;
612865 }
613 .vsg-faq-q {
866 .vsg-page .vsg-faq:hover {
867 border-color: rgba(140,109,255,0.35);
868 transform: translateY(-2px);
869 }
870 .vsg-page .vsg-faq-q {
614871 margin: 0 0 10px;
872 font-family: var(--font-display);
615873 font-size: 16px;
616874 font-weight: 600;
617875 color: var(--text-strong);
618876 letter-spacing: -0.015em;
619877 }
620 .vsg-faq-a {
878 .vsg-page .vsg-faq-a {
621879 margin: 0;
622880 color: var(--text-muted);
623881 font-size: 14px;
624882 line-height: 1.6;
625883 }
626884 @media (max-width: 720px) {
627 .vsg-faq-grid { grid-template-columns: 1fr; }
885 .vsg-page .vsg-faq-grid { grid-template-columns: 1fr; }
628886 }
629887
630888 /* ---------- CTA ---------- */
631 .vsg-cta-section {
889 .vsg-page .vsg-cta-section {
890 position: relative;
632891 text-align: center;
633 padding: 56px 24px;
634 background: var(--accent-gradient-soft, rgba(140,109,255,0.06));
892 padding: clamp(40px, 5vw, 64px) 24px;
893 background: var(--bg-elevated);
635894 border: 1px solid var(--border);
636 border-radius: 18px;
637 }
638 .vsg-cta-title {
895 border-radius: 20px;
896 overflow: hidden;
897 box-shadow: 0 1px 0 rgba(255,255,255,0.04), 0 18px 44px -18px rgba(0,0,0,0.42);
898 }
899 .vsg-page .vsg-cta-section::before {
900 content: '';
901 position: absolute;
902 top: 0; left: 0; right: 0;
903 height: 2px;
904 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
905 opacity: 0.75;
906 }
907 .vsg-page .vsg-cta-orb {
908 position: absolute;
909 inset: -25% auto auto 50%;
910 transform: translateX(-50%);
911 width: 620px; height: 360px;
912 background: radial-gradient(ellipse, rgba(140,109,255,0.18), rgba(54,197,214,0.08) 45%, transparent 70%);
913 filter: blur(80px);
914 opacity: 0.6;
915 pointer-events: none;
916 z-index: 0;
917 }
918 .vsg-page .vsg-cta-section > * { position: relative; z-index: 1; }
919 .vsg-page .vsg-cta-title {
920 font-family: var(--font-display);
639921 font-size: clamp(26px, 4vw, 44px);
640922 line-height: 1.1;
641923 margin: 0 0 14px;
642 letter-spacing: -0.025em;
924 letter-spacing: -0.028em;
925 font-weight: 800;
643926 color: var(--text-strong);
644927 }
645 .vsg-cta-sub {
928 .vsg-page .vsg-cta-sub {
646929 max-width: 560px;
647930 margin: 0 auto 28px;
648931 color: var(--text-muted);
649932 line-height: 1.55;
650933 font-size: 15px;
651934 }
652 .vsg-cta-buttons {
935 .vsg-page .vsg-cta-buttons {
653936 display: flex;
654937 gap: 12px;
655938 justify-content: center;
656939