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