Marketing site and documentation for the Pest testing framework. Two independent front-ends live in one Laravel app; know which one you are touching before you edit.
The repo has two separate front-end pipelines. They do not share build tooling, CSS, or JS. Editing the wrong one is the most common mistake — read this section first.
A standalone static Vite project at resources/www/, completely separate from Laravel's asset pipeline. It is plain HTML + Tailwind v4 + Alpine.js — no Blade, no @vite.
| Path | What it is |
|---|---|
resources/www/index.html |
The entire landing page markup. Static HTML; interactivity via Alpine x-data attributes. This is the file you edit to change the homepage. |
resources/www/main.css |
Tailwind v4 entry: @import 'tailwindcss', @plugin 'tailwindcss-motion', the Google-Fonts @import (Archivo + Instrument Sans + JetBrains Mono), the @theme design tokens, and the marquee @keyframes. Design tokens go here. |
resources/www/main.js |
JS entry. Imports main.css, boots Alpine.js + tippy.js (registers an x-tooltip directive, currently unused). Page interactivity (tabs, spotlight, copy, mobile menu) lives inline as Alpine x-data/@-handlers in index.html, not here. |
resources/www/public/ |
Static assets served as-is: favicon.svg, og*.png/jpg, assets/, sponsors/ (marquee logos), logos/ (stack tiles). |
resources/www/mockups/ |
Working directory for mockup assets. |
resources/www/package.json, vite.config.js |
Its own npm project (Vite 4, Alpine, tippy, tailwindcss-motion, Tailwind v4). base: 'www', builds to ../../public/www. |
- Dev:
cd resources/www && npm install && npm run dev. - Build:
cd resources/www && npm run build→ writes topublic/www/. - Served by:
routes/web.php→Route::get('/')returnsfile_get_contents(public_path('www/index.html')). It serves the built artifact, so changes toresources/www/do not appear at/until you build. Iterate on the dev server (npm run dev, athttp://localhost:<port>/www), then build and re-verify. public/www/= generated output (hashed*.css/*.js,index.html, and copies of everything inresources/www/public/). Never hand-edit; it is overwritten by every build.- Asset paths: reference public assets root-absolute in the source (
/logos/x.svg,/sponsors/x.png,/assets/x.png). Vite'sbase: 'www'rewrites them to/www/…at build so they resolve when the page is served at/. In dev both/logos/x.svgand/www/logos/x.svgresolve. Below-the-fold<img>s useloading="lazy".
Laravel + Blade, driven by markdown. A different pipeline entirely.
- Content:
resources/docs/{version}/*.md— versions3.x,4.x,5.x,master. Default version comes fromconfig/site.php(site.defaultVersion); default page isinstallation. Docs content is also mirrored from thepestphp/docsGitHub repo (see the "Edit this page" link indocs.blade.php). - Controller:
app/Http/Controllers/DocsController.php→ parses markdown throughapp/Support/Documentation.php+app/Support/MarkdownParser.php(+app/Support/MarkdownStylers/) → renders Blade viewresources/views/docs.blade.php. - Blade chrome: layouts in
resources/views/layouts/(base.blade.php,app.blade.php,welcome.blade.php), partials inresources/views/_partials/, components inresources/views/components/(e.g.<x-app-layout>,<x-docs.sidebar>,<x-docs.sponsors>). - Docs CSS/JS:
resources/css/app.css(Tailwind v4 +@tailwindcss/typographyforprose prose-invert, plus DocSearch CSS) andresources/js/app.js, bundled by the rootvite.config.jsvialaravel-vite-plugin. Selection color and scrollbar styling live here.
| Route / concern | Code | View |
|---|---|---|
IDE / editor setup (/docs/editor-setup) |
app/Http/Controllers/IDEPluginsController.php |
resources/views/ide.blade.php |
llms.txt, llms-full.txt, /docs/{page}/llms.txt, /docs/{page}.md |
app/Http/Controllers/LlmsTxtController.php |
— (markdown/plain-text responses) |
| Content negotiation | / and /docs/{page} also return raw markdown when the client sends Accept: text/markdown and not text/html. |
— |
routes/web.php— all web routing (alsoapi.php,channels.php,console.php).config/site.php— site title, default docs version, and related site config.resources/img/— legacy site imagery (bg-head*,pattern.svg,bg-pest-4.jpg).resources/lang/— translations.redesign.md(repo root) — an outdated brand brief describing an earlier direction (Exo font, purple/indigo beam hero). Ignore it; the<design-system>below is the authoritative visual direction for the landing page and is what is actually shipped.
- Changing the homepage look/copy →
resources/www/(static HTML +main.css@theme+ Alpine), then rebuild. - Changing docs content →
resources/docs/{version}/*.md. - Changing docs layout/theme →
resources/views/**+resources/css/app.css.
This is the complete visual specification for the landing page (resources/www/), and the spec the shipped index.html already implements. It is self-contained on purpose: use it to add elements that belong or build new sections without diffing the whole file. Every value below is a real value, not an approximation.
The theme is dark-only. All values below are the canonical dark values. Write them as plain (unprefixed) classes — do not add light-mode counterparts or dark: variants. If any copied markup carries dark: pairs, keep only the dark value.
The design language is a developer blueprint: an engineering drawing of a testing framework. A near-black zinc canvas is framed by hairline rails, annotated with rose crosshair registration marks, and stamped with a 56px blueprint grid that fades out radially. All labels are monospace and lowercase, prefixed with // like code comments. Real terminal output is the hero imagery — green checkmarks and PASS chips are the product screenshots. One accent color (rose) does all the pointing. Headings are set in a wide, heavy grotesque (Archivo, stretched to 112%) with exactly one italic rose phrase per heading. The voice is agent-aware and confident: short declarative sentences, numbers as proof.
- Tailwind CSS v4 — already the CSS framework in
resources/www/. Config is CSS-based (@themeinresources/www/main.css); there is notailwind.config.js. Use arbitrary values freely where the scale doesn't fit (rounded-[10px],leading-[1.85],pt-19,gap-4.5,h-15.5,size-2.25). - Markup — one static
resources/www/index.html. There is no Blade/@foreachhere: repeated blocks (cards, chips, tiles) are either literal repeated HTML, or driven with Alpinex-forover an inline data array when the list is long. Preferx-forfor the sponsor marquee, feature chips, stack tiles, testimonials, and stats. - Interactivity — use Alpine.js (already a dependency), not ad-hoc scripts. The prototype's three behaviors map to Alpine as:
- Tabs (code panel):
x-data="{ tab: 'browser' }", tab buttons@click="tab = 'browser'"+:aria-selected="tab === 'browser'", panesx-show="tab === 'browser'"(or toggleinvisible opacity-0classes with:classto preserve height — see §6.7). - Pointer spotlight:
@pointermoveon the hero sets CSS vars--spot-x/--spot-y; gate to(hover: hover)before binding. - Copy button:
x-data="{ copied: false }",@click="navigator.clipboard.writeText('composer require pestphp/pest --dev'); copied = true; setTimeout(() => copied = false, 2000)", labelx-text="copied ? 'copied!' : 'copy'".
- Tabs (code panel):
- Fonts — loaded via a Google-Fonts
@importat the top ofresources/www/main.css, mapped to the@themetokens below:- Archivo (display) — needs the width axis:
family=Archivo:ital,wdth,wght@0,62..125,400..900;1,62..125,400..900. Headings always usefont-stretch-112%. - JetBrains Mono (mono) — weights 400/500/700 + italics.
- Instrument Sans (body) — weights 400/500/600.
- Archivo (display) — needs the width axis:
- Tokens in
resources/www/main.css:
@theme {
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--font-display: 'Archivo', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
--text-3xs: 9px; --text-3xs--line-height: 1.4;
--text-2xs: 11px; --text-2xs--line-height: 1.4;
--text-xs-plus: 12.5px; --text-xs-plus--line-height: 1.4;
--animate-marquee: marquee 30s linear infinite;
@keyframes marquee { to { transform: translateX(-50%); } }
}<body>classes:font-synthesis-none bg-zinc-900 font-sans text-zinc-200 antialiased selection:bg-rose-600 selection:text-white.
Tailwind's built-in zinc ramp plus rose as the single accent. No custom hex values anywhere.
| Role | Class | Usage |
|---|---|---|
| Page canvas | bg-zinc-900 |
body, sticky header (bg-zinc-900/85 + backdrop-blur-md) |
| Card / raised surface | bg-zinc-800 |
all cards, tab panels, search input, code chips |
| Inset terminal / well | bg-zinc-900 |
terminal blocks inside zinc-800 cards, chips inside cards |
Rule: a surface nested inside zinc-800 drops back to zinc-900. Skeleton/placeholder shapes inside wells use bg-zinc-700 (base) and bg-zinc-600 (emphasis). Subtle translucent surfaces (secondary buttons) use bg-white/5.
- Every hairline is
border-zinc-700— section frames, card outlines, dividers, chips. Exactly one border color. - Hover borders on interactive cards/inputs:
hover:border-zinc-200/20. Secondary pill buttons:border-white/15 hover:border-white/30. - Avatar outline:
outline-1 -outline-offset-1 outline-white/10.
| Role | Class |
|---|---|
| Primary text | text-zinc-200 |
| Secondary / body copy | text-zinc-400 |
Tertiary / terminal comments, $ prompts |
text-zinc-500 |
| Inside dark terminal blocks | text-zinc-300 (code), text-zinc-100 (bold emphasis) |
text-rose-500— kickers'//, italic heading phrases, stat icons, "view more" links.bg-rose-500— primary buttons, "new" badges, crosshair marks, tab active underline (aria-selected:border-rose-500).hover:bg-rose-600— primary button hover.hover:text-rose-400— icon-link hover.text-rose-400— rose accents inside terminal blocks (comment//,agent →labels, "changed").after:bg-rose-950— the highlighter stripe behind the hero's italic phrase (::afterbar,h-2.5,bottom-1.5,-z-10).- Spotlight grid lines:
rose-800/rose-900. Selection:selection:bg-rose-600 selection:text-white.
- Success:
text-green-400(✓, stats),bg-green-500(dots, progress fills), PASS chip =bg-green-400 px-1.5 font-bold text-zinc-900. - "Watching" badge:
border-green-500/20 bg-green-500/10 text-green-400pill +size-1.5 rounded-full bg-green-500dot. - Failure:
text-red-400(×, FAILED text),bg-red-500(FAIL chip,text-zinc-900),border-red-500(hr),text-red-500(→ gutter). - Numbers in failure output:
text-yellow-400.
| Token | Class |
|---|---|
Function / method / test-fn names (it, eval, arch, visit, expect, toBe, …) |
text-green-400 |
| Strings | text-yellow-200 |
Keyword (function) + operators (->, =, ::, =>) |
text-pink-400 |
Variables ($user, $this) |
text-zinc-100 |
Class names / namespaces (Http, App\Models\User) |
text-cyan-300 |
Named arguments (tries:) |
text-orange-300 |
| Numbers | text-violet-300 |
Properties after -> (email, posts, not), punctuation |
text-zinc-300 (plain) |
| Comments, prompts | text-zinc-500 |
The accent is pink (function keyword + every operator — ->/=/::/=>), everything callable (incl. it/eval/arch) is green, strings are yellow, numbers violet, class/type names cyan, variables near-white, named args orange. This applies to PHP code panes only — terminal-output blocks keep their own grammar (rose // / → / agent labels, green ✓, red ×/FAIL, §3 above), do not recolor those.
Code samples are hand-written HTML: each line is a <div>, indentation is literal (4 per level), blank lines are <div> </div>. No syntax-highlighter library — keep it that way; it guarantees pixel-exact output.
Three families, strict roles. Never mix roles.
Always the pair font-display font-stretch-112%. Never use Archivo without the stretch.
- H1 (hero):
text-[clamp(30px,8.6vw,40px)] sm:text-[clamp(34px,4vw,50px)] font-extrabold leading-[1.05] tracking-tight. Manual<br>breaks; sensitive phrases wrapped inwhitespace-nowrap. - H2 (section):
text-3xl sm:text-4xl font-extrabold tracking-tight text-balance,mt-4after its kicker. - H3 (card title):
text-lg font-bold text-balance mb-1.5. - Stat value:
text-xl font-bold leading-tight(block). Built-on tile title:text-lg font-bold. - Every H1/H2 contains exactly one accented phrase:
<em class="text-rose-500">(italic comes free fromem). In the hero it additionally gets the rose-950 highlighter bar (§3).
- Lede paragraphs:
text-lg text-zinc-400,max-w-[48ch]in the hero,text-pretty. - Card body:
text-sm/relaxed text-zinc-400,text-pretty. - Nav links:
text-sm font-medium text-zinc-400 underline-offset-4 hover:underline. - Buttons:
text-sm font-semibold.
For: kickers, terminal output, code, chips, kbd, stat labels, footer utility links, the install command. Sizes: text-xs (kickers, terminals — with tracking-widest on kickers/labels), text-xs-plus 12.5px (code panes, tile subtitles), text-2xs 11px (micro labels, badges, kbd), text-3xs 9px (tiny badges, browser-mock URL).
- Code pane line-height:
leading-[1.85]. Terminal line-height:leading-[1.9]. - Uppercase mono labels get
uppercase tracking-widest(e.g. "BUILT ON").
<div class="flex items-center gap-2.5 font-mono text-xs tracking-widest text-zinc-400">
<span class="font-bold text-rose-500">//</span> section label here
</div>Card-level variant (above H3): <div class="text-2xs mb-2 font-mono tracking-widest text-rose-500">// card label</div> — all rose, lowercase.
The page sits in a rail frame: <main class="container relative mx-auto border-x border-zinc-700">. Tailwind v4's container (max-width = current breakpoint) centered, with 1px vertical rails running the full page height. Header and footer are full-bleed (container mx-auto inside, no rails). Sections stack inside <main> separated by border-y/border-t/border-b border-zinc-700 — sections touch; there is no vertical gap between framed sections. Whitespace lives inside sections as padding.
- Horizontal content padding:
px-5 sm:px-10— everywhere, no exceptions. - Section heading block:
pt-19 ... pb-10(76px top, 40px below heading). Content grid below:pb-19. - Big CTA section:
py-20 lg:py-24, content centeredmax-w-2xl mx-auto text-center. - Card grid gap:
gap-4.5(18px). - Header:
h-15.5(62px),sticky top-0 z-50,border-b border-zinc-700 bg-zinc-900/85 backdrop-blur-md.
- Hero:
grid items-center gap-12 lg:grid-cols-[1.05fr_0.95fr],pt-14 pb-12 lg:pt-20 lg:pb-[72px]. - Bento (feature cards):
grid gap-4.5 lg:grid-cols-5; cards spanlg:col-span-3/lg:col-span-2alternating per row (3+2, 2+3, 3+2). Single column belowlg. - Fact strips (Built on, stats): bordered cells, not gapped —
grid grid-cols-2 lg:grid-cols-[auto_repeat(4,1fr)]withborder-r/border-bon cells (see §8). - Stack tiles:
grid grid-cols-2 sm:grid-cols-4 gap-4.5. - Testimonials:
grid gap-4.5 lg:grid-cols-4(1 intro column + 3 cards).
Rose "+" marks pinned to section corners, like printing registration marks. Top-left (mirror with -right-1.25, -bottom-1.25):
<span class="-left-1.25 -top-1.25 size-2.25 before:w-2.25 after:h-2.25 pointer-events-none absolute z-10
before:absolute before:left-0 before:top-1 before:h-px before:bg-rose-500 before:content-['']
after:absolute after:left-1 after:top-0 after:w-px after:bg-rose-500 after:content-['']"></span>Parent section must be relative. Marks sit on border intersections (the -1.25 offset centers the 9px mark on the 1px border). Use 2 (top corners) or 4 (all corners) per framed section. A fixed pair also rides under the sticky header (fixed top-14.25 inset-x-0 z-10 pointer-events-none wrapping a container relative mx-auto) so the rails always appear pinned.
<div class="bg-size-[56px_56px] absolute inset-0 opacity-50
bg-[linear-gradient(var(--color-zinc-700)_1px,transparent_1px),linear-gradient(90deg,var(--color-zinc-700)_1px,transparent_1px)]
mask-[radial-gradient(ellipse_90%_80%_at_60%_20%,#000_30%,transparent_75%)]"></div>56px cell, 1px zinc-700 lines at 50% opacity, faded by a radial mask so it never reaches the edges. Hero mask centered at 60% 20%; CTA mask ellipse_70%_90%_at_50%_45%. Parent needs relative overflow-hidden; the content sibling needs relative.
A second, brighter grid copy (rose lines: rose-800 horizontal / rose-900 vertical) stacked on top, masked by a 220px pointer-following circle intersected with the same radial fade:
<div class="... mask-[radial-gradient(220px_circle_at_var(--spot-x,50%)_var(--spot-y,50%),#000,transparent),radial-gradient(ellipse_90%_80%_at_60%_20%,#000_30%,transparent_75%)]
mask-intersect pointer-events-none opacity-0 transition-opacity duration-300" x-ref="spotlight"></div>Alpine (gate to (hover: hover)): on @pointermove at the hero, set --spot-x/--spot-y from the pointer position and raise opacity to 1; on @pointerleave set opacity 0.
The product screenshots of this site. Inside a card:
<div class="mx-4 mt-4 rounded-[10px] bg-zinc-900 px-4 py-3.5 font-mono text-xs leading-[1.9] text-zinc-300 overflow-x-auto">
<div><span class="text-zinc-500">$</span> ./vendor/bin/pest</div>
<div> <span class="bg-green-400 px-1.5 font-bold text-zinc-900">PASS</span> Tests\Feature\PostTest</div>
<div> <span class="text-green-400">✓</span> it creates a post <span class="text-zinc-500">— 0.11s</span></div>
<div>Tests: <span class="text-green-400">1 passed</span> <span class="text-zinc-500">(2 assertions)</span> · Duration: <span class="text-green-400">0.14s</span></div>
</div>Grammar: $ prompt in zinc-500 · PASS/FAIL as filled chips with zinc-900 text · ✓ green-400 / × red-400 · timings in zinc-500 after an em dash · summary line with colored counts. Terminal comments: <span class="text-rose-400">//</span> label in zinc-500. Progress bars: track h-1.5 rounded-full bg-zinc-700 overflow-hidden, fill h-full rounded-full bg-green-500 at an arbitrary width (w-[96%]), min-w-1.5 so tiny values stay visible.
<article class="flex flex-col overflow-hidden rounded-2xl border border-zinc-700 bg-zinc-800">
<!-- media: inset mx-4 mt-4, rounded-[10px], usually a terminal/code block; flex-1 to fill -->
<div class="mt-auto px-6 pb-6 pt-5">
<div class="text-2xs mb-2 font-mono tracking-widest text-rose-500">// kicker</div>
<h3 class="font-display font-stretch-112% mb-1.5 text-balance text-lg font-bold">Title</h3>
<p class="text-pretty text-sm/relaxed text-zinc-400">Body copy.</p>
</div>
</article>Radius scale: card rounded-2xl (16px) → inset media rounded-[10px] → tiny inner elements rounded-md/rounded-sm. Media is always inset 16px (mx-4 mt-4); the text block is always bottom-anchored (mt-auto).
Everything clickable is a pill (rounded-full).
- Primary:
active:scale-96 inline-flex items-center gap-2 rounded-full bg-rose-500 px-4 py-2.5 text-sm font-semibold text-white transition-[background-color,scale] hover:bg-rose-600 - Secondary: same box,
border border-white/15 bg-white/5 hover:border-white/30 transition-[border-color,scale] - Command / copy pill: mono card pill —
rounded-full border border-zinc-700 bg-zinc-800 py-2.5 pl-5 pr-4 font-mono text-xs sm:text-sm hover:border-zinc-200/20, with<span class="text-zinc-500">$</span> command(truncate) and a nested mini-pillcopybadge (text-2xs rounded-full border border-zinc-700 px-2 py-0.5 text-zinc-400); label swaps to "copied!" for 2s (§2 Alpine snippet). - Feature chips:
flex items-center gap-1.5 rounded-full border border-zinc-700 bg-zinc-900 px-3 py-1.5 font-mono text-xs text-zinc-400+size-3.5stroke icon; optionalnewbadge:text-3xs -mr-1 rounded-full bg-rose-500 px-1.5 py-px font-bold uppercase tracking-wide text-white. - Text links:
underline-offset-4 hover:underline; arrow links end with→.
Container is the card. Tab bar: flex overflow-x-auto border-b border-zinc-700 px-2 with role="tablist". Each tab: cursor-pointer whitespace-nowrap border-b-2 border-transparent px-3 py-3 font-mono text-xs text-zinc-400 transition-colors hover:text-zinc-200 aria-selected:border-rose-500 aria-selected:text-zinc-200 — state styled purely from aria-selected via the aria-selected: variant. Panes stack in one grid cell (grid parent, every pane col-start-1 row-start-1) and toggle invisible opacity-0 with transition-opacity duration-200, so the panel keeps the height of its tallest pane (no layout shift). With Alpine, bind :aria-selected on tabs and :class on panes off a single tab value; keep the paired code-pane + terminal-pane switching together.
Duplicated list (second copy aria-hidden="true"), track flex w-max animate-marquee hover:[animation-play-state:paused] motion-reduce:animate-none, keyframes translate to -50%. Wrapper edge fade: mask-[linear-gradient(90deg,transparent,black_8%,black_92%,transparent)] relative overflow-hidden. Each cell: flex items-center border-r border-zinc-700 px-12 py-7; logos are white/light SVGs at explicit heights (h-5–h-6, w-auto) — use light-colored originals as-is on the dark canvas.
Label cell + equal data cells divided by hairlines, no gaps. Data cell: flex flex-col justify-center gap-0.5 px-5 sm:px-6 py-5.5 border-r border-zinc-700 with <b class="font-display font-stretch-112% text-lg font-bold"> + <span class="text-xs-plus text-zinc-400">. Stat variant adds a leading icon: size-6 shrink-0 text-rose-500 stroke SVG, value text-xl.
Fake browser for UI illustrations: header flex items-center gap-1.5 border-b border-zinc-700 bg-zinc-900 px-3 py-2 with three size-2 rounded-full bg-zinc-700 dots and a URL pill (text-3xs ml-1.5 flex-1 rounded-md border border-zinc-700 bg-zinc-800 px-2.5 py-0.5 font-mono text-zinc-400). Body: skeleton bars h-2 rounded-sm bg-zinc-700 (accent bg-zinc-600), assertion highlight = ring-2 ring-green-500/70 around real text + a green result badge (text-3xs rounded-full bg-green-600 px-2 py-0.5 font-mono font-bold text-white).
All inline SVG, viewBox="0 0 24 24", fill="none" stroke="currentColor", stroke-width 1.6 (nav/stat icons) or 1.8 (chip icons), stroke-linecap="round" stroke-linejoin="round". Sizes: size-3.5 (chips), size-4 (nav), size-5 (search), size-6 (stats). Simple geometric single-path glyphs — draw new ones in the same weight, never import an icon font.
The PEST wordmark is one SVG path (viewBox="0 0 381 100"), defined once in the header with id="pest-wordmark" and fill-zinc-200, reused in the footer via <svg class="mt-10 h-auto w-full fill-white/10"><use href="#pest-wordmark"/></svg> — a giant ghost watermark closing the page. <use> only works if the source SVG is in the same document (it is — one index.html).
<figure class="flex flex-col rounded-2xl border border-zinc-700 bg-zinc-800 p-6"> — blockquote text-pretty text-sm/relaxed in real curly quotes “…”, then figcaption bottom-anchored (mt-auto ... pt-5): size-9 avatar (rounded-full outline-1 -outline-offset-1 outline-white/10, GitHub ?size=72 for 2×), name text-sm font-semibold block, role text-xs text-zinc-400, leading-tight.
- Press: every button/tile gets
active:scale-96. Transition only what changes:transition-[background-color,scale],transition-[border-color,scale],transition-colors,transition-opacity. - Durations: 200ms (tab panes), 300ms (spotlight), Tailwind default (~150ms) elsewhere, 30s marquee. Nothing bounces; no keyframe animations besides the marquee. (
tailwindcss-motionis available but the design intentionally stays restrained.) - Reduced motion:
motion-reduce:animate-noneon the marquee; the spotlight is gated to(hover: hover)devices. - Hover grammar: text links underline (
underline-offset-4); cards/inputs lighten their border (hover:border-zinc-200/20); primary buttons deepen (hover:bg-rose-600); marquee pauses. - Copy-to-clipboard:
navigator.clipboard.writeText(...), swap label to "copied!", restore after 2000ms. - Status dots are static (
size-1.5 rounded-full bg-green-500) — no pulsing.
Only two breakpoints: sm (640px) and lg (1024px), mobile-first. md, xl, 2xl are never used — keep it that way.
- Padding:
px-5→sm:px-10on every content block. Cell paddingpx-5 sm:px-6orpx-5 sm:px-8. - Hero: single column stack (
gap-12) →lg:grid-cols-[1.05fr_0.95fr]. H1 uses viewport clamps:text-[clamp(30px,8.6vw,40px)]thensm:text-[clamp(34px,4vw,50px)]— the only fluid type on the page. Watchwhitespace-nowrapspans in H1: phrases must fit 320px at the clamp minimum. - Header: nav links
hidden lg:flex; search boxhidden sm:flex(fixedw-56); logo + GitHub always visible. There is no theme toggle (dark-only). Belowlg, a hamburger (lg:hidden) opens a slide-in mobile menu driven byx-data="{ mobileMenuIsOpen: false }"on<body>— it carries the nav links (Documentation/YouTube/Sponsor) that are otherwise hidden. - Bento: 1 column →
lg:grid-cols-5withlg:col-span-3/lg:col-span-2. Inside the browser-testing card, media splitslg:grid-cols-2. - Fact strip (Built on):
grid-cols-2with the label cellcol-span-2 border-b→ atlg:lg:grid-cols-[auto_repeat(4,1fr)], label becomeslg:col-span-1 lg:border-b-0 lg:border-r. Cells alternateborder-r(kept on mobile 2-col) andlg:border-r(only for 4-col). When adding cells, re-derive which needborder-rvslg:border-rfor both layouts. - Stats strip:
grid-cols-2 lg:grid-cols-4; per-cell borders are data-driven: cell 1border-b border-r lg:border-b-0, cell 2border-b lg:border-b-0 lg:border-r, cell 3border-r, cell 4 none. - Stack tiles:
grid-cols-2 sm:grid-cols-4. Testimonials: 1 col →lg:grid-cols-4. Sponsor strip: label row stacks above the marquee (border-b) →lg:grid-cols-[auto_1fr]side-by-side (lg:border-b-0 lg:border-r). - Overflow safety: every code/terminal pane has
overflow-x-auto; tab barsoverflow-x-auto; command pill texttruncate; long tokenswhitespace-nowrap. Nothing may cause page-level horizontal scroll. - Footer:
flex flex-col items-center gap-4 sm:flex-row justify-between. - Buttons wrap with
flex flex-wrap items-center gap-3(+justify-centerwhen centered).
- Headings: sentence case, short, benefit-first. Exactly one italic
<em class="text-rose-500">phrase per H1/H2 — the emotional word ("choose Pest?", "beautifully", "Again."). In the hero, "and AI agents" is the emphasized phrase (gets the rose highlighter bar). - Kickers: lowercase, terse, comment-style —
// test with confidence,// agent browser,// ai evals. - Body copy: 2–3 declarative sentences. State the mechanism, then the payoff. No exclamation marks, no "simply/just/easily", no marketing superlatives — proof is numeric ("A 15-second suite comes back in under a second", "96%", "70M+").
- Agent-native tone: agents are first-class users, addressed matter-of-factly: "agents start here:", "Your agents verify their own work", "fast enough for an agent to run it after every edit". Write new copy assuming both a human and a coding agent will read it.
- Terminal samples are copy too: realistic, plausible output (real commands, real durations, believable counts) — never lorem-ipsum filler.
- Footer utility links are mono paths:
/llms.txt,/source,/docs.
<section class="relative border-t border-zinc-700">(orborder-y) inside the railed<main>.- Add crosshair marks at the new border intersections (§6.1) — top two minimum.
- Heading block:
pt-19 px-5 pb-10 sm:px-10→ kicker (§4) → H2 with one rose<em>. - Content:
px-5 sm:px-10 pb-19, grid withgap-4.5, cards per §6.5. - Any code/terminal content follows §6.4 grammar and the §3 syntax palette.
- Check 320px, 640px, 1024px: paddings switch, grids collapse, nothing overflows.
- New assets →
resources/www/public/(SVG preferred, stroke style per §6.11). Rebuild to see changes at/.
- Loop: edit
resources/www/→ check on the dev server →npm run build→ confirm the built page still matches (the/route servespublic/www/index.html, not the source). - Dark-only: write dark values as plain classes; never add
dark:variants or a theme toggle.<body>isbg-zinc-900. - Wordmark: keep the header
<svg id="pest-wordmark">— the footer watermark is a<use href="#pest-wordmark">and only resolves because both live in the sameindex.html. - Logo colors on the dark canvas: brand-colored SVGs (Laravel red, Drupal blue, CodeIgniter orange, Statamic lime, Livewire pink, Symfony white, sponsor logos) render as-is — do not
invert. Only truly-black/currentColorglyphs (WordPress, Inertia) getinvertto flip them white. Inverting an already-white logo turns it black (invisible) — the classic mistake here. - No page-level horizontal scroll:
<html>carriesoverflow-x-clipto absorb the ~4px bleed from the decorative corner crosshair marks (fixed-positioned marks escape<body>clipping). Keep it. - Interactivity is inline Alpine (§2): tabs, pointer spotlight, copy-to-clipboard, and the mobile menu are all
x-data/@-handlers inindex.html. The spotlight is gated to(hover: hover); the marquee keepsmotion-reduce:animate-none. Verify the browser console is error-free after any change.
This is the personality layer for the landing page (resources/www/); design-system §9 is the mechanical layer (one <em> per heading, kicker syntax, terminal grammar). Follow both.
The voice is distilled from three touchstones, deliberately weighted:
- Vercel (vercel.com) — the lead. Forward-looking, agent-native, punchy. Parallel and recursive phrasing for emphasis. The future already ships.
- Resend (resend.com) — concision. The category one-liner ("Pest is the … for …"), fragments over sentences, zero fluff.
- Laravel (laravel.com) — craft & joy. Elegance is a feature; testing is a pleasure. The human developer is the heart, never displaced.
One-sentence identity: Pest is an agent-native testing framework that PHP developers love to reach for. Agents are the amplifier, not an audience swap — write every line for a coding agent and the developer reading over its shoulder.
- Agent-native by default (Vercel — lead register). Agents are first-class operators: they run the suite after every edit, verify their own changes end-to-end, and grade their own AI features. Address them matter-of-factly, never as a novelty. Favor recursive/parallel phrasing ("your agents verify their own work", "tests fast enough for an agent to run after every edit").
- Forward-looking, proven now (Vercel). State the future in the present tense, then back it with real terminal output or a number. Confidence, never speculation.
- Still for PHP developers (Laravel). The developer is never sidelined. Craft, elegance, and the "joy of testing" stay the emotional core — agents make that joy compound. Exactly one warm, human note per view. Warmth, never whimsy.
- Category one-liner + concision (Resend). Every section reduces to one declarative sentence. Fragments beat sentences. Cut every word that carries no weight.
- Numbers are the proof. "96%", "70M+", "under a second", "0 js errors" — specifics carry the claims, adjectives do not.
- Do: "A 15-second suite comes back in under a second — fast enough for an agent to run after every edit." Don't: "Blazing-fast tests that supercharge your workflow!"
- Do: "Your agents verify their own work." Don't: "AI-powered testing for the future!"
- Do: name the mechanism, then the payoff. Don't: superlatives, "simply/just/easily/powerful", exclamation marks, or hedges ("might", "could help").
Before shipping a line: could Vercel ship its confidence? Is it as tight as Resend? Does it keep one note of Laravel craft? And would it read naturally to a coding agent? If not, rewrite.