diff --git a/CLAUDE.md b/CLAUDE.md index 568b205..fc2c9a7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -46,11 +46,12 @@ them ships something the user will reject: 1. **Don't clone competitors.** The user pointed at getadb.com for "vibe"; an early landing borrowed too literally. The user called it out as unethical. *Take sensibility, invent composition.* Same applies to - any future reference shared. + any future reference shared — including **traces.com**, which is the + closest thing to a direct competitor (session sharing for coding + agents), so its composition is especially off-limits. 2. **Don't over-index on Cloudflare.** It's an implementation detail. - Status pill says `live · v1`. Footer says - `htmlbin v1 · open source · agent-friendly`. Internal code comments - can name Cloudflare; user-facing copy cannot. + Internal code comments can name Cloudflare; user-facing copy cannot. + No "powered by", no "edge:", no platform names in the UI. 3. **No Webflow.** Off-limits in the public version. 4. **Don't add signup/login/email/dashboard.** The device-code flow is the entire UX. Adding auth surfaces breaks the product thesis. @@ -92,13 +93,26 @@ Token prefix is **`hb_`** (short for htmlbin). ## Design — one-liner -White paper, Geist + Geist Mono, single red accent (`#D93025`, Gmail/Google red), HTTP-style -memo at the top of every page, vim-modeline-style breadcrumb in the top -bar, monochrome dark code blocks. **One** deliberate exception to "no -fake mac chrome": the prompt block on `/` carries traffic-light dots and -a static `claude` pill in the top-right — it's the primary CTA and earns -the visual handle. Everywhere else stays flat. The HTTP-memo is a real -`
` so users can collapse it. +Geist + Geist Mono, single red accent (`#D93025`, Gmail/Google red), +monochrome dark code blocks. **Two surfaces, deliberately different — +read [DESIGN.md](./DESIGN.md) §0 before touching either:** + +- **Landing (`/`)** — a real marketing page. Tinted `#F4F5F6` surface + with a dot texture, centred 1080px shell, product nav with one solid + button, all-black display headline (never an accent-coloured word), and + a real drop embedded in an iframe as evidence above the fold. Classes + are `.l*` or scoped to `body.landing`. +- **App chrome (`/verify`, `/p/:slug`, gate, 404)** — a document. Pure + white, left-aligned 720px column, vim-modeline breadcrumb, and the + HTTP-style memo (a real `
`) as the first thing on the + page. Unprefixed classes, so editing one reaches all of these. + +**No fake window chrome anywhere any more.** The traffic-light dots on +the old prompt block were a sanctioned exception; that exception is +withdrawn. Related and broader: no fake *output* of any kind — the +landing shows a literal trimmed `GET /api/onboard` response rather than +a stylised drawing of one, because the old memo rendered `to:`/`from:`/ +`re:` as HTTP headers when they are actually RFC 5322 email headers. **Single source of truth:** [`src/styles.ts`](./src/styles.ts) → served at `/style.css`. Every view imports `STYLE_HREF` (= @@ -107,12 +121,24 @@ CSS edit, so the edge cache busts on deploy without a manual version change. Per-page overrides should stay tiny. Don't hard-code `/style.css` in new views; import the constant. -**Landing examples + prompt copy.** The "what people are building" -list on `/` is hand-curated — edit the `EXAMPLES` array at the top of -[`src/views/landing.ts`](./src/views/landing.ts) and redeploy to -rotate. The single prompt-block payload (`AGENT_PROMPT`) lives in the -same file; we deliberately don't ship a tabbed alternative because we -don't have a CLI and `curl` gets flagged as unsafe by careful agents. +**Landing content lives in `src/views/landing.ts`:** + +- `EXAMPLES` — the hand-curated "a few pages people have published" list. +- `AGENT_PROMPT` / `CLI_PROMPT` / `SKILL_PROMPT` — clipboard payloads for + the three tabs. **Keep each visible pane to the same number of rendered + lines**: the panes share one CSS grid cell so the slab can't jump on tab + change, which means it sizes to the tallest, and a shorter pane then + shows dead black space. This has regressed twice. +- `SHOWCASE_SLUG` — the drop embedded as evidence. Uses `/p//raw`, + which does **not** bump `view_count`, so homepage traffic doesn't + inflate that drop's counter. No fallback: delete the drop and the + frame goes blank. +- `src/views/logos.ts` — inlined agent brand marks. Only ship a mark + whose identity you verified against the source asset; that file + documents which products are deliberately text-only and why. + +**Copy has a spec now** — see DESIGN.md §9 for voice and word budgets +(headline 5–10 words, lede 12–15). Every CTA needs a verb. The full design doc with rationale, components, and don'ts is in [DESIGN.md](./DESIGN.md). diff --git a/DESIGN.md b/DESIGN.md index f8b8de4..0619fbf 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -7,26 +7,70 @@ and every page in this app reflects the change. --- -## 1. Philosophy +## 0. Two surfaces, one vocabulary + +**Read this before editing anything else.** The product has two visual +systems, on purpose, and they are not interchangeable: + +| | **Landing** (`/`) | **App chrome** (everything else) | +|---|---|---| +| Job | convince a stranger in ten seconds | get out of the way of the work | +| Pages | `/` only | `/verify`, `/p/:slug`, the passcode gate, `/404` | +| Surface | `--page` `#F4F5F6` + dot texture | pure white | +| Layout | centred, 1080px shell | left-aligned, 720px single column | +| Header | real nav + one solid button (`.lnav`) | mono breadcrumb (`.page-head`) | +| Opens with | display headline + subhead | the HTTP memo (`details.req`) | +| Prefix | `.l*` classes, or scoped to `body.landing` | unprefixed classes | + +Shared across both: Geist + Geist Mono, one red accent, no gradients as +decoration, no glassmorphism, no fake window chrome, no emoji. + +**Which one am I editing?** If the class starts with `.l` or the rule is +scoped to `body.landing`, it is the landing and only the landing. Every +unprefixed rule still reaches `/verify` and the viewer, so changing one +changes pages you may not be looking at. + +--- -htmlbin is a **document, not a marketing site**. Every page reads like -the output of a curl that an agent ran — formal, terse, unembellished. -The whole product is one paragraph, one URL, and a Bearer token; the -design should match that economy. +## 1. Philosophy -The aesthetic borrows from three places: -- **Vercel** (geometric sans, sharp hairlines, white-on-white density) +The **app chrome** is a document, not a marketing site. Every one of +those pages reads like the output of a curl an agent ran — formal, terse, +unembellished. The product is one paragraph, one URL and a Bearer token, +and that economy is the point. + +The **landing** used to follow the same rule, and that was a mistake. +Held next to pages that do this job well it read as *unstyled* rather +than restrained: a left-aligned document on pure white, opening with a +collapsed memo. Fixing it by subtraction — pulling out the accent word, +the subhead, the motion, the shadows — made it worse. Every one of those +cuts was defensible in isolation and the sum was a text file. + +> **Austerity is not professionalism.** The pages worth learning from are +> *heavily* designed; they just don't spend the budget on slop +> vocabulary. They pay for a real surface, composition, typographic +> scale, and evidence. Diagnose with a checklist, but fix by adding. + +So the landing gets to be a landing: a tinted surface, centred +composition, display type carrying a claim, and a real product shown +above the fold. The app chrome stays a document. + +The aesthetic borrows from: +- **Vercel** (geometric sans, sharp hairlines, density) - **Sentry** (single saturated red as the only color; everything else near-monochrome) -- **HTTP itself** (the memo on every page is literally an HTTP request) +- **HTTP itself** — but only where it is actually real. See §5.2. What we **avoid**: - Anthropic editorial / italic display serif - Warm cream paper, IBM Plex Serif, deep forest greens - Generic AI-slop aesthetics (purple-blue gradients, generic Inter, etc.) -- Marketing hero patterns ("Give your X a Y", subhead-with-period, etc.) - Anything that overlaps with [getadb.com](https://getadb.com)'s composition — that was an early near-clone we course-corrected away from +- Anything that overlaps with **traces.com**'s composition. It is the + closest thing to a competitor in this space (session sharing for coding + agents), so borrowing its layout is both a rule-#1 violation and + strategically dumb. Take sensibility, invent composition. > **The North Star:** if a developer cracks this site open at 11pm and > doesn't immediately know whether to take it seriously, we lost. @@ -46,14 +90,13 @@ What we **avoid**: - Lede: 19px - Inline code: 0.86em (relative) - Mono microcopy (headers, footers, labels): 11–13px -- Hero headline on `/`: 40–52px, Geist 700, with `` for the red word. - This is the one place the landing page does carry a marketing headline - ("API for *agents* to share HTML.") — the HTTP-memo sits above it as - the document's first line. +- Landing headline: `clamp(38px, 5.6vw, 66px)`, Geist 700, **all black**. + No ``, no accent-coloured word — see §7. +- Landing lede: 19.5px, `--ink-soft`, max 60ch **Letter-spacing:** -- Tight on big sans (`-0.025em` on h1.title) -- Open on uppercase mono labels (`0.06–0.08em`) +- Tight on big sans (`-0.035em` on the landing `h1`, `-0.025em` on `h1.title`) +- Open on uppercase mono labels (`0.06–0.12em`) - Default on everything else --- @@ -87,14 +130,49 @@ appears sparingly — it's a signal, not a treatment. --code-fg: #FAFAFA --code-dim: #A3A3A3 --code-em: #FF6470 /* a slightly desaturated red on dark */ + +/* radius scale — three steps, nothing else. Before this existed the + sheet carried nine ad-hoc radii, which reads as unconsidered. */ +--r-sm: 4px /* inline code, controls, small buttons */ +--r-md: 8px /* blocks, nav button */ +--r-lg: 14px /* the prompt slab, the evidence frame */ + +/* terminal-syntax colors. Tokenised because they used to be hardcoded + at each use site, which let a second accent (two different blues) + leak into a palette this doc calls "one red". */ +--ok: #1F8A3A /* success on light */ +--ok-on-dark: #34D058 /* success inside code surfaces */ +--syn-key: #3B6EE8 /* command keyword, light surfaces */ +--syn-key-dark: #82B1FF +``` + +**Landing-only tokens** (scoped to `body.landing`): + +```css +--page: #F4F5F6 /* the tinted surface white cards sit on */ +--card: #FFFFFF +--lshell: 1080px ``` **Rules of thumb:** -- Red is for **emphasis**, **hover**, and the **angle brackets** in the - wordmark. Never for body text. Never for headlines. -- Green appears as a 6×6px dot on the status pill, that's it. -- The dark code block is the only inversion on the page — keep it scarce. -- No gradients. No shadows above 1px. No glow. +- Red is for **emphasis**, **hover**, the **angle brackets** in the + wordmark, and the **primary CTA fill**. Never for body text. **Never + for a word inside a headline.** +- Green appears as a small dot or a success state, never as type. +- The dark code block is the only inversion — keep it scarce. +- No shadows above 1px. No glow. Never a coloured shadow under a + coloured button. +- **No gradients, with two named exceptions**, both functional rather + than decorative, both landing-only: + 1. The dot texture on `body.landing` is a `radial-gradient`. It + renders as dots, not a colour wash, and it is what gives white + cards something to sit on. + 2. The bottom fade on `.shot` is a `linear-gradient`. Without it the + embedded page crops mid-sentence and reads as broken rather than + truncated. + + Anything else gradient-shaped is still forbidden. If you want a third + exception, it has to be load-bearing in the same way. --- @@ -119,33 +197,49 @@ appears sparingly — it's a signal, not a treatment. ## 5. Components -### 5.1 Top bar (`.page-head`) +### 5.1 Top bar — app chrome (`.page-head`) + +Used by `/verify`, the viewer, the gate and `/404`. **Not** the landing. + +``` + / GET /verify · v1 ● live · v1 · $ npm i -g … · /api/onboard +``` + +- Mono 12px, transparent background, no hairline — it reads as the + document's first line rather than a chrome strip +- The install pill and `/api/onboard` link collapse to a single + `@htmlbin/cli` link under 720px +- We never write "Cloudflare" or any impl detail here — project rule + +### 5.1b Landing nav (`.lnav`) + +The landing gets a real product header instead, because a mono +breadcrumb reads as terminal output and this page has to look like a +product to someone who has never heard of it. ``` -┌────────────────────────────────────────────────────────────────────┐ -│ ● live · v1 /llms.txt /api/onboard /openapi│ -└────────────────────────────────────────────────────────────────────┘ + Docs Patterns CLI ⃝ GitHub [ Get the CLI ] ``` -- 12px vertical padding, monospace 12px text -- Background `--bg-2` so it sits slightly back from the page -- Hairline below -- Status pill: green-dot + `live · v1` (we never write "Cloudflare" or - any impl detail here — that's a project rule) +- Sticky, `rgba(244,245,246,.92)`, hairline below +- Wordmark keeps the red angle brackets (§4) — that is the through-line + between the two systems +- Exactly **one** solid button. It deep-links to the CLI's `#install` + anchor, not the repo root: the button names an action, so it should + land on the command rather than a README to scan +- Nav links hide under 820px; the wordmark and both right-hand items stay -### 5.2 The memo (`.req`) +### 5.2 The memo (`details.req`) — app chrome only -The most important component. Every public page opens with this. Reads -exactly like the verbose output of `curl -v`, color-coded: +Opens `/verify` and the viewer. Reads like the verbose output of +`curl -v`, colour-coded: ``` -▸ GET / HTTP/1.1 +▾ GET /verify HTTP/1.1 host: htmlbin.dev to: any agent reading this from: htmlbin re: publishing HTML to a public URL - date: May 9, 2026 - accept: text/agent-friendly, text/markdown, application/json 200 OK content-type: text/html; charset=utf-8 ``` @@ -154,53 +248,106 @@ exactly like the verbose output of `curl -v`, color-coded: - Header keys in `--ink-soft` with a colon suffix in `--ink-softer` - Header values in `--ink` weight 500 - The `re:` value usually highlights one phrase in red (``) -- The `▸` prefix sits at `left: -22px` (hidden on mobile) +- The `▸` prefix sits at `left: -22px` (hidden on mobile), rotating to `▾` - The trailing `200 OK` line uses `--green-dot` for the status code - -The memo opens every public page. On `/` the hero headline ("API for -*agents* to share HTML.") sits *below* the memo — the memo is still the -first line of the document, the hero is just one short sentence under -it. **Don't** add a third row of marketing prose between them. - -### 5.3 Prompt block (`.prompt`) - -Dark single-pane card holding the prompt the human pastes into their -agent. This is a deliberate exception to the "no fake mac chrome" -rule (5.4); it earns its keep because the prompt block is the single -most important clickable surface on the landing page and needs to -stand out. The visual rhythm (dots on the left, pill on the right) -is borrowed loosely from the paperclip onboarding card; the *content* -is a single real prompt — we deliberately don't fake a CLI we don't -ship or a `curl` command that careful agents flag as unsafe. - -- Background `--code-bg` (`#0A0A0A`), 14px border-radius, soft shadow -- Title bar (`.prompt-chrome`): - - Three traffic-light dots on the left (red `#FF5F57`, yellow - `#FEBC2E`, green `#28C840`) — small (11px), purely decorative - - `copy` pill on the right (`.prompt-mark`): mono, soft - white-on-dark, with a small copy glyph in front of the label. - Doubles as the in-chrome copy button — click and the prompt - body lands in the clipboard; the pill swaps to a check icon - and `copied` for ~1.6s. It's a button, not a static label. - The earlier centered `iterm2` title is retired, and the - earlier `claude` agent-affinity label is replaced by the - plain `copy` label (the function is now the affordance). - The large red `Copy prompt` CTA below the card stays for - discoverability — both controls share `data-copy` and one - event listener (`.js-copy-prompt` in `landing.ts`). - - **No** hairline beneath the title bar — title and body share the - same surface so the card reads as one continuous slab -- Body (`.prompt-body`): single `
` with the prompt. 13.5px mono,
-  `--code-fg`, 22px padding.
-- CTA button **below** the block (`.copy-cta`):
-  - Solid red (`--red`), white text, 13px mono uppercase
-  - Label `Copy prompt`, with the copy/clipboard SVG
-  - "Copied" state goes solid green
-- Cue line **above** the block (`.prompt-cue`): muted mono, short:
-  `↓ paste into your agent`.
-- Aftermath line **below** the CTA (`.prompt-aftermath`): explains
-  what happens after copying. Muted regular sans.
-- Emphasis text in `--code-em`.
+- **Default it open.** It shipped closed for a while, which meant the
+  page announced its whole conceit as one grey line and then abandoned it
+
+**Why it is not on the landing any more.** `to:`, `from:` and `re:` are
+not HTTP headers — they are RFC 5322 *email* headers. The component built
+to signal protocol-seriousness was displaying invented ones, and the
+engineers most worth impressing are exactly the ones who notice. It is
+the same failure as fake window chrome: decorative output that isn't
+real.
+
+The landing now shows the actual thing instead — a trimmed but literal
+`GET /api/onboard` response in `.onb`, captioned "This is the response,
+trimmed." That endpoint returns ~13 KB across 21 real sections, so
+showing it beats drawing it.
+
+The memo stays on `/verify` and the viewer because there it sits above
+genuine request context, and those pages are documents by design. If you
+ever restyle it, keep the invented headers out of any *new* surface.
+
+### 5.2b The onboard block (`.onb`) — landing
+
+Dark slab, same family as `.lbox`, holding real JSON from
+`GET /api/onboard`. Header strip carries the request line and a count
+(`one document · 21 sections`). Syntax colours come from `--syn-*` and
+`--ok-on-dark`; the body is `overflow-x: auto` so long lines scroll
+inside the block instead of widening the page.
+
+**Every character in it has to be true.** If the API shape changes, this
+block changes. A stale fake is worse than no block.
+
+### 5.3 The action block (`.lbox`) — landing
+
+Dark slab holding whatever the visitor is meant to copy, with three tabs
+(`agent` / `cli` / `skill`) and a primary CTA below it.
+
+- `--code-bg`, `--r-lg`, one 1px shadow. No large ambient blur.
+- **No traffic-light dots.** The old `.prompt` block had them as a
+  deliberate exception; they were the most toy-like thing on the fold,
+  and none of the pages worth learning from use fake window chrome. The
+  exception is withdrawn.
+- Tab strip: mono 12.5px, active tab marked by a red bottom border. The
+  strip has **no fill or radius of its own** — a rounded filled track
+  holding rounded filled tabs inside a rounded card is three nested
+  rounded surfaces, and it made a third chip cluster in one small bar.
+- Body is a CSS grid with all panes in the same cell, so the slab never
+  jumps height when you switch tabs. **The consequence: it sizes to the
+  tallest pane.** Keep every payload to the same number of rendered
+  lines or the shorter ones show dead black space. This has regressed
+  twice; check it after any copy edit.
+
+### 5.3b Primary CTA (`.lcta`)
+
+- Solid `--red`, white, sans 14.5px weight 500, `--r-md`, centred under
+  the block. No coloured shadow (§3).
+- The label names what the active tab will actually put on the
+  clipboard — `Copy the prompt` / `Copy the command` / `Copy the
+  install` — and updates on tab change so the verb stays true.
+- Success state goes `--ok` with the label `Copied` for ~1.6s.
+- Microcopy under it (`.lcta-fine`) carries the risk reducers:
+  `Free · no signup · one human click to start`.
+
+**Exactly one copy affordance.** There used to be two — an in-chrome
+pill and a big button 40px apart — which read as indecision. There also
+used to be *none* with a verb on it, which was worse: the page's whole
+job is starting the device-code flow, and it asked for nothing. One
+button, one verb.
+
+### 5.3c Works-with strip (`.works`) — landing
+
+Agent marks the CLI already auto-detects, as social proof.
+
+- **No box per item.** Outlined pills read as a tag list, and an empty
+  mark slot inside each one reads as an unchecked checkbox. Marks and
+  names sit directly on the page; uniform size and one ink colour is
+  what makes a logo row read as a single unit.
+- Marks are inlined SVG paths from `src/views/logos.ts`, muted to 62%
+  opacity, full ink on hover.
+- **Only ship a mark whose identity you verified.** That file documents
+  which products are deliberately text-only: Aider and Devin are not in
+  simple-icons, and its `amp` slug is Google AMP rather than
+  Sourcegraph's Amp. A wrong logo reads worse than no logo, and these
+  are other companies' trademarks.
+
+### 5.3d Evidence frame (`.frame`) — landing
+
+A real drop, live in an iframe, above the fold.
+
+- White card on the tinted page, `--r-lg`, hairline, one soft shadow
+- Header bar carries the real URL, the version, and an
+  `open the live page ↗` link that sits **next to the version it acts
+  on** and opens in a new tab. It used to be 11.5px in the faintest ink
+  on the page, floated hard right, where it read as decoration.
+- `.shot::after` fades the bottom so the crop is deliberate (§3)
+- Source is `/p//raw`, which does **not** bump `view_count`, so
+  homepage traffic doesn't inflate that drop's counter. Check that is
+  still true before changing the route.
+- It is a **real** drop, never a screenshot and never a mockup. Pick a
+  light one: the block above it is already dark.
 
 ### 5.4 Body prose (`.body`)
 
@@ -230,77 +377,153 @@ Two-column mono row at 11.5px in `--ink-soft`:
 Background `--bg-2`, 1px top hairline. **No** "powered by" or implementation
 references. The hosting platform is an implementation detail.
 
-### 5.7 Hero headline (`section.hero`, landing only)
+### 5.7 Landing hero (`.lhero`)
+
+The first thing on the page. No memo above it, no eyebrow pill, nothing
+between the nav and the headline.
 
-The one place the page carries a marketing-style headline. Sits directly
-under the HTTP-memo, before the prompt block.
+- `h1` — Geist 700, `clamp(38px, 5.6vw, 66px)`, `-0.035em`, max 17ch,
+  centred, **all black**
+- Lede — 19.5px, `--ink-soft`, max 60ch, centred
+- 96px top padding on desktop, 56px under 820px
 
-- `h1` — Geist 700, 40–52px, tight letter-spacing (`-0.025em`)
-- `` inside the headline shows the emphasized word in red, **upright**
-  (never italic — italics belong to serif design languages we avoid)
-- One short subline (`

`) beneath, body sans, `--ink-soft` +**No accent-coloured word in the headline.** One red word in a large +sans headline is the single most templated move in this category, and +none of the pages worth learning from do it. Emphasis comes from scale +and weight. If you want to mark one phrase, an underline is the move +(that is what the one reference doing it well uses) — never coloured +letters. + +**No eyebrow pill above the headline.** There was one reading +`v1 · LIVE`. "Live" is tautological — the page rendered, so it is live — +and "v1" is not a version anyone selects. The slot was imported from a +reference page that uses it for a genuine warning. **Only keep a slot if +something true goes in it.** --- -## 6. Page anatomy (single-column, 720px max) +## 6. Page anatomy + +### 6a. Landing (`/`) — centred, 1080px shell -The whole page reads as **one continuous document.** No horizontal rules -between sections. No chrome strip on top with a fill or a border. The -breadcrumb at the top is just the document's first line; everything -flows from there. Whitespace + typography do the sectioning work that -hairlines normally would. +Vertical order, top to bottom. Everything is centred. ``` - - / GET / · v1 /llms.txt /api/onboard /openapi - - ▸ GET / HTTP/1.1 - host: htmlbin.dev - to: any agent reading this - from: htmlbin - re: publishing HTML to a public URL - 200 OK content-type: text/html; charset=utf-8 - - API for *agents* to share HTML. ◀ hero headline (one line, sans) - Agent-native, end to end. ◀ subline - - ↓ paste into your agent - ┌──────────────────────────────────────────────────────┐ - │ ●●● [ claude ]│ - │ │ - │ Please publish to htmlbin │ - │ https://htmlbin.dev/llms.txt │ - │ │ - └──────────────────────────────────────────────────────┘ - [ Copy prompt ] ◀ red CTA, mono uppercase - - First publish needs one human click; after that, the agent owns it. - - ↓ a few drops people have made - - /p/gDMy7Vb how htmlbin works - /p/1Wyf23j cross-platform gstack — pr #1111 - /p/ztx4J9P workers nav — three redesigns - /p/i2taphP google logo — animation playground - - — htmlbin agent-card · /api/onboard - - htmlbin v1 · open source · agent-friendly htmlbin.dev + Docs Patterns CLI ⃝ GitHub [ Get the CLI ] + ─────────────────────────────────────────────────────────────────── + + Send your agent's work as a link, not a file. + A URL that survives every revision. Free, no signup, … + + ┌───────────────────────────────────────┐ + │ agent cli skill │ + │ Explain this as an HTML page — … │ + └───────────────────────────────────────┘ + [ Copy the prompt ] + Free · no signup · one human click + + Works with the agent you already use + ✳ Claude Code ▣ Cursor ✿ Codex ▤ Cline + Also Aider, Amp, Devin. Auto-detected … + + WHAT YOU SEND SOMEONE + ┌─────────────────────────────────────────────────────────────┐ + │ htmlbin.dev/p/ztx4J9P · v1 open the live page ↗ │ + ├─────────────────────────────────────────────────────────────┤ + │ [ a real drop, live in an iframe ] │ + └─────────────────────────────────────────────────────────────┘ + A real drop, not a screenshot. + + FOR THE AGENT READING THIS + ┌───────────────────────────────────────┐ + │ GET htmlbin.dev/api/onboard │ + │ { "summary": …, "publish": { … } } │ + └───────────────────────────────────────┘ + + versions tags + Revise without breaking … Find anything you published. + patterns passcodes + Start from a real structure. Keep some pages private. + + A FEW PAGES PEOPLE HAVE PUBLISHED + /p/gDMy7Vb how htmlbin works EXPLAINER + /p/1Wyf23j cross-platform gstack … PR WRITEUP + + ─────────────────────────────────────────────────────────────────── + — htmlbin agent-card · /api/onboard · @utsengar ``` -The viewer page (`/p/:slug`) uses a slim variant of this — a single -viewer-bar with the breadcrumb in front of the title, then full-bleed -iframe. The viewer-bar *does* keep one hairline beneath, because there -the iframe is foreign content and we need the visual demarcation. +**Section rhythm:** `.lsec` carries 92px top padding. The evidence block +gets `.lsec-tight` at 48px instead, because it is a continuation of the +fold rather than a new section — and starting it higher shows more of the +embedded page. + +**The four questions the fold has to answer,** in this order. If an edit +breaks one, it is a regression regardless of how it looks: -**Mobile:** 22px gutters, 16px base font, the right-side nav links in -the breadcrumb collapse away (the breadcrumb itself remains). +1. *What is it?* — headline +2. *What problem does it solve?* — headline's second clause +3. *Why is it different?* — lede +4. *What do I do next?* — the CTA, with a verb on it -**The unification rule:** if you're tempted to add an `


` or a -`border-bottom` to "section" the document, *don't*. Use whitespace and -type weight instead. The class `hr.rule` is intentionally `display:none` -in the global stylesheet so legacy markup keeps working without -producing a line. +The page shipped for a while answering only the first, which is how it +ended up with no call to action at all. + +**Bottom spacing must be padding, not margin.** `.footer-merged` is the +last child of a `
` with `padding: 0`, so a bottom *margin* collapses +through `main` and out of `body` — landing outside the tinted background +box, where `html`'s white shows through as a strip. `min-height: 100vh` +on `body.landing` is the backstop. + +### 6b. App chrome — one continuous document, 720px max + +`/verify`, the gate and `/404` read as **one continuous document.** No +horizontal rules between sections. No chrome strip with a fill or border. +The breadcrumb is just the document's first line. Whitespace and +typography do the sectioning work hairlines normally would. + +``` + ← / GET /verify · v1 ● live · v1 · $ npm i … · /api/onboard + + ▾ GET /verify HTTP/1.1 + host: htmlbin.dev + to: any agent reading this + re: publishing HTML to a public URL + 200 OK content-type: text/html; charset=utf-8 + + Your agent is asking us to mint a token. … + + VERIFICATION CODE + ( ABCD-EFGH ) + + ⃝ Sign in with GitHub +``` + +The viewer (`/p/:slug`) uses a slim variant: one viewer-bar with the +breadcrumb in front of the title, then a full-bleed iframe. That bar +*does* keep a hairline beneath it, because the iframe below is foreign +content and needs the demarcation. + +**Mobile:** 22px gutters, 16px base font, the right-side items in the +breadcrumb collapse (the breadcrumb itself stays). + +**The unification rule:** if you are tempted to add an `
` or a +`border-bottom` to "section" these pages, *don't*. Use whitespace and +type weight. `hr.rule` is intentionally `display: none` in the global +stylesheet so legacy markup keeps working without producing a line. + +### 6c. The mobile floor applies to us too + +The product tells agents to keep drops readable at 360px +(`skills/htmlbin/SKILL.md`). Our own pages are held to the same bar: no +page-level horizontal overflow at 360px or 768px, long lines scrolling +inside their own container rather than widening the document. + +**Testing note:** headless Chrome clamps its own window to a 500px +minimum, so `--window-size=360,…` silently renders at 500 and crops into +a 360px canvas. That looks exactly like broken mobile and is not. Render +the page inside an iframe of the width you actually want, or measure +`document.documentElement.scrollWidth` against `clientWidth`. --- @@ -310,38 +533,61 @@ These are not preferences; they're rules. Violating any of them breaks the design language. - **No Anthropic editorial italic serif.** No Instrument Serif. No IBM - Plex Serif. No display-italic h1. -- **No warm cream paper.** Background is pure white. + Plex Serif. No display-italic h1. No `font-style: italic` anywhere. +- **No warm cream paper.** The app chrome is pure white; the landing is + the cool grey `--page`. Neither is cream. - **No orange.** That belongs to getadb.com. Our accent is red. - **No black square logomark with a letterform inside.** Wordmark only. -- **No fake macOS terminal chrome anywhere except the prompt block.** - The prompt block (5.3) gets dots + a static `claude` pill on the right - intentionally — it's the primary CTA and needs the visual handle. - Everywhere else (HTTP-memo, viewer, raw HTML, passcode gate) stays - flat and chromeless. The earlier centered `iterm2` title is retired. -- **No "Are you an agent?" callout.** That phrasing is getadb's. We - address agents through the *whole* memo, not through a sidebar. +- **No fake window chrome. No exceptions any more.** `.prompt` used to + carry traffic-light dots as a sanctioned exception; they were the most + toy-like element on the fold and the exception is withdrawn. Tabs and + a copy button are enough to say "this is a thing you copy". +- **No fake output of any kind.** This is the general rule the traffic + lights were a special case of. Don't render invented HTTP headers, + invented terminal transcripts, invented log lines, or a drawing of an + API response. If a block looks like machine output, every character in + it has to be real. See §5.2. +- **No accent-coloured word inside a headline.** Emphasis is scale and + weight. An underline is acceptable; coloured letters are not. +- **No empty imported slots.** If a layout slot came from a reference + page, it only stays if something true goes in it. An eyebrow pill with + no news, a subhead that restates the headline, and a status dot with + no status are all the same mistake. +- **No status indicator without variance to report.** "Live" on a page + that just rendered is tautological. +- **No "Are you an agent?" callout.** That phrasing is getadb's. - **No "powered by" / "built on Cloudflare" / "edge:" / impl details - in user-facing copy.** Status pill says `live · v1`. Footer says - `htmlbin v1 · open source · agent-friendly`. The platform is an - implementation detail. + in user-facing copy.** The platform is an implementation detail. - **No headline pattern of the form "Give your agent a [X]"** or "No [X]. No [Y]." That's getadb's exact rhythm. -- **No marketing prose above the fold.** The memo is the hero. - **No emojis** (unless the user explicitly asks). No icon font. - Inline SVG only, used very sparingly. -- **No horizontal rules between sections.** No `
`, no - `border-bottom` on the page-head, no top border on the footer. The - page is one document; whitespace separates sections. -- **Motion budget is tight.** The only allowed motion is: (a) the - status-dot pulse on the `live · v1` pill in the top bar, (b) button - hover/click transitions, and (c) a **one-shot** word-by-word fade on - the landing-page H1 + subhead at first paint (runs once, never - loops). All three respect `prefers-reduced-motion: reduce`. Nothing - else moves — no scroll-linked reveals, no parallax, no auto-cycling - tickers. If you want to add a new motion source, treat that as a - design decision and get sign-off, not a CSS PR. -- **No purple-blue gradients.** Period. + Inline SVG only, used sparingly. +- **No horizontal rules between sections in the app chrome.** Whitespace + separates sections. (The landing's nav hairline and the evidence + frame's internal rule are structural, not sectioning.) +- **No approximated third-party logos.** Ship a mark only if you + verified its identity against the source asset. Text is the fallback. +- **Motion budget is tight.** Allowed: button hover/click transitions, + and the memo's 0.18s open reveal. Anything beyond that is a design + decision needing sign-off, not a CSS PR. + + Current state, verified against the code rather than assumed: + + | Animation | Where | Status | + |---|---|---| + | button hover / active | both surfaces | keep | + | `reqOpen` (memo reveal, 0.18s once) | app chrome | keep | + | `live-pulse` (infinite, 2.6s) | **still live** on `/verify` + viewer via `pageHead()` | **should go** | + | `hero-word-in` (staggered H1 fade) | **dead CSS** — no view emits `.wf` any more | delete | + + The pulse is gone from the landing only because the landing stopped + using `pageHead()`, not because anyone removed it. An infinite pulsing + dot is a named AI tell and reports no variance (§7, "no status + indicator without variance"). Retiring it means editing + `chrome.ts`'s `.live-pill` markup and the `.live-dot::after` rule, + which touches two pages — worth doing, not done yet. +- **No purple-blue gradients.** Period. See §3 for the only two + gradients that exist and why. --- @@ -370,9 +616,59 @@ To restyle the whole product: 1. Edit `src/styles.ts` 2. Save — wrangler hot-reloads, hash bumps automatically +> **Gotcha: `STYLES_CSS` is a TypeScript template literal.** A backtick +> anywhere inside it — including inside a CSS comment — terminates the +> string and the build fails with a parse error pointing at the comment. +> Don't quote property names in backticks when explaining a rule. This +> has broken the build twice. + --- -## 9. Vocabulary +## 9. Copy + +The doc had no copy section for a long time, which is how the landing +shipped with no call to action on it at all. + +**Voice: calm, clinical, no hype.** State the mechanic. No "boost your +productivity", no "supercharge", no exclamation marks, no em-dash asides +stacked three deep. + +**Lengths**, measured off the pages that do this well rather than +guessed — Vercel ships a 5-word headline; Linear 8 words + a 13-word +subhead; Resend 8 + 12: + +| Slot | Budget | +|---|---| +| Landing headline | 5–10 words | +| Landing lede | 12–15 words, one or two short sentences | +| Section eyebrow | 3–5 words | +| Capability heading | one short sentence | +| Capability body | ≤ 2 sentences | + +**Rules:** + +- **The headline states a job, not a mechanic.** "Your agent writes HTML. + You get a URL." describes what happens; a reader who does not already + feel the pain has no reason to care. "Send your agent's work as a link, + not a file." names the job, and the second clause carries the pain. +- **The lede carries the differentiator, not a risk reducer.** Nobody is + blocked on auth, so "one human click" does not belong in the highest + value slot on the page. Risk reducers go under the CTA as microcopy. +- **Every CTA has a verb** and names what actually happens. If the label + can go stale when state changes (tabs, modes), update it in JS. +- **Parallel structure in any scannable list.** Four capability headings + in four different grammatical shapes reads as unconsidered. +- **No internal vocabulary on the marketing surface.** "Mints a new + version", "slug stays put" and "drop" are all words we say to each + other. `/` says *page*; the API, skill and docs keep the product + vocabulary (§10). +- **Claims have to be checkable.** "Free" is true because there is no + billing code. "Works with" lists only agents the CLI actually detects. + If you cannot verify a claim, cut it. + +--- + +## 10. Vocabulary Just two words, used as ordinary English (not coined terms): @@ -386,7 +682,7 @@ that's the whole vocabulary. --- -## 10. Discoverability surface +## 11. Discoverability surface These exist for agents, not humans. They follow the same minimalism rule (no fluff, machine-parseable, content-negotiated where useful): @@ -410,7 +706,7 @@ in the same change. They're a single contract. --- -## 11. Future taste decisions +## 12. Future taste decisions Not every aesthetic call has been made. When the moment comes: @@ -429,3 +725,35 @@ Not every aesthetic call has been made. When the moment comes: meta-pile of social tags beyond `og:title`, `og:description`, `og:url`. - **Settings UI for humans:** there isn't one. If users want self-serve mgmt, an agent does it for them via the API. That's the whole product. + +--- + +## 13. Known drift — fix these before adding anything + +Everything below is verified against the code, not guessed. This list +exists because the doc once described a page that hadn't shipped, and a +later audit trusted the doc and reached a confidently wrong conclusion. + +- **`live-pulse` still runs** on `/verify` and the viewer. §7 explains + why it should go; retiring it means editing `chrome.ts` and the + `.live-dot::after` rule. +- **Dead CSS:** `.hero`, `.hero h1 .wf`, `@keyframes hero-word-in`, + `.hero-sub`, `.prompt*` (`.prompt-chrome`, `.prompt-mark`, + `.prompt-cue`, `.prompt-aftermath`, `.tabs`, `.tab`, `.tab-panel`), + `.copy-cta`, `.term-*`, `.caps-cue`, `.examples .cue`. The landing was + the only consumer and it no longer emits any of it. Deleting is safe + but it is a large diff, so it wants its own PR with a careful grep. +- **"Open source" is claimed with no LICENSE file to back it.** The + landing redesign happened to drop the phrase from the nav and footer, + so it is no longer on any HTML page — but `/llms.txt` still opens with + "Open source. Edge-hosted." (`discoverability.ts:156`), and there is no + LICENSE in this repo. The CLI repo is MIT; this one is unlicensed, and + `package.json` has no `license` field either. Either add a license or + drop the claim. This is a decision, not a copy edit. +- **The landing hardcodes one showcase slug** (`SHOWCASE_SLUG` in + `landing.ts`). If that drop is deleted the evidence frame goes blank. + No fallback. + +**When you change a component, change this doc in the same PR.** The +audit trail in git is not a substitute — nobody greps history before +trusting a design doc.