main pici / website / index.html
Eric Bower  ·  2026-05-26
   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, viewport-fit=cover">
   6<meta name="apple-mobile-web-app-capable" content="yes">
   7<meta name="apple-mobile-web-app-status-bar-style" content="black">
   8<title>pici – Your Build is a Terminal.</title>
   9<style>
  10/* ============================================================
  11   pici landing page – terminal aesthetic
  12   Palette: Catppuccin Mocha + terminal green accents
  13   ============================================================ */
  14
  15*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
  16
  17:root {
  18  --bg:        #1e1e2e;
  19  --bg-deep:   #11111b;
  20  --surface:   #313244;
  21  --text:      #cdd6f4;
  22  --text-dim:  #a6adc8;
  23  --blue:      #89b4fa;
  24  --green:     #a6e3a1;
  25  --red:       #f38ba8;
  26  --yellow:    #f9e2af;
  27  --mauve:     #f5c2e7;
  28  --mono:      ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
  29}
  30
  31html { scroll-behavior: smooth; }
  32
  33body {
  34  font-family: var(--mono);
  35  background: var(--bg);
  36  color: var(--text);
  37  font-size: 15px;
  38  line-height: 1.6;
  39  overflow-x: hidden;
  40}
  41
  42/* Scanline overlay */
  43body::after {
  44  content: "";
  45  position: fixed;
  46  inset: 0;
  47  pointer-events: none;
  48  background: repeating-linear-gradient(
  49    0deg,
  50    transparent,
  51    transparent 2px,
  52    rgba(0, 0, 0, 0.03) 2px,
  53    rgba(0, 0, 0, 0.03) 4px
  54  );
  55  z-index: 9999;
  56}
  57
  58a { color: var(--blue); text-decoration: none; }
  59a:hover { text-decoration: underline; }
  60
  61/* ============================================================
  62   Layout
  63   ============================================================ */
  64
  65.container {
  66  max-width: 960px;
  67  margin: 0 auto;
  68  padding: 0 1.5rem;
  69}
  70
  71section {
  72  padding: 5rem 0;
  73  border-bottom: 1px solid var(--surface);
  74}
  75
  76/* ============================================================
  77   Navigation
  78   ============================================================ */
  79
  80nav {
  81  z-index: 1;
  82  position: sticky;
  83  top: 0;
  84  background: var(--bg-deep);
  85  border-bottom: 1px solid var(--surface);
  86  padding: 0.75rem 0;
  87}
  88
  89nav .container {
  90  display: flex;
  91  align-items: center;
  92  justify-content: space-between;
  93  gap: 1rem;
  94}
  95
  96nav .logo {
  97  font-size: 1.1rem;
  98  font-weight: 700;
  99  color: var(--green);
 100}
 101
 102nav .logo span { color: var(--text-dim); font-weight: 400; }
 103
 104@media (max-width: 700px) {
 105  nav .logo span { display: none; }
 106}
 107
 108nav ul {
 109  list-style: none;
 110  display: flex;
 111  gap: 1.5rem;
 112  padding-left: 1rem;
 113  border-left: 1px solid var(--surface);
 114}
 115
 116nav ul a {
 117  color: var(--text-dim);
 118  font-size: 0.7rem;
 119  text-transform: uppercase;
 120  letter-spacing: 0.05em;
 121}
 122
 123nav ul a:hover { color: var(--text); text-decoration: none; }
 124
 125/* ============================================================
 126   Hero
 127   ============================================================ */
 128
 129.hero {
 130  padding: 7rem 0 5rem;
 131  text-align: center;
 132  border-bottom: 1px solid var(--surface);
 133}
 134
 135.hero h1 {
 136  font-size: clamp(1.8rem, 5vw, 2.8rem);
 137  font-weight: 700;
 138  line-height: 1.2;
 139  margin-bottom: 1rem;
 140}
 141
 142.hero h1 .accent { color: var(--green); }
 143
 144.hero .subtitle {
 145  font-size: 1.05rem;
 146  color: var(--text-dim);
 147  max-width: 600px;
 148  margin: 0 auto 2rem;
 149}
 150
 151.hero .cta-row {
 152  display: flex;
 153  gap: 1rem;
 154  justify-content: center;
 155  flex-wrap: wrap;
 156}
 157
 158.btn {
 159  display: inline-block;
 160  padding: 0.7rem 1.5rem;
 161  border-radius: 4px;
 162  font-family: var(--mono);
 163  font-size: 0.9rem;
 164  font-weight: 600;
 165  cursor: pointer;
 166  border: 1px solid transparent;
 167  transition: all 0.15s ease;
 168}
 169
 170.btn-primary {
 171  background: var(--green);
 172  color: var(--bg-deep);
 173}
 174
 175.btn-primary:hover {
 176  background: #b8f0b3;
 177  text-decoration: none;
 178}
 179
 180.btn-secondary {
 181  background: transparent;
 182  color: var(--text);
 183  border-color: var(--surface);
 184}
 185
 186.btn-secondary:hover {
 187  border-color: var(--text-dim);
 188  text-decoration: none;
 189}
 190
 191/* ============================================================
 192   Terminal window component
 193   ============================================================ */
 194
 195.terminal {
 196  background: var(--bg-deep);
 197  border: 1px solid var(--surface);
 198  border-radius: 8px;
 199  overflow: hidden;
 200  margin: 2rem 0;
 201}
 202
 203.terminal-bar {
 204  display: flex;
 205  align-items: center;
 206  gap: 0.5rem;
 207  padding: 0.6rem 1rem;
 208  background: var(--surface);
 209  font-size: 0.75rem;
 210  color: var(--text-dim);
 211}
 212
 213.terminal-bar .dot {
 214  width: 10px;
 215  height: 10px;
 216  border-radius: 50%;
 217  display: inline-block;
 218}
 219
 220.terminal-bar .dot.red    { background: var(--red); }
 221.terminal-bar .dot.yellow { background: var(--yellow); }
 222.terminal-bar .dot.green  { background: var(--green); }
 223
 224.terminal-body {
 225  padding: 1rem 1.25rem;
 226  font-size: 0.85rem;
 227  line-height: 1.7;
 228  overflow-x: auto;
 229}
 230
 231.terminal-body .prompt { color: var(--green); }
 232.terminal-body .cmd    { color: var(--text); }
 233.terminal-body .output { color: var(--text-dim); }
 234.terminal-body .error  { color: var(--red); }
 235.terminal-body .info   { color: var(--blue); }
 236.terminal-body .comment { color: #6c7086; }
 237.terminal-body .cursor {
 238  display: inline-block;
 239  width: 8px;
 240  height: 1em;
 241  background: var(--green);
 242  vertical-align: text-bottom;
 243  animation: blink 1s step-end infinite;
 244}
 245
 246@keyframes blink {
 247  50% { opacity: 0; }
 248}
 249
 250/* ============================================================
 251   Section headers
 252   ============================================================ */
 253
 254.section-label {
 255  font-size: 0.75rem;
 256  text-transform: uppercase;
 257  letter-spacing: 0.1em;
 258  color: var(--mauve);
 259  margin-bottom: 0.5rem;
 260}
 261
 262h2 {
 263  font-size: 1.5rem;
 264  font-weight: 700;
 265  margin-bottom: 1rem;
 266  color: var(--text);
 267}
 268
 269h2 .anchor {
 270  color: var(--text-dim);
 271  opacity: 0;
 272  transition: opacity 0.15s ease;
 273  font-weight: 400;
 274  font-size: 0.85em;
 275  text-decoration: none;
 276}
 277
 278h2:hover .anchor {
 279  opacity: 1;
 280}
 281
 282h2 .anchor:hover {
 283  color: var(--green);
 284  text-decoration: none;
 285}
 286
 287h3 {
 288  font-size: 1.1rem;
 289  font-weight: 600;
 290  margin-bottom: 0.5rem;
 291  color: var(--blue);
 292}
 293
 294p { margin-bottom: 1rem; }
 295
 296/* ============================================================
 297   Two-column layout
 298   ============================================================ */
 299
 300.two-col {
 301  display: grid;
 302  grid-template-columns: 1fr 1fr;
 303  gap: 3rem;
 304  align-items: start;
 305}
 306
 307@media (max-width: 700px) {
 308  .two-col { grid-template-columns: 1fr; }
 309}
 310
 311/* ============================================================
 312   Feature grid
 313   ============================================================ */
 314
 315.feature-grid {
 316  display: grid;
 317  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
 318  gap: 1.5rem;
 319  margin-top: 2rem;
 320}
 321
 322.feature-card {
 323  background: var(--bg-deep);
 324  border: 1px solid var(--surface);
 325  border-radius: 8px;
 326  padding: 1.5rem;
 327}
 328
 329.feature-card h3 { margin-top: 0.5rem; }
 330.feature-card .icon { font-size: 1.5rem; }
 331
 332/* ============================================================
 333   Image placeholder
 334   ============================================================ */
 335
 336.img-placeholder {
 337  background: var(--bg-deep);
 338  border: 2px dashed var(--surface);
 339  border-radius: 8px;
 340  display: flex;
 341  flex-direction: column;
 342  align-items: center;
 343  justify-content: center;
 344  min-height: 250px;
 345  color: var(--text-dim);
 346  font-size: 0.8rem;
 347  text-align: center;
 348  padding: 2rem;
 349}
 350
 351.img-placeholder .icon { font-size: 2rem; margin-bottom: 0.5rem; }
 352.img-placeholder .label { font-size: 0.85rem; color: var(--mauve); }
 353.img-placeholder .dims { font-size: 0.7rem; color: #6c7086; margin-top: 0.25rem; }
 354
 355/* ============================================================
 356   Code comparison (old vs new)
 357   ============================================================ */
 358
 359.code-compare {
 360  display: grid;
 361  grid-template-columns: 1fr 1fr;
 362  gap: 1.5rem;
 363}
 364
 365@media (max-width: 700px) {
 366  .code-compare { grid-template-columns: 1fr; }
 367}
 368
 369.code-compare .label {
 370  font-size: 0.7rem;
 371  text-transform: uppercase;
 372  letter-spacing: 0.1em;
 373  color: var(--text-dim);
 374  margin-bottom: 0.5rem;
 375}
 376
 377.code-compare .label.bad { color: var(--red); }
 378.code-compare .label.good { color: var(--green); }
 379
 380/* ============================================================
 381   CTA / signup section
 382   ============================================================ */
 383
 384.cta-section {
 385  text-align: center;
 386  padding: 5rem 0;
 387  border-bottom: none;
 388}
 389
 390.cta-section h2 {
 391  font-size: 1.8rem;
 392  margin-bottom: 0.5rem;
 393  display: block;
 394}
 395
 396.cta-section .subtitle {
 397  color: var(--text-dim);
 398  margin-bottom: 2rem;
 399}
 400
 401.signup-form {
 402  display: flex;
 403  gap: 0.5rem;
 404  justify-content: center;
 405  flex-wrap: wrap;
 406  max-width: 500px;
 407  margin: 0 auto;
 408}
 409
 410.signup-form input[type="email"] {
 411  flex: 1;
 412  min-width: 200px;
 413  padding: 0.7rem 1rem;
 414  border-radius: 4px;
 415  border: 1px solid var(--surface);
 416  background: var(--bg-deep);
 417  color: var(--text);
 418  font-family: var(--mono);
 419  font-size: 0.9rem;
 420  outline: none;
 421}
 422
 423.signup-form input[type="email"]:focus {
 424  border-color: var(--green);
 425}
 426
 427.signup-form input[type="email"]::placeholder {
 428  color: #6c7086;
 429}
 430
 431.signup-form .btn {
 432  white-space: nowrap;
 433}
 434
 435.signup-note {
 436  font-size: 0.75rem;
 437  color: #6c7086;
 438  margin-top: 1rem;
 439}
 440
 441/* ============================================================
 442   Footer
 443   ============================================================ */
 444
 445footer {
 446  padding: 2rem 0;
 447  border-top: 1px solid var(--surface);
 448  text-align: center;
 449  font-size: 0.8rem;
 450  color: var(--text-dim);
 451}
 452
 453footer a { color: var(--blue); }
 454
 455/* ============================================================
 456   Inline code
 457   ============================================================ */
 458
 459code {
 460  background: var(--surface);
 461  color: var(--mauve);
 462  padding: 0.15em 0.4em;
 463  border-radius: 3px;
 464  font-size: 0.85em;
 465  font-family: var(--mono);
 466}
 467
 468/* ============================================================
 469   Highlighted list
 470   ============================================================ */
 471
 472.check-list {
 473  list-style: none;
 474  padding: 0;
 475}
 476
 477.check-list li {
 478  padding: 0.3rem 0;
 479  padding-left: 1.5rem;
 480  position: relative;
 481}
 482
 483.check-list li::before {
 484  content: "✓";
 485  position: absolute;
 486  left: 0;
 487  color: var(--green);
 488  font-weight: 700;
 489}
 490
 491/* ============================================================
 492   Diagram / flow
 493   ============================================================ */
 494
 495.flow {
 496  display: flex;
 497  align-items: center;
 498  justify-content: center;
 499  gap: 0.5rem;
 500  flex-wrap: wrap;
 501  margin: 2rem 0;
 502  font-size: 0.85rem;
 503}
 504
 505.flow .node {
 506  background: var(--surface);
 507  padding: 0.5rem 1rem;
 508  border-radius: 4px;
 509  font-size: 0.8rem;
 510}
 511
 512.flow .arrow {
 513  color: var(--text-dim);
 514  font-size: 1.2rem;
 515}
 516
 517/* ============================================================
 518   Details / FAQ
 519   ============================================================ */
 520
 521details {
 522  background: var(--bg-deep);
 523  border: 1px solid var(--surface);
 524  border-radius: 8px;
 525  margin-bottom: 1rem;
 526}
 527
 528details[open] {
 529  border-color: var(--green);
 530}
 531
 532details summary {
 533  padding: 1rem 1.25rem;
 534  cursor: pointer;
 535  font-weight: 600;
 536  color: var(--text);
 537  list-style: none;
 538  display: flex;
 539  align-items: center;
 540}
 541
 542details summary::-webkit-details-marker {
 543  display: none;
 544}
 545
 546details summary::before {
 547  content: "+";
 548  margin-right: 0.75rem;
 549  color: var(--green);
 550  font-weight: 700;
 551  font-size: 1.1rem;
 552}
 553
 554details[open] summary::before {
 555  content: "-";
 556}
 557
 558details summary:hover {
 559  color: var(--green);
 560}
 561
 562details .faq-answer {
 563  padding: 0 1.25rem 1.25rem;
 564  color: var(--text-dim);
 565  line-height: 1.7;
 566}
 567</style>
 568</head>
 569<body>
 570
 571<!-- ============================================================
 572     NAV
 573     ============================================================ -->
 574<nav>
 575  <div class="container">
 576    <div class="logo">pici <span>: terminal-first CI</span></div>
 577    <ul>
 578      <li><a href="#how">How</a></li>
 579      <li><a href="#infra">Infra</a></li>
 580      <li><a href="#features">Features</a></li>
 581      <li><a href="#faq">FAQ</a></li>
 582      <li><a href="#beta">Beta</a></li>
 583    </ul>
 584  </div>
 585</nav>
 586
 587<!-- ============================================================
 588     HERO
 589     ============================================================ -->
 590<section class="hero">
 591  <div class="container">
 592    <h1>Your build is a terminal.</h1>
 593    <p class="subtitle">
 594      <code>pici</code> is a CI system where every build step runs in an attachable terminal session.
 595      Your pipeline is a bash script. It runs the same locally and in CI.
 596    </p>
 597    <div class="cta-row">
 598      <a href="#beta" class="btn btn-primary">Sign Up for Beta</a>
 599    </div>
 600  </div>
 601</section>
 602
 603<!-- ============================================================
 604     PROBLEM – The CI Pain
 605     ============================================================ -->
 606<section id="problem">
 607  <div class="container">
 608    <div class="two-col">
 609      <div>
 610        <p class="section-label">The Problem</p>
 611        <h2>CI gives you logs. Not answers.</h2>
 612        <p>
 613          A build fails. You scroll through 2,000 lines of log output. You guess what went wrong.
 614          You re-run the job and wait 10 minutes to find out you were wrong. Repeat.
 615        </p>
 616        <p>
 617          Debugging CI shouldn't require you to guess what happened.
 618        </p>
 619      </div>
 620      <div class="terminal">
 621        <div class="terminal-bar">
 622          <span class="dot red"></span>
 623          <span class="dot yellow"></span>
 624          <span class="dot green"></span>
 625          <span style="margin-left: 0.5rem;">ci-myrepo-test: build log</span>
 626        </div>
 627        <div class="terminal-body">
 628          <div class="output">Line 1847: ...</div>
 629          <div class="output">Line 1848: ...</div>
 630          <div class="output">Line 1849: ...</div>
 631          <div class="error">Line 1850: FAIL: test_connect (db.TimeoutError)</div>
 632          <div class="output">Line 1851: ...</div>
 633          <div class="output">Line 1852: ...</div>
 634          <div class="output">...</div>
 635          <div class="comment"># scroll up... scroll up... scroll up...</div>
 636          <div class="comment"># what was the state before this?</div>
 637          <div class="comment"># re-run the job? wait 10 more minutes?</div>
 638        </div>
 639      </div>
 640    </div>
 641  </div>
 642</section>
 643
 644<!-- ============================================================
 645     SOLUTION – Attach
 646     ============================================================ -->
 647<section id="how">
 648  <div class="container">
 649    <p class="section-label">The Solution</p>
 650    <h2>Attach to your failing build.</h2>
 651    <div class="two-col">
 652      <div>
 653        <p>
 654          Every build step in <code>pici</code> runs as a <strong>real terminal session</strong>: a PTY you can attach to.
 655          No "enable debugging" or "re-run with SSH" buttons.
 656        </p>
 657        <p>
 658          A test fails? Attach to the session, press <code></code> + <code>Enter</code> to rerun the command,
 659          inspect the environment, and fix the issue before trying again.
 660        </p>
 661      </div>
 662      <div>
 663        <div class="terminal">
 664          <div class="terminal-bar">
 665            <span class="dot red"></span>
 666            <span class="dot yellow"></span>
 667            <span class="dot green"></span>
 668            <span style="margin-left: 0.5rem;">zmx attach ci.myrepo.test</span>
 669          </div>
 670          <div class="terminal-body">
 671            <div><span class="prompt">$ </span><span class="cmd">zmx attach ci.myrepo.test</span></div>
 672            <div><span class="info">→ attached to session ci.myrepo.test</span></div>
 673            <div>&nbsp;</div>
 674            <div class="output">$ go test ./...</div>
 675            <div class="output">ok    myapp/core    8.2s</div>
 676            <div class="error">FAIL  myapp/api     11.4s</div>
 677            <div class="error">Error: dial tcp 10.0.1.5:5432: connection refused</div>
 678            <div>&nbsp;</div>
 679            <div><span class="comment"># ↑ you're in the live session</span></div>
 680            <div><span class="comment"># ↑ press ↑ + Enter to rerun, or inspect freely</span></div>
 681            <div>&nbsp;</div>
 682            <div><span class="prompt">$ </span><span class="cmd">cat .env | grep DB</span></div>
 683            <div class="output">DB_HOST=postgres.internal:5432</div>
 684            <div>&nbsp;</div>
 685            <div><span class="prompt">$ </span><span class="cmd">nslookup postgres.internal</span></div>
 686            <div class="error">;; no servers could be reached</div>
 687            <div>&nbsp;</div>
 688            <div><span class="comment"># ↑ found it: DNS not resolving in this container</span></div>
 689          </div>
 690        </div>
 691      </div>
 692    </div>
 693  </div>
 694</section>
 695
 696<!-- ============================================================
 697     JOB ENGINE – zmx as the parallel task runner
 698     ============================================================ -->
 699<section id="engine">
 700  <div class="container">
 701    <p class="section-label">Job Engine</p>
 702    <h2>Parallel tasks, zero config, and zero install.</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;">pico.sh</span>
 710        </div>
 711        <div class="terminal-body">
 712          <div><span class="comment">#!/usr/bin/env bash</span></div>
 713          <div><span class="cmd">set -euo pipefail</span></div>
 714          <div>&nbsp;</div>
 715          <div><span class="comment"># This step runs until it's finished</span></div>
 716          <div><span class="cmd">zmx run lint docker run golangci-lint run</span></div>
 717          <div><span class="info">  → No issues found!</span></div>
 718          <div>&nbsp;</div>
 719          <div><span class="comment"># These run in parallel</span></div>
 720          <div><span class="cmd">zmx run test -d go test ./...</span></div>
 721          <div><span class="info">  → started session: ci.myrepo.test</span></div>
 722          <div>&nbsp;</div>
 723          <div><span class="cmd">zmx run build -d go build -o bin/pici .</span></div>
 724          <div><span class="info">  → started session: ci.myrepo.build</span></div>
 725          <div>&nbsp;</div>
 726          <div><span class="comment"># Wait for all tasks to finish</span></div>
 727          <div><span class="cmd">zmx wait "*"</span></div>
 728          <div>&nbsp;</div>
 729          <div><span class="comment"># Runs identically locally and in CI</span></div>
 730        </div>
 731      </div>
 732      <div>
 733        <p>
 734          Your pipeline is <code>pico.sh</code>: a bash script that runs <strong>identically on your machine
 735          and on the CI runner</strong>; no yaml required.
 736        </p>
 737        <p>
 738          <a href="https://zmx.sh"><strong>zmx</strong></a> is the job engine. Each <code>zmx run</code> spawns
 739          a parallel terminal session. <code>zmx wait "*"</code> blocks until they all finish.
 740          Run tasks sequentially by just calling them one after another, or in parallel by using the
 741          detach (<code>-d</code>) flag.
 742        </p>
 743        <ul class="check-list">
 744          <li>Same <code>zmx run</code> commands locally and in CI</li>
 745          <li>Sequential by default, parallel when you want it</li>
 746          <li>Every step is an attachable terminal session</li>
 747          <li>Bring your own isolation: use docker in your step commands</li>
 748          <li>No YAML matrices, no <code>run: parallel</code> keywords</li>
 749        </ul>
 750      </div>
 751    </div>
 752  </div>
 753</section>
 754
 755<!-- ============================================================
 756     GIT IS OPTIONAL
 757     ============================================================ -->
 758<section id="no-git">
 759  <div class="container">
 760    <p class="section-label">Trigger Model</p>
 761    <h2>Git is optional. rsync + ssh pubsub is the core.</h2>
 762    <div class="two-col">
 763      <div>
 764        <p>
 765          Most CI systems are glued to Git webhooks. <code>pici</code> isn't. The core loop is simple:
 766          <strong>rsync a workspace</strong> and <strong>publish an event over SSH</strong>.
 767        </p>
 768        <p>
 769          Git post-receive hooks are just one trigger. You can fire builds from anything:
 770          a cron job, a file watcher, a webhook receiver on your own server, a button press.
 771        </p>
 772      </div>
 773      <div>
 774        <img src="flow.png" style="width: 100%; border-radius: 10px;" />
 775      </div>
 776    </div>
 777  </div>
 778</section>
 779
 780<!-- ============================================================
 781     MANAGED SERVICE
 782     ============================================================ -->
 783<section id="cd">
 784  <div class="container">
 785    <p class="section-label">Managed Service</p>
 786    <h2>Managed CI at&nbsp<a href="https://ci.pico.sh">ci.pico.sh</a>.</h2>
 787    <div class="two-col">
 788      <div>
 789        <p>
 790          Trigger builds by publishing events over SSH. Your pipeline runs as real terminal
 791          sessions you can attach to. No web console, no API keys, no OAuth.
 792        </p>
 793        <p>
 794          The managed service runs on our own hardware, not a cloud provider. Self-hosted
 795          is coming once the runner is ready.
 796        </p>
 797        <ul class="check-list">
 798          <li>SSH keys are your auth</li>
 799          <li>SSH pubsub is your event bus</li>
 800          <li>Our hardware, not a hyperscaler</li>
 801          <li>Self-hosted coming soon</li>
 802        </ul>
 803      </div>
 804      <div class="terminal">
 805        <div class="terminal-bar">
 806          <span class="dot red"></span>
 807          <span class="dot yellow"></span>
 808          <span class="dot green"></span>
 809          <span style="margin-left: 0.5rem;">ci.pico.sh</span>
 810        </div>
 811        <div class="terminal-body">
 812          <div><span class="prompt">$ </span><span class="cmd">echo '{"type":"push"}' | ssh pipe.pico.sh pub build.event</span></div>
 813          <div><span class="output">subscribe to this channel: ssh pipe.pico.sh sub build.event</span></div>
 814          <div>&nbsp;</div>
 815          <div><span class="info">🚀 starting job ci.myrepo.a3f2b8c1</span></div>
 816          <div><span class="info">📦 syncing workspace</span></div>
 817          <div><span class="info">✅ workspace ready</span></div>
 818          <div><span class="info">🏃 launching sessions...</span></div>
 819          <div><span class="info">✅ job launched</span></div>
 820        </div>
 821      </div>
 822    </div>
 823  </div>
 824</section>
 825
 826<!-- ============================================================
 827     PHILOSOPHY
 828     ============================================================ -->
 829<section id="philosophy">
 830  <div class="container" style="max-width: 640px; margin: 0 auto; text-align: center;">
 831    <p class="section-label">Who It's For</p>
 832    <h2 style="justify-content: center;">CI that works for you, not against you.</h2>
 833    <p>
 834      <code>pici</code> is built for individuals and small teams who want to move fast. You write a bash
 835      script, you push your code, your build runs, and if something breaks you jump into the
 836      terminal and fix it. No YAML labyrinths, no approval workflows, no complex pipeline DAGs.
 837    </p>
 838    <p>
 839      We're not building marketplace integrations, compliance reports, or workflow bureaucracy.
 840      What you will find is the CI system you'd write for yourself.
 841    </p>
 842  </div>
 843</section>
 844
 845<!-- ============================================================
 846     POWERED BY cd.pico.sh
 847     ============================================================ -->
 848<section id="infra">
 849  <div class="container">
 850    <p class="section-label">Infrastructure</p>
 851    <h2>Powered by&nbsp<a href="https://cd.pico.sh">cd.pico.sh</a></h2>
 852    <div class="two-col">
 853      <div>
 854        <p>
 855          <strong>ci.pico.sh</strong> runs on <a href="https://cd.pico.sh"><strong>cd.pico.sh</strong></a>: our SSH VM service
 856          on hardware we own.
 857        </p>
 858        <p>
 859          Push a <code>docker-compose.yml</code> to an SSH endpoint and your containers are live.
 860          Label a service and it gets a public HTTP URL. There's no provider lock-in because it's a tool
 861          you likely already use for local development.
 862        </p>
 863        <p>
 864          The same platform that runs your CI runs your apps.
 865        </p>
 866        <ul class="check-list">
 867          <li>Docker Compose via <code>git push</code></li>
 868          <li>Expose HTTP services with compose labels</li>
 869          <li>Our hardware, not a hyperscaler</li>
 870          <li>Limited hardware availability: when it's gone, it's gone</li>
 871        </ul>
 872      </div>
 873      <div class="terminal">
 874        <div class="terminal-bar">
 875          <span class="dot red"></span>
 876          <span class="dot yellow"></span>
 877          <span class="dot green"></span>
 878          <span style="margin-left: 0.5rem;">cd.pico.sh</span>
 879        </div>
 880        <div class="terminal-body">
 881          <div><span class="comment">--- docker-compose.yml ---</span></div>
 882          <div>services:</div>
 883          <div>&nbsp;&nbsp;echo:</div>
 884          <div>&nbsp;&nbsp;&nbsp;&nbsp;build: .</div>
 885          <div>&nbsp;&nbsp;&nbsp;&nbsp;networks:</div>
 886          <div>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- default</div>
 887          <div>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- picd-ingress</div>
 888          <div>&nbsp;&nbsp;&nbsp;&nbsp;labels:</div>
 889          <div>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;traefik.enable: true</div>
 890          <div>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;traefik.http.routers.echo.rule: Host(`echo-&lt;user&gt;.apps.pico.sh`)</div>
 891          <div>&nbsp;</div>
 892          <div>networks:</div>
 893          <div>&nbsp;&nbsp;picd-ingress:</div>
 894          <div>&nbsp;&nbsp;&nbsp;&nbsp;external: true</div>
 895          <div>&nbsp;</div>
 896          <div><span class="prompt">$ </span><span class="cmd">git remote add picd ssh://cd.pico.sh/user/project.git</span></div>
 897          <div><span class="prompt">$ </span><span class="cmd">git push picd main</span></div>
 898          <div>&nbsp;</div>
 899          <div><span class="info">→ containers live</span></div>
 900          <div><span class="info">→ echo-&lt;user&gt;.apps.pico.sh is live</span></div>
 901        </div>
 902      </div>
 903    </div>
 904  </div>
 905</section>
 906
 907<!-- ============================================================
 908     FEATURES
 909     ============================================================ -->
 910<section id="features">
 911  <div class="container">
 912    <p class="section-label">Features</p>
 913    <h2>Built for terminals, humans, and automation</h2>
 914    <div class="feature-grid">
 915      <div class="feature-card">
 916        <h3>Terminal Friendly</h3>
 917        <p>
 918          Because <code>pici</code> doesn't rely on git-ops and triggering a
 919          build is done with rsync + ssh, we have the building blocks for you
 920          or a code agent to trigger builds, monitor progress, and attach to
 921          failures. Start as many jobs as you need and read results
 922          synchronously.
 923        </p>
 924      </div>
 925      <div class="feature-card">
 926        <h3>SSH-First</h3>
 927        <p>
 928          SSH keys are your auth and we even support using SSH certificates for RBAC control.
 929          Rsync to upload workspaces, build artifacts, and ssh pubsub as your event bus.
 930        </p>
 931      </div>
 932      <div class="feature-card">
 933        <h3>Static Site Artifacts</h3>
 934        <p>
 935          Build artifacts are plain HTML + CSS. There's no app server or build step.
 936          Serve the directory with any static host: nginx, s3, <a href="https://pgs.sh">pgs.sh</a>,
 937          <code>python -m http.server</code>.
 938          It's a static site with zero runtime dependencies.
 939        </p>
 940      </div>
 941      <div class="feature-card">
 942        <h3>Build Attestation</h3>
 943        <p>
 944          Automatic provenance baked into every job: runner hostname, OS, arch, repo, branch,
 945          commit, and workspace checksum.
 946        </p>
 947      </div>
 948    </div>
 949  </div>
 950</section>
 951
 952<!-- ============================================================
 953     COMPARISON
 954     ============================================================ -->
 955<section id="compare">
 956  <div class="container">
 957    <p class="section-label">Comparison</p>
 958    <h2>The difference is the terminal.</h2>
 959    <div class="code-compare">
 960      <div>
 961        <div class="label bad">Other CI</div>
 962        <div class="terminal">
 963          <div class="terminal-bar">
 964            <span class="dot red"></span>
 965            <span class="dot yellow"></span>
 966            <span class="dot green"></span>
 967            <span style="margin-left: 0.5rem;">build log</span>
 968          </div>
 969          <div class="terminal-body">
 970            <div class="output">$ go test ./...</div>
 971            <div class="output">ok    myapp/db    12.4s</div>
 972            <div class="error">FAIL  myapp/api (14.2s)</div>
 973            <div class="error">Error: connection refused</div>
 974            <div class="output">...</div>
 975            <div>&nbsp;</div>
 976            <div class="comment"># ← that's all you get</div>
 977            <div class="comment"># ← can't inspect the environment</div>
 978            <div class="comment"># ← can't rerun just the failing step</div>
 979            <div class="comment"># ← re-run entire pipeline? wait 10 min</div>
 980          </div>
 981        </div>
 982      </div>
 983      <div>
 984        <div class="label good"><code>pici</code></div>
 985        <div class="terminal">
 986          <div class="terminal-bar">
 987            <span class="dot red"></span>
 988            <span class="dot yellow"></span>
 989            <span class="dot green"></span>
 990            <span style="margin-left: 0.5rem;">zmx attach ci.myrepo.api</span>
 991          </div>
 992          <div class="terminal-body">
 993            <div class="output">$ go test ./...</div>
 994            <div class="output">ok    myapp/db    12.4s</div>
 995            <div class="error">FAIL  myapp/api (14.2s)</div>
 996            <div class="error">Error: connection refused</div>
 997            <div>&nbsp;</div>
 998            <div class="comment"># you're in the session, inspect freely</div>
 999            <div><span class="prompt">$ </span><span class="cmd">cat .env</span></div>
1000            <div class="output">DB_HOST=postgres.internal:5432</div>
1001            <div><span class="prompt">$ </span><span class="cmd">nc -zv postgres.internal 5432</span></div>
1002            <div class="error">Connection refused</div>
1003            <div><span class="comment"># ↑ found it: wrong hostname</span></div>
1004            <div><span class="comment"># ↑ press ↑ to rerun the test after fixing</span></div>
1005          </div>
1006        </div>
1007      </div>
1008    </div>
1009  </div>
1010</section>
1011
1012<!-- ============================================================
1013     FAQ
1014     ============================================================ -->
1015<section id="faq">
1016  <div class="container" style="max-width: 720px; margin: 0 auto;">
1017    <p class="section-label">FAQ</p>
1018    <h2>Frequently asked questions.</h2>
1019
1020    <details>
1021      <summary>Where's the source code?</summary>
1022      <div class="faq-answer">
1023        <p>
1024          It's coming. <code>pici</code> is still in early development and not yet ready for public
1025          review. We'll open-source everything when the API, runner, and tooling are stable enough
1026          to be useful. Sign up for the beta below to get notified.
1027        </p>
1028      </div>
1029    </details>
1030
1031    <details>
1032      <summary>Can I self-host?</summary>
1033      <div class="faq-answer">
1034        <p>
1035          Yes, self-hosting is a core goal. Once the source is released you'll be able to run
1036          <code>pici</code> on your own infra with your own isolation strategy: docker, namespaces,
1037          bare metal. You choose.
1038        </p>
1039      </div>
1040    </details>
1041
1042    <details>
1043      <summary>How much does it cost?</summary>
1044      <div class="faq-answer">
1045        <p>
1046          The managed service at <code>ci.pico.sh</code> requires <a href="https://cd.pico.sh">cd.pico.sh</a>
1047          hosting. Pricing is TBD and will be announced with the beta. Self-hosted is free.
1048        </p>
1049      </div>
1050    </details>
1051
1052    <details>
1053      <summary>How are secrets handled?</summary>
1054      <div class="faq-answer">
1055        <p>
1056          Secrets are managed through <a href="https://cd.pico.sh">cd.pico.sh</a>'s environment variable
1057          system. Inject them into your build sessions the same way you'd set any env var. No secret
1058          scanning, no vault, no extra tooling.
1059        </p>
1060      </div>
1061    </details>
1062  </div>
1063</section>
1064
1065<!-- ============================================================
1066     CTA – Sign up for beta
1067     ============================================================ -->
1068<section class="cta-section" id="beta">
1069  <div class="container">
1070    <h2>Get early access to <code>pici</code>.</h2>
1071    <p class="subtitle">
1072      We're opening the managed beta at <code>ci.pico.sh</code>.
1073    </p>
1074    <form class="signup-form" method="POST" action="/pgs/forms/pici-beta">
1075      <input type="email" name="email" placeholder="you@example.com" required autocomplete="email">
1076      <button type="submit" class="btn btn-primary">Sign Up for Beta</button>
1077    </form>
1078    <p class="signup-note">No spam. We'll email you when beta slots open.</p>
1079  </div>
1080</section>
1081
1082<!-- ============================================================
1083     FOOTER
1084     ============================================================ -->
1085<footer>
1086  <div class="container">
1087    <p>
1088      <a href="https://pico.sh">pico.sh</a> &middot;
1089      <a href="mailto:hello@pico.sh">hello@pico.sh</a>
1090    </p>
1091    <p style="margin-top: 0.5rem;">
1092      Built by <a href="https://pico.sh">pico</a>. Self-hostable. SSH-first. Terminal-native.
1093    </p>
1094  </div>
1095</footer>
1096
1097<script>
1098// Auto-generate anchor links for all h2 elements (skip CTA)
1099document.querySelectorAll('h2').forEach(h2 => {
1100  if (h2.closest('.cta-section')) return;
1101  const section = h2.closest('section, [id]');
1102  if (section && section.id) {
1103    const a = document.createElement('a');
1104    a.href = '#' + section.id;
1105    a.className = 'anchor';
1106    a.textContent = '#';
1107    a.title = 'Copy link to section';
1108    a.addEventListener('click', e => {
1109      e.preventDefault();
1110      history.replaceState(null, null, '#' + section.id);
1111      window.scrollTo({ top: h2.offsetTop - 80, behavior: 'smooth' });
1112    });
1113    h2.appendChild(a);
1114  }
1115});
1116</script>
1117
1118</body>
1119</html>