Commit d43b626

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