Blame · Line-by-line history
landing-pro.tsx
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| f8e5fea | 1 | /** |
| 2 | * LandingProPage — comprehensive marketing homepage. | |
| 3 | * | |
| 4 | * Design: Stripe/Linear aesthetic — pure white, Inter typography, | |
| 5 | * single indigo accent (#4353c9), generous whitespace. No cyberpunk | |
| 6 | * elements (no gradient text fills, no glowing orbs, no pulsing dots, | |
| 7 | * no ticker animations). Sections alternate #ffffff / #fafafb. | |
| 8 | * | |
| 9 | * Self-contained standalone document (own <html>, own CSS, own fonts) | |
| 10 | * — does NOT use the dark app Layout so the marketing surface stays | |
| 11 | * pristine. CSS scoped under .lp-* to avoid any bleed into the app. | |
| 12 | * | |
| 13 | * Data: accepts publicStats (L4) and liveFeed (L3 demo activity) from | |
| 14 | * the route. Both are optional — page degrades gracefully when absent. | |
| 15 | */ | |
| 16 | ||
| 17 | import type { FC } from "hono/jsx"; | |
| 18 | import type { PublicStats } from "../lib/public-stats"; | |
| 19 | import type { | |
| 20 | QueuedAiBuildIssue, | |
| 21 | RecentAutoMerge, | |
| 22 | RecentAiReview, | |
| 23 | DemoActivityEntry, | |
| 24 | } from "../lib/demo-activity"; | |
| 25 | import { DEMO_USERNAME } from "../lib/demo-seed"; | |
| 26 | ||
| 27 | // ─── prop interfaces ─────────────────────────────────────────────── | |
| 28 | ||
| 29 | export interface LandingProLiveFeed { | |
| 30 | queued: QueuedAiBuildIssue[]; | |
| 31 | merges: RecentAutoMerge[]; | |
| 32 | reviews: RecentAiReview[]; | |
| 33 | reviewCount: number; | |
| 34 | feed: DemoActivityEntry[]; | |
| 35 | } | |
| 36 | ||
| 37 | export interface LandingProProps { | |
| 38 | stats?: { publicRepos?: number; users?: number }; | |
| 39 | publicStats?: PublicStats | null; | |
| 40 | liveFeed?: LandingProLiveFeed | null; | |
| 41 | } | |
| 42 | ||
| 43 | // ─── helper: relative time ───────────────────────────────────────── | |
| 44 | ||
| 45 | export function relTime( | |
| 46 | value: string | Date | null | undefined, | |
| 47 | now: number = Date.now() | |
| 48 | ): string { | |
| 49 | if (value === null || value === undefined) return "just now"; | |
| 50 | const t = value instanceof Date ? value.getTime() : new Date(value).getTime(); | |
| 51 | if (!Number.isFinite(t)) return "just now"; | |
| 52 | const d = now - t; | |
| 53 | if (d < 0) return "just now"; | |
| 54 | const s = Math.floor(d / 1000); | |
| 55 | if (s < 60) return "just now"; | |
| 56 | const m = Math.floor(s / 60); | |
| 57 | if (m < 60) return `${m}m ago`; | |
| 58 | const h = Math.floor(m / 60); | |
| 59 | if (h < 24) return `${h}h ago`; | |
| 60 | return `${Math.floor(h / 24)}d ago`; | |
| 61 | } | |
| 62 | ||
| 63 | // ─── page ────────────────────────────────────────────────────────── | |
| 64 | ||
| 65 | export const LandingProPage: FC<LandingProProps> = ({ | |
| 66 | publicStats, | |
| 67 | liveFeed, | |
| 68 | } = {}) => { | |
| 69 | const title = "Gluecron — The AI-native git platform"; | |
| 70 | const desc = | |
| 71 | "The git platform that does the work. AI code review on every PR, secrets scanned at push, features built from plain-English issues. Self-hosted, git-native, Claude-first."; | |
| 72 | ||
| 73 | const liveQueued = liveFeed?.queued ?? []; | |
| 74 | const liveMerges = liveFeed?.merges ?? []; | |
| 75 | const liveReviews = liveFeed?.reviews ?? []; | |
| 76 | const liveReviewCount = liveFeed?.reviewCount ?? 0; | |
| 77 | const liveEntries = liveFeed?.feed ?? []; | |
| 78 | ||
| 79 | return ( | |
| 80 | <html lang="en"> | |
| 81 | <head> | |
| 82 | <meta charset="UTF-8" /> | |
| 83 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| 84 | <meta name="theme-color" content="#ffffff" /> | |
| 85 | <title>{title}</title> | |
| 86 | <meta name="description" content={desc} /> | |
| 87 | <meta property="og:title" content={title} /> | |
| 88 | <meta property="og:description" content={desc} /> | |
| 89 | <meta property="og:type" content="website" /> | |
| 90 | <meta name="twitter:card" content="summary_large_image" /> | |
| 91 | <link rel="icon" type="image/svg+xml" href="/icon.svg" /> | |
| 92 | <link rel="preconnect" href="https://fonts.googleapis.com" /> | |
| 93 | <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="" /> | |
| 94 | <link | |
| 95 | rel="stylesheet" | |
| 96 | href="https://fonts.googleapis.com/css2?family=Inter:wght@400;450;500;600&family=Inter+Tight:wght@600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" | |
| 97 | /> | |
| 98 | <style dangerouslySetInnerHTML={{ __html: css }} /> | |
| 99 | </head> | |
| 100 | <body> | |
| 101 | ||
| 102 | {/* ── Nav ────────────────────────────────────────────────── */} | |
| 103 | <header class="lp-nav" id="lp-nav"> | |
| 104 | <div class="lp-nav-in"> | |
| 105 | <a href="/" class="lp-logo" aria-label="Gluecron home"> | |
| 106 | <span class="lp-logo-mark" aria-hidden="true" /> | |
| 107 | gluecron | |
| 108 | </a> | |
| 109 | <nav class="lp-nav-links" aria-label="Primary"> | |
| 110 | <a href="#platform">Platform</a> | |
| 111 | <a href="#ai">AI suite</a> | |
| 112 | <a href="#security">Security</a> | |
| 113 | <a href="/pricing">Pricing</a> | |
| 114 | <a href="/explore">Explore</a> | |
| 115 | </nav> | |
| 116 | <div class="lp-nav-ctas"> | |
| 117 | <a href="/login" class="lp-btn lp-btn-ghost">Sign in</a> | |
| 118 | <a href="/register" class="lp-btn lp-btn-solid">Start building</a> | |
| 119 | </div> | |
| 120 | </div> | |
| 121 | </header> | |
| 122 | ||
| 123 | {/* ── Hero ───────────────────────────────────────────────── */} | |
| 124 | <section class="lp-hero"> | |
| 125 | <div class="lp-wrap lp-hero-in"> | |
| 126 | <div class="lp-hero-text"> | |
| 127 | <div class="lp-kicker">The AI-native git platform</div> | |
| 128 | <h1 class="lp-h1"> | |
| 129 | The platform<br />that does the work. | |
| 130 | </h1> | |
| 131 | <p class="lp-hero-sub"> | |
| 132 | AI code review on every PR. Secrets scanned at push. | |
| 133 | Features built from plain-English issues. | |
| 134 | Gluecron closes the loop from spec to deployed code — | |
| 135 | while you focus on what matters. | |
| 136 | </p> | |
| 137 | ||
| 138 | <div class="lp-hero-ctas"> | |
| 139 | <a href="/register" class="lp-btn lp-btn-solid lp-btn-lg"> | |
| 140 | Sign up free | |
| 141 | </a> | |
| 142 | <a href="/import" class="lp-btn lp-btn-outline lp-btn-lg"> | |
| 143 | Migrate from GitHub | |
| 144 | </a> | |
| 145 | </div> | |
| 146 | <div class="lp-hero-links"> | |
| 147 | <a href="/demo">Try the live demo</a> | |
| 148 | <span aria-hidden="true">·</span> | |
| 149 | <a href="/vs-github">Compare to GitHub</a> | |
| 150 | <span aria-hidden="true">·</span> | |
| 151 | <a href="/pricing">See pricing</a> | |
| 152 | </div> | |
| 153 | ||
| 154 | {publicStats && ( | |
| 155 | <dl class="lp-hero-stats"> | |
| 156 | <div class="lp-hero-stat"> | |
| 157 | <dt>{publicStats.weeklyPrsAutoMerged.toLocaleString()}</dt> | |
| 158 | <dd>PRs auto-merged this week</dd> | |
| 159 | </div> | |
| 160 | <div class="lp-hero-stat"> | |
| 161 | <dt>{publicStats.weeklyIssuesBuiltByAi.toLocaleString()}</dt> | |
| 162 | <dd>issues built by AI</dd> | |
| 163 | </div> | |
| 164 | <div class="lp-hero-stat"> | |
| 165 | <dt>{`~${Math.round(publicStats.weeklyHoursSaved)}h`}</dt> | |
| 166 | <dd>saved by AI this week</dd> | |
| 167 | </div> | |
| 168 | <div class="lp-hero-stat"> | |
| 169 | <dt>{publicStats.weeklyDeploysShipped.toLocaleString()}</dt> | |
| 170 | <dd>deploys shipped</dd> | |
| 171 | </div> | |
| 172 | </dl> | |
| 173 | )} | |
| 174 | </div> | |
| 175 | ||
| 176 | {/* Product mock card */} | |
| 177 | <div class="lp-hero-card" aria-hidden="true"> | |
| 178 | <div class="lp-hc-bar"> | |
| 179 | <span class="lp-hc-dot" /><span class="lp-hc-dot" /><span class="lp-hc-dot" /> | |
| 180 | <span class="lp-hc-path">gluecron.com / your-org / api · PR #128</span> | |
| 181 | </div> | |
| 182 | <div class="lp-hc-body"> | |
| 183 | <div class="lp-hc-pr-row"> | |
| 184 | <span class="lp-badge lp-badge-merged">Merged</span> | |
| 185 | <span class="lp-hc-title">Fix race condition in token refresh</span> | |
| 186 | </div> | |
| 187 | <div class="lp-hc-review"> | |
| 188 | <div class="lp-hc-ava">C</div> | |
| 189 | <div class="lp-hc-rev"> | |
| 190 | <div class="lp-hc-rev-head">Claude review · <em>approved</em></div> | |
| 191 | <div class="lp-hc-rev-body"> | |
| 192 | Mutex now guards the refresh path. The double-fetch under contention is resolved. Gates green. Auto-merging. | |
| 193 | </div> | |
| 194 | </div> | |
| 195 | </div> | |
| 196 | <div class="lp-hc-checks"> | |
| 197 | <span class="lp-check lp-check-ok">✓ gate: security</span> | |
| 198 | <span class="lp-check lp-check-ok">✓ gate: tests</span> | |
| 199 | <span class="lp-check lp-check-ok">✓ review: Claude</span> | |
| 200 | <span class="lp-check lp-check-ok">✓ deploy: live</span> | |
| 201 | </div> | |
| 202 | <div class="lp-hc-meta"> | |
| 203 | Deployed to <code>api.your-org.com</code> · 4.1s · push-to-live | |
| 204 | </div> | |
| 205 | </div> | |
| 206 | </div> | |
| 207 | </div> | |
| 208 | </section> | |
| 209 | ||
| 210 | {/* ── Trust strip ────────────────────────────────────────── */} | |
| 211 | <div class="lp-trust-strip"> | |
| 212 | <div class="lp-wrap lp-trust-in"> | |
| 213 | <span>Self-hosted on your hardware</span> | |
| 214 | <span class="lp-sep" aria-hidden="true" /> | |
| 215 | <span>Git-native · Smart HTTP + SSH</span> | |
| 216 | <span class="lp-sep" aria-hidden="true" /> | |
| 217 | <span>Claude-powered AI review</span> | |
| 218 | <span class="lp-sep" aria-hidden="true" /> | |
| 219 | <span>MCP-native for Claude Code, Cursor</span> | |
| 220 | <span class="lp-sep" aria-hidden="true" /> | |
| 221 | <span>Free to start</span> | |
| 222 | </div> | |
| 223 | </div> | |
| 224 | ||
| 225 | {/* ── Live-now autopilot ─────────────────────────────────── */} | |
| 226 | <section class="lp-sec lp-sec-soft" aria-labelledby="lp-live-h"> | |
| 227 | <div class="lp-wrap"> | |
| 228 | <div class="lp-sec-head"> | |
| 229 | <div class="lp-kicker"> | |
| 230 | <span class="lp-live-dot" aria-hidden="true">●</span> Live now | |
| 231 | </div> | |
| 232 | <h2 id="lp-live-h" class="lp-h2"> | |
| 233 | Claude is working on demo repos as you read this. | |
| 234 | </h2> | |
| 235 | <p class="lp-sub"> | |
| 236 | Real data from the public <code>{DEMO_USERNAME}/*</code> repos. | |
| 237 | Refreshes every 30 seconds. | |
| 238 | </p> | |
| 239 | </div> | |
| 240 | ||
| 241 | <div class="lp-live-grid" data-livenow-grid> | |
| 242 | <LiveCard title="Issues queued for AI"> | |
| 243 | <ul class="lp-live-list" data-livecard="queued"> | |
| 244 | {liveQueued.length === 0 ? ( | |
| 245 | <li class="lp-live-empty">Quiet right now.</li> | |
| 246 | ) : ( | |
| 247 | liveQueued.slice(0, 3).map((i) => ( | |
| 248 | <li class="lp-live-row" data-row-id={`queued|${i.repo}|${i.number}`}> | |
| 249 | <a href={`/${DEMO_USERNAME}/${i.repo}/issues/${i.number}`} class="lp-live-link"> | |
| 250 | <span class="lp-live-num">#{i.number}</span> | |
| 251 | {" "}{i.title} | |
| 252 | </a> | |
| 253 | <span class="lp-live-meta">{i.repo}</span> | |
| 254 | </li> | |
| 255 | )) | |
| 256 | )} | |
| 257 | </ul> | |
| 258 | </LiveCard> | |
| 259 | ||
| 260 | <LiveCard title="Recently merged by AI"> | |
| 261 | <ul class="lp-live-list" data-livecard="merges"> | |
| 262 | {liveMerges.length === 0 ? ( | |
| 263 | <li class="lp-live-empty">No auto-merges in the last 24h.</li> | |
| 264 | ) : ( | |
| 265 | liveMerges.slice(0, 3).map((m) => ( | |
| 266 | <li class="lp-live-row" data-row-id={`merges|${m.repo}|${m.number}`}> | |
| 267 | <a href={`/${DEMO_USERNAME}/${m.repo}/pulls/${m.number}`} class="lp-live-link"> | |
| 268 | <span class="lp-live-num">#{m.number}</span> | |
| 269 | {" "}{m.title} | |
| 270 | </a> | |
| 271 | <span class="lp-live-meta"> | |
| 272 | {m.repo} ·{" "} | |
| 273 | <span data-rel={m.mergedAt instanceof Date ? m.mergedAt.toISOString() : String(m.mergedAt)}> | |
| 274 | {relTime(m.mergedAt)} | |
| 275 | </span> | |
| 276 | </span> | |
| 277 | </li> | |
| 278 | )) | |
| 279 | )} | |
| 280 | </ul> | |
| 281 | </LiveCard> | |
| 282 | ||
| 283 | <LiveCard title="AI reviews posted"> | |
| 284 | <div class="lp-live-bignum"> | |
| 285 | <span data-livecard-count="reviews" data-tick-target={String(liveReviewCount)}> | |
| 286 | {liveReviewCount} | |
| 287 | </span> | |
| 288 | <span class="lp-live-bignum-label">today</span> | |
| 289 | </div> | |
| 290 | <ul class="lp-live-list" data-livecard="reviews"> | |
| 291 | {liveReviews.length === 0 ? ( | |
| 292 | <li class="lp-live-empty">No reviews in the last 24h.</li> | |
| 293 | ) : ( | |
| 294 | liveReviews.slice(0, 2).map((r) => ( | |
| 295 | <li class="lp-live-row" data-row-id={`reviews|${r.repo}|${r.prNumber}`}> | |
| 296 | <a href={`/${DEMO_USERNAME}/${r.repo}/pulls/${r.prNumber}`} class="lp-live-link"> | |
| 297 | <span class="lp-live-num">#{r.prNumber}</span> | |
| 298 | {" "}{r.commentSnippet} | |
| 299 | </a> | |
| 300 | <span class="lp-live-meta">{r.repo}</span> | |
| 301 | </li> | |
| 302 | )) | |
| 303 | )} | |
| 304 | </ul> | |
| 305 | </LiveCard> | |
| 306 | ||
| 307 | <LiveCard title="Activity feed"> | |
| 308 | <ul class="lp-live-list" data-livecard="feed"> | |
| 309 | {liveEntries.length === 0 ? ( | |
| 310 | <li class="lp-live-empty">Quiet right now — check back in a minute.</li> | |
| 311 | ) : ( | |
| 312 | liveEntries.slice(0, 6).map((e) => { | |
| 313 | const path = e.ref.type === "pr" ? "pulls" : "issues"; | |
| 314 | const kindLabel = | |
| 315 | e.kind === "auto_merge.merged" ? "auto-merged" : | |
| 316 | e.kind === "ai_build.dispatched" ? "AI-built" : | |
| 317 | "AI review"; | |
| 318 | const id = `${e.kind}|${e.repo}|${e.ref.type}|${e.ref.number}`; | |
| 319 | return ( | |
| 320 | <li class="lp-live-feedrow" data-row-id={id}> | |
| 321 | <span class={`lp-feed-kind lp-feed-kind-${e.kind.replace(/\./g, "-")}`}> | |
| 322 | {kindLabel} | |
| 323 | </span> | |
| 324 | {" "} | |
| 325 | <a | |
| 326 | class="lp-live-link" | |
| 327 | href={`/${DEMO_USERNAME}/${e.repo}/${path}/${e.ref.number}`} | |
| 328 | > | |
| 329 | {e.repo} #{e.ref.number} | |
| 330 | </a> | |
| 331 | {" "} | |
| 332 | <span | |
| 333 | class="lp-live-rel" | |
| 334 | data-rel={e.at instanceof Date ? e.at.toISOString() : String(e.at)} | |
| 335 | > | |
| 336 | {relTime(e.at)} | |
| 337 | </span> | |
| 338 | </li> | |
| 339 | ); | |
| 340 | }) | |
| 341 | )} | |
| 342 | </ul> | |
| 343 | </LiveCard> | |
| 344 | </div> | |
| 345 | ||
| 346 | <div class="lp-live-cta"> | |
| 347 | <a href="/register" class="lp-btn lp-btn-solid">Sign up free</a> | |
| 348 | <a href="/demo" class="lp-text-link">Open the live demo →</a> | |
| 349 | </div> | |
| 350 | </div> | |
| 351 | <script dangerouslySetInnerHTML={{ __html: liveNowJs }} /> | |
| 352 | </section> | |
| 353 | ||
| 354 | {/* ── Platform breadth ───────────────────────────────────── */} | |
| 355 | <section class="lp-sec" id="platform" aria-labelledby="lp-platform-h"> | |
| 356 | <div class="lp-wrap"> | |
| 357 | <div class="lp-sec-head"> | |
| 358 | <div class="lp-kicker">The platform</div> | |
| 359 | <h2 id="lp-platform-h" class="lp-h2"> | |
| 360 | Everything your team needs.<br />Nothing you don't. | |
| 361 | </h2> | |
| 362 | <p class="lp-sub"> | |
| 363 | Gluecron ships the surfaces GitHub charges extra for — and the | |
| 364 | ones it never built. AI is a first-class contributor, not a plugin. | |
| 365 | </p> | |
| 366 | </div> | |
| 367 | ||
| 368 | <div class="lp-platform-grid"> | |
| 369 | <PlatformGroup title="Code hosting"> | |
| 370 | {["Git Smart HTTP · SSH keys", "Forks · Stars · Topics", "Templates · Mirroring", "Releases · Protected tags", "Push policy rulesets", "Commit signature verify"]} | |
| 371 | </PlatformGroup> | |
| 372 | <PlatformGroup title="Collaboration"> | |
| 373 | {["Issues · PRs · Inline review", "Draft PRs · Merge queues", "Discussions · Wikis", "Projects / kanban", "Gists · Reactions", "Mentions · Notifications"]} | |
| 374 | </PlatformGroup> | |
| 375 | <PlatformGroup title="AI suite"> | |
| 376 | {["Spec-to-PR from plain English", "AI code review on every PR", "AI auto-merge when gates pass", "PR triage · Issue triage", "Sleep Mode — ships overnight", "AI changelog · AI test stubs"]} | |
| 377 | </PlatformGroup> | |
| 378 | <PlatformGroup title="Security"> | |
| 379 | {["Secret scanner (15 patterns)", "AI semantic security review", "GateTest integration", "Dependency CVE alerts", "2FA · Passkeys · SAML SSO", "Audit log (100% coverage)"]} | |
| 380 | </PlatformGroup> | |
| 381 | <PlatformGroup title="CI / CD"> | |
| 382 | {["Workflow runner (YAML-native)", "Cron triggers · Secrets", "Branch protection · Gates", "Required status checks", "Auto-repair on gate failure", "Protected tags enforcement"]} | |
| 383 | </PlatformGroup> | |
| 384 | <PlatformGroup title="Platform"> | |
| 385 | {["Organizations · Teams · Roles", "npm package registry", "Pages / static hosting", "App marketplace", "Protected environments", "Billing · Quotas"]} | |
| 386 | </PlatformGroup> | |
| 387 | <PlatformGroup title="Observability"> | |
| 388 | {["DORA metrics dashboard", "Developer velocity", "Repository health score", "Hot files heatmap", "Traffic analytics", "Org insights"]} | |
| 389 | </PlatformGroup> | |
| 390 | <PlatformGroup title="Integrations"> | |
| 391 | {["REST API v2 · GraphQL", "MCP server (Claude, Cursor)", "Official CLI · VS Code ext", "Webhooks (HMAC-signed)", "OAuth provider · GitHub Apps", "GitHub import (single + bulk)"]} | |
| 392 | </PlatformGroup> | |
| 393 | </div> | |
| 394 | </div> | |
| 395 | </section> | |
| 396 | ||
| 397 | {/* ── Developer painkiller ───────────────────────────────── */} | |
| 398 | <section class="lp-sec lp-sec-soft"> | |
| 399 | <div class="lp-wrap"> | |
| 400 | <div class="lp-sec-head"> | |
| 401 | <div class="lp-kicker">Why developers switch</div> | |
| 402 | <h2 class="lp-h2">Pain out. Productivity in.</h2> | |
| 403 | <p class="lp-sub"> | |
| 404 | Every friction point in modern development has a specific answer in Gluecron. | |
| 405 | </p> | |
| 406 | </div> | |
| 407 | ||
| 408 | <div class="lp-pain-table"> | |
| 409 | <PainRow | |
| 410 | problem="Waiting on CI to queue before a gate runs" | |
| 411 | fix="Gates fire at the moment of push — before the branch even moves" | |
| 412 | /> | |
| 413 | <PainRow | |
| 414 | problem="Manual code review as a bottleneck" | |
| 415 | fix="AI reviews every PR in under 30 seconds, line-level, with risk flags" | |
| 416 | /> | |
| 417 | <PainRow | |
| 418 | problem="A failed gate halts your whole day" | |
| 419 | fix="Auto-repair tries to fix it, re-runs the gate, and continues — you may never see it" | |
| 420 | /> | |
| 421 | <PainRow | |
| 422 | problem="AI as a sidebar you talk at" | |
| 423 | fix="Claude has a bot account, makes commits, and appears in your git history like a teammate" | |
| 424 | /> | |
| 425 | <PainRow | |
| 426 | problem="Platform lock-in and surprise bills" | |
| 427 | fix="Self-hosted, single Bun binary, git-native — run it on a $6 VPS" | |
| 428 | /> | |
| 429 | <PainRow | |
| 430 | problem="Dependency CVEs discovered after merge" | |
| 431 | fix="CVE scanner runs on every push touching a manifest; opens issues automatically" | |
| 432 | /> | |
| 433 | </div> | |
| 434 | </div> | |
| 435 | </section> | |
| 436 | ||
| 437 | {/* ── AI showcase ────────────────────────────────────────── */} | |
| 438 | <section class="lp-sec" id="ai" aria-labelledby="lp-ai-h"> | |
| 439 | <div class="lp-wrap"> | |
| 440 | <div class="lp-sec-head"> | |
| 441 | <div class="lp-kicker">AI-native, not AI-bolted-on</div> | |
| 442 | <h2 id="lp-ai-h" class="lp-h2"> | |
| 443 | Claude does the work.<br />You stay in control. | |
| 444 | </h2> | |
| 445 | <p class="lp-sub"> | |
| 446 | Every AI action is attributed, auditable, and reversible. | |
| 447 | The manual path is always one click away. | |
| 448 | </p> | |
| 449 | </div> | |
| 450 | ||
| 451 | <div class="lp-ai-features"> | |
| 452 | ||
| 453 | <AiFeature | |
| 454 | n="01" | |
| 455 | title="Spec-to-PR" | |
| 456 | body="Describe a feature in plain English — or just label an issue ai:build. Claude opens a branch, writes the change, and submits a draft PR against your gates. Spec to PR in under 90 seconds." | |
| 457 | link={{ href: "/demo", label: "Try on a live repo" }} | |
| 458 | mock={[ | |
| 459 | { icon: "●", color: "muted", text: "issue #42 labelled ai:build" }, | |
| 460 | { icon: "↗", color: "brand", text: "branch opened: feat/add-auth-provider" }, | |
| 461 | { icon: "↗", color: "brand", text: "PR #43 created — diff ready for review" }, | |
| 462 | { icon: "✓", color: "green", text: "gates queued — waiting on your approval" }, | |
| 463 | ]} | |
| 464 | /> | |
| 465 | ||
| 466 | <AiFeature | |
| 467 | n="02" | |
| 468 | title="AI code review on every PR" | |
| 469 | body="The moment a PR opens, Claude reviews the diff: line-level comments, security flags, logic issues, and a verdict. Under 30 seconds. Every PR, every time, before a human has to look." | |
| 470 | link={{ href: "/vs-github", label: "Compare to Copilot" }} | |
| 471 | mock={[ | |
| 472 | { icon: "C", color: "brand", text: "Claude review · PR #128" }, | |
| 473 | { icon: " ", color: "muted", text: "auth.ts:84 — mutex guards refresh path ✓" }, | |
| 474 | { icon: " ", color: "muted", text: "No security issues found" }, | |
| 475 | { icon: "✓", color: "green", text: "approved — auto-merging" }, | |
| 476 | ]} | |
| 477 | reverse | |
| 478 | /> | |
| 479 | ||
| 480 | <AiFeature | |
| 481 | n="03" | |
| 482 | title="Sleep Mode" | |
| 483 | body="Enable Sleep Mode and set your wake-up hour. Gluecron's autopilot works overnight: merging ready PRs, building labelled issues, repairing failed gates, fixing secrets. You wake up to a digest of what shipped." | |
| 484 | link={{ href: "/sleep-mode", label: "See Sleep Mode" }} | |
| 485 | mock={[ | |
| 486 | { icon: "✉", color: "brand", text: "Your overnight digest — 9:00 AM" }, | |
| 487 | { icon: "✓", color: "green", text: "3 PRs auto-merged" }, | |
| 488 | { icon: "✓", color: "green", text: "1 feature built from issue #37" }, | |
| 489 | { icon: "✓", color: "green", text: "2 secrets auto-repaired before push" }, | |
| 490 | ]} | |
| 491 | /> | |
| 492 | ||
| 493 | <AiFeature | |
| 494 | n="04" | |
| 495 | title="Auto-repair on gate failure" | |
| 496 | body="When a gate fails, auto-repair runs: it reads the failure, drafts a fix, commits it with a bot account, and re-runs the gate. If it can't fix it, it opens an incident issue with a root-cause summary. You see the problem only if the robot couldn't solve it." | |
| 497 | link={{ href: "/demo", label: "Watch a live repair" }} | |
| 498 | mock={[ | |
| 499 | { icon: "✗", color: "red", text: "gate: tests — 2 failing" }, | |
| 500 | { icon: "↗", color: "brand", text: "auto-repair: reading failure log" }, | |
| 501 | { icon: "↗", color: "brand", text: "patch drafted — +9 −3" }, | |
| 502 | { icon: "✓", color: "green", text: "gates re-run — all 412 passing" }, | |
| 503 | ]} | |
| 504 | reverse | |
| 505 | /> | |
| 506 | ||
| 507 | </div> | |
| 508 | </div> | |
| 509 | </section> | |
| 510 | ||
| 511 | {/* ── Security ───────────────────────────────────────────── */} | |
| 512 | <section class="lp-sec lp-sec-soft" id="security" aria-labelledby="lp-sec-h"> | |
| 513 | <div class="lp-wrap"> | |
| 514 | <div class="lp-sec-head"> | |
| 515 | <div class="lp-kicker">Security</div> | |
| 516 | <h2 id="lp-sec-h" class="lp-h2">Secure by default at every layer.</h2> | |
| 517 | <p class="lp-sub"> | |
| 518 | Security isn't a settings toggle. On Gluecron it runs automatically | |
| 519 | on every push, every PR, and every dependency update. | |
| 520 | </p> | |
| 521 | </div> | |
| 522 | ||
| 523 | <div class="lp-security-grid"> | |
| 524 | <SecurityCard | |
| 525 | title="Push-time secret scanner" | |
| 526 | body="15 regex patterns + AI semantic scan runs before the branch moves. AWS keys, GitHub tokens, private keys, database URLs — stopped cold." | |
| 527 | /> | |
| 528 | <SecurityCard | |
| 529 | title="AI security review" | |
| 530 | body="Claude reviews every diff for SSRF, SQL injection, XSS, and unsafe deserialization patterns. Line-level findings posted as PR comments." | |
| 531 | /> | |
| 532 | <SecurityCard | |
| 533 | title="Dependency CVE alerts" | |
| 534 | body="Scans package.json, go.mod, Cargo.toml, requirements.txt, and Gemfile on every push. Critical findings open issues automatically." | |
| 535 | /> | |
| 536 | <SecurityCard | |
| 537 | title="Push policy rulesets" | |
| 538 | body="Enforce commit message patterns, block file paths, cap file sizes, and forbid force-pushes — enforced at the HTTP layer, not advisory." | |
| 539 | /> | |
| 540 | <SecurityCard | |
| 541 | title="Commit signature verification" | |
| 542 | body="GPG and SSH key registration with Verified badges on every commit. Full OpenPGP packet parsing, SSHSIG support, fingerprint matching." | |
| 543 | /> | |
| 544 | <SecurityCard | |
| 545 | title="Enterprise-grade auth" | |
| 546 | body="TOTP 2FA, passkeys (WebAuthn), SAML/OIDC SSO (Okta, Azure AD, Google Workspace), personal access tokens, OAuth provider." | |
| 547 | /> | |
| 548 | </div> | |
| 549 | </div> | |
| 550 | </section> | |
| 551 | ||
| 552 | {/* ── For teams ──────────────────────────────────────────── */} | |
| 553 | <section class="lp-sec"> | |
| 554 | <div class="lp-wrap"> | |
| 555 | <div class="lp-sec-head"> | |
| 556 | <div class="lp-kicker">For teams</div> | |
| 557 | <h2 class="lp-h2">Built for organisations, not just solo developers.</h2> | |
| 558 | <p class="lp-sub"> | |
| 559 | Everything a growing engineering team needs — including the parts | |
| 560 | enterprise platforms charge separately for. | |
| 561 | </p> | |
| 562 | </div> | |
| 563 | ||
| 564 | <div class="lp-teams-grid"> | |
| 565 | <TeamCard | |
| 566 | title="Organizations & teams" | |
| 567 | items={[ | |
| 568 | "Org-owned repositories", | |
| 569 | "Team-based CODEOWNERS (@org/team)", | |
| 570 | "Role-based access (read / write / admin)", | |
| 571 | "Per-repo collaborator invites", | |
| 572 | "Org-wide secrets manager (AES-256-GCM)", | |
| 573 | ]} | |
| 574 | /> | |
| 575 | <TeamCard | |
| 576 | title="Enterprise auth" | |
| 577 | items={[ | |
| 578 | "SAML/OIDC SSO (Okta, Azure AD, Google)", | |
| 579 | "TOTP 2FA + recovery codes", | |
| 580 | "Passkeys / WebAuthn", | |
| 581 | "Email-domain allowlists", | |
| 582 | "Auto-provision on first SSO login", | |
| 583 | ]} | |
| 584 | /> | |
| 585 | <TeamCard | |
| 586 | title="Compliance & audit" | |
| 587 | items={[ | |
| 588 | "100% audit log coverage", | |
| 589 | "Per-repo + org-level audit views", | |
| 590 | "Signed commits with Verified badges", | |
| 591 | "Protected environments (reviewer-gated)", | |
| 592 | "DORA metrics for engineering health", | |
| 593 | ]} | |
| 594 | /> | |
| 595 | <TeamCard | |
| 596 | title="Developer insights" | |
| 597 | items={[ | |
| 598 | "DORA metrics (deploy freq, lead time, MTTR)", | |
| 599 | "Developer velocity dashboard", | |
| 600 | "Repository health score (0–100)", | |
| 601 | "Hot files heatmap (churn-based risk tiers)", | |
| 602 | "Org-wide green rate and PR activity", | |
| 603 | ]} | |
| 604 | /> | |
| 605 | </div> | |
| 606 | </div> | |
| 607 | </section> | |
| 608 | ||
| 609 | {/* ── Developer ecosystem ────────────────────────────────── */} | |
| 610 | <section class="lp-sec lp-sec-soft"> | |
| 611 | <div class="lp-wrap"> | |
| 612 | <div class="lp-sec-head"> | |
| 613 | <div class="lp-kicker">Developer ecosystem</div> | |
| 614 | <h2 class="lp-h2">Meet developers where they work.</h2> | |
| 615 | </div> | |
| 616 | ||
| 617 | <div class="lp-eco-grid"> | |
| 618 | <EcoCard | |
| 619 | title="Official CLI" | |
| 620 | sub="gluecron repo ls · gluecron issues · gluecron gql" | |
| 621 | body="A Bun-compiled single binary. Login, repo CRUD, issue listing, GraphQL queries, and server info from your terminal." | |
| 622 | link="/install" | |
| 623 | /> | |
| 624 | <EcoCard | |
| 625 | title="VS Code extension" | |
| 626 | sub="Explain · Semantic search · Generate tests · Open on web" | |
| 627 | body="Four commands wired to your active Gluecron remote. AI explain-this-file and test generation without leaving the editor." | |
| 628 | link="/vscode" | |
| 629 | /> | |
| 630 | <EcoCard | |
| 631 | title="MCP server" | |
| 632 | sub="Claude Code · Claude Desktop · Cursor" | |
| 633 | body="Model Context Protocol tools for repo search, file reads, issue and PR management — all natively available to Claude and Cursor." | |
| 634 | link="/mcp" | |
| 635 | /> | |
| 636 | <EcoCard | |
| 637 | title="Full API surface" | |
| 638 | sub="REST v2 · GraphQL · Webhooks" | |
| 639 | body="A comprehensive REST API v2, a GraphQL mirror, HMAC-signed webhook delivery, and an OAuth 2.0 provider for third-party apps." | |
| 640 | link="/api" | |
| 641 | /> | |
| 642 | </div> | |
| 643 | </div> | |
| 644 | </section> | |
| 645 | ||
| 646 | {/* ── Comparison ─────────────────────────────────────────── */} | |
| 647 | <section class="lp-sec"> | |
| 648 | <div class="lp-wrap"> | |
| 649 | <div class="lp-sec-head"> | |
| 650 | <div class="lp-kicker">vs the incumbent</div> | |
| 651 | <h2 class="lp-h2">Everything GitHub charges for.<br />And the parts they never built.</h2> | |
| 652 | </div> | |
| 653 | ||
| 654 | <div class="lp-compare"> | |
| 655 | <div class="lp-compare-head" aria-hidden="true"> | |
| 656 | <div /> | |
| 657 | <div>GitHub</div> | |
| 658 | <div>Gluecron</div> | |
| 659 | </div> | |
| 660 | <CompareRow feature="Git hosting + Smart HTTP push" them="✓" us="✓" /> | |
| 661 | <CompareRow feature="Issues, PRs, code review" them="✓" us="✓" /> | |
| 662 | <CompareRow feature="Workflow runner" them="Metered minutes" us="Self-hosted, unmetered" ours /> | |
| 663 | <CompareRow feature="AI code review on every PR" them="Copilot subscription" us="Built in, always on" ours /> | |
| 664 | <CompareRow feature="Spec-to-PR (issue → draft PR)" them="—" us="✓" ours /> | |
| 665 | <CompareRow feature="Auto-repair on gate failure" them="—" us="✓" ours /> | |
| 666 | <CompareRow feature="Secret scanner on push" them="Paid add-on" us="Built in" ours /> | |
| 667 | <CompareRow feature="MCP server (Claude / Cursor)" them="—" us="✓" ours /> | |
| 668 | <CompareRow feature="DORA metrics + velocity dashboard" them="Paid add-on" us="Built in" ours /> | |
| 669 | <CompareRow feature="Self-host on your own hardware" them="Enterprise tier" us="Single binary, free" ours /> | |
| 670 | <CompareRow feature="npm package registry" them="✓" us="✓" /> | |
| 671 | <CompareRow feature="Pre-receive policy enforcement" them="GitHub Enterprise" us="✓ on all plans" ours /> | |
| 672 | </div> | |
| 673 | ||
| 674 | <div class="lp-compare-foot"> | |
| 675 | <a href="/vs-github" class="lp-text-link">See the full 26-row comparison →</a> | |
| 676 | </div> | |
| 677 | </div> | |
| 678 | </section> | |
| 679 | ||
| 680 | {/* ── Pricing teaser ─────────────────────────────────────── */} | |
| 681 | <section class="lp-sec lp-sec-soft"> | |
| 682 | <div class="lp-wrap"> | |
| 683 | <div class="lp-sec-head"> | |
| 684 | <div class="lp-kicker">Pricing</div> | |
| 685 | <h2 class="lp-h2">Free to start. Honest at scale.</h2> | |
| 686 | <p class="lp-sub"> | |
| 687 | Self-hosting is free forever. Hosted plans price the AI calls, | |
| 688 | not the seats. You own your data either way. | |
| 689 | </p> | |
| 690 | </div> | |
| 691 | ||
| 692 | <div class="lp-price-grid"> | |
| 693 | <PricingCard | |
| 694 | tier="Free" | |
| 695 | price="$0" | |
| 696 | cadence="forever" | |
| 697 | desc="For personal projects and open source. Public and private repos, full AI suite, fair quotas." | |
| 698 | features={["Unlimited public repos", "3 private repos", "5K AI calls / month", "Community support"]} | |
| 699 | cta="Start free" | |
| 700 | href="/register" | |
| 701 | /> | |
| 702 | <PricingCard | |
| 703 | tier="Pro" | |
| 704 | price="$12" | |
| 705 | cadence="per user / month" | |
| 706 | desc="For working developers. Lifts every quota, adds priority routing, no Gluecron branding on deploys." | |
| 707 | features={["Unlimited private repos", "100K AI calls / month", "Priority queue", "Custom domains"]} | |
| 708 | cta="Go Pro" | |
| 709 | href="/settings/billing" | |
| 710 | highlight | |
| 711 | /> | |
| 712 | <PricingCard | |
| 713 | tier="Team" | |
| 714 | price="Talk to us" | |
| 715 | cadence="custom" | |
| 716 | desc="For orgs running production on Gluecron. SSO, audit retention, enterprise SLA, on-prem." | |
| 717 | features={["SSO + SCIM", "On-prem deploy", "Dedicated capacity", "24/7 incident response"]} | |
| 718 | cta="Contact us" | |
| 719 | href="mailto:hello@gluecron.com" | |
| 720 | /> | |
| 721 | </div> | |
| 722 | ||
| 723 | <div class="lp-price-foot"> | |
| 724 | <a href="/pricing" class="lp-text-link">Full pricing details →</a> | |
| 725 | </div> | |
| 726 | </div> | |
| 727 | </section> | |
| 728 | ||
| 729 | {/* ── One-command install ────────────────────────────────── */} | |
| 730 | <section class="lp-sec"> | |
| 731 | <div class="lp-wrap lp-install-wrap"> | |
| 732 | <div class="lp-install-text"> | |
| 733 | <div class="lp-kicker">One command to start</div> | |
| 734 | <h2 class="lp-h2 lp-h2-tight">Already on GitHub? Migrate in 30 seconds.</h2> | |
| 735 | <p class="lp-sub"> | |
| 736 | Signs you in, mints a PAT, imports your repo, wires the Claude Desktop MCP | |
| 737 | server, and drops the Gluecron skill files — all in one shot. | |
| 738 | </p> | |
| 739 | <a href="/import" class="lp-btn lp-btn-solid lp-btn-lg" style="margin-top:20px"> | |
| 740 | Or import a single repo → | |
| 741 | </a> | |
| 742 | </div> | |
| 743 | <div class="lp-install-terminal" aria-label="One-line install command"> | |
| 744 | <div class="lp-term-bar"> | |
| 745 | <span class="lp-term-dot" /><span class="lp-term-dot" /><span class="lp-term-dot" /> | |
| 746 | <span class="lp-term-title">Terminal</span> | |
| 747 | </div> | |
| 748 | <div class="lp-term-body"> | |
| 749 | <div class="lp-term-line"> | |
| 750 | <span class="lp-term-prompt">$</span> | |
| 751 | <span id="lp-install-cmd">curl -sSL gluecron.com/install | bash</span> | |
| 752 | <button | |
| 753 | type="button" | |
| 754 | class="lp-copy-btn" | |
| 755 | data-copy-target="lp-install-cmd" | |
| 756 | aria-label="Copy install command" | |
| 757 | > | |
| 758 | Copy | |
| 759 | </button> | |
| 760 | </div> | |
| 761 | <div class="lp-term-out lp-term-ok">✓ signed in as you@example.com</div> | |
| 762 | <div class="lp-term-out lp-term-ok">✓ PAT created (admin scope)</div> | |
| 763 | <div class="lp-term-out lp-term-ok">✓ your-repo imported from GitHub</div> | |
| 764 | <div class="lp-term-out lp-term-ok">✓ MCP server added to Claude Desktop</div> | |
| 765 | <div class="lp-term-out lp-term-ok">✓ gluecron-pr, gluecron-issue skills ready</div> | |
| 766 | </div> | |
| 767 | </div> | |
| 768 | </div> | |
| 769 | </section> | |
| 770 | ||
| 771 | {/* ── Closing CTA ────────────────────────────────────────── */} | |
| 772 | <section class="lp-cta"> | |
| 773 | <div class="lp-wrap lp-cta-in"> | |
| 774 | <div class="lp-kicker">Ready when you are</div> | |
| 775 | <h2 class="lp-cta-h">Stop managing the platform.<br />Start shipping the product.</h2> | |
| 776 | <p class="lp-cta-sub"> | |
| 777 | Free to start. Self-hosted-friendly. MCP-native. | |
| 778 | Migrate from GitHub in one command. | |
| 779 | </p> | |
| 780 | <div class="lp-cta-btns"> | |
| 781 | <a href="/register" class="lp-btn lp-btn-solid lp-btn-xl"> | |
| 782 | Create your account | |
| 783 | </a> | |
| 784 | <a href="/import" class="lp-btn lp-btn-outline lp-btn-xl"> | |
| 785 | Migrate a repo | |
| 786 | </a> | |
| 787 | </div> | |
| 788 | <div class="lp-cta-links"> | |
| 789 | <a href="/demo">Try the live demo</a> | |
| 790 | <span aria-hidden="true">·</span> | |
| 791 | <a href="/vs-github">Compare to GitHub</a> | |
| 792 | <span aria-hidden="true">·</span> | |
| 793 | <a href="/pricing">See pricing</a> | |
| 794 | </div> | |
| 795 | </div> | |
| 796 | </section> | |
| 797 | ||
| 798 | {/* ── Footer ─────────────────────────────────────────────── */} | |
| 799 | <footer class="lp-footer"> | |
| 800 | <div class="lp-wrap lp-footer-in"> | |
| 801 | <div class="lp-footer-brand"> | |
| 802 | <a href="/" class="lp-logo"> | |
| 803 | <span class="lp-logo-mark" aria-hidden="true" /> | |
| 804 | gluecron | |
| 805 | </a> | |
| 806 | <p class="lp-footer-tag"> | |
| 807 | The AI-native git platform.<br /> | |
| 808 | Self-hosted, auditable, git-native. | |
| 809 | </p> | |
| 810 | </div> | |
| 811 | <div class="lp-footer-cols"> | |
| 812 | <div class="lp-footer-col"> | |
| 813 | <h4>Product</h4> | |
| 814 | <a href="#platform">Features</a> | |
| 815 | <a href="#ai">AI suite</a> | |
| 816 | <a href="#security">Security</a> | |
| 817 | <a href="/pricing">Pricing</a> | |
| 818 | <a href="/vs-github">vs GitHub</a> | |
| 819 | <a href="/sleep-mode">Sleep Mode</a> | |
| 820 | </div> | |
| 821 | <div class="lp-footer-col"> | |
| 822 | <h4>Platform</h4> | |
| 823 | <a href="/explore">Explore repos</a> | |
| 824 | <a href="/demo">Live demo</a> | |
| 825 | <a href="/marketplace">Marketplace</a> | |
| 826 | <a href="/install">Install script</a> | |
| 827 | <a href="/api">API docs</a> | |
| 828 | <a href="/mcp">MCP server</a> | |
| 829 | </div> | |
| 830 | <div class="lp-footer-col"> | |
| 831 | <h4>Developers</h4> | |
| 832 | <a href="/install">CLI install</a> | |
| 833 | <a href="/vscode">VS Code ext</a> | |
| 834 | <a href="/api/graphql">GraphQL explorer</a> | |
| 835 | <a href="/help">Migration guide</a> | |
| 836 | <a href="/import">Import from GitHub</a> | |
| 837 | <a href="/connect/claude-guide">Claude Code guide</a> | |
| 838 | </div> | |
| 839 | <div class="lp-footer-col"> | |
| 840 | <h4>Account</h4> | |
| 841 | <a href="/login">Sign in</a> | |
| 842 | <a href="/register">Register</a> | |
| 843 | <a href="/settings">Settings</a> | |
| 844 | <a href="/settings/billing">Billing</a> | |
| 845 | <a href="/settings/tokens">API tokens</a> | |
| 846 | <a href="/status">Platform status</a> | |
| 847 | </div> | |
| 848 | </div> | |
| 849 | </div> | |
| 850 | <div class="lp-footer-bottom"> | |
| 851 | <div class="lp-wrap lp-footer-bottom-in"> | |
| 852 | <span>© {new Date().getFullYear()} Gluecron</span> | |
| 853 | <span>Self-hosted · Git-native · Claude-first</span> | |
| 854 | </div> | |
| 855 | </div> | |
| 856 | </footer> | |
| 857 | ||
| 858 | <script dangerouslySetInnerHTML={{ __html: copyJs }} /> | |
| 859 | </body> | |
| 860 | </html> | |
| 861 | ); | |
| 862 | }; | |
| 863 | ||
| 864 | // ─── sub-components ──────────────────────────────────────────────── | |
| 865 | ||
| 866 | const LiveCard: FC<{ title: string; children?: any }> = ({ title, children }) => ( | |
| 867 | <article class="lp-live-card"> | |
| 868 | <h3 class="lp-live-card-title">{title}</h3> | |
| 869 | {children} | |
| 870 | </article> | |
| 871 | ); | |
| 872 | ||
| 873 | const PlatformGroup: FC<{ title: string; children: string[] }> = ({ title, children }) => ( | |
| 874 | <div class="lp-plat-group"> | |
| 875 | <h3 class="lp-plat-title">{title}</h3> | |
| 876 | <ul class="lp-plat-list"> | |
| 877 | {children.map((item) => ( | |
| 878 | <li>{item}</li> | |
| 879 | ))} | |
| 880 | </ul> | |
| 881 | </div> | |
| 882 | ); | |
| 883 | ||
| 884 | const PainRow: FC<{ problem: string; fix: string }> = ({ problem, fix }) => ( | |
| 885 | <div class="lp-pain-row"> | |
| 886 | <div class="lp-pain-problem"> | |
| 887 | <span class="lp-pain-icon lp-pain-no" aria-label="Before">✗</span> | |
| 888 | {problem} | |
| 889 | </div> | |
| 890 | <div class="lp-pain-arrow" aria-hidden="true">→</div> | |
| 891 | <div class="lp-pain-fix"> | |
| 892 | <span class="lp-pain-icon lp-pain-yes" aria-label="After">✓</span> | |
| 893 | {fix} | |
| 894 | </div> | |
| 895 | </div> | |
| 896 | ); | |
| 897 | ||
| 898 | interface MockLine { icon: string; color: "brand" | "green" | "red" | "muted"; text: string; } | |
| 899 | ||
| 900 | const AiFeature: FC<{ | |
| 901 | n: string; | |
| 902 | title: string; | |
| 903 | body: string; | |
| 904 | link: { href: string; label: string }; | |
| 905 | mock: MockLine[]; | |
| 906 | reverse?: boolean; | |
| 907 | }> = ({ n, title, body, link, mock, reverse }) => ( | |
| 908 | <div class={`lp-ai-feature${reverse ? " lp-ai-feature-rev" : ""}`}> | |
| 909 | <div class="lp-ai-text"> | |
| 910 | <div class="lp-ai-n">{n}</div> | |
| 911 | <h3 class="lp-ai-title">{title}</h3> | |
| 912 | <p class="lp-ai-body">{body}</p> | |
| 913 | <a href={link.href} class="lp-text-link">{link.label} →</a> | |
| 914 | </div> | |
| 915 | <div class="lp-ai-mock" aria-hidden="true"> | |
| 916 | <div class="lp-mock-bar"> | |
| 917 | <span class="lp-mock-dot" /><span class="lp-mock-dot" /><span class="lp-mock-dot" /> | |
| 918 | </div> | |
| 919 | <div class="lp-mock-body"> | |
| 920 | {mock.map((line) => ( | |
| 921 | <div class={`lp-mock-line lp-mock-${line.color}`}> | |
| 922 | <span class="lp-mock-icon">{line.icon}</span> | |
| 923 | <span>{line.text}</span> | |
| 924 | </div> | |
| 925 | ))} | |
| 926 | </div> | |
| 927 | </div> | |
| 928 | </div> | |
| 929 | ); | |
| 930 | ||
| 931 | const SecurityCard: FC<{ title: string; body: string }> = ({ title, body }) => ( | |
| 932 | <div class="lp-sec-card"> | |
| 933 | <h3 class="lp-sec-card-title">{title}</h3> | |
| 934 | <p class="lp-sec-card-body">{body}</p> | |
| 935 | </div> | |
| 936 | ); | |
| 937 | ||
| 938 | const TeamCard: FC<{ title: string; items: string[] }> = ({ title, items }) => ( | |
| 939 | <div class="lp-team-card"> | |
| 940 | <h3 class="lp-team-title">{title}</h3> | |
| 941 | <ul class="lp-team-list"> | |
| 942 | {items.map((item) => ( | |
| 943 | <li> | |
| 944 | <span aria-hidden="true">✓</span> | |
| 945 | {item} | |
| 946 | </li> | |
| 947 | ))} | |
| 948 | </ul> | |
| 949 | </div> | |
| 950 | ); | |
| 951 | ||
| 952 | const EcoCard: FC<{ title: string; sub: string; body: string; link: string }> = ({ | |
| 953 | title, sub, body, link, | |
| 954 | }) => ( | |
| 955 | <div class="lp-eco-card"> | |
| 956 | <h3 class="lp-eco-title">{title}</h3> | |
| 957 | <div class="lp-eco-sub">{sub}</div> | |
| 958 | <p class="lp-eco-body">{body}</p> | |
| 959 | <a href={link} class="lp-text-link">Learn more →</a> | |
| 960 | </div> | |
| 961 | ); | |
| 962 | ||
| 963 | const CompareRow: FC<{ feature: string; them: string; us: string; ours?: boolean }> = ({ | |
| 964 | feature, them, us, ours, | |
| 965 | }) => ( | |
| 966 | <div class={`lp-cmp-row${ours ? " lp-cmp-ours" : ""}`}> | |
| 967 | <div class="lp-cmp-feature">{feature}</div> | |
| 968 | <div class="lp-cmp-them">{them}</div> | |
| 969 | <div class={`lp-cmp-us${ours ? " lp-cmp-us-hl" : ""}`}>{us}</div> | |
| 970 | </div> | |
| 971 | ); | |
| 972 | ||
| 973 | const PricingCard: FC<{ | |
| 974 | tier: string; price: string; cadence: string; desc: string; | |
| 975 | features: string[]; cta: string; href: string; highlight?: boolean; | |
| 976 | }> = ({ tier, price, cadence, desc, features, cta, href, highlight }) => ( | |
| 977 | <div class={`lp-price-card${highlight ? " lp-price-hl" : ""}`}> | |
| 978 | {highlight && <div class="lp-price-badge">Most popular</div>} | |
| 979 | <div class="lp-price-tier">{tier}</div> | |
| 980 | <div class="lp-price-amount"> | |
| 981 | <span class="lp-price-num">{price}</span> | |
| 982 | <span class="lp-price-cad">{cadence}</span> | |
| 983 | </div> | |
| 984 | <p class="lp-price-desc">{desc}</p> | |
| 985 | <ul class="lp-price-features"> | |
| 986 | {features.map((f) => <li><span aria-hidden="true">✓</span>{f}</li>)} | |
| 987 | </ul> | |
| 988 | <a | |
| 989 | href={href} | |
| 990 | class={`lp-btn ${highlight ? "lp-btn-solid" : "lp-btn-outline"} lp-btn-block`} | |
| 991 | > | |
| 992 | {cta} | |
| 993 | </a> | |
| 994 | </div> | |
| 995 | ); | |
| 996 | ||
| 997 | // ─── JavaScript ──────────────────────────────────────────────────── | |
| 998 | ||
| 999 | const copyJs = ` | |
| 1000 | (function(){ | |
| 1001 | document.addEventListener('click',function(e){ | |
| 1002 | var btn=e.target.closest('[data-copy-target]'); | |
| 1003 | if(!btn) return; | |
| 1004 | var el=document.getElementById(btn.getAttribute('data-copy-target')); | |
| 1005 | if(!el) return; | |
| 1006 | var text=el.textContent||''; | |
| 1007 | navigator.clipboard.writeText(text.trim()).then(function(){ | |
| 1008 | var orig=btn.textContent;btn.textContent='Copied!'; | |
| 1009 | setTimeout(function(){btn.textContent=orig;},1500); | |
| 1010 | }).catch(function(){}); | |
| 1011 | }); | |
| 1012 | })(); | |
| 1013 | `; | |
| 1014 | ||
| 1015 | // Plain-JS poller for the live-now section. Hits the L3 endpoints every 30s. | |
| 1016 | const liveNowJs = ` | |
| 1017 | (function(){ | |
| 1018 | try{ | |
| 1019 | var DEMO=${JSON.stringify(DEMO_USERNAME)}; | |
| 1020 | var INTERVAL=30000; | |
| 1021 | function esc(s){return String(s==null?'':s).replace(/[&<>"']/g,function(c){return{'&':'&','<':'<','>':'>','"':'"',"'":'''}[c];});} | |
| 1022 | function rel(v){ | |
| 1023 | if(v==null) return 'just now'; | |
| 1024 | var t=(v instanceof Date)?v.getTime():(typeof v==='number'?v:new Date(v).getTime()); | |
| 1025 | if(!isFinite(t)) return 'just now'; | |
| 1026 | var d=Date.now()-t;if(d<0) return 'just now'; | |
| 1027 | var s=Math.floor(d/1000);if(s<60) return 'just now'; | |
| 1028 | var m=Math.floor(s/60);if(m<60) return m+'m ago'; | |
| 1029 | var h=Math.floor(m/60);if(h<24) return h+'h ago'; | |
| 1030 | return Math.floor(h/24)+'d ago'; | |
| 1031 | } | |
| 1032 | function diffMount(ul,newHtml){ | |
| 1033 | if(!ul) return; | |
| 1034 | var prev={}; | |
| 1035 | ul.querySelectorAll('[data-row-id]').forEach(function(n){prev[n.getAttribute('data-row-id')]=true;}); | |
| 1036 | ul.innerHTML=newHtml; | |
| 1037 | ul.querySelectorAll('[data-row-id]').forEach(function(n){ | |
| 1038 | if(!prev[n.getAttribute('data-row-id')]){n.style.background='rgba(67,83,201,.06)';setTimeout(function(){n.style.background='';},1200);} | |
| 1039 | }); | |
| 1040 | } | |
| 1041 | function pollQueued(){ | |
| 1042 | fetch('/api/v2/demo/queued',{credentials:'omit'}).then(function(r){return r.json();}).then(function(d){ | |
| 1043 | var ul=document.querySelector('[data-livecard="queued"]');if(!ul) return; | |
| 1044 | var items=(d&&d.items)||[]; | |
| 1045 | if(!items.length){ul.innerHTML='<li class="lp-live-empty">Quiet right now.</li>';return;} | |
| 1046 | diffMount(ul,items.slice(0,3).map(function(i){ | |
| 1047 | var id='queued|'+i.repo+'|'+i.number; | |
| 1048 | return '<li class="lp-live-row" data-row-id="'+esc(id)+'"><a href="/'+esc(DEMO)+'/'+esc(i.repo)+'/issues/'+i.number+'" class="lp-live-link"><span class="lp-live-num">#'+i.number+'</span> '+esc(i.title)+'</a><span class="lp-live-meta">'+esc(i.repo)+'</span></li>'; | |
| 1049 | }).join('')); | |
| 1050 | }).catch(function(){}); | |
| 1051 | } | |
| 1052 | function pollMerges(){ | |
| 1053 | fetch('/api/v2/demo/merges',{credentials:'omit'}).then(function(r){return r.json();}).then(function(d){ | |
| 1054 | var ul=document.querySelector('[data-livecard="merges"]');if(!ul) return; | |
| 1055 | var items=(d&&d.items)||[]; | |
| 1056 | if(!items.length){ul.innerHTML='<li class="lp-live-empty">No auto-merges in the last 24h.</li>';return;} | |
| 1057 | diffMount(ul,items.slice(0,3).map(function(m){ | |
| 1058 | var id='merges|'+m.repo+'|'+m.number; | |
| 1059 | return '<li class="lp-live-row" data-row-id="'+esc(id)+'"><a href="/'+esc(DEMO)+'/'+esc(m.repo)+'/pulls/'+m.number+'" class="lp-live-link"><span class="lp-live-num">#'+m.number+'</span> '+esc(m.title)+'</a><span class="lp-live-meta">'+esc(m.repo)+' <span data-rel="'+esc(m.mergedAt)+'">'+esc(rel(m.mergedAt))+'</span></span></li>'; | |
| 1060 | }).join('')); | |
| 1061 | }).catch(function(){}); | |
| 1062 | } | |
| 1063 | function pollReviews(){ | |
| 1064 | fetch('/api/v2/demo/reviews',{credentials:'omit'}).then(function(r){return r.json();}).then(function(d){ | |
| 1065 | var ul=document.querySelector('[data-livecard="reviews"]');if(!ul) return; | |
| 1066 | var cnt=document.querySelector('[data-livecard-count="reviews"]'); | |
| 1067 | if(cnt&&typeof d.count==='number') cnt.textContent=d.count; | |
| 1068 | var items=(d&&d.items)||[]; | |
| 1069 | if(!items.length){ul.innerHTML='<li class="lp-live-empty">No reviews in the last 24h.</li>';return;} | |
| 1070 | diffMount(ul,items.slice(0,2).map(function(r){ | |
| 1071 | var id='reviews|'+r.repo+'|'+r.prNumber; | |
| 1072 | return '<li class="lp-live-row" data-row-id="'+esc(id)+'"><a href="/'+esc(DEMO)+'/'+esc(r.repo)+'/pulls/'+r.prNumber+'" class="lp-live-link"><span class="lp-live-num">#'+r.prNumber+'</span> '+esc(r.commentSnippet)+'</a><span class="lp-live-meta">'+esc(r.repo)+'</span></li>'; | |
| 1073 | }).join('')); | |
| 1074 | }).catch(function(){}); | |
| 1075 | } | |
| 1076 | function pollFeed(){ | |
| 1077 | fetch('/api/v2/demo/activity',{credentials:'omit'}).then(function(r){return r.json();}).then(function(d){ | |
| 1078 | var ul=document.querySelector('[data-livecard="feed"]');if(!ul) return; | |
| 1079 | var entries=(d&&d.entries)||[]; | |
| 1080 | if(!entries.length){ul.innerHTML='<li class="lp-live-empty">Quiet right now.</li>';return;} | |
| 1081 | diffMount(ul,entries.slice(0,6).map(function(e){ | |
| 1082 | var path=(e.ref&&e.ref.type==='pr')?'pulls':'issues'; | |
| 1083 | var num=(e.ref&&e.ref.number)||0; | |
| 1084 | var label=e.kind==='auto_merge.merged'?'auto-merged':e.kind==='ai_build.dispatched'?'AI-built':'AI review'; | |
| 1085 | var kc=String(e.kind||'').replace(/\\./g,'-'); | |
| 1086 | var id=e.kind+'|'+e.repo+'|'+(e.ref&&e.ref.type)+'|'+num; | |
| 1087 | return '<li class="lp-live-feedrow" data-row-id="'+esc(id)+'"><span class="lp-feed-kind lp-feed-kind-'+esc(kc)+'">'+esc(label)+'</span> <a class="lp-live-link" href="/'+esc(DEMO)+'/'+esc(e.repo)+'/'+path+'/'+num+'">'+esc(e.repo)+' #'+num+'</a> <span data-rel="'+esc(e.at)+'">'+esc(rel(e.at))+'</span></li>'; | |
| 1088 | }).join('')); | |
| 1089 | }).catch(function(){}); | |
| 1090 | } | |
| 1091 | function refreshRel(){ | |
| 1092 | document.querySelectorAll('[data-rel]').forEach(function(el){el.textContent=rel(el.getAttribute('data-rel'));}); | |
| 1093 | } | |
| 1094 | function tick(){pollQueued();pollMerges();pollReviews();pollFeed();} | |
| 1095 | setInterval(tick,INTERVAL); | |
| 1096 | setInterval(refreshRel,60000); | |
| 1097 | }catch(err){} | |
| 1098 | })(); | |
| 1099 | `; | |
| 1100 | ||
| 1101 | // ─── CSS ─────────────────────────────────────────────────────────── | |
| 1102 | ||
| 1103 | const css = ` | |
| 1104 | /* ── tokens ─────────────────────────────────────────────────────── */ | |
| 1105 | :root{ | |
| 1106 | --lp-brand: #4353c9; | |
| 1107 | --lp-brand-h: #3848b6; | |
| 1108 | --lp-bg: #ffffff; | |
| 1109 | --lp-soft: #fafafb; | |
| 1110 | --lp-ink: #0a0b0d; | |
| 1111 | --lp-ink-2: #3a3d45; | |
| 1112 | --lp-muted: #6b7280; | |
| 1113 | --lp-border: rgba(0,0,0,.08); | |
| 1114 | --lp-shadow: 0 1px 3px rgba(0,0,0,.04),0 8px 20px rgba(0,0,0,.06); | |
| 1115 | --lp-green: #059669; | |
| 1116 | --lp-red: #dc2626; | |
| 1117 | --lp-max: 1200px; | |
| 1118 | --lp-r: 10px; | |
| 1119 | } | |
| 1120 | *{box-sizing:border-box;margin:0;padding:0} | |
| 1121 | html{scroll-behavior:smooth;font-size:17px;line-height:1.6; | |
| 1122 | -webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility} | |
| 1123 | body{background:var(--lp-bg);color:var(--lp-ink); | |
| 1124 | font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif} | |
| 1125 | a{color:inherit;text-decoration:none} | |
| 1126 | img{display:block;max-width:100%} | |
| 1127 | ||
| 1128 | /* ── layout ─────────────────────────────────────────────────────── */ | |
| 1129 | .lp-wrap{max-width:var(--lp-max);margin:0 auto;padding:0 24px} | |
| 1130 | .lp-sec{padding:96px 0} | |
| 1131 | .lp-sec-soft{background:var(--lp-soft)} | |
| 1132 | ||
| 1133 | /* ── typography ─────────────────────────────────────────────────── */ | |
| 1134 | .lp-h1{font-family:'Inter Tight','Inter',sans-serif;font-weight:800; | |
| 1135 | font-size:clamp(42px,6.5vw,76px);line-height:1.02;letter-spacing:-.025em; | |
| 1136 | color:var(--lp-ink)} | |
| 1137 | .lp-h2{font-family:'Inter Tight','Inter',sans-serif;font-weight:700; | |
| 1138 | font-size:clamp(32px,4.5vw,52px);line-height:1.08;letter-spacing:-.022em; | |
| 1139 | color:var(--lp-ink)} | |
| 1140 | .lp-h2-tight{font-size:clamp(28px,3.5vw,42px)} | |
| 1141 | .lp-kicker{font-size:13px;font-weight:600;letter-spacing:.06em;text-transform:uppercase; | |
| 1142 | color:var(--lp-brand);margin-bottom:12px} | |
| 1143 | .lp-sub{font-size:18px;color:var(--lp-ink-2);line-height:1.65;max-width:60ch} | |
| 1144 | .lp-text-link{color:var(--lp-brand);font-size:15px;font-weight:500} | |
| 1145 | .lp-text-link:hover{color:var(--lp-brand-h);text-decoration:underline} | |
| 1146 | code{font-family:'JetBrains Mono',ui-monospace,monospace;font-size:.88em; | |
| 1147 | background:rgba(0,0,0,.05);padding:2px 5px;border-radius:4px} | |
| 1148 | ||
| 1149 | /* ── buttons ─────────────────────────────────────────────────────── */ | |
| 1150 | .lp-btn{display:inline-flex;align-items:center;gap:6px;font-family:inherit; | |
| 1151 | font-weight:600;font-size:15px;border-radius:var(--lp-r);padding:10px 18px; | |
| 1152 | border:1px solid transparent;cursor:pointer;text-decoration:none; | |
| 1153 | transition:transform .12s,box-shadow .2s,background .15s,border-color .15s; | |
| 1154 | white-space:nowrap} | |
| 1155 | .lp-btn:hover{transform:translateY(-1px)} | |
| 1156 | .lp-btn-solid{background:var(--lp-ink);color:#fff;border-color:var(--lp-ink)} | |
| 1157 | .lp-btn-solid:hover{box-shadow:0 6px 18px rgba(10,11,13,.22)} | |
| 1158 | .lp-btn-outline{color:var(--lp-ink);border-color:rgba(0,0,0,.2)} | |
| 1159 | .lp-btn-outline:hover{border-color:var(--lp-ink);background:rgba(0,0,0,.03)} | |
| 1160 | .lp-btn-ghost{color:var(--lp-ink-2);border-color:var(--lp-border)} | |
| 1161 | .lp-btn-ghost:hover{color:var(--lp-ink);border-color:rgba(0,0,0,.2)} | |
| 1162 | .lp-btn-lg{padding:13px 22px;font-size:16px;border-radius:12px} | |
| 1163 | .lp-btn-xl{padding:15px 28px;font-size:17px;border-radius:14px} | |
| 1164 | .lp-btn-block{width:100%;justify-content:center} | |
| 1165 | ||
| 1166 | /* ── badge ───────────────────────────────────────────────────────── */ | |
| 1167 | .lp-badge{font-size:12px;font-weight:700;padding:3px 8px;border-radius:6px} | |
| 1168 | .lp-badge-merged{color:var(--lp-green);background:rgba(5,150,105,.1)} | |
| 1169 | ||
| 1170 | /* ── nav ─────────────────────────────────────────────────────────── */ | |
| 1171 | .lp-nav{position:sticky;top:0;z-index:50;background:rgba(255,255,255,.88); | |
| 1172 | backdrop-filter:blur(12px) saturate(180%);border-bottom:1px solid transparent; | |
| 1173 | transition:border-color .2s,background .2s} | |
| 1174 | .lp-nav.stuck{border-bottom-color:var(--lp-border)} | |
| 1175 | .lp-nav-in{max-width:var(--lp-max);margin:0 auto;padding:14px 24px; | |
| 1176 | display:flex;align-items:center;gap:20px} | |
| 1177 | .lp-logo{display:inline-flex;align-items:center;gap:9px; | |
| 1178 | font-family:'Inter Tight',sans-serif;font-weight:700;font-size:18px; | |
| 1179 | letter-spacing:-.02em;color:var(--lp-ink)} | |
| 1180 | .lp-logo:hover{color:var(--lp-ink)} | |
| 1181 | .lp-logo-mark{width:18px;height:18px;border-radius:6px;background:var(--lp-brand);display:inline-block} | |
| 1182 | .lp-nav-links{display:flex;gap:24px;margin-left:10px} | |
| 1183 | .lp-nav-links a{font-size:15px;font-weight:500;color:var(--lp-ink-2);transition:color .15s} | |
| 1184 | .lp-nav-links a:hover{color:var(--lp-ink)} | |
| 1185 | .lp-nav-ctas{margin-left:auto;display:flex;align-items:center;gap:10px} | |
| 1186 | @media(max-width:720px){.lp-nav-links{display:none}.lp-nav-ctas .lp-btn-ghost{display:none}} | |
| 1187 | ||
| 1188 | /* ── hero ────────────────────────────────────────────────────────── */ | |
| 1189 | .lp-hero{padding:80px 0 60px;border-bottom:1px solid var(--lp-border)} | |
| 1190 | .lp-hero-in{display:grid;grid-template-columns:1fr 1fr;gap:64px;align-items:center} | |
| 1191 | .lp-hero-text{display:flex;flex-direction:column;gap:0} | |
| 1192 | .lp-hero-text .lp-kicker{margin-bottom:16px} | |
| 1193 | .lp-hero-sub{font-size:18px;color:var(--lp-ink-2);line-height:1.65;margin:20px 0 0} | |
| 1194 | .lp-hero-ctas{display:flex;gap:12px;flex-wrap:wrap;margin-top:28px} | |
| 1195 | .lp-hero-links{display:flex;gap:10px;align-items:center;margin-top:14px;font-size:14px;color:var(--lp-muted)} | |
| 1196 | .lp-hero-links a{color:var(--lp-brand);font-weight:500} | |
| 1197 | .lp-hero-links span{color:var(--lp-border)} | |
| 1198 | .lp-hero-stats{display:flex;gap:28px;margin-top:32px;padding-top:24px; | |
| 1199 | border-top:1px solid var(--lp-border);list-style:none;flex-wrap:wrap} | |
| 1200 | .lp-hero-stat dt{font-family:'Inter Tight',sans-serif;font-weight:700;font-size:22px; | |
| 1201 | color:var(--lp-ink);letter-spacing:-.01em} | |
| 1202 | .lp-hero-stat dd{font-size:13px;color:var(--lp-muted);margin-top:2px} | |
| 1203 | @media(max-width:900px){ | |
| 1204 | .lp-hero-in{grid-template-columns:1fr} | |
| 1205 | .lp-hero-card{order:-1} | |
| 1206 | } | |
| 1207 | ||
| 1208 | /* ── hero card mock ──────────────────────────────────────────────── */ | |
| 1209 | .lp-hero-card{background:#fff;border:1px solid var(--lp-border);border-radius:16px; | |
| 1210 | box-shadow:var(--lp-shadow);overflow:hidden} | |
| 1211 | .lp-hc-bar{display:flex;align-items:center;gap:6px;padding:10px 14px; | |
| 1212 | border-bottom:1px solid var(--lp-border);background:var(--lp-soft)} | |
| 1213 | .lp-hc-dot{width:10px;height:10px;border-radius:50%;background:#d1d5db} | |
| 1214 | .lp-hc-path{margin-left:8px;font-family:'JetBrains Mono',monospace;font-size:12px;color:var(--lp-muted)} | |
| 1215 | .lp-hc-body{padding:18px} | |
| 1216 | .lp-hc-pr-row{display:flex;align-items:center;gap:10px;margin-bottom:14px} | |
| 1217 | .lp-hc-title{font-size:14px;font-weight:600;color:var(--lp-ink)} | |
| 1218 | .lp-hc-review{display:flex;gap:10px;margin-bottom:14px} | |
| 1219 | .lp-hc-ava{width:30px;height:30px;border-radius:50%;background:var(--lp-brand); | |
| 1220 | color:#fff;font-size:13px;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0} | |
| 1221 | .lp-hc-rev{flex:1} | |
| 1222 | .lp-hc-rev-head{font-size:12.5px;font-weight:600;color:var(--lp-ink-2);margin-bottom:4px} | |
| 1223 | .lp-hc-rev-head em{color:var(--lp-green);font-style:normal} | |
| 1224 | .lp-hc-rev-body{font-size:13px;color:var(--lp-ink-2);line-height:1.5} | |
| 1225 | .lp-hc-checks{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px} | |
| 1226 | .lp-check{font-family:'JetBrains Mono',monospace;font-size:12px;padding:3px 8px; | |
| 1227 | border-radius:5px;font-weight:500} | |
| 1228 | .lp-check-ok{color:var(--lp-green);background:rgba(5,150,105,.08)} | |
| 1229 | .lp-hc-meta{font-size:12px;color:var(--lp-muted);font-family:'JetBrains Mono',monospace} | |
| 1230 | ||
| 1231 | /* ── trust strip ─────────────────────────────────────────────────── */ | |
| 1232 | .lp-trust-strip{border-bottom:1px solid var(--lp-border);padding:14px 0;background:var(--lp-soft)} | |
| 1233 | .lp-trust-in{display:flex;align-items:center;gap:20px;flex-wrap:wrap; | |
| 1234 | font-size:13.5px;font-weight:500;color:var(--lp-muted);justify-content:center} | |
| 1235 | .lp-sep{width:1px;height:14px;background:var(--lp-border)} | |
| 1236 | ||
| 1237 | /* ── section header ─────────────────────────────────────────────── */ | |
| 1238 | .lp-sec-head{max-width:680px;margin-bottom:52px} | |
| 1239 | .lp-sec-head .lp-h2{margin:8px 0 16px} | |
| 1240 | ||
| 1241 | /* ── live-now ────────────────────────────────────────────────────── */ | |
| 1242 | .lp-live-dot{color:var(--lp-brand);font-size:10px;margin-right:4px} | |
| 1243 | .lp-live-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:32px} | |
| 1244 | @media(max-width:1000px){.lp-live-grid{grid-template-columns:repeat(2,1fr)}} | |
| 1245 | @media(max-width:560px){.lp-live-grid{grid-template-columns:1fr}} | |
| 1246 | .lp-live-card{background:#fff;border:1px solid var(--lp-border);border-radius:var(--lp-r); | |
| 1247 | padding:20px;box-shadow:var(--lp-shadow)} | |
| 1248 | .lp-live-card-title{font-size:13px;font-weight:600;color:var(--lp-ink-2); | |
| 1249 | text-transform:uppercase;letter-spacing:.05em;margin-bottom:14px} | |
| 1250 | .lp-live-list{list-style:none;display:flex;flex-direction:column;gap:10px} | |
| 1251 | .lp-live-empty{font-size:13.5px;color:var(--lp-muted)} | |
| 1252 | .lp-live-row{display:flex;flex-direction:column;gap:3px} | |
| 1253 | .lp-live-link{font-size:13.5px;color:var(--lp-ink);font-weight:500;line-height:1.4} | |
| 1254 | .lp-live-link:hover{color:var(--lp-brand)} | |
| 1255 | .lp-live-num{font-family:'JetBrains Mono',monospace;font-size:11.5px;color:var(--lp-muted)} | |
| 1256 | .lp-live-meta{font-size:12px;color:var(--lp-muted)} | |
| 1257 | .lp-live-rel{color:var(--lp-muted)} | |
| 1258 | .lp-live-bignum{display:flex;align-items:baseline;gap:6px;margin-bottom:10px} | |
| 1259 | .lp-live-bignum span:first-child{font-family:'Inter Tight',sans-serif;font-size:32px; | |
| 1260 | font-weight:700;color:var(--lp-ink)} | |
| 1261 | .lp-live-bignum-label{font-size:13.5px;color:var(--lp-muted)} | |
| 1262 | .lp-live-feedrow{display:flex;align-items:baseline;gap:5px;flex-wrap:wrap; | |
| 1263 | font-size:13px;line-height:1.5} | |
| 1264 | .lp-feed-kind{font-size:11.5px;font-weight:600;padding:2px 6px;border-radius:4px} | |
| 1265 | .lp-feed-kind-auto_merge-merged{color:var(--lp-green);background:rgba(5,150,105,.09)} | |
| 1266 | .lp-feed-kind-ai_build-dispatched{color:var(--lp-brand);background:rgba(67,83,201,.09)} | |
| 1267 | .lp-feed-kind-ai_review-posted{color:#7c3aed;background:rgba(124,58,237,.09)} | |
| 1268 | .lp-live-cta{display:flex;align-items:center;gap:16px} | |
| 1269 | ||
| 1270 | /* ── platform grid ───────────────────────────────────────────────── */ | |
| 1271 | .lp-platform-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:2px; | |
| 1272 | border:1px solid var(--lp-border);border-radius:14px;overflow:hidden; | |
| 1273 | background:var(--lp-border)} | |
| 1274 | @media(max-width:900px){.lp-platform-grid{grid-template-columns:repeat(2,1fr)}} | |
| 1275 | @media(max-width:500px){.lp-platform-grid{grid-template-columns:1fr}} | |
| 1276 | .lp-plat-group{background:#fff;padding:24px;display:flex;flex-direction:column;gap:12px} | |
| 1277 | .lp-plat-title{font-size:13px;font-weight:700;text-transform:uppercase; | |
| 1278 | letter-spacing:.06em;color:var(--lp-brand)} | |
| 1279 | .lp-plat-list{list-style:none;display:flex;flex-direction:column;gap:6px} | |
| 1280 | .lp-plat-list li{font-size:13.5px;color:var(--lp-ink-2)} | |
| 1281 | ||
| 1282 | /* ── pain points ─────────────────────────────────────────────────── */ | |
| 1283 | .lp-pain-table{display:flex;flex-direction:column;gap:0; | |
| 1284 | border:1px solid var(--lp-border);border-radius:14px;overflow:hidden} | |
| 1285 | .lp-pain-row{display:grid;grid-template-columns:1fr 40px 1fr;align-items:center; | |
| 1286 | gap:16px;padding:20px 24px;border-bottom:1px solid var(--lp-border)} | |
| 1287 | .lp-pain-row:last-child{border-bottom:0} | |
| 1288 | .lp-pain-row:hover{background:var(--lp-soft)} | |
| 1289 | .lp-pain-problem{display:flex;align-items:flex-start;gap:10px;font-size:15px;color:var(--lp-ink-2)} | |
| 1290 | .lp-pain-fix{display:flex;align-items:flex-start;gap:10px;font-size:15px;color:var(--lp-ink);font-weight:500} | |
| 1291 | .lp-pain-arrow{font-size:18px;color:var(--lp-muted);text-align:center} | |
| 1292 | .lp-pain-icon{font-size:13px;font-weight:700;margin-top:2px;flex-shrink:0} | |
| 1293 | .lp-pain-no{color:var(--lp-red)} | |
| 1294 | .lp-pain-yes{color:var(--lp-green)} | |
| 1295 | @media(max-width:700px){ | |
| 1296 | .lp-pain-row{grid-template-columns:1fr;gap:8px} | |
| 1297 | .lp-pain-arrow{display:none} | |
| 1298 | } | |
| 1299 | ||
| 1300 | /* ── AI features ─────────────────────────────────────────────────── */ | |
| 1301 | .lp-ai-features{display:flex;flex-direction:column;gap:64px} | |
| 1302 | .lp-ai-feature{display:grid;grid-template-columns:1fr 1fr;gap:64px;align-items:center} | |
| 1303 | .lp-ai-feature-rev{direction:rtl} | |
| 1304 | .lp-ai-feature-rev>*{direction:ltr} | |
| 1305 | @media(max-width:800px){.lp-ai-feature,.lp-ai-feature-rev{grid-template-columns:1fr;direction:ltr}} | |
| 1306 | .lp-ai-n{font-family:'JetBrains Mono',monospace;font-size:11px; | |
| 1307 | font-weight:500;color:var(--lp-muted);margin-bottom:10px;letter-spacing:.04em} | |
| 1308 | .lp-ai-title{font-family:'Inter Tight',sans-serif;font-weight:700; | |
| 1309 | font-size:clamp(22px,3vw,30px);line-height:1.12;letter-spacing:-.018em; | |
| 1310 | color:var(--lp-ink);margin-bottom:14px} | |
| 1311 | .lp-ai-body{font-size:16px;color:var(--lp-ink-2);line-height:1.65;margin-bottom:18px} | |
| 1312 | /* AI mock terminal */ | |
| 1313 | .lp-ai-mock{background:#fff;border:1px solid var(--lp-border);border-radius:12px; | |
| 1314 | overflow:hidden;box-shadow:var(--lp-shadow)} | |
| 1315 | .lp-mock-bar{display:flex;align-items:center;gap:6px;padding:10px 14px; | |
| 1316 | border-bottom:1px solid var(--lp-border);background:var(--lp-soft)} | |
| 1317 | .lp-mock-dot{width:10px;height:10px;border-radius:50%;background:#d1d5db} | |
| 1318 | .lp-mock-body{padding:16px;display:flex;flex-direction:column;gap:10px} | |
| 1319 | .lp-mock-line{display:flex;align-items:flex-start;gap:10px;font-size:13.5px;line-height:1.5} | |
| 1320 | .lp-mock-icon{font-family:'JetBrains Mono',monospace;font-size:12px; | |
| 1321 | width:18px;text-align:center;flex-shrink:0;margin-top:1px} | |
| 1322 | .lp-mock-brand .lp-mock-icon,.lp-mock-brand span{color:var(--lp-brand)} | |
| 1323 | .lp-mock-green .lp-mock-icon,.lp-mock-green span{color:var(--lp-green)} | |
| 1324 | .lp-mock-red .lp-mock-icon,.lp-mock-red span{color:var(--lp-red)} | |
| 1325 | .lp-mock-muted .lp-mock-icon,.lp-mock-muted span{color:var(--lp-muted)} | |
| 1326 | ||
| 1327 | /* ── security cards ──────────────────────────────────────────────── */ | |
| 1328 | .lp-security-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px} | |
| 1329 | @media(max-width:900px){.lp-security-grid{grid-template-columns:repeat(2,1fr)}} | |
| 1330 | @media(max-width:560px){.lp-security-grid{grid-template-columns:1fr}} | |
| 1331 | .lp-sec-card{background:#fff;border:1px solid var(--lp-border);border-radius:var(--lp-r); | |
| 1332 | padding:24px;box-shadow:var(--lp-shadow);transition:transform .15s,box-shadow .2s} | |
| 1333 | .lp-sec-card:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,0,.07),0 20px 40px rgba(0,0,0,.06)} | |
| 1334 | .lp-sec-card-title{font-size:15px;font-weight:700;color:var(--lp-ink);margin-bottom:10px} | |
| 1335 | .lp-sec-card-body{font-size:14px;color:var(--lp-ink-2);line-height:1.6} | |
| 1336 | ||
| 1337 | /* ── teams ───────────────────────────────────────────────────────── */ | |
| 1338 | .lp-teams-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:16px} | |
| 1339 | @media(max-width:1000px){.lp-teams-grid{grid-template-columns:repeat(2,1fr)}} | |
| 1340 | @media(max-width:560px){.lp-teams-grid{grid-template-columns:1fr}} | |
| 1341 | .lp-team-card{background:#fff;border:1px solid var(--lp-border);border-radius:var(--lp-r);padding:24px;box-shadow:var(--lp-shadow)} | |
| 1342 | .lp-team-title{font-size:15px;font-weight:700;color:var(--lp-ink);margin-bottom:14px; | |
| 1343 | padding-bottom:12px;border-bottom:1px solid var(--lp-border)} | |
| 1344 | .lp-team-list{list-style:none;display:flex;flex-direction:column;gap:8px} | |
| 1345 | .lp-team-list li{display:flex;align-items:flex-start;gap:8px;font-size:14px;color:var(--lp-ink-2);line-height:1.4} | |
| 1346 | .lp-team-list li span{color:var(--lp-green);font-size:12px;margin-top:2px;flex-shrink:0} | |
| 1347 | ||
| 1348 | /* ── ecosystem ───────────────────────────────────────────────────── */ | |
| 1349 | .lp-eco-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:16px} | |
| 1350 | @media(max-width:900px){.lp-eco-grid{grid-template-columns:repeat(2,1fr)}} | |
| 1351 | @media(max-width:500px){.lp-eco-grid{grid-template-columns:1fr}} | |
| 1352 | .lp-eco-card{background:#fff;border:1px solid var(--lp-border);border-radius:var(--lp-r); | |
| 1353 | padding:24px;box-shadow:var(--lp-shadow);display:flex;flex-direction:column;gap:10px} | |
| 1354 | .lp-eco-title{font-size:15px;font-weight:700;color:var(--lp-ink)} | |
| 1355 | .lp-eco-sub{font-family:'JetBrains Mono',monospace;font-size:12px;color:var(--lp-brand)} | |
| 1356 | .lp-eco-body{font-size:14px;color:var(--lp-ink-2);line-height:1.6;flex:1} | |
| 1357 | ||
| 1358 | /* ── comparison ──────────────────────────────────────────────────── */ | |
| 1359 | .lp-compare{border:1px solid var(--lp-border);border-radius:14px;overflow:hidden} | |
| 1360 | .lp-compare-head{display:grid;grid-template-columns:1fr 160px 160px; | |
| 1361 | gap:0;padding:12px 20px;background:var(--lp-soft); | |
| 1362 | font-size:13px;font-weight:600;color:var(--lp-muted);text-transform:uppercase;letter-spacing:.04em; | |
| 1363 | border-bottom:1px solid var(--lp-border)} | |
| 1364 | .lp-cmp-row{display:grid;grid-template-columns:1fr 160px 160px; | |
| 1365 | gap:0;padding:14px 20px;border-bottom:1px solid var(--lp-border); | |
| 1366 | font-size:14.5px;align-items:center} | |
| 1367 | .lp-cmp-row:last-child{border-bottom:0} | |
| 1368 | .lp-cmp-ours{background:rgba(67,83,201,.025)} | |
| 1369 | .lp-cmp-feature{color:var(--lp-ink-2)} | |
| 1370 | .lp-cmp-them{color:var(--lp-muted);font-size:14px} | |
| 1371 | .lp-cmp-us{color:var(--lp-muted);font-size:14px} | |
| 1372 | .lp-cmp-us-hl{color:var(--lp-green);font-weight:600} | |
| 1373 | .lp-compare-foot{margin-top:16px} | |
| 1374 | @media(max-width:700px){ | |
| 1375 | .lp-compare-head,.lp-cmp-row{grid-template-columns:1fr 100px 100px} | |
| 1376 | .lp-cmp-row{font-size:13px} | |
| 1377 | } | |
| 1378 | ||
| 1379 | /* ── pricing ─────────────────────────────────────────────────────── */ | |
| 1380 | .lp-price-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px} | |
| 1381 | @media(max-width:800px){.lp-price-grid{grid-template-columns:1fr}} | |
| 1382 | .lp-price-card{background:#fff;border:1px solid var(--lp-border);border-radius:14px; | |
| 1383 | padding:28px;box-shadow:var(--lp-shadow);display:flex;flex-direction:column;gap:0;position:relative} | |
| 1384 | .lp-price-hl{border-color:var(--lp-brand)} | |
| 1385 | .lp-price-badge{position:absolute;top:-12px;left:50%;transform:translateX(-50%); | |
| 1386 | background:var(--lp-brand);color:#fff;font-size:12px;font-weight:700; | |
| 1387 | padding:4px 12px;border-radius:999px} | |
| 1388 | .lp-price-tier{font-size:13px;font-weight:700;text-transform:uppercase; | |
| 1389 | letter-spacing:.05em;color:var(--lp-brand);margin-bottom:12px} | |
| 1390 | .lp-price-amount{display:flex;align-items:baseline;gap:6px;margin-bottom:12px} | |
| 1391 | .lp-price-num{font-family:'Inter Tight',sans-serif;font-size:36px;font-weight:800; | |
| 1392 | color:var(--lp-ink);letter-spacing:-.02em} | |
| 1393 | .lp-price-cad{font-size:14px;color:var(--lp-muted)} | |
| 1394 | .lp-price-desc{font-size:14px;color:var(--lp-ink-2);line-height:1.6;margin-bottom:20px} | |
| 1395 | .lp-price-features{list-style:none;display:flex;flex-direction:column;gap:8px; | |
| 1396 | margin-bottom:24px;flex:1} | |
| 1397 | .lp-price-features li{display:flex;align-items:flex-start;gap:8px;font-size:14px;color:var(--lp-ink-2)} | |
| 1398 | .lp-price-features li span{color:var(--lp-green);font-size:12px;margin-top:2px} | |
| 1399 | .lp-price-foot{margin-top:16px} | |
| 1400 | ||
| 1401 | /* ── install section ─────────────────────────────────────────────── */ | |
| 1402 | .lp-install-wrap{display:grid;grid-template-columns:1fr 1fr;gap:64px;align-items:center} | |
| 1403 | @media(max-width:800px){.lp-install-wrap{grid-template-columns:1fr}} | |
| 1404 | .lp-install-text{display:flex;flex-direction:column} | |
| 1405 | .lp-install-text .lp-kicker{margin-bottom:16px} | |
| 1406 | .lp-install-terminal{background:#fff;border:1px solid var(--lp-border);border-radius:14px; | |
| 1407 | overflow:hidden;box-shadow:var(--lp-shadow)} | |
| 1408 | .lp-term-bar{display:flex;align-items:center;gap:6px;padding:10px 16px; | |
| 1409 | background:var(--lp-soft);border-bottom:1px solid var(--lp-border)} | |
| 1410 | .lp-term-dot{width:10px;height:10px;border-radius:50%;background:#d1d5db} | |
| 1411 | .lp-term-title{margin-left:8px;font-size:12px;color:var(--lp-muted);font-family:'JetBrains Mono',monospace} | |
| 1412 | .lp-term-body{padding:16px;display:flex;flex-direction:column;gap:8px} | |
| 1413 | .lp-term-line{display:flex;align-items:center;gap:10px;font-family:'JetBrains Mono',monospace;font-size:13px} | |
| 1414 | .lp-term-prompt{color:var(--lp-brand);font-weight:500} | |
| 1415 | .lp-term-out{font-family:'JetBrains Mono',monospace;font-size:13px;color:var(--lp-muted);padding-left:20px} | |
| 1416 | .lp-term-ok{color:var(--lp-green)} | |
| 1417 | .lp-copy-btn{margin-left:auto;font-size:12px;font-family:'Inter',sans-serif;font-weight:600; | |
| 1418 | padding:4px 10px;border:1px solid var(--lp-border);border-radius:6px; | |
| 1419 | background:#fff;color:var(--lp-ink-2);cursor:pointer;transition:background .15s,color .15s} | |
| 1420 | .lp-copy-btn:hover{background:var(--lp-soft);color:var(--lp-ink)} | |
| 1421 | ||
| 1422 | /* ── closing CTA ─────────────────────────────────────────────────── */ | |
| 1423 | .lp-cta{padding:96px 0;border-top:1px solid var(--lp-border)} | |
| 1424 | .lp-cta-in{text-align:center;display:flex;flex-direction:column;align-items:center;gap:0} | |
| 1425 | .lp-cta-in .lp-kicker{margin-bottom:16px} | |
| 1426 | .lp-cta-h{font-family:'Inter Tight',sans-serif;font-weight:800; | |
| 1427 | font-size:clamp(32px,5vw,56px);line-height:1.06;letter-spacing:-.025em; | |
| 1428 | color:var(--lp-ink);margin-bottom:16px} | |
| 1429 | .lp-cta-sub{font-size:18px;color:var(--lp-ink-2);max-width:52ch;margin-bottom:32px} | |
| 1430 | .lp-cta-btns{display:flex;gap:12px;flex-wrap:wrap;justify-content:center;margin-bottom:20px} | |
| 1431 | .lp-cta-links{display:flex;gap:12px;align-items:center;font-size:14px;color:var(--lp-muted)} | |
| 1432 | .lp-cta-links a{color:var(--lp-brand);font-weight:500} | |
| 1433 | .lp-cta-links span{color:var(--lp-border)} | |
| 1434 | ||
| 1435 | /* ── footer ──────────────────────────────────────────────────────── */ | |
| 1436 | .lp-footer{border-top:1px solid var(--lp-border);padding:64px 0 0} | |
| 1437 | .lp-footer-in{display:grid;grid-template-columns:280px 1fr;gap:64px;margin-bottom:48px} | |
| 1438 | @media(max-width:800px){.lp-footer-in{grid-template-columns:1fr;gap:32px}} | |
| 1439 | .lp-footer-brand{display:flex;flex-direction:column;gap:12px} | |
| 1440 | .lp-footer-tag{font-size:14px;color:var(--lp-muted);line-height:1.6} | |
| 1441 | .lp-footer-cols{display:grid;grid-template-columns:repeat(4,1fr);gap:32px} | |
| 1442 | @media(max-width:700px){.lp-footer-cols{grid-template-columns:repeat(2,1fr)}} | |
| 1443 | .lp-footer-col{display:flex;flex-direction:column;gap:10px} | |
| 1444 | .lp-footer-col h4{font-size:13px;font-weight:700;color:var(--lp-ink); | |
| 1445 | text-transform:uppercase;letter-spacing:.05em;margin-bottom:4px} | |
| 1446 | .lp-footer-col a{font-size:14px;color:var(--lp-muted);transition:color .15s} | |
| 1447 | .lp-footer-col a:hover{color:var(--lp-ink)} | |
| 1448 | .lp-footer-bottom{border-top:1px solid var(--lp-border);padding:20px 0} | |
| 1449 | .lp-footer-bottom-in{display:flex;justify-content:space-between;align-items:center; | |
| 1450 | font-size:13px;color:var(--lp-muted)} | |
| 1451 | ||
| 1452 | /* ── nav stuck JS class ──────────────────────────────────────────── */ | |
| 1453 | .lp-nav.lp-stuck{border-bottom-color:var(--lp-border)} | |
| 1454 | `; |