Commit d43b626
Eric Bower
·
2026-05-09 11:37:27 -0400 EDT
parent b1eeb51
feat: website
2 files changed,
+897,
-1
M
pico.sh
+1,
-1
1@@ -1,7 +1,7 @@
2 #!/usr/bin/env bash
3 set -euo pipefail
4
5-ZMX_SESSION_PREFIX="${ZMX_SESSION_PREFIX:-ci.}"
6+export ZMX_SESSION_PREFIX="${ZMX_SESSION_PREFIX:-ci.pici}"
7 JOB_ID="${PICO_CI_JOB_ID:-local}"
8 REPO="${PICO_CI_REPO:-unknown}"
9 EVENT_TYPE="${PICO_CI_EVENT_TYPE:-manual}"
+896,
-0
1@@ -0,0 +1,896 @@
2+<!DOCTYPE html>
3+<html lang="en">
4+<head>
5+<meta charset="UTF-8">
6+<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+<title>pici — Your Build Isn't a Log. It's a Terminal.</title>
8+<style>
9+/* ============================================================
10+ pici landing page — terminal aesthetic
11+ Palette: Catppuccin Mocha + terminal green accents
12+ ============================================================ */
13+
14+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
15+
16+:root {
17+ --bg: #1e1e2e;
18+ --bg-deep: #11111b;
19+ --surface: #313244;
20+ --text: #cdd6f4;
21+ --text-dim: #a6adc8;
22+ --blue: #89b4fa;
23+ --green: #a6e3a1;
24+ --red: #f38ba8;
25+ --yellow: #f9e2af;
26+ --mauve: #f5c2e7;
27+ --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
28+}
29+
30+html { scroll-behavior: smooth; }
31+
32+body {
33+ font-family: var(--mono);
34+ background: var(--bg);
35+ color: var(--text);
36+ font-size: 15px;
37+ line-height: 1.6;
38+ overflow-x: hidden;
39+}
40+
41+/* Scanline overlay */
42+body::after {
43+ content: "";
44+ position: fixed;
45+ inset: 0;
46+ pointer-events: none;
47+ background: repeating-linear-gradient(
48+ 0deg,
49+ transparent,
50+ transparent 2px,
51+ rgba(0, 0, 0, 0.03) 2px,
52+ rgba(0, 0, 0, 0.03) 4px
53+ );
54+ z-index: 9999;
55+}
56+
57+a { color: var(--blue); text-decoration: none; }
58+a:hover { text-decoration: underline; }
59+
60+/* ============================================================
61+ Layout
62+ ============================================================ */
63+
64+.container {
65+ max-width: 960px;
66+ margin: 0 auto;
67+ padding: 0 1.5rem;
68+}
69+
70+section {
71+ padding: 5rem 0;
72+ border-bottom: 1px solid var(--surface);
73+}
74+
75+/* ============================================================
76+ Navigation
77+ ============================================================ */
78+
79+nav {
80+ position: sticky;
81+ top: 0;
82+ z-index: 100;
83+ background: var(--bg-deep);
84+ border-bottom: 1px solid var(--surface);
85+ padding: 0.75rem 0;
86+}
87+
88+nav .container {
89+ display: flex;
90+ align-items: center;
91+ justify-content: space-between;
92+}
93+
94+nav .logo {
95+ font-size: 1.1rem;
96+ font-weight: 700;
97+ color: var(--green);
98+}
99+
100+nav .logo span { color: var(--text-dim); font-weight: 400; }
101+
102+nav ul {
103+ list-style: none;
104+ display: flex;
105+ gap: 1.5rem;
106+}
107+
108+nav ul a {
109+ color: var(--text-dim);
110+ font-size: 0.85rem;
111+ text-transform: uppercase;
112+ letter-spacing: 0.05em;
113+}
114+
115+nav ul a:hover { color: var(--text); text-decoration: none; }
116+
117+/* ============================================================
118+ Hero
119+ ============================================================ */
120+
121+.hero {
122+ padding: 7rem 0 5rem;
123+ text-align: center;
124+ border-bottom: 1px solid var(--surface);
125+}
126+
127+.hero h1 {
128+ font-size: clamp(1.8rem, 5vw, 2.8rem);
129+ font-weight: 700;
130+ line-height: 1.2;
131+ margin-bottom: 1rem;
132+}
133+
134+.hero h1 .accent { color: var(--green); }
135+
136+.hero .subtitle {
137+ font-size: 1.05rem;
138+ color: var(--text-dim);
139+ max-width: 600px;
140+ margin: 0 auto 2rem;
141+}
142+
143+.hero .cta-row {
144+ display: flex;
145+ gap: 1rem;
146+ justify-content: center;
147+ flex-wrap: wrap;
148+}
149+
150+.btn {
151+ display: inline-block;
152+ padding: 0.7rem 1.5rem;
153+ border-radius: 4px;
154+ font-family: var(--mono);
155+ font-size: 0.9rem;
156+ font-weight: 600;
157+ cursor: pointer;
158+ border: 1px solid transparent;
159+ transition: all 0.15s ease;
160+}
161+
162+.btn-primary {
163+ background: var(--green);
164+ color: var(--bg-deep);
165+}
166+
167+.btn-primary:hover {
168+ background: #b8f0b3;
169+ text-decoration: none;
170+}
171+
172+.btn-secondary {
173+ background: transparent;
174+ color: var(--text);
175+ border-color: var(--surface);
176+}
177+
178+.btn-secondary:hover {
179+ border-color: var(--text-dim);
180+ text-decoration: none;
181+}
182+
183+/* ============================================================
184+ Terminal window component
185+ ============================================================ */
186+
187+.terminal {
188+ background: var(--bg-deep);
189+ border: 1px solid var(--surface);
190+ border-radius: 8px;
191+ overflow: hidden;
192+ margin: 2rem 0;
193+}
194+
195+.terminal-bar {
196+ display: flex;
197+ align-items: center;
198+ gap: 0.5rem;
199+ padding: 0.6rem 1rem;
200+ background: var(--surface);
201+ font-size: 0.75rem;
202+ color: var(--text-dim);
203+}
204+
205+.terminal-bar .dot {
206+ width: 10px;
207+ height: 10px;
208+ border-radius: 50%;
209+ display: inline-block;
210+}
211+
212+.terminal-bar .dot.red { background: var(--red); }
213+.terminal-bar .dot.yellow { background: var(--yellow); }
214+.terminal-bar .dot.green { background: var(--green); }
215+
216+.terminal-body {
217+ padding: 1rem 1.25rem;
218+ font-size: 0.85rem;
219+ line-height: 1.7;
220+ overflow-x: auto;
221+}
222+
223+.terminal-body .prompt { color: var(--green); }
224+.terminal-body .cmd { color: var(--text); }
225+.terminal-body .output { color: var(--text-dim); }
226+.terminal-body .error { color: var(--red); }
227+.terminal-body .info { color: var(--blue); }
228+.terminal-body .comment { color: #6c7086; }
229+.terminal-body .cursor {
230+ display: inline-block;
231+ width: 8px;
232+ height: 1em;
233+ background: var(--green);
234+ vertical-align: text-bottom;
235+ animation: blink 1s step-end infinite;
236+}
237+
238+@keyframes blink {
239+ 50% { opacity: 0; }
240+}
241+
242+/* ============================================================
243+ Section headers
244+ ============================================================ */
245+
246+.section-label {
247+ font-size: 0.75rem;
248+ text-transform: uppercase;
249+ letter-spacing: 0.1em;
250+ color: var(--mauve);
251+ margin-bottom: 0.5rem;
252+}
253+
254+h2 {
255+ font-size: 1.5rem;
256+ font-weight: 700;
257+ margin-bottom: 1rem;
258+ color: var(--text);
259+}
260+
261+h3 {
262+ font-size: 1.1rem;
263+ font-weight: 600;
264+ margin-bottom: 0.5rem;
265+ color: var(--blue);
266+}
267+
268+p { margin-bottom: 1rem; }
269+
270+/* ============================================================
271+ Two-column layout
272+ ============================================================ */
273+
274+.two-col {
275+ display: grid;
276+ grid-template-columns: 1fr 1fr;
277+ gap: 3rem;
278+ align-items: start;
279+}
280+
281+@media (max-width: 700px) {
282+ .two-col { grid-template-columns: 1fr; }
283+}
284+
285+/* ============================================================
286+ Feature grid
287+ ============================================================ */
288+
289+.feature-grid {
290+ display: grid;
291+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
292+ gap: 1.5rem;
293+ margin-top: 2rem;
294+}
295+
296+.feature-card {
297+ background: var(--bg-deep);
298+ border: 1px solid var(--surface);
299+ border-radius: 8px;
300+ padding: 1.5rem;
301+}
302+
303+.feature-card h3 { margin-top: 0.5rem; }
304+.feature-card .icon { font-size: 1.5rem; }
305+
306+/* ============================================================
307+ Image placeholder
308+ ============================================================ */
309+
310+.img-placeholder {
311+ background: var(--bg-deep);
312+ border: 2px dashed var(--surface);
313+ border-radius: 8px;
314+ display: flex;
315+ flex-direction: column;
316+ align-items: center;
317+ justify-content: center;
318+ min-height: 250px;
319+ color: var(--text-dim);
320+ font-size: 0.8rem;
321+ text-align: center;
322+ padding: 2rem;
323+}
324+
325+.img-placeholder .icon { font-size: 2rem; margin-bottom: 0.5rem; }
326+.img-placeholder .label { font-size: 0.85rem; color: var(--mauve); }
327+.img-placeholder .dims { font-size: 0.7rem; color: #6c7086; margin-top: 0.25rem; }
328+
329+/* ============================================================
330+ Code comparison (old vs new)
331+ ============================================================ */
332+
333+.code-compare {
334+ display: grid;
335+ grid-template-columns: 1fr 1fr;
336+ gap: 1.5rem;
337+}
338+
339+@media (max-width: 700px) {
340+ .code-compare { grid-template-columns: 1fr; }
341+}
342+
343+.code-compare .label {
344+ font-size: 0.7rem;
345+ text-transform: uppercase;
346+ letter-spacing: 0.1em;
347+ color: var(--text-dim);
348+ margin-bottom: 0.5rem;
349+}
350+
351+.code-compare .label.bad { color: var(--red); }
352+.code-compare .label.good { color: var(--green); }
353+
354+/* ============================================================
355+ CTA / signup section
356+ ============================================================ */
357+
358+.cta-section {
359+ text-align: center;
360+ padding: 5rem 0;
361+ border-bottom: none;
362+}
363+
364+.cta-section h2 {
365+ font-size: 1.8rem;
366+ margin-bottom: 0.5rem;
367+}
368+
369+.cta-section .subtitle {
370+ color: var(--text-dim);
371+ margin-bottom: 2rem;
372+}
373+
374+.signup-form {
375+ display: flex;
376+ gap: 0.5rem;
377+ justify-content: center;
378+ flex-wrap: wrap;
379+ max-width: 500px;
380+ margin: 0 auto;
381+}
382+
383+.signup-form input[type="email"] {
384+ flex: 1;
385+ min-width: 200px;
386+ padding: 0.7rem 1rem;
387+ border-radius: 4px;
388+ border: 1px solid var(--surface);
389+ background: var(--bg-deep);
390+ color: var(--text);
391+ font-family: var(--mono);
392+ font-size: 0.9rem;
393+ outline: none;
394+}
395+
396+.signup-form input[type="email"]:focus {
397+ border-color: var(--green);
398+}
399+
400+.signup-form input[type="email"]::placeholder {
401+ color: #6c7086;
402+}
403+
404+.signup-form .btn {
405+ white-space: nowrap;
406+}
407+
408+.signup-note {
409+ font-size: 0.75rem;
410+ color: #6c7086;
411+ margin-top: 1rem;
412+}
413+
414+/* ============================================================
415+ Footer
416+ ============================================================ */
417+
418+footer {
419+ padding: 2rem 0;
420+ border-top: 1px solid var(--surface);
421+ text-align: center;
422+ font-size: 0.8rem;
423+ color: var(--text-dim);
424+}
425+
426+footer a { color: var(--blue); }
427+
428+/* ============================================================
429+ Inline code
430+ ============================================================ */
431+
432+code {
433+ background: var(--surface);
434+ color: var(--mauve);
435+ padding: 0.15em 0.4em;
436+ border-radius: 3px;
437+ font-size: 0.85em;
438+ font-family: var(--mono);
439+}
440+
441+/* ============================================================
442+ Highlighted list
443+ ============================================================ */
444+
445+.check-list {
446+ list-style: none;
447+ padding: 0;
448+}
449+
450+.check-list li {
451+ padding: 0.3rem 0;
452+ padding-left: 1.5rem;
453+ position: relative;
454+}
455+
456+.check-list li::before {
457+ content: "✓";
458+ position: absolute;
459+ left: 0;
460+ color: var(--green);
461+ font-weight: 700;
462+}
463+
464+/* ============================================================
465+ Diagram / flow
466+ ============================================================ */
467+
468+.flow {
469+ display: flex;
470+ align-items: center;
471+ justify-content: center;
472+ gap: 0.5rem;
473+ flex-wrap: wrap;
474+ margin: 2rem 0;
475+ font-size: 0.85rem;
476+}
477+
478+.flow .node {
479+ background: var(--surface);
480+ padding: 0.5rem 1rem;
481+ border-radius: 4px;
482+ font-size: 0.8rem;
483+}
484+
485+.flow .arrow {
486+ color: var(--text-dim);
487+ font-size: 1.2rem;
488+}
489+</style>
490+</head>
491+<body>
492+
493+<!-- ============================================================
494+ NAV
495+ ============================================================ -->
496+<nav>
497+ <div class="container">
498+ <div class="logo">pici <span>— terminal-first CI</span></div>
499+ <ul>
500+ <li><a href="#how">How It Works</a></li>
501+ <li><a href="#features">Features</a></li>
502+ <li><a href="#beta">Beta</a></li>
503+ </ul>
504+ </div>
505+</nav>
506+
507+<!-- ============================================================
508+ HERO
509+ ============================================================ -->
510+<section class="hero">
511+ <div class="container">
512+ <h1>Your Build Isn't a Log.<br><span class="accent">It's a Terminal.</span></h1>
513+ <p class="subtitle">
514+ <code>pici</code> is a CI system where every build step runs in an attachable terminal session.
515+ Your pipeline is a bash script. It runs the same locally and in CI.
516+ </p>
517+ <div class="cta-row">
518+ <a href="#beta" class="btn btn-primary">Sign Up for Beta</a>
519+ <a href="https://github.com/picosh/pici" class="btn btn-secondary">View on GitHub</a>
520+ </div>
521+ </div>
522+</section>
523+
524+<!-- ============================================================
525+ PROBLEM — The CI Pain
526+ ============================================================ -->
527+<section id="problem">
528+ <div class="container">
529+ <div class="two-col">
530+ <div>
531+ <p class="section-label">The Problem</p>
532+ <h2>CI gives you logs. Not answers.</h2>
533+ <p>
534+ A build fails. You scroll through 2,000 lines of log output. You guess what went wrong.
535+ You re-run the job and wait 10 minutes to find out you were wrong. Repeat.
536+ </p>
537+ <p>
538+ Debugging CI should not be an archaeological dig through log files in a browser.
539+ </p>
540+ </div>
541+ <div class="terminal">
542+ <div class="terminal-bar">
543+ <span class="dot red"></span>
544+ <span class="dot yellow"></span>
545+ <span class="dot green"></span>
546+ <span style="margin-left: 0.5rem;">ci-myrepo-test — build log</span>
547+ </div>
548+ <div class="terminal-body">
549+ <div class="output">Line 1847: ...</div>
550+ <div class="output">Line 1848: ...</div>
551+ <div class="output">Line 1849: ...</div>
552+ <div class="error">Line 1850: FAIL: test_connect (db.TimeoutError)</div>
553+ <div class="output">Line 1851: ...</div>
554+ <div class="output">Line 1852: ...</div>
555+ <div class="output">...</div>
556+ <div class="comment"># scroll up... scroll up... scroll up...</div>
557+ <div class="comment"># what was the state before this?</div>
558+ <div class="comment"># re-run the job? wait 10 more minutes?</div>
559+ </div>
560+ </div>
561+ </div>
562+ </div>
563+</section>
564+
565+<!-- ============================================================
566+ SOLUTION — Attach
567+ ============================================================ -->
568+<section id="how">
569+ <div class="container">
570+ <p class="section-label">The Solution</p>
571+ <h2>Jump into your failing build.</h2>
572+ <div class="two-col">
573+ <div>
574+ <p>
575+ Every build step in pici runs as a <strong>real terminal session</strong> — a PTY you can attach to.
576+ No "enable debugging" flag. No "re-run with SSH" button. No waiting for a tunnel.
577+ </p>
578+ <p>
579+ A test fails? Attach to the session, press <code>↑</code> + <code>Enter</code> to rerun the command,
580+ inspect the environment, fix the issue. You're already there.
581+ </p>
582+ <ul class="check-list">
583+ <li><code>zmx attach ci.myrepo.test</code> — you're in</li>
584+ <li>Press <code>↑</code> to rerun the failing command</li>
585+ <li>Inspect files, env vars, network state</li>
586+ <li>No debug mode — it's just a terminal</li>
587+ </ul>
588+ </div>
589+ <div>
590+ <!-- IMAGE PLACEHOLDER: screenshot of zmx attach to a failing session -->
591+ <div class="img-placeholder">
592+ <div class="icon">🖥️</div>
593+ <div class="label">Screenshot: attaching to a failing build session</div>
594+ <div class="dims">~600×400 — show terminal with zmx attach output</div>
595+ </div>
596+ </div>
597+ </div>
598+ </div>
599+</section>
600+
601+<!-- ============================================================
602+ JOB ENGINE — zmx as the parallel task runner
603+ ============================================================ -->
604+<section id="engine">
605+ <div class="container">
606+ <p class="section-label">Job Engine</p>
607+ <h2>Parallel tasks. Zero config. Same commands everywhere.</h2>
608+ <div class="two-col">
609+ <div class="terminal">
610+ <div class="terminal-bar">
611+ <span class="dot red"></span>
612+ <span class="dot yellow"></span>
613+ <span class="dot green"></span>
614+ <span style="margin-left: 0.5rem;">pico.sh</span>
615+ </div>
616+ <div class="terminal-body">
617+ <div><span class="comment">#!/usr/bin/env bash</span></div>
618+ <div><span class="cmd">set -euo pipefail</span></div>
619+ <div> </div>
620+ <div><span class="comment"># These run in parallel — no config needed</span></div>
621+ <div><span class="cmd">zmx run lint docker run golangci-lint run</span></div>
622+ <div><span class="output"> TODO: add example output</span></div>
623+ <div> </div>
624+ <div><span class="cmd">zmx run test go test ./...</span></div>
625+ <div><span class="output"> TODO: add example output</span></div>
626+ <div> </div>
627+ <div><span class="cmd">zmx run build to build -o bin/pici .</span></div>
628+ <div><span class="output"> TODO: add example output</span></div>
629+ <div> </div>
630+ <div><span class="comment"># Wait for all to finish</span></div>
631+ <div><span class="cmd">zmx wait "*"</span></div>
632+ <div> </div>
633+ <div><span class="comment"># Runs identically locally and in CI</span></div>
634+ </div>
635+ </div>
636+ <div>
637+ <p>
638+ Your pipeline is <code>pico.sh</code>: a bash script that runs <strong>identically on your machine
639+ and on the CI runner</strong>. No YAML, no DSL, no "it works on my machine" gap.
640+ </p>
641+ <p>
642+ <strong>zmx</strong> is the job engine. Each <code>zmx run</code> spawns a parallel terminal session.
643+ <code>zmx wait "*"</code> blocks until they all finish. Run tasks sequentially by just calling them
644+ one after another, or in parallel; it's bash, you control the flow.
645+ </p>
646+ <ul class="check-list">
647+ <li>Same <code>zmx run</code> commands locally and in CI</li>
648+ <li>Parallel by default, sequential when you want it</li>
649+ <li>Every step is an attachable terminal session</li>
650+ <li>No YAML matrices, no <code>run: parallel</code> keywords</li>
651+ </ul>
652+ </div>
653+ </div>
654+ </div>
655+</section>
656+
657+<!-- ============================================================
658+ GIT IS OPTIONAL
659+ ============================================================ -->
660+<section id="no-git">
661+ <div class="container">
662+ <p class="section-label">Trigger Model</p>
663+ <h2>Git is optional. rsync + ssh pubsub is the core.</h2>
664+ <div class="two-col">
665+ <div>
666+ <p>
667+ Most CI systems are glued to Git webhooks. pici isn't. The core loop is simple:
668+ <strong>rsync a workspace</strong> and <strong>publish an event over SSH</strong>.
669+ </p>
670+ <p>
671+ Git post-receive hooks are just one trigger. You can fire builds from anything:
672+ a cron job, a file watcher, a webhook receiver on your own server, a button press.
673+ </p>
674+ <div class="flow">
675+ <div class="node">rsync workspace</div>
676+ <div class="arrow">→</div>
677+ <div class="node">ssh pub event</div>
678+ <div class="arrow">→</div>
679+ <div class="node">pico.sh runs</div>
680+ <div class="arrow">→</div>
681+ <div class="node">artifacts synced</div>
682+ </div>
683+ </div>
684+ <div>
685+ <!-- IMAGE PLACEHOLDER: diagram of the rsync + SSH pubsub flow -->
686+ <div class="img-placeholder">
687+ <div class="icon">📡</div>
688+ <div class="label">Diagram: rsync + SSH pubsub trigger flow</div>
689+ <div class="dims">~600×300 — show workspace → rsync → pipe.pico.sh → runner</div>
690+ </div>
691+ </div>
692+ </div>
693+ </div>
694+</section>
695+
696+<!-- ============================================================
697+ SELF-HOST OR MANAGED
698+ ============================================================ -->
699+<section id="deploy">
700+ <div class="container">
701+ <p class="section-label">Deployment</p>
702+ <h2>Self-host or use our managed service.</h2>
703+ <div class="two-col">
704+ <div class="terminal">
705+ <div class="terminal-bar">
706+ <span class="dot red"></span>
707+ <span class="dot yellow"></span>
708+ <span class="dot green"></span>
709+ <span style="margin-left: 0.5rem;">self-hosted</span>
710+ </div>
711+ <div class="terminal-body">
712+ <div><span class="prompt">$ </span><span class="cmd">go build -o pici .</span></div>
713+ <div><span class="prompt">$ </span><span class="cmd">./pici runner --event '...'</span></div>
714+ <div><span class="info">🚀 starting job ci.myrepo.a3f2b8c1</span></div>
715+ <div><span class="info">📦 syncing workspace</span></div>
716+ <div><span class="info">✅ workspace ready</span></div>
717+ <div><span class="info">🔍 found pico.sh</span></div>
718+ <div><span class="info">🏃 launching sessions...</span></div>
719+ <div><span class="info">✅ job launched</span></div>
720+ </div>
721+ </div>
722+ <div class="terminal">
723+ <div class="terminal-bar">
724+ <span class="dot red"></span>
725+ <span class="dot yellow"></span>
726+ <span class="dot green"></span>
727+ <span style="margin-left: 0.5rem;">ci.pico.sh</span>
728+ </div>
729+ <div class="terminal-body">
730+ <div><span class="prompt">$ </span><span class="cmd">echo '{"type":"release"}' | ssh pipe.pico.sh pub build.event</span></div>
731+ <div><span class="output">subscribe to this channel: ssh pipe.pico.sh sub build.event</span></div>
732+ <div> </div>
733+ <div><span class="info">🚀 starting job ci.myrepo.a3f2b8c1</span></div>
734+ <div><span class="info">📦 syncing workspace</span></div>
735+ <div><span class="info">✅ workspace ready</span></div>
736+ <div><span class="info">🏃 launching sessions...</span></div>
737+ <div><span class="info">✅ job launched</span></div>
738+ <div> </div>
739+ <div><span class="comment"># same pico.sh, same zmx sessions</span></div>
740+ <div><span class="comment"># same attachable debugging</span></div>
741+ </div>
742+ </div>
743+ </div>
744+ <p style="text-align:center; margin-top: 1.5rem; color: var(--text-dim);">
745+ Same <code>pico.sh</code>. Same <code>zmx attach</code>. Same experience. Your infra or ours.
746+ </p>
747+ </div>
748+</section>
749+
750+<!-- ============================================================
751+ FEATURES GRID
752+ ============================================================ -->
753+<section id="features">
754+ <div class="container">
755+ <p class="section-label">Features</p>
756+ <h2>Everything else just works.</h2>
757+ <div class="feature-grid">
758+ <div class="feature-card">
759+ <div class="icon">🔌</div>
760+ <h3>SSH-First</h3>
761+ <p>No HTTP APIs. No webhooks to configure. No OAuth tokens. SSH keys are your auth, SSH pubsub is your event bus.</p>
762+ </div>
763+ <div class="feature-card">
764+ <div class="icon">📊</div>
765+ <h3>JSONL Status Stream</h3>
766+ <p><code>pici monitor | curl -sd"$line" $WEBHOOK</code> — pipe build status to Discord, Slack, or any endpoint.</p>
767+ </div>
768+ <div class="feature-card">
769+ <div class="icon">🧹</div>
770+ <h3>Auto-Cancel on Push</h3>
771+ <p>New commit? The old job is killed automatically. No stale builds wasting resources.</p>
772+ </div>
773+ <div class="feature-card">
774+ <div class="icon">🗑️</div>
775+ <h3>Auto Garbage Collection</h3>
776+ <p>Old sessions cleaned up automatically. No manual cleanup scripts.</p>
777+ </div>
778+ <div class="feature-card">
779+ <div class="icon">📦</div>
780+ <h3>Live Artifacts</h3>
781+ <p>HTML session pages generated during the build, not just after. See progress in real-time.</p>
782+ </div>
783+ <div class="feature-card">
784+ <div class="icon">🔐</div>
785+ <h3>Build Attestation</h3>
786+ <p>Automatic provenance: runner hostname, OS, arch, repo, branch, commit, workspace checksum.</p>
787+ </div>
788+ </div>
789+ </div>
790+</section>
791+
792+<!-- ============================================================
793+ COMPARISON — Other CI vs pici
794+ ============================================================ -->
795+<section id="compare">
796+ <div class="container">
797+ <p class="section-label">Comparison</p>
798+ <h2>Other CI gives you logs. pici gives you a shell.</h2>
799+ <div class="code-compare">
800+ <div>
801+ <div class="label bad">Other CI</div>
802+ <div class="terminal">
803+ <div class="terminal-bar">
804+ <span class="dot red"></span>
805+ <span class="dot yellow"></span>
806+ <span class="dot green"></span>
807+ <span style="margin-left: 0.5rem;">build log</span>
808+ </div>
809+ <div class="terminal-body">
810+ <div class="output">$ npm test</div>
811+ <div class="output">PASS src/utils.test.js</div>
812+ <div class="error">FAIL src/db.test.js</div>
813+ <div class="error"> ● connects to database</div>
814+ <div class="error"> TimeoutError: connect ETIMED OUT</div>
815+ <div class="output">...</div>
816+ <div class="comment"># ...that's all you get.</div>
817+ <div class="comment"># Re-run the job? Wait 12 min.</div>
818+ <div class="comment"># Enable "debug mode"? Where's that?</div>
819+ </div>
820+ </div>
821+ </div>
822+ <div>
823+ <div class="label good">pici</div>
824+ <div class="terminal">
825+ <div class="terminal-bar">
826+ <span class="dot red"></span>
827+ <span class="dot yellow"></span>
828+ <span class="dot green"></span>
829+ <span style="margin-left: 0.5rem;">zmx attach ci.myrepo.test</span>
830+ </div>
831+ <div class="terminal-body">
832+ <div class="output">$ npm test</div>
833+ <div class="output">PASS src/utils.test.js</div>
834+ <div class="error">FAIL src/db.test.js</div>
835+ <div class="error"> ● connects to database</div>
836+ <div class="error"> TimeoutError: connect ETIMED OUT</div>
837+ <div> </div>
838+ <div><span class="comment"># you're already in the session</span></div>
839+ <div><span class="prompt">$ </span><span class="cmd">env | grep DB_HOST</span></div>
840+ <div class="output">DB_HOST=postgres.internal:5432</div>
841+ <div><span class="prompt">$ </span><span class="cmd">nc -zv postgres.internal 5432</span></div>
842+ <div class="output">Connection refused</div>
843+ <div><span class="comment"># ↑ found it — network config issue</span></div>
844+ </div>
845+ </div>
846+ </div>
847+ </div>
848+ </div>
849+</section>
850+
851+<!-- ============================================================
852+ CTA — Sign up for beta
853+ ============================================================ -->
854+<section class="cta-section" id="beta">
855+ <div class="container">
856+ <h2>Get early access to pici.</h2>
857+ <p class="subtitle">
858+ We're opening the managed beta at <code>ci.pico.sh</code>.<br>
859+ Self-hosted is available now on <a href="https://github.com/picosh/pici">GitHub</a>.
860+ </p>
861+ <form class="signup-form" action="#" method="POST" onsubmit="handleSignup(event)">
862+ <input type="email" name="email" placeholder="you@example.com" required autocomplete="email">
863+ <button type="submit" class="btn btn-primary">Sign Up for Beta</button>
864+ </form>
865+ <p class="signup-note">No spam. We'll email you when beta slots open.</p>
866+ </div>
867+</section>
868+
869+<!-- ============================================================
870+ FOOTER
871+ ============================================================ -->
872+<footer>
873+ <div class="container">
874+ <p>
875+ <a href="https://github.com/picosh/pici">GitHub</a> ·
876+ <a href="https://pico.sh">pico.sh</a> ·
877+ <a href="mailto:hello@pico.sh">hello@pico.sh</a>
878+ </p>
879+ <p style="margin-top: 0.5rem;">
880+ Built by <a href="https://pico.sh">pico</a>. Self-hostable. SSH-first. Terminal-native.
881+ </p>
882+ </div>
883+</footer>
884+
885+<script>
886+function handleSignup(e) {
887+ e.preventDefault();
888+ const form = e.target;
889+ const email = form.email.value;
890+ // TODO: wire up to your backend / pipe.pico.sh / etc.
891+ // For now, just show a confirmation
892+ form.innerHTML = `<p style="color: var(--green); padding: 0.7rem 0;">✓ You're on the list, ${email}.</p>`;
893+}
894+</script>
895+
896+</body>
897+</html>