Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

marketing.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.

marketing.tsxBlame1348 lines · 1 contributor
b0148e9Claude1/**
2 * Marketing surface — public pages that don't fit the app shell.
3 * Pricing, features, about. Logged-out and logged-in safe (softAuth).
4 *
5 * All pages use the new Editorial-Technical design system: .display,
6 * .eyebrow, .section-header, .stagger, .gradient-text utilities are
7 * defined globally in src/views/layout.tsx.
8 */
9
10import { Hono } from "hono";
11import type { FC } from "hono/jsx";
12import { Layout } from "../views/layout";
13import { softAuth } from "../middleware/auth";
14import type { AuthEnv } from "../middleware/auth";
15
16const marketing = new Hono<AuthEnv>();
17marketing.use("*", softAuth);
18
19// ============================================================
20// /pricing
21// ============================================================
22
23marketing.get("/pricing", (c) => {
24 const user = c.get("user");
25 return c.html(
26 <Layout title="Pricing — gluecron" user={user}>
27 <PricingPage />
28 </Layout>,
29 );
30});
31
32const PricingPage: FC = () => (
33 <>
fa880f2Claude34 <style dangerouslySetInnerHTML={{ __html: pricingCss }} />
93812e4Claude35 <div class="mkt-page-pricing mkt-root">
b0148e9Claude36 <header class="mkt-hero">
93812e4Claude37 <div class="mkt-hero-inner">
38 <div class="mkt-eyebrow">
39 <span class="mkt-eyebrow-pill" aria-hidden="true">
40 <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
41 <line x1="12" y1="1" x2="12" y2="23" />
42 <path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
43 </svg>
44 </span>
45 Pricing
46 </div>
47 <h1 class="display mkt-hero-title">
64aa989Claude48 Honest pricing for teams that ship.
93812e4Claude49 </h1>
50 <p class="mkt-hero-sub">
51 Self-hosting is free forever. Hosted plans price the AI calls, not
52 the seats. No one pays per developer.
53 </p>
54 </div>
b0148e9Claude55 </header>
56
57 <section class="mkt-pricing-grid stagger">
58 <PricingTier
59 tier="Free"
60 price="$0"
61 cadence="forever"
62 desc="For personal projects + open source. Full AI suite included."
63 features={[
64 "Unlimited public repos",
65 "3 private repos",
66 "5,000 AI calls / month",
67 "GateTest + auto-repair",
68 "Webhooks + workflows",
69 "Community support",
70 ]}
71 cta="Start free"
72 href="/register"
73 />
74 <PricingTier
75 tier="Pro"
76 price="$12"
77 cadence="per user / month"
78 desc="For working developers. Lifts every quota and adds priority routing."
79 features={[
80 "Unlimited private repos",
81 "100,000 AI calls / month",
82 "Priority AI queue",
83 "Custom domains",
84 "Advanced analytics",
44f1a02Claude85 "EU data residency (Frankfurt)",
b0148e9Claude86 "Email support",
87 ]}
88 cta="Go Pro"
89 href="/settings/billing"
90 highlight
91 />
92 <PricingTier
93 tier="Team"
94 price="$29"
95 cadence="per user / month"
96 desc="For organisations running production on Gluecron."
97 features={[
98 "Everything in Pro",
99 "Unlimited AI calls",
100 "Org-level SSO + SCIM",
101 "Audit log retention",
102 "SLA-backed uptime",
103 "Slack support channel",
104 ]}
105 cta="Talk to us"
106 href="mailto:hello@gluecron.com"
107 />
108 <PricingTier
109 tier="Enterprise"
110 price="Custom"
111 cadence="contact us"
112 desc="On-prem deploy, dedicated capacity, 24/7 incident response."
113 features={[
114 "Everything in Team",
115 "On-prem / VPC deploy",
116 "Dedicated AI capacity",
117 "Private model routing",
118 "DPA + custom contracts",
119 "24/7 incident response",
120 ]}
121 cta="Contact sales"
122 href="mailto:enterprise@gluecron.com"
123 />
124 </section>
125
126 <section class="mkt-section">
127 <div class="section-header">
128 <div class="eyebrow">Self-hosted</div>
129 <h2>Run it on your own metal. No license, no telemetry.</h2>
130 <p>
131 Gluecron is a single Bun binary plus Postgres. Deploy to Fly,
132 Railway, your own VPS, or air-gapped infra. Free forever for
133 self-hosters of any size.
134 </p>
135 </div>
8cfb00eClaude136 <div class="mkt-selfhost-card">
b0148e9Claude137 <div class="mkt-selfhost-grid">
138 <div class="mkt-selfhost-cell">
139 <div class="mkt-selfhost-num">$0</div>
140 <div class="mkt-selfhost-label">License</div>
141 </div>
142 <div class="mkt-selfhost-cell">
143 <div class="mkt-selfhost-num">∞</div>
144 <div class="mkt-selfhost-label">Users</div>
145 </div>
146 <div class="mkt-selfhost-cell">
147 <div class="mkt-selfhost-num">∞</div>
148 <div class="mkt-selfhost-label">Repos</div>
149 </div>
150 <div class="mkt-selfhost-cell">
151 <div class="mkt-selfhost-num">0</div>
152 <div class="mkt-selfhost-label">Telemetry</div>
153 </div>
154 </div>
155 <div class="mkt-selfhost-cta">
156 <a href="/help" class="btn btn-primary btn-lg">Self-host guide</a>
157 <a
158 href="https://github.com/ccantynz-alt/Gluecron.com"
159 class="btn btn-ghost btn-lg"
160 >
161 View source
162 </a>
163 </div>
164 </div>
165 </section>
166
167 <section class="mkt-section">
168 <div class="section-header">
169 <div class="eyebrow">Questions</div>
170 <h2>The fine print, in plain English.</h2>
171 </div>
172 <div class="mkt-faq">
173 <FaqItem
174 q="What counts as an AI call?"
175 a="Every Claude inference: PR review, security scan, spec-to-PR draft, commit message suggestion, chat reply. We don't bill for failed calls or retries on our end."
176 />
177 <FaqItem
178 q="Can I bring my own Anthropic key?"
179 a="Yes. Pro and above can supply ANTHROPIC_API_KEY; calls run against your account and don't count toward our quota. Useful for orgs with prepaid commits or enterprise rate limits."
180 />
181 <FaqItem
182 q="What happens if I hit my AI quota?"
183 a="AI features degrade gracefully — gates still run, code still hosts. AI suggestions queue at the back of the line. No surprise overage bills, ever."
184 />
185 <FaqItem
186 q="Do you charge for runner minutes?"
187 a="No. Workflow runs are unmetered on hosted plans. Self-hosters bring their own compute, of course."
188 />
189 <FaqItem
190 q="What's the uptime SLA?"
191 a="Team and Enterprise carry a 99.9% monthly SLA with credits. Free and Pro are best-effort but we publish live status at /status and historical incidents in CHANGELOG.md."
192 />
193 <FaqItem
194 q="How do I migrate off Gluecron?"
195 a="Same way you migrate off GitHub: git remote set-url and push. We're git-compatible to the byte. No vendor lock, no migration tax."
196 />
44f1a02Claude197 <FaqItem
198 q="Is EU data residency available?"
199 a="Yes. Pro plan and above can choose the EU (Frankfurt) data region when creating a repository. All repository data — git objects, issues, PRs — is stored and processed in the EU region. The region is set at creation time and cannot be changed afterwards. You can configure it in the New Repository form or see the current region on your repository Settings page."
200 />
b0148e9Claude201 </div>
202 </section>
203
204 <CtaBlock />
205 </div>
206 </>
207);
208
209const PricingTier: FC<{
210 tier: string;
211 price: string;
212 cadence: string;
213 desc: string;
214 features: string[];
215 cta: string;
216 href: string;
217 highlight?: boolean;
218}> = ({ tier, price, cadence, desc, features, cta, href, highlight }) => (
219 <div class={`mkt-tier${highlight ? " mkt-tier-hl" : ""}`}>
220 {highlight && <div class="mkt-tier-badge">Most popular</div>}
221 <div class="mkt-tier-name">{tier}</div>
222 <div class="mkt-tier-amount">
223 <span class="mkt-tier-num">{price}</span>
224 <span class="mkt-tier-cad">{cadence}</span>
225 </div>
226 <p class="mkt-tier-desc">{desc}</p>
227 <ul class="mkt-tier-features">
228 {features.map((f) => (
229 <li>
230 <span class="mkt-tier-check">{"✓"}</span>
231 {f}
232 </li>
233 ))}
234 </ul>
235 <a
236 href={href}
237 class={`btn ${highlight ? "btn-primary" : "btn-secondary"} btn-block`}
238 style="margin-top:auto"
239 >
240 {cta}
241 </a>
242 </div>
243);
244
245const FaqItem: FC<{ q: string; a: string }> = ({ q, a }) => (
246 <details class="mkt-faq-item">
247 <summary class="mkt-faq-q">
248 <span>{q}</span>
249 <span class="mkt-faq-toggle" aria-hidden="true">{"+"}</span>
250 </summary>
251 <p class="mkt-faq-a">{a}</p>
252 </details>
253);
254
255// ============================================================
256// /features
257// ============================================================
258
259marketing.get("/features", (c) => {
260 const user = c.get("user");
261 return c.html(
262 <Layout title="Features — gluecron" user={user}>
263 <FeaturesPage />
264 </Layout>,
265 );
266});
267
268const FeaturesPage: FC = () => (
269 <>
fa880f2Claude270 <style dangerouslySetInnerHTML={{ __html: featuresCss }} />
93812e4Claude271 <div class="mkt-page-features mkt-root">
b0148e9Claude272 <header class="mkt-hero">
93812e4Claude273 <div class="mkt-hero-inner">
274 <div class="mkt-eyebrow">
275 <span class="mkt-eyebrow-pill" aria-hidden="true">
276 <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
277 <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
278 </svg>
279 </span>
280 Features
281 </div>
282 <h1 class="display mkt-hero-title">
64aa989Claude283 A complete dev platform. Nothing extra to buy.
93812e4Claude284 </h1>
285 <p class="mkt-hero-sub">
286 Hosting, CI, AI review, security scanning, deploy webhooks,
287 marketplace, packages, pages — every surface you need, ready on
288 day one.
289 </p>
290 </div>
b0148e9Claude291 </header>
292
93812e4Claude293 {/* 3-column feature grid teaser — at-a-glance pitch above the deep dives. */}
294 <section class="mkt-section mkt-feature-teaser">
295 <div class="mkt-teaser-grid">
296 <div class="mkt-teaser-card">
297 <div class="mkt-teaser-icon" aria-hidden="true">
298 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
299 <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
300 </svg>
301 </div>
302 <h3 class="mkt-teaser-title">AI is a teammate</h3>
303 <p class="mkt-teaser-desc">
304 Claude reviews every PR, drafts changelogs, opens incident
305 issues, fixes failing gates — labelled and revertable.
306 </p>
307 </div>
308 <div class="mkt-teaser-card">
309 <div class="mkt-teaser-icon" aria-hidden="true">
310 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
311 <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
312 </svg>
313 </div>
314 <h3 class="mkt-teaser-title">Green by default</h3>
315 <p class="mkt-teaser-desc">
316 Branch protection, secret scanning, required checks, merge
479dcd9Claude317 queue — pre-wired on every new repo, so problems are caught
318 before they ship.
93812e4Claude319 </p>
320 </div>
321 <div class="mkt-teaser-card">
322 <div class="mkt-teaser-icon" aria-hidden="true">
323 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
324 <polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
325 </svg>
326 </div>
327 <h3 class="mkt-teaser-title">Real-time everything</h3>
328 <p class="mkt-teaser-desc">
329 SSE for logs, comments, deploys, presence. No polling, no
330 spinners on tabs you've already loaded.
331 </p>
332 </div>
333 </div>
334 </section>
335
b0148e9Claude336 <FeatureCategory
337 eyebrow="Code intelligence"
338 title="The AI is a teammate, not an upsell."
339 items={[
340 {
341 title: "AI code review",
342 desc: "Real Claude review on every PR open. Inline file/line comments. Idempotent across re-runs.",
343 },
344 {
345 title: "Spec-to-PR",
346 desc: "Drop a feature spec in plain English. AI drafts the entire PR — branch, commits, description.",
347 },
348 {
349 title: "AI security review",
350 desc: "Sonnet 4 reads diffs for OWASP-class issues. Posts as inline review comments, not noise.",
351 },
352 {
353 title: "Auto-repair",
354 desc: "Failed gate? AI tries to fix it and pushes a follow-up commit. Your repo self-corrects.",
355 },
356 {
357 title: "AI commit messages",
358 desc: "One-click 'suggest with AI' on the web editor. Concise, conventional-commit format.",
359 },
360 {
361 title: "AI changelogs",
362 desc: "Generated on release create. Plus an arbitrary-range viewer at /:repo/ai/changelog.",
363 },
364 {
365 title: "AI incident responder",
366 desc: "Failed deploy? AI opens an issue with the failing logs + suggested fix + linked PR.",
367 },
368 {
369 title: "AI test generation",
370 desc: "Drops test stubs for uncovered functions. You review, edit, commit.",
371 },
372 ]}
373 />
374
375 <FeatureCategory
376 eyebrow="Quality gate"
479dcd9Claude377 title="Catch problems before they reach production."
b0148e9Claude378 items={[
379 {
380 title: "GateTest integration",
381 desc: "Push triggers GateTest. Results post back as inline annotations on the commit.",
382 },
383 {
384 title: "Secret scanner",
385 desc: "15 patterns, runs on every push. Blocks the push if a real secret leaks.",
386 },
387 {
388 title: "Branch protection",
389 desc: "Required checks, code-owner reviews, push restrictions, force-push blocking.",
390 },
391 {
392 title: "Repository rulesets",
393 desc: "Named policy bundles. Six rule types from commit message regex to max file size.",
394 },
395 {
396 title: "Required checks matrix",
397 desc: "Per branch-protection list of named checks that must pass before merge.",
398 },
399 {
400 title: "Protected tags",
401 desc: "Owners declare patterns (v*, release-*) that only owners can push.",
402 },
403 {
404 title: "Merge queue",
405 desc: "Serialised merge with re-test against latest base. No more 'green when merged, red on main'.",
406 },
407 {
408 title: "Pre-receive policy",
409 desc: "Ref-name patterns and push policies enforced at the HTTP layer with 403s.",
410 },
411 ]}
412 />
413
414 <FeatureCategory
415 eyebrow="Real-time"
416 title="No polling, no refresh, no waiting."
417 items={[
418 {
419 title: "Live workflow logs",
420 desc: "Step-by-step output streams over SSE the moment your runner emits it.",
421 },
422 {
423 title: "Live PR comments",
424 desc: "New comment in another tab? You see a 'reload to view' banner immediately.",
425 },
426 {
427 title: "Live deploy events",
428 desc: "Crontech-Gluecron event bus pushes deploy state. Watch deploys happen.",
429 },
430 {
431 title: "Live presence",
432 desc: "See who's looking at the same PR right now. Avoid double review work.",
433 },
434 ]}
435 />
436
437 <FeatureCategory
438 eyebrow="Platform"
439 title="Everything GitHub charges extra for."
440 items={[
441 {
442 title: "Workflow runner",
443 desc: "Drop yaml in `.gluecron/workflows/`. Runs on push. Cron triggers, secrets, matrix.",
444 },
445 {
446 title: "Packages registry",
447 desc: "npm protocol. Publish, install, yank with `glc_` PAT auth. Container registry deferred.",
448 },
449 {
450 title: "Pages hosting",
451 desc: "Serves blobs from the latest gh-pages commit. Custom domains, short cache headers.",
452 },
453 {
454 title: "Marketplace + apps",
455 desc: "Install third-party apps with permission scopes. App-bot push auth via ghi_ tokens.",
456 },
457 {
458 title: "Discussions",
459 desc: "Categorised threads, pinned, locked, Q&A answers. Zero extra config.",
460 },
461 {
462 title: "Wikis + Gists + Projects",
463 desc: "All shipped, all free, all integrated. No upsell tier.",
464 },
465 ]}
466 />
467
468 <FeatureCategory
469 eyebrow="Identity + governance"
470 title="Enterprise-tier auth from day one."
471 items={[
472 {
473 title: "TOTP + WebAuthn",
474 desc: "Both 2FA paths shipped. Passkey-only login if you want it.",
475 },
476 {
477 title: "OIDC SSO",
478 desc: "Okta, Azure AD, Auth0, Google Workspace. Auto-create users. Email-domain allowlist.",
479 },
480 {
481 title: "OAuth provider",
482 desc: "Third-party apps request scoped access to user repos. Standard auth-code flow.",
483 },
484 {
485 title: "Personal access tokens",
486 desc: "SHA-256 hashed, scoped, revocable. The clean way to script Gluecron.",
487 },
488 {
489 title: "Audit log (per-user + per-repo)",
490 desc: "Every sensitive action recorded. Browseable at /settings/audit.",
491 },
492 {
493 title: "Site admin panel",
494 desc: "/admin for site-wide flags: registration locks, banners, read-only mode.",
495 },
496 ]}
497 />
498
499 <FeatureCategory
500 eyebrow="Integrations"
501 title="Speak every protocol your tools use."
502 items={[
503 {
504 title: "MCP server",
505 desc: "Claude Desktop, Cursor, Code, Cline plug in natively. Read + scoped write tools.",
506 },
507 {
508 title: "REST API v2",
509 desc: "Full CRUD across resources. Versioned, documented, stable.",
510 },
511 {
512 title: "GraphQL endpoint",
513 desc: "Single-request fetch for complex client views. GraphiQL explorer at /api/graphql.",
514 },
515 {
516 title: "Webhooks",
517 desc: "HMAC-signed outbound to your URLs on push, issue, PR, star, comment, deploy.",
518 },
519 {
520 title: "Smart-HTTP git",
521 desc: "git push, git pull, git clone — exactly what your tools already speak.",
522 },
523 {
524 title: "VS Code extension",
525 desc: "Explain, open-on-web, semantic search, generate tests — all from the editor.",
526 },
527 ]}
528 />
529
530 <CtaBlock />
531 </div>
532 </>
533);
534
535const FeatureCategory: FC<{
536 eyebrow: string;
537 title: string;
538 items: Array<{ title: string; desc: string }>;
539}> = ({ eyebrow, title, items }) => (
540 <section class="mkt-section">
541 <div class="section-header left">
542 <div class="eyebrow">{eyebrow}</div>
543 <h2>{title}</h2>
544 </div>
545 <div class="mkt-feat-grid stagger">
546 {items.map((it) => (
547 <div class="mkt-feat-cell">
548 <h3 class="mkt-feat-title">{it.title}</h3>
549 <p class="mkt-feat-desc">{it.desc}</p>
550 </div>
551 ))}
552 </div>
553 </section>
554);
555
556// ============================================================
557// /about
558// ============================================================
559
560marketing.get("/about", (c) => {
561 const user = c.get("user");
562 return c.html(
563 <Layout title="About — gluecron" user={user}>
564 <AboutPage />
565 </Layout>,
566 );
567});
568
569const AboutPage: FC = () => (
570 <>
fa880f2Claude571 <style dangerouslySetInnerHTML={{ __html: aboutCss }} />
93812e4Claude572 <div class="mkt-page-about mkt-root">
b0148e9Claude573 <header class="mkt-hero">
93812e4Claude574 <div class="mkt-hero-inner">
575 <div class="mkt-eyebrow">
576 <span class="mkt-eyebrow-pill" aria-hidden="true">
577 <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
578 <circle cx="12" cy="12" r="10" />
579 <line x1="12" y1="16" x2="12" y2="12" />
580 <line x1="12" y1="8" x2="12.01" y2="8" />
581 </svg>
582 </span>
583 About
584 </div>
585 <h1 class="display mkt-hero-title">
64aa989Claude586 We're building the git platform for AI-assisted teams.
93812e4Claude587 </h1>
588 <p class="mkt-hero-sub">
479dcd9Claude589 More and more code is written — and reviewed — with AI assistance.
590 The platforms hosting that code were designed for an earlier
591 workflow. Gluecron is built for this one.
93812e4Claude592 </p>
593 </div>
b0148e9Claude594 </header>
595
596 <section class="mkt-section">
597 <div class="mkt-prose">
598 <div class="eyebrow">Mission</div>
599 <h2>An IDE for your repo. A teammate, not a sidebar.</h2>
600 <p>
601 GitHub treats AI as a feature you bolt onto your workflow. We
602 treat it as a peer that opens PRs, reviews diffs, fixes
603 regressions, and ships its own changes — visibly, accountably,
604 with a real identity in your history.
605 </p>
606 <p>
607 Everything is green by default. Every new repo auto-configures
608 gates, branch protection, labels, CODEOWNERS, and a welcome issue.
609 Users opt out per feature. Defaults are maximum-green so
479dcd9Claude610 <strong> broken changes are caught before they reach production
611 — or your customers.</strong>
b0148e9Claude612 </p>
613 </div>
614 </section>
615
616 <section class="mkt-section">
617 <div class="section-header">
618 <div class="eyebrow">Principles</div>
619 <h2>Six rules we don't compromise on.</h2>
620 </div>
621 <div class="mkt-principles stagger">
622 <PrincipleCard
623 n="01"
624 title="No vendor lock"
625 desc="We're git-compatible to the byte. Migrate off Gluecron the same way you migrate off GitHub: git remote set-url. Your code is yours."
626 />
627 <PrincipleCard
628 n="02"
629 title="No surprise bills"
630 desc="AI quotas degrade gracefully when hit. No overage, no automatic upgrade. We publish prices in dollars, not credits."
631 />
632 <PrincipleCard
633 n="03"
634 title="Self-host is first-class"
635 desc="Single Bun binary, single Postgres, zero telemetry. The same product the hosted version runs on, free forever for self-hosters."
636 />
637 <PrincipleCard
638 n="04"
639 title="AI is accountable"
640 desc="Every AI commit is signed by an app-bot identity. Every AI comment is labelled. You can audit, revert, or disable any AI agent at any time."
641 />
642 <PrincipleCard
643 n="05"
644 title="Real-time over polling"
645 desc="SSE for logs, comments, deploys, presence. The web should feel like a desktop app. No spinners on a tab you've already loaded."
646 />
647 <PrincipleCard
648 n="06"
649 title="Open by default"
650 desc="REST + GraphQL + MCP + Smart-HTTP + webhooks. Your tools speak our platform without asking. Programmatic access is not an enterprise tier."
651 />
652 </div>
653 </section>
654
655 <section class="mkt-section">
656 <div class="section-header">
657 <div class="eyebrow">Stack</div>
658 <h2>Built on the boring, fast parts.</h2>
659 </div>
660 <div class="mkt-stack">
661 <StackPill name="Bun" desc="runtime" />
662 <StackPill name="Hono" desc="server framework" />
663 <StackPill name="Drizzle" desc="ORM" />
664 <StackPill name="Neon Postgres" desc="primary database" />
665 <StackPill name="Claude Sonnet 4" desc="AI review + chat" />
666 <StackPill name="Claude Haiku 4.5" desc="AI commits + summaries" />
667 <StackPill name="Smart-HTTP git" desc="protocol" />
668 <StackPill name="Fly.io" desc="deploy target" />
669 </div>
670 </section>
671
672 <section class="mkt-section mkt-contact">
673 <div class="section-header">
674 <div class="eyebrow">Contact</div>
675 <h2>Three places to find us.</h2>
676 </div>
677 <div class="mkt-contact-grid">
678 <ContactCard
679 label="Product + sales"
680 email="hello@gluecron.com"
681 line="For demos, pricing questions, partnership ideas."
682 />
683 <ContactCard
684 label="Security"
685 email="security@gluecron.com"
686 line="Responsible disclosure. PGP key on request."
687 />
688 <ContactCard
689 label="Support"
690 email="support@gluecron.com"
691 line="Bugs, account help, anything that's broken."
692 />
693 </div>
694 </section>
695
696 <CtaBlock />
697 </div>
698 </>
699);
700
701const PrincipleCard: FC<{ n: string; title: string; desc: string }> = ({
702 n,
703 title,
704 desc,
705}) => (
706 <div class="mkt-principle">
707 <div class="mkt-principle-num">{n}</div>
708 <h3 class="mkt-principle-title">{title}</h3>
709 <p class="mkt-principle-desc">{desc}</p>
710 </div>
711);
712
713const StackPill: FC<{ name: string; desc: string }> = ({ name, desc }) => (
714 <div class="mkt-stack-pill">
715 <span class="mkt-stack-name">{name}</span>
716 <span class="mkt-stack-desc">{desc}</span>
717 </div>
718);
719
720const ContactCard: FC<{ label: string; email: string; line: string }> = ({
721 label,
722 email,
723 line,
724}) => (
725 <a href={`mailto:${email}`} class="mkt-contact-card">
726 <div class="mkt-contact-label">{label}</div>
727 <div class="mkt-contact-email">{email}</div>
728 <p class="mkt-contact-line">{line}</p>
729 </a>
730);
731
732// ============================================================
733// Shared closing CTA block
734// ============================================================
735
736const CtaBlock: FC = () => (
737 <section class="mkt-cta">
738 <div class="mkt-cta-card">
739 <div class="mkt-cta-bg" aria-hidden="true" />
740 <div class="eyebrow">Get started</div>
741 <h2 class="mkt-cta-title">
742 Stop maintaining the platform.<br />
8cfb00eClaude743 Start shipping the product.
b0148e9Claude744 </h2>
745 <div class="mkt-cta-buttons">
746 <a href="/register" class="btn btn-primary btn-xl">
747 Create your account
748 <span aria-hidden="true">{"→"}</span>
749 </a>
750 <a href="/import" class="btn btn-ghost btn-xl">
751 Migrate from GitHub
752 </a>
753 </div>
754 </div>
755 </section>
756);
757
758// ============================================================
759// Styles — namespaced under .mkt- so they don't leak
760// ============================================================
761
762const sharedMktCss = `
763 .mkt-root {
764 max-width: 1180px;
765 margin: 0 auto;
766 padding: 0 16px;
767 }
768
93812e4Claude769 /* ───── 2026 hero polish ─────
770 Three pages share the same shell (.mkt-page-pricing/-features/-about);
771 each adds its own page-scoped class to qualify selectors so this
772 block can't leak. */
773 .mkt-page-pricing .mkt-hero,
774 .mkt-page-features .mkt-hero,
775 .mkt-page-about .mkt-hero {
b0148e9Claude776 position: relative;
93812e4Claude777 text-align: center;
778 margin: var(--s-10) auto var(--s-12);
779 max-width: 980px;
780 padding: clamp(28px, 4vw, 56px) clamp(24px, 4vw, 48px);
781 background: var(--bg-elevated);
782 border: 1px solid var(--border);
783 border-radius: 22px;
784 overflow: hidden;
785 box-shadow: 0 1px 0 rgba(255,255,255,0.04), 0 22px 56px -20px rgba(0,0,0,0.45);
b0148e9Claude786 }
93812e4Claude787 .mkt-page-pricing .mkt-hero::before,
788 .mkt-page-features .mkt-hero::before,
789 .mkt-page-about .mkt-hero::before {
b0148e9Claude790 content: '';
791 position: absolute;
93812e4Claude792 top: 0; left: 0; right: 0;
793 height: 2px;
64aa989Claude794 background: var(--border-strong);
795 opacity: 1;
93812e4Claude796 pointer-events: none;
797 z-index: 2;
798 }
799 .mkt-page-pricing .mkt-hero-inner,
800 .mkt-page-features .mkt-hero-inner,
801 .mkt-page-about .mkt-hero-inner { position: relative; z-index: 1; }
802
803 .mkt-page-pricing .mkt-eyebrow,
804 .mkt-page-features .mkt-eyebrow,
805 .mkt-page-about .mkt-eyebrow {
806 display: inline-flex;
807 align-items: center;
808 gap: 8px;
809 font-family: var(--font-mono);
810 font-size: 11.5px;
811 text-transform: uppercase;
812 letter-spacing: 0.14em;
813 color: var(--text-muted);
814 font-weight: 600;
815 margin-bottom: 14px;
b0148e9Claude816 }
93812e4Claude817 .mkt-page-pricing .mkt-eyebrow-pill,
818 .mkt-page-features .mkt-eyebrow-pill,
819 .mkt-page-about .mkt-eyebrow-pill {
820 display: inline-flex;
821 align-items: center;
822 justify-content: center;
823 width: 18px; height: 18px;
824 border-radius: 6px;
479dcd9Claude825 background: rgba(91,110,232,0.12);
826 color: var(--text-link);
827 box-shadow: inset 0 0 0 1px rgba(91,110,232,0.30);
93812e4Claude828 }
829 .mkt-page-pricing .mkt-grad,
830 .mkt-page-features .mkt-grad,
831 .mkt-page-about .mkt-grad {
64aa989Claude832 color: var(--text-strong);
8cfb00eClaude833 background: none;
834 -webkit-background-clip: unset;
835 background-clip: unset;
93812e4Claude836 }
837 .mkt-page-pricing .mkt-hero-title,
838 .mkt-page-features .mkt-hero-title,
839 .mkt-page-about .mkt-hero-title {
840 font-family: var(--font-display);
b0148e9Claude841 font-size: clamp(36px, 6.5vw, 76px);
842 line-height: 1.02;
843 letter-spacing: -0.038em;
93812e4Claude844 font-weight: 800;
b0148e9Claude845 margin: 0 0 var(--s-5);
93812e4Claude846 color: var(--text-strong);
b0148e9Claude847 }
93812e4Claude848 .mkt-page-pricing .mkt-hero-sub,
849 .mkt-page-features .mkt-hero-sub,
850 .mkt-page-about .mkt-hero-sub {
b0148e9Claude851 font-size: clamp(15px, 1.5vw, 18px);
852 color: var(--text-muted);
853 max-width: 640px;
854 margin: 0 auto;
855 line-height: 1.55;
856 }
857
858 /* Section spacing */
859 .mkt-section { margin: var(--s-16) auto; }
860
861 /* Closing CTA */
862 .mkt-cta { margin: var(--s-20) auto var(--s-12); }
863 .mkt-cta-card {
864 position: relative;
865 text-align: center;
866 padding: var(--s-14) var(--s-7);
867 border: 1px solid var(--border-strong);
868 border-radius: var(--r-2xl);
869 background: var(--bg-elevated);
870 overflow: hidden;
871 isolation: isolate;
872 }
873 .mkt-cta-bg {
874 position: absolute;
875 inset: 0;
876 z-index: -1;
64aa989Claude877 background: transparent;
b0148e9Claude878 }
879 .mkt-cta-card .eyebrow { justify-content: center; }
880 .mkt-cta-title {
881 font-family: var(--font-display);
882 font-size: clamp(28px, 4vw, 52px);
883 line-height: 1.05;
884 letter-spacing: -0.03em;
885 font-weight: 600;
886 margin: var(--s-3) 0 var(--s-7);
887 color: var(--text-strong);
888 }
889 .mkt-cta-buttons {
890 display: flex;
891 gap: 12px;
892 justify-content: center;
893 flex-wrap: wrap;
894 }
895
896 @media (max-width: 640px) {
897 .mkt-hero { padding: var(--s-10) 0 var(--s-8); }
898 .mkt-section { margin: var(--s-12) auto; }
899 .mkt-cta-buttons .btn { width: 100%; justify-content: center; }
900 }
901`;
902
903const pricingCss = sharedMktCss + `
904 .mkt-pricing-grid {
905 display: grid;
906 grid-template-columns: repeat(4, 1fr);
907 gap: 12px;
908 margin: var(--s-12) auto var(--s-16);
909 align-items: stretch;
910 }
911 .mkt-tier {
912 position: relative;
913 background: var(--bg-elevated);
914 border: 1px solid var(--border);
915 border-radius: var(--r-lg);
916 padding: var(--s-7) var(--s-6);
917 display: flex;
918 flex-direction: column;
919 gap: var(--s-4);
920 transition: border-color var(--t-base) var(--ease), transform var(--t-base) var(--ease-out-quart);
921 }
922 .mkt-tier:hover { border-color: var(--border-strong); transform: translateY(-3px); }
923 .mkt-tier-hl {
479dcd9Claude924 border-color: rgba(91,110,232,0.40);
925 box-shadow: var(--elev-2), 0 0 0 1px rgba(91,110,232,0.30);
b0148e9Claude926 background:
479dcd9Claude927 linear-gradient(180deg, rgba(91,110,232,0.05), transparent 50%),
b0148e9Claude928 var(--bg-elevated);
929 }
479dcd9Claude930 .mkt-tier-hl:hover { border-color: rgba(91,110,232,0.60); }
b0148e9Claude931 .mkt-tier-badge {
932 position: absolute;
933 top: -10px;
934 left: 50%;
935 transform: translateX(-50%);
936 padding: 3px 12px;
64aa989Claude937 background: var(--accent);
b0148e9Claude938 color: #fff;
939 font-family: var(--font-mono);
940 font-size: 10px;
941 letter-spacing: 0.1em;
942 text-transform: uppercase;
943 font-weight: 600;
944 border-radius: var(--r-full);
479dcd9Claude945 box-shadow: 0 4px 14px -2px rgba(0,0,0,0.40);
b0148e9Claude946 white-space: nowrap;
947 }
948 .mkt-tier-name {
949 font-family: var(--font-mono);
950 font-size: 11px;
951 text-transform: uppercase;
952 letter-spacing: 0.16em;
953 color: var(--text-muted);
954 }
955 .mkt-tier-amount {
956 display: flex;
957 align-items: baseline;
958 gap: 6px;
959 flex-wrap: wrap;
960 }
961 .mkt-tier-num {
962 font-family: var(--font-display);
963 font-size: 36px;
964 font-weight: 600;
965 letter-spacing: -0.03em;
966 color: var(--text-strong);
967 }
968 .mkt-tier-cad {
969 font-size: 12px;
970 color: var(--text-faint);
971 }
972 .mkt-tier-desc {
973 font-size: var(--t-sm);
974 color: var(--text-muted);
975 line-height: 1.5;
976 margin: 0;
977 }
978 .mkt-tier-features {
979 list-style: none;
980 padding: 0;
981 margin: 0;
982 display: flex;
983 flex-direction: column;
984 gap: 7px;
985 font-size: var(--t-sm);
986 color: var(--text);
987 }
988 .mkt-tier-features li {
989 display: flex;
990 align-items: flex-start;
991 gap: 9px;
992 line-height: 1.45;
993 }
994 .mkt-tier-check {
995 color: var(--accent);
996 font-weight: 600;
997 flex-shrink: 0;
998 line-height: 1.45;
999 }
1000
1001 .mkt-selfhost-card {
1002 max-width: 880px;
1003 margin: 0 auto;
1004 padding: var(--s-10);
1005 text-align: center;
1006 }
1007 .mkt-selfhost-grid {
1008 display: grid;
1009 grid-template-columns: repeat(4, 1fr);
1010 gap: var(--s-6);
1011 margin-bottom: var(--s-8);
1012 padding-bottom: var(--s-8);
1013 border-bottom: 1px solid var(--border-subtle);
1014 }
1015 .mkt-selfhost-cell { text-align: center; }
1016 .mkt-selfhost-num {
1017 font-family: var(--font-display);
1018 font-size: 44px;
1019 font-weight: 600;
1020 letter-spacing: -0.03em;
1021 color: var(--text-strong);
1022 line-height: 1;
1023 }
1024 .mkt-selfhost-label {
1025 font-family: var(--font-mono);
1026 font-size: 11px;
1027 text-transform: uppercase;
1028 letter-spacing: 0.14em;
1029 color: var(--text-muted);
1030 margin-top: var(--s-2);
1031 }
1032 .mkt-selfhost-cta {
1033 display: flex;
1034 gap: 12px;
1035 justify-content: center;
1036 flex-wrap: wrap;
1037 }
1038
1039 .mkt-faq {
1040 max-width: 760px;
1041 margin: 0 auto;
1042 border: 1px solid var(--border);
1043 border-radius: var(--r-lg);
1044 overflow: hidden;
1045 background: var(--bg-elevated);
1046 }
1047 .mkt-faq-item {
1048 border-bottom: 1px solid var(--border-subtle);
1049 }
1050 .mkt-faq-item:last-child { border-bottom: none; }
1051 .mkt-faq-q {
1052 display: flex;
1053 justify-content: space-between;
1054 align-items: center;
1055 gap: 16px;
1056 padding: 18px 24px;
1057 cursor: pointer;
1058 font-size: var(--t-md);
1059 font-weight: 500;
1060 color: var(--text-strong);
1061 list-style: none;
1062 transition: background var(--t-fast) var(--ease);
1063 }
1064 .mkt-faq-q::-webkit-details-marker { display: none; }
1065 .mkt-faq-q:hover { background: var(--bg-hover); }
1066 .mkt-faq-toggle {
1067 font-family: var(--font-mono);
1068 font-size: 18px;
1069 color: var(--text-muted);
1070 transition: transform var(--t-base) var(--ease-spring);
1071 flex-shrink: 0;
1072 }
1073 .mkt-faq-item[open] .mkt-faq-toggle { transform: rotate(45deg); color: var(--accent); }
1074 .mkt-faq-a {
1075 padding: 0 24px 20px;
1076 color: var(--text-muted);
1077 font-size: var(--t-sm);
1078 line-height: 1.6;
1079 margin: 0;
1080 }
1081
1082 @media (max-width: 960px) {
1083 .mkt-pricing-grid { grid-template-columns: repeat(2, 1fr); }
1084 }
1085 @media (max-width: 640px) {
1086 .mkt-pricing-grid { grid-template-columns: 1fr; }
1087 .mkt-selfhost-grid { grid-template-columns: repeat(2, 1fr); }
1088 .mkt-selfhost-card { padding: var(--s-7); }
1089 }
1090`;
1091
1092const featuresCss = sharedMktCss + `
93812e4Claude1093 /* 3-col feature teaser — sits between the hero and the deep category grids */
1094 .mkt-page-features .mkt-feature-teaser { margin: var(--s-8) auto var(--s-12); }
1095 .mkt-page-features .mkt-teaser-grid {
1096 display: grid;
1097 grid-template-columns: repeat(3, 1fr);
1098 gap: 14px;
1099 }
1100 .mkt-page-features .mkt-teaser-card {
1101 position: relative;
1102 padding: var(--s-7) var(--s-6);
1103 background: var(--bg-elevated);
1104 border: 1px solid var(--border);
1105 border-radius: 16px;
1106 transition: border-color 180ms ease, transform 180ms ease, box-shadow 180ms ease;
1107 overflow: hidden;
1108 }
1109 .mkt-page-features .mkt-teaser-card::before {
1110 content: '';
1111 position: absolute;
1112 top: 0; left: 0; right: 0;
1113 height: 1px;
479dcd9Claude1114 background: linear-gradient(90deg, transparent 0%, rgba(91,110,232,0.40) 50%, transparent 100%);
1115 opacity: 0.6;
93812e4Claude1116 }
1117 .mkt-page-features .mkt-teaser-card:hover {
479dcd9Claude1118 border-color: rgba(91,110,232,0.40);
93812e4Claude1119 transform: translateY(-3px);
479dcd9Claude1120 box-shadow: 0 14px 36px -18px rgba(0,0,0,0.45);
93812e4Claude1121 }
1122 .mkt-page-features .mkt-teaser-icon {
1123 display: inline-flex;
1124 align-items: center;
1125 justify-content: center;
1126 width: 40px; height: 40px;
1127 border-radius: 12px;
479dcd9Claude1128 background: rgba(91,110,232,0.12);
1129 color: var(--text-link);
1130 box-shadow: inset 0 0 0 1px rgba(91,110,232,0.28);
93812e4Claude1131 margin-bottom: var(--s-4);
1132 }
1133 .mkt-page-features .mkt-teaser-title {
1134 font-family: var(--font-display);
1135 font-size: 18px;
1136 font-weight: 700;
1137 letter-spacing: -0.018em;
1138 color: var(--text-strong);
1139 margin: 0 0 var(--s-2);
1140 }
1141 .mkt-page-features .mkt-teaser-desc {
1142 font-size: var(--t-sm);
1143 color: var(--text-muted);
1144 line-height: 1.55;
1145 margin: 0;
1146 }
1147 @media (max-width: 880px) {
1148 .mkt-page-features .mkt-teaser-grid { grid-template-columns: 1fr; }
1149 }
1150
b0148e9Claude1151 .mkt-feat-grid {
1152 display: grid;
1153 grid-template-columns: repeat(2, 1fr);
1154 gap: 1px;
1155 background: var(--border-subtle);
1156 border: 1px solid var(--border);
1157 border-radius: var(--r-lg);
1158 overflow: hidden;
1159 }
1160 .mkt-feat-cell {
1161 padding: var(--s-6);
1162 background: var(--bg-elevated);
1163 transition: background var(--t-fast) var(--ease);
1164 position: relative;
1165 }
1166 .mkt-feat-cell:hover { background: var(--bg-surface); }
1167 .mkt-feat-cell::before {
1168 content: '';
1169 position: absolute;
1170 left: var(--s-6);
1171 top: var(--s-6);
1172 width: 4px;
1173 height: 4px;
1174 border-radius: 50%;
1175 background: var(--accent);
1176 opacity: 0;
1177 transition: opacity var(--t-fast) var(--ease);
1178 }
1179 .mkt-feat-cell:hover::before { opacity: 1; }
1180 .mkt-feat-title {
1181 font-family: var(--font-display);
1182 font-size: var(--t-md);
1183 font-weight: 600;
1184 letter-spacing: -0.012em;
1185 margin: 0 0 var(--s-2);
1186 color: var(--text-strong);
1187 padding-left: 14px;
1188 }
1189 .mkt-feat-desc {
1190 font-size: var(--t-sm);
1191 color: var(--text-muted);
1192 line-height: 1.55;
1193 margin: 0;
1194 padding-left: 14px;
1195 }
1196 @media (max-width: 720px) {
1197 .mkt-feat-grid { grid-template-columns: 1fr; }
1198 }
1199`;
1200
1201const aboutCss = sharedMktCss + `
1202 .mkt-prose {
1203 max-width: 720px;
1204 margin: 0 auto;
1205 }
1206 .mkt-prose h2 {
1207 font-size: clamp(24px, 3vw, 36px);
1208 line-height: 1.15;
1209 letter-spacing: -0.025em;
1210 margin: var(--s-3) 0 var(--s-5);
1211 }
1212 .mkt-prose p {
1213 color: var(--text);
1214 font-size: var(--t-md);
1215 line-height: 1.7;
1216 margin: 0 0 var(--s-4);
1217 }
1218 .mkt-prose p strong { color: var(--text-strong); font-weight: 600; }
1219
1220 .mkt-principles {
1221 display: grid;
1222 grid-template-columns: repeat(3, 1fr);
1223 gap: 16px;
1224 }
1225 .mkt-principle {
1226 background: var(--bg-elevated);
1227 border: 1px solid var(--border);
1228 border-radius: var(--r-lg);
1229 padding: var(--s-7);
1230 transition: border-color var(--t-base) var(--ease), transform var(--t-base) var(--ease-out-quart);
1231 }
1232 .mkt-principle:hover { border-color: var(--border-strong); transform: translateY(-2px); }
1233 .mkt-principle-num {
1234 font-family: var(--font-mono);
1235 font-size: 11px;
1236 color: var(--accent);
1237 background: var(--accent-gradient-faint);
479dcd9Claude1238 border: 1px solid rgba(91,110,232,0.30);
b0148e9Claude1239 padding: 3px 9px;
1240 border-radius: var(--r-full);
1241 letter-spacing: 0.06em;
1242 display: inline-block;
1243 margin-bottom: var(--s-4);
1244 }
1245 .mkt-principle-title {
1246 font-family: var(--font-display);
1247 font-size: 19px;
1248 font-weight: 600;
1249 letter-spacing: -0.018em;
1250 margin: 0 0 var(--s-2);
1251 color: var(--text-strong);
1252 }
1253 .mkt-principle-desc {
1254 font-size: var(--t-sm);
1255 color: var(--text-muted);
1256 line-height: 1.6;
1257 margin: 0;
1258 }
1259
1260 .mkt-stack {
1261 display: flex;
1262 flex-wrap: wrap;
1263 gap: 10px;
1264 justify-content: center;
1265 max-width: 880px;
1266 margin: 0 auto;
1267 }
1268 .mkt-stack-pill {
1269 display: inline-flex;
1270 align-items: center;
1271 gap: 8px;
1272 padding: 9px 16px;
1273 background: var(--bg-elevated);
1274 border: 1px solid var(--border);
1275 border-radius: var(--r-full);
1276 transition: border-color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
1277 }
479dcd9Claude1278 .mkt-stack-pill:hover { border-color: rgba(91,110,232,0.4); transform: translateY(-1px); }
b0148e9Claude1279 .mkt-stack-name {
1280 font-family: var(--font-display);
1281 font-weight: 600;
1282 color: var(--text-strong);
1283 font-size: var(--t-sm);
1284 letter-spacing: -0.01em;
1285 }
1286 .mkt-stack-desc {
1287 font-family: var(--font-mono);
1288 font-size: 11px;
1289 color: var(--text-faint);
1290 letter-spacing: 0.04em;
1291 }
1292
1293 .mkt-contact-grid {
1294 display: grid;
1295 grid-template-columns: repeat(3, 1fr);
1296 gap: 16px;
1297 max-width: 880px;
1298 margin: 0 auto;
1299 }
1300 .mkt-contact-card {
1301 display: block;
1302 padding: var(--s-7);
1303 background: var(--bg-elevated);
1304 border: 1px solid var(--border);
1305 border-radius: var(--r-lg);
1306 text-decoration: none;
1307 transition: border-color var(--t-base) var(--ease), transform var(--t-base) var(--ease-out-quart);
1308 }
1309 .mkt-contact-card:hover {
479dcd9Claude1310 border-color: rgba(91,110,232,0.4);
b0148e9Claude1311 transform: translateY(-2px);
1312 text-decoration: none;
1313 }
1314 .mkt-contact-label {
1315 font-family: var(--font-mono);
1316 font-size: 11px;
1317 text-transform: uppercase;
1318 letter-spacing: 0.14em;
1319 color: var(--text-muted);
1320 margin-bottom: var(--s-3);
1321 }
1322 .mkt-contact-email {
1323 font-family: var(--font-display);
1324 font-size: var(--t-md);
1325 font-weight: 600;
1326 letter-spacing: -0.012em;
1327 color: var(--accent);
1328 margin-bottom: var(--s-2);
1329 word-break: break-all;
1330 }
1331 .mkt-contact-line {
1332 font-size: var(--t-sm);
1333 color: var(--text-muted);
1334 line-height: 1.5;
1335 margin: 0;
1336 }
1337
1338 @media (max-width: 880px) {
1339 .mkt-principles { grid-template-columns: repeat(2, 1fr); }
1340 .mkt-contact-grid { grid-template-columns: 1fr; }
1341 }
1342 @media (max-width: 560px) {
1343 .mkt-principles { grid-template-columns: 1fr; }
1344 }
1345`;
1346
4551df3Claude1347
b0148e9Claude1348export default marketing;