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.tsxBlame1367 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-orb" aria-hidden="true" />
38 <div class="mkt-hero-inner">
39 <div class="mkt-eyebrow">
40 <span class="mkt-eyebrow-pill" aria-hidden="true">
41 <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
42 <line x1="12" y1="1" x2="12" y2="23" />
43 <path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
44 </svg>
45 </span>
46 Pricing
47 </div>
48 <h1 class="display mkt-hero-title">
49 Honest pricing for{" "}
50 <span class="mkt-grad">teams that ship.</span>
51 </h1>
52 <p class="mkt-hero-sub">
53 Self-hosting is free forever. Hosted plans price the AI calls, not
54 the seats. No one pays per developer.
55 </p>
56 </div>
b0148e9Claude57 </header>
58
59 <section class="mkt-pricing-grid stagger">
60 <PricingTier
61 tier="Free"
62 price="$0"
63 cadence="forever"
64 desc="For personal projects + open source. Full AI suite included."
65 features={[
66 "Unlimited public repos",
67 "3 private repos",
68 "5,000 AI calls / month",
69 "GateTest + auto-repair",
70 "Webhooks + workflows",
71 "Community support",
72 ]}
73 cta="Start free"
74 href="/register"
75 />
76 <PricingTier
77 tier="Pro"
78 price="$12"
79 cadence="per user / month"
80 desc="For working developers. Lifts every quota and adds priority routing."
81 features={[
82 "Unlimited private repos",
83 "100,000 AI calls / month",
84 "Priority AI queue",
85 "Custom domains",
86 "Advanced analytics",
87 "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>
137 <div class="mkt-selfhost-card surface-glow">
138 <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 />
198 </div>
199 </section>
200
201 <CtaBlock />
202 </div>
203 </>
204);
205
206const PricingTier: FC<{
207 tier: string;
208 price: string;
209 cadence: string;
210 desc: string;
211 features: string[];
212 cta: string;
213 href: string;
214 highlight?: boolean;
215}> = ({ tier, price, cadence, desc, features, cta, href, highlight }) => (
216 <div class={`mkt-tier${highlight ? " mkt-tier-hl" : ""}`}>
217 {highlight && <div class="mkt-tier-badge">Most popular</div>}
218 <div class="mkt-tier-name">{tier}</div>
219 <div class="mkt-tier-amount">
220 <span class="mkt-tier-num">{price}</span>
221 <span class="mkt-tier-cad">{cadence}</span>
222 </div>
223 <p class="mkt-tier-desc">{desc}</p>
224 <ul class="mkt-tier-features">
225 {features.map((f) => (
226 <li>
227 <span class="mkt-tier-check">{"✓"}</span>
228 {f}
229 </li>
230 ))}
231 </ul>
232 <a
233 href={href}
234 class={`btn ${highlight ? "btn-primary" : "btn-secondary"} btn-block`}
235 style="margin-top:auto"
236 >
237 {cta}
238 </a>
239 </div>
240);
241
242const FaqItem: FC<{ q: string; a: string }> = ({ q, a }) => (
243 <details class="mkt-faq-item">
244 <summary class="mkt-faq-q">
245 <span>{q}</span>
246 <span class="mkt-faq-toggle" aria-hidden="true">{"+"}</span>
247 </summary>
248 <p class="mkt-faq-a">{a}</p>
249 </details>
250);
251
252// ============================================================
253// /features
254// ============================================================
255
256marketing.get("/features", (c) => {
257 const user = c.get("user");
258 return c.html(
259 <Layout title="Features — gluecron" user={user}>
260 <FeaturesPage />
261 </Layout>,
262 );
263});
264
265const FeaturesPage: FC = () => (
266 <>
fa880f2Claude267 <style dangerouslySetInnerHTML={{ __html: featuresCss }} />
93812e4Claude268 <div class="mkt-page-features mkt-root">
b0148e9Claude269 <header class="mkt-hero">
93812e4Claude270 <div class="mkt-hero-orb" aria-hidden="true" />
271 <div class="mkt-hero-inner">
272 <div class="mkt-eyebrow">
273 <span class="mkt-eyebrow-pill" aria-hidden="true">
274 <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
275 <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" />
276 </svg>
277 </span>
278 Features
279 </div>
280 <h1 class="display mkt-hero-title">
281 A complete dev platform.{" "}
282 <span class="mkt-grad">Nothing extra to buy.</span>
283 </h1>
284 <p class="mkt-hero-sub">
285 Hosting, CI, AI review, security scanning, deploy webhooks,
286 marketplace, packages, pages — every surface you need, ready on
287 day one.
288 </p>
289 </div>
b0148e9Claude290 </header>
291
93812e4Claude292 {/* 3-column feature grid teaser — at-a-glance pitch above the deep dives. */}
293 <section class="mkt-section mkt-feature-teaser">
294 <div class="mkt-teaser-grid">
295 <div class="mkt-teaser-card">
296 <div class="mkt-teaser-icon" aria-hidden="true">
297 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
298 <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" />
299 </svg>
300 </div>
301 <h3 class="mkt-teaser-title">AI is a teammate</h3>
302 <p class="mkt-teaser-desc">
303 Claude reviews every PR, drafts changelogs, opens incident
304 issues, fixes failing gates — labelled and revertable.
305 </p>
306 </div>
307 <div class="mkt-teaser-card">
308 <div class="mkt-teaser-icon" aria-hidden="true">
309 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
310 <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
311 </svg>
312 </div>
313 <h3 class="mkt-teaser-title">Green by default</h3>
314 <p class="mkt-teaser-desc">
315 Branch protection, secret scanning, required checks, merge
316 queue — pre-wired on every new repo. Nothing broken ships.
317 </p>
318 </div>
319 <div class="mkt-teaser-card">
320 <div class="mkt-teaser-icon" aria-hidden="true">
321 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
322 <polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
323 </svg>
324 </div>
325 <h3 class="mkt-teaser-title">Real-time everything</h3>
326 <p class="mkt-teaser-desc">
327 SSE for logs, comments, deploys, presence. No polling, no
328 spinners on tabs you've already loaded.
329 </p>
330 </div>
331 </div>
332 </section>
333
b0148e9Claude334 <FeatureCategory
335 eyebrow="Code intelligence"
336 title="The AI is a teammate, not an upsell."
337 items={[
338 {
339 title: "AI code review",
340 desc: "Real Claude review on every PR open. Inline file/line comments. Idempotent across re-runs.",
341 },
342 {
343 title: "Spec-to-PR",
344 desc: "Drop a feature spec in plain English. AI drafts the entire PR — branch, commits, description.",
345 },
346 {
347 title: "AI security review",
348 desc: "Sonnet 4 reads diffs for OWASP-class issues. Posts as inline review comments, not noise.",
349 },
350 {
351 title: "Auto-repair",
352 desc: "Failed gate? AI tries to fix it and pushes a follow-up commit. Your repo self-corrects.",
353 },
354 {
355 title: "AI commit messages",
356 desc: "One-click 'suggest with AI' on the web editor. Concise, conventional-commit format.",
357 },
358 {
359 title: "AI changelogs",
360 desc: "Generated on release create. Plus an arbitrary-range viewer at /:repo/ai/changelog.",
361 },
362 {
363 title: "AI incident responder",
364 desc: "Failed deploy? AI opens an issue with the failing logs + suggested fix + linked PR.",
365 },
366 {
367 title: "AI test generation",
368 desc: "Drops test stubs for uncovered functions. You review, edit, commit.",
369 },
370 ]}
371 />
372
373 <FeatureCategory
374 eyebrow="Quality gate"
375 title="Nothing broken ever reaches production."
376 items={[
377 {
378 title: "GateTest integration",
379 desc: "Push triggers GateTest. Results post back as inline annotations on the commit.",
380 },
381 {
382 title: "Secret scanner",
383 desc: "15 patterns, runs on every push. Blocks the push if a real secret leaks.",
384 },
385 {
386 title: "Branch protection",
387 desc: "Required checks, code-owner reviews, push restrictions, force-push blocking.",
388 },
389 {
390 title: "Repository rulesets",
391 desc: "Named policy bundles. Six rule types from commit message regex to max file size.",
392 },
393 {
394 title: "Required checks matrix",
395 desc: "Per branch-protection list of named checks that must pass before merge.",
396 },
397 {
398 title: "Protected tags",
399 desc: "Owners declare patterns (v*, release-*) that only owners can push.",
400 },
401 {
402 title: "Merge queue",
403 desc: "Serialised merge with re-test against latest base. No more 'green when merged, red on main'.",
404 },
405 {
406 title: "Pre-receive policy",
407 desc: "Ref-name patterns and push policies enforced at the HTTP layer with 403s.",
408 },
409 ]}
410 />
411
412 <FeatureCategory
413 eyebrow="Real-time"
414 title="No polling, no refresh, no waiting."
415 items={[
416 {
417 title: "Live workflow logs",
418 desc: "Step-by-step output streams over SSE the moment your runner emits it.",
419 },
420 {
421 title: "Live PR comments",
422 desc: "New comment in another tab? You see a 'reload to view' banner immediately.",
423 },
424 {
425 title: "Live deploy events",
426 desc: "Crontech-Gluecron event bus pushes deploy state. Watch deploys happen.",
427 },
428 {
429 title: "Live presence",
430 desc: "See who's looking at the same PR right now. Avoid double review work.",
431 },
432 ]}
433 />
434
435 <FeatureCategory
436 eyebrow="Platform"
437 title="Everything GitHub charges extra for."
438 items={[
439 {
440 title: "Workflow runner",
441 desc: "Drop yaml in `.gluecron/workflows/`. Runs on push. Cron triggers, secrets, matrix.",
442 },
443 {
444 title: "Packages registry",
445 desc: "npm protocol. Publish, install, yank with `glc_` PAT auth. Container registry deferred.",
446 },
447 {
448 title: "Pages hosting",
449 desc: "Serves blobs from the latest gh-pages commit. Custom domains, short cache headers.",
450 },
451 {
452 title: "Marketplace + apps",
453 desc: "Install third-party apps with permission scopes. App-bot push auth via ghi_ tokens.",
454 },
455 {
456 title: "Discussions",
457 desc: "Categorised threads, pinned, locked, Q&A answers. Zero extra config.",
458 },
459 {
460 title: "Wikis + Gists + Projects",
461 desc: "All shipped, all free, all integrated. No upsell tier.",
462 },
463 ]}
464 />
465
466 <FeatureCategory
467 eyebrow="Identity + governance"
468 title="Enterprise-tier auth from day one."
469 items={[
470 {
471 title: "TOTP + WebAuthn",
472 desc: "Both 2FA paths shipped. Passkey-only login if you want it.",
473 },
474 {
475 title: "OIDC SSO",
476 desc: "Okta, Azure AD, Auth0, Google Workspace. Auto-create users. Email-domain allowlist.",
477 },
478 {
479 title: "OAuth provider",
480 desc: "Third-party apps request scoped access to user repos. Standard auth-code flow.",
481 },
482 {
483 title: "Personal access tokens",
484 desc: "SHA-256 hashed, scoped, revocable. The clean way to script Gluecron.",
485 },
486 {
487 title: "Audit log (per-user + per-repo)",
488 desc: "Every sensitive action recorded. Browseable at /settings/audit.",
489 },
490 {
491 title: "Site admin panel",
492 desc: "/admin for site-wide flags: registration locks, banners, read-only mode.",
493 },
494 ]}
495 />
496
497 <FeatureCategory
498 eyebrow="Integrations"
499 title="Speak every protocol your tools use."
500 items={[
501 {
502 title: "MCP server",
503 desc: "Claude Desktop, Cursor, Code, Cline plug in natively. Read + scoped write tools.",
504 },
505 {
506 title: "REST API v2",
507 desc: "Full CRUD across resources. Versioned, documented, stable.",
508 },
509 {
510 title: "GraphQL endpoint",
511 desc: "Single-request fetch for complex client views. GraphiQL explorer at /api/graphql.",
512 },
513 {
514 title: "Webhooks",
515 desc: "HMAC-signed outbound to your URLs on push, issue, PR, star, comment, deploy.",
516 },
517 {
518 title: "Smart-HTTP git",
519 desc: "git push, git pull, git clone — exactly what your tools already speak.",
520 },
521 {
522 title: "VS Code extension",
523 desc: "Explain, open-on-web, semantic search, generate tests — all from the editor.",
524 },
525 ]}
526 />
527
528 <CtaBlock />
529 </div>
530 </>
531);
532
533const FeatureCategory: FC<{
534 eyebrow: string;
535 title: string;
536 items: Array<{ title: string; desc: string }>;
537}> = ({ eyebrow, title, items }) => (
538 <section class="mkt-section">
539 <div class="section-header left">
540 <div class="eyebrow">{eyebrow}</div>
541 <h2>{title}</h2>
542 </div>
543 <div class="mkt-feat-grid stagger">
544 {items.map((it) => (
545 <div class="mkt-feat-cell">
546 <h3 class="mkt-feat-title">{it.title}</h3>
547 <p class="mkt-feat-desc">{it.desc}</p>
548 </div>
549 ))}
550 </div>
551 </section>
552);
553
554// ============================================================
555// /about
556// ============================================================
557
558marketing.get("/about", (c) => {
559 const user = c.get("user");
560 return c.html(
561 <Layout title="About — gluecron" user={user}>
562 <AboutPage />
563 </Layout>,
564 );
565});
566
567const AboutPage: FC = () => (
568 <>
fa880f2Claude569 <style dangerouslySetInnerHTML={{ __html: aboutCss }} />
93812e4Claude570 <div class="mkt-page-about mkt-root">
b0148e9Claude571 <header class="mkt-hero">
93812e4Claude572 <div class="mkt-hero-orb" aria-hidden="true" />
573 <div class="mkt-hero-inner">
574 <div class="mkt-eyebrow">
575 <span class="mkt-eyebrow-pill" aria-hidden="true">
576 <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
577 <circle cx="12" cy="12" r="10" />
578 <line x1="12" y1="16" x2="12" y2="12" />
579 <line x1="12" y1="8" x2="12.01" y2="8" />
580 </svg>
581 </span>
582 About
583 </div>
584 <h1 class="display mkt-hero-title">
585 We're building the platform{" "}
586 <span class="mkt-grad">software writes itself on.</span>
587 </h1>
588 <p class="mkt-hero-sub">
589 Most code in 2026 is written by AI. Most reviews are too. The
590 platforms hosting that code were built for a previous era.
591 Gluecron is built for this one.
592 </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
610 <strong> nothing broken ever reaches production, the website,
611 or the customer.</strong>
612 </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 />
743 <span class="gradient-text">Start shipping the product.</span>
744 </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;
794 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
795 opacity: 0.78;
796 pointer-events: none;
797 z-index: 2;
798 }
799 .mkt-page-pricing .mkt-hero-orb,
800 .mkt-page-features .mkt-hero-orb,
801 .mkt-page-about .mkt-hero-orb {
802 position: absolute;
803 inset: -28% -10% auto auto;
804 width: 520px; height: 520px;
805 background: radial-gradient(circle, rgba(140,109,255,0.22), rgba(54,197,214,0.10) 45%, transparent 70%);
806 filter: blur(80px);
807 opacity: 0.75;
b0148e9Claude808 pointer-events: none;
93812e4Claude809 z-index: 0;
810 }
811 .mkt-page-pricing .mkt-hero-inner,
812 .mkt-page-features .mkt-hero-inner,
813 .mkt-page-about .mkt-hero-inner { position: relative; z-index: 1; }
814
815 .mkt-page-pricing .mkt-eyebrow,
816 .mkt-page-features .mkt-eyebrow,
817 .mkt-page-about .mkt-eyebrow {
818 display: inline-flex;
819 align-items: center;
820 gap: 8px;
821 font-family: var(--font-mono);
822 font-size: 11.5px;
823 text-transform: uppercase;
824 letter-spacing: 0.14em;
825 color: var(--text-muted);
826 font-weight: 600;
827 margin-bottom: 14px;
b0148e9Claude828 }
93812e4Claude829 .mkt-page-pricing .mkt-eyebrow-pill,
830 .mkt-page-features .mkt-eyebrow-pill,
831 .mkt-page-about .mkt-eyebrow-pill {
832 display: inline-flex;
833 align-items: center;
834 justify-content: center;
835 width: 18px; height: 18px;
836 border-radius: 6px;
837 background: rgba(140,109,255,0.14);
838 color: #b69dff;
839 box-shadow: inset 0 0 0 1px rgba(140,109,255,0.35);
840 }
841 .mkt-page-pricing .mkt-grad,
842 .mkt-page-features .mkt-grad,
843 .mkt-page-about .mkt-grad {
844 background-image: linear-gradient(135deg, #a48bff 0%, #8c6dff 50%, #36c5d6 100%);
845 -webkit-background-clip: text;
846 background-clip: text;
847 -webkit-text-fill-color: transparent;
848 color: transparent;
849 }
850 .mkt-page-pricing .mkt-hero-title,
851 .mkt-page-features .mkt-hero-title,
852 .mkt-page-about .mkt-hero-title {
853 font-family: var(--font-display);
b0148e9Claude854 font-size: clamp(36px, 6.5vw, 76px);
855 line-height: 1.02;
856 letter-spacing: -0.038em;
93812e4Claude857 font-weight: 800;
b0148e9Claude858 margin: 0 0 var(--s-5);
93812e4Claude859 color: var(--text-strong);
b0148e9Claude860 }
93812e4Claude861 .mkt-page-pricing .mkt-hero-sub,
862 .mkt-page-features .mkt-hero-sub,
863 .mkt-page-about .mkt-hero-sub {
b0148e9Claude864 font-size: clamp(15px, 1.5vw, 18px);
865 color: var(--text-muted);
866 max-width: 640px;
867 margin: 0 auto;
868 line-height: 1.55;
869 }
870
871 /* Section spacing */
872 .mkt-section { margin: var(--s-16) auto; }
873
874 /* Closing CTA */
875 .mkt-cta { margin: var(--s-20) auto var(--s-12); }
876 .mkt-cta-card {
877 position: relative;
878 text-align: center;
879 padding: var(--s-14) var(--s-7);
880 border: 1px solid var(--border-strong);
881 border-radius: var(--r-2xl);
882 background: var(--bg-elevated);
883 overflow: hidden;
884 isolation: isolate;
885 }
886 .mkt-cta-bg {
887 position: absolute;
888 inset: 0;
889 z-index: -1;
890 background:
891 radial-gradient(60% 100% at 50% 0%, rgba(140,109,255,0.16), transparent 65%),
892 radial-gradient(40% 80% at 80% 100%, rgba(54,197,214,0.10), transparent 65%);
893 }
894 .mkt-cta-card .eyebrow { justify-content: center; }
895 .mkt-cta-title {
896 font-family: var(--font-display);
897 font-size: clamp(28px, 4vw, 52px);
898 line-height: 1.05;
899 letter-spacing: -0.03em;
900 font-weight: 600;
901 margin: var(--s-3) 0 var(--s-7);
902 color: var(--text-strong);
903 }
904 .mkt-cta-buttons {
905 display: flex;
906 gap: 12px;
907 justify-content: center;
908 flex-wrap: wrap;
909 }
910
911 @media (max-width: 640px) {
912 .mkt-hero { padding: var(--s-10) 0 var(--s-8); }
913 .mkt-section { margin: var(--s-12) auto; }
914 .mkt-cta-buttons .btn { width: 100%; justify-content: center; }
915 }
916`;
917
918const pricingCss = sharedMktCss + `
919 .mkt-pricing-grid {
920 display: grid;
921 grid-template-columns: repeat(4, 1fr);
922 gap: 12px;
923 margin: var(--s-12) auto var(--s-16);
924 align-items: stretch;
925 }
926 .mkt-tier {
927 position: relative;
928 background: var(--bg-elevated);
929 border: 1px solid var(--border);
930 border-radius: var(--r-lg);
931 padding: var(--s-7) var(--s-6);
932 display: flex;
933 flex-direction: column;
934 gap: var(--s-4);
935 transition: border-color var(--t-base) var(--ease), transform var(--t-base) var(--ease-out-quart);
936 }
937 .mkt-tier:hover { border-color: var(--border-strong); transform: translateY(-3px); }
938 .mkt-tier-hl {
939 border-color: rgba(140,109,255,0.40);
940 box-shadow: var(--elev-2), 0 0 0 1px rgba(140,109,255,0.30);
941 background:
942 linear-gradient(180deg, rgba(140,109,255,0.05), transparent 50%),
943 var(--bg-elevated);
944 }
945 .mkt-tier-hl:hover { border-color: rgba(140,109,255,0.60); }
946 .mkt-tier-badge {
947 position: absolute;
948 top: -10px;
949 left: 50%;
950 transform: translateX(-50%);
951 padding: 3px 12px;
952 background: var(--accent-gradient);
953 color: #fff;
954 font-family: var(--font-mono);
955 font-size: 10px;
956 letter-spacing: 0.1em;
957 text-transform: uppercase;
958 font-weight: 600;
959 border-radius: var(--r-full);
960 box-shadow: 0 4px 14px -2px rgba(140,109,255,0.45);
961 white-space: nowrap;
962 }
963 .mkt-tier-name {
964 font-family: var(--font-mono);
965 font-size: 11px;
966 text-transform: uppercase;
967 letter-spacing: 0.16em;
968 color: var(--text-muted);
969 }
970 .mkt-tier-amount {
971 display: flex;
972 align-items: baseline;
973 gap: 6px;
974 flex-wrap: wrap;
975 }
976 .mkt-tier-num {
977 font-family: var(--font-display);
978 font-size: 36px;
979 font-weight: 600;
980 letter-spacing: -0.03em;
981 color: var(--text-strong);
982 }
983 .mkt-tier-cad {
984 font-size: 12px;
985 color: var(--text-faint);
986 }
987 .mkt-tier-desc {
988 font-size: var(--t-sm);
989 color: var(--text-muted);
990 line-height: 1.5;
991 margin: 0;
992 }
993 .mkt-tier-features {
994 list-style: none;
995 padding: 0;
996 margin: 0;
997 display: flex;
998 flex-direction: column;
999 gap: 7px;
1000 font-size: var(--t-sm);
1001 color: var(--text);
1002 }
1003 .mkt-tier-features li {
1004 display: flex;
1005 align-items: flex-start;
1006 gap: 9px;
1007 line-height: 1.45;
1008 }
1009 .mkt-tier-check {
1010 color: var(--accent);
1011 font-weight: 600;
1012 flex-shrink: 0;
1013 line-height: 1.45;
1014 }
1015
1016 .mkt-selfhost-card {
1017 max-width: 880px;
1018 margin: 0 auto;
1019 padding: var(--s-10);
1020 text-align: center;
1021 }
1022 .mkt-selfhost-grid {
1023 display: grid;
1024 grid-template-columns: repeat(4, 1fr);
1025 gap: var(--s-6);
1026 margin-bottom: var(--s-8);
1027 padding-bottom: var(--s-8);
1028 border-bottom: 1px solid var(--border-subtle);
1029 }
1030 .mkt-selfhost-cell { text-align: center; }
1031 .mkt-selfhost-num {
1032 font-family: var(--font-display);
1033 font-size: 44px;
1034 font-weight: 600;
1035 letter-spacing: -0.03em;
1036 color: var(--text-strong);
1037 line-height: 1;
1038 background: var(--accent-gradient);
1039 -webkit-background-clip: text;
1040 background-clip: text;
1041 -webkit-text-fill-color: transparent;
1042 }
1043 .mkt-selfhost-label {
1044 font-family: var(--font-mono);
1045 font-size: 11px;
1046 text-transform: uppercase;
1047 letter-spacing: 0.14em;
1048 color: var(--text-muted);
1049 margin-top: var(--s-2);
1050 }
1051 .mkt-selfhost-cta {
1052 display: flex;
1053 gap: 12px;
1054 justify-content: center;
1055 flex-wrap: wrap;
1056 }
1057
1058 .mkt-faq {
1059 max-width: 760px;
1060 margin: 0 auto;
1061 border: 1px solid var(--border);
1062 border-radius: var(--r-lg);
1063 overflow: hidden;
1064 background: var(--bg-elevated);
1065 }
1066 .mkt-faq-item {
1067 border-bottom: 1px solid var(--border-subtle);
1068 }
1069 .mkt-faq-item:last-child { border-bottom: none; }
1070 .mkt-faq-q {
1071 display: flex;
1072 justify-content: space-between;
1073 align-items: center;
1074 gap: 16px;
1075 padding: 18px 24px;
1076 cursor: pointer;
1077 font-size: var(--t-md);
1078 font-weight: 500;
1079 color: var(--text-strong);
1080 list-style: none;
1081 transition: background var(--t-fast) var(--ease);
1082 }
1083 .mkt-faq-q::-webkit-details-marker { display: none; }
1084 .mkt-faq-q:hover { background: var(--bg-hover); }
1085 .mkt-faq-toggle {
1086 font-family: var(--font-mono);
1087 font-size: 18px;
1088 color: var(--text-muted);
1089 transition: transform var(--t-base) var(--ease-spring);
1090 flex-shrink: 0;
1091 }
1092 .mkt-faq-item[open] .mkt-faq-toggle { transform: rotate(45deg); color: var(--accent); }
1093 .mkt-faq-a {
1094 padding: 0 24px 20px;
1095 color: var(--text-muted);
1096 font-size: var(--t-sm);
1097 line-height: 1.6;
1098 margin: 0;
1099 }
1100
1101 @media (max-width: 960px) {
1102 .mkt-pricing-grid { grid-template-columns: repeat(2, 1fr); }
1103 }
1104 @media (max-width: 640px) {
1105 .mkt-pricing-grid { grid-template-columns: 1fr; }
1106 .mkt-selfhost-grid { grid-template-columns: repeat(2, 1fr); }
1107 .mkt-selfhost-card { padding: var(--s-7); }
1108 }
1109`;
1110
1111const featuresCss = sharedMktCss + `
93812e4Claude1112 /* 3-col feature teaser — sits between the hero and the deep category grids */
1113 .mkt-page-features .mkt-feature-teaser { margin: var(--s-8) auto var(--s-12); }
1114 .mkt-page-features .mkt-teaser-grid {
1115 display: grid;
1116 grid-template-columns: repeat(3, 1fr);
1117 gap: 14px;
1118 }
1119 .mkt-page-features .mkt-teaser-card {
1120 position: relative;
1121 padding: var(--s-7) var(--s-6);
1122 background: var(--bg-elevated);
1123 border: 1px solid var(--border);
1124 border-radius: 16px;
1125 transition: border-color 180ms ease, transform 180ms ease, box-shadow 180ms ease;
1126 overflow: hidden;
1127 }
1128 .mkt-page-features .mkt-teaser-card::before {
1129 content: '';
1130 position: absolute;
1131 top: 0; left: 0; right: 0;
1132 height: 1px;
1133 background: linear-gradient(90deg, transparent 0%, rgba(140,109,255,0.40) 30%, rgba(54,197,214,0.40) 70%, transparent 100%);
1134 opacity: 0.7;
1135 }
1136 .mkt-page-features .mkt-teaser-card:hover {
1137 border-color: rgba(140,109,255,0.40);
1138 transform: translateY(-3px);
1139 box-shadow: 0 14px 36px -18px rgba(140,109,255,0.30);
1140 }
1141 .mkt-page-features .mkt-teaser-icon {
1142 display: inline-flex;
1143 align-items: center;
1144 justify-content: center;
1145 width: 40px; height: 40px;
1146 border-radius: 12px;
1147 background: linear-gradient(135deg, rgba(140,109,255,0.18), rgba(54,197,214,0.10));
1148 color: #b69dff;
1149 box-shadow: inset 0 0 0 1px rgba(140,109,255,0.30);
1150 margin-bottom: var(--s-4);
1151 }
1152 .mkt-page-features .mkt-teaser-title {
1153 font-family: var(--font-display);
1154 font-size: 18px;
1155 font-weight: 700;
1156 letter-spacing: -0.018em;
1157 color: var(--text-strong);
1158 margin: 0 0 var(--s-2);
1159 }
1160 .mkt-page-features .mkt-teaser-desc {
1161 font-size: var(--t-sm);
1162 color: var(--text-muted);
1163 line-height: 1.55;
1164 margin: 0;
1165 }
1166 @media (max-width: 880px) {
1167 .mkt-page-features .mkt-teaser-grid { grid-template-columns: 1fr; }
1168 }
1169
b0148e9Claude1170 .mkt-feat-grid {
1171 display: grid;
1172 grid-template-columns: repeat(2, 1fr);
1173 gap: 1px;
1174 background: var(--border-subtle);
1175 border: 1px solid var(--border);
1176 border-radius: var(--r-lg);
1177 overflow: hidden;
1178 }
1179 .mkt-feat-cell {
1180 padding: var(--s-6);
1181 background: var(--bg-elevated);
1182 transition: background var(--t-fast) var(--ease);
1183 position: relative;
1184 }
1185 .mkt-feat-cell:hover { background: var(--bg-surface); }
1186 .mkt-feat-cell::before {
1187 content: '';
1188 position: absolute;
1189 left: var(--s-6);
1190 top: var(--s-6);
1191 width: 4px;
1192 height: 4px;
1193 border-radius: 50%;
1194 background: var(--accent);
1195 opacity: 0;
1196 transition: opacity var(--t-fast) var(--ease);
1197 }
1198 .mkt-feat-cell:hover::before { opacity: 1; }
1199 .mkt-feat-title {
1200 font-family: var(--font-display);
1201 font-size: var(--t-md);
1202 font-weight: 600;
1203 letter-spacing: -0.012em;
1204 margin: 0 0 var(--s-2);
1205 color: var(--text-strong);
1206 padding-left: 14px;
1207 }
1208 .mkt-feat-desc {
1209 font-size: var(--t-sm);
1210 color: var(--text-muted);
1211 line-height: 1.55;
1212 margin: 0;
1213 padding-left: 14px;
1214 }
1215 @media (max-width: 720px) {
1216 .mkt-feat-grid { grid-template-columns: 1fr; }
1217 }
1218`;
1219
1220const aboutCss = sharedMktCss + `
1221 .mkt-prose {
1222 max-width: 720px;
1223 margin: 0 auto;
1224 }
1225 .mkt-prose h2 {
1226 font-size: clamp(24px, 3vw, 36px);
1227 line-height: 1.15;
1228 letter-spacing: -0.025em;
1229 margin: var(--s-3) 0 var(--s-5);
1230 }
1231 .mkt-prose p {
1232 color: var(--text);
1233 font-size: var(--t-md);
1234 line-height: 1.7;
1235 margin: 0 0 var(--s-4);
1236 }
1237 .mkt-prose p strong { color: var(--text-strong); font-weight: 600; }
1238
1239 .mkt-principles {
1240 display: grid;
1241 grid-template-columns: repeat(3, 1fr);
1242 gap: 16px;
1243 }
1244 .mkt-principle {
1245 background: var(--bg-elevated);
1246 border: 1px solid var(--border);
1247 border-radius: var(--r-lg);
1248 padding: var(--s-7);
1249 transition: border-color var(--t-base) var(--ease), transform var(--t-base) var(--ease-out-quart);
1250 }
1251 .mkt-principle:hover { border-color: var(--border-strong); transform: translateY(-2px); }
1252 .mkt-principle-num {
1253 font-family: var(--font-mono);
1254 font-size: 11px;
1255 color: var(--accent);
1256 background: var(--accent-gradient-faint);
1257 border: 1px solid rgba(140,109,255,0.30);
1258 padding: 3px 9px;
1259 border-radius: var(--r-full);
1260 letter-spacing: 0.06em;
1261 display: inline-block;
1262 margin-bottom: var(--s-4);
1263 }
1264 .mkt-principle-title {
1265 font-family: var(--font-display);
1266 font-size: 19px;
1267 font-weight: 600;
1268 letter-spacing: -0.018em;
1269 margin: 0 0 var(--s-2);
1270 color: var(--text-strong);
1271 }
1272 .mkt-principle-desc {
1273 font-size: var(--t-sm);
1274 color: var(--text-muted);
1275 line-height: 1.6;
1276 margin: 0;
1277 }
1278
1279 .mkt-stack {
1280 display: flex;
1281 flex-wrap: wrap;
1282 gap: 10px;
1283 justify-content: center;
1284 max-width: 880px;
1285 margin: 0 auto;
1286 }
1287 .mkt-stack-pill {
1288 display: inline-flex;
1289 align-items: center;
1290 gap: 8px;
1291 padding: 9px 16px;
1292 background: var(--bg-elevated);
1293 border: 1px solid var(--border);
1294 border-radius: var(--r-full);
1295 transition: border-color var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
1296 }
1297 .mkt-stack-pill:hover { border-color: rgba(140,109,255,0.4); transform: translateY(-1px); }
1298 .mkt-stack-name {
1299 font-family: var(--font-display);
1300 font-weight: 600;
1301 color: var(--text-strong);
1302 font-size: var(--t-sm);
1303 letter-spacing: -0.01em;
1304 }
1305 .mkt-stack-desc {
1306 font-family: var(--font-mono);
1307 font-size: 11px;
1308 color: var(--text-faint);
1309 letter-spacing: 0.04em;
1310 }
1311
1312 .mkt-contact-grid {
1313 display: grid;
1314 grid-template-columns: repeat(3, 1fr);
1315 gap: 16px;
1316 max-width: 880px;
1317 margin: 0 auto;
1318 }
1319 .mkt-contact-card {
1320 display: block;
1321 padding: var(--s-7);
1322 background: var(--bg-elevated);
1323 border: 1px solid var(--border);
1324 border-radius: var(--r-lg);
1325 text-decoration: none;
1326 transition: border-color var(--t-base) var(--ease), transform var(--t-base) var(--ease-out-quart);
1327 }
1328 .mkt-contact-card:hover {
1329 border-color: rgba(140,109,255,0.4);
1330 transform: translateY(-2px);
1331 text-decoration: none;
1332 }
1333 .mkt-contact-label {
1334 font-family: var(--font-mono);
1335 font-size: 11px;
1336 text-transform: uppercase;
1337 letter-spacing: 0.14em;
1338 color: var(--text-muted);
1339 margin-bottom: var(--s-3);
1340 }
1341 .mkt-contact-email {
1342 font-family: var(--font-display);
1343 font-size: var(--t-md);
1344 font-weight: 600;
1345 letter-spacing: -0.012em;
1346 color: var(--accent);
1347 margin-bottom: var(--s-2);
1348 word-break: break-all;
1349 }
1350 .mkt-contact-line {
1351 font-size: var(--t-sm);
1352 color: var(--text-muted);
1353 line-height: 1.5;
1354 margin: 0;
1355 }
1356
1357 @media (max-width: 880px) {
1358 .mkt-principles { grid-template-columns: repeat(2, 1fr); }
1359 .mkt-contact-grid { grid-template-columns: 1fr; }
1360 }
1361 @media (max-width: 560px) {
1362 .mkt-principles { grid-template-columns: 1fr; }
1363 }
1364`;
1365
4551df3Claude1366
b0148e9Claude1367export default marketing;