Set the --cleanup-after flag in your lifecycle.yaml to control TTL. Defaults to PR_CLOSED.
",
+ "css": ".ds-prose { font-family: Inter, ui-sans-serif, system-ui, sans-serif; font-size: 16px; font-weight: 400; line-height: 1.65; color: #020817; max-width: 65ch; margin: 0; } .ds-code-inline { padding: 2px 6px; background: #E2E8F0; color: #020817; font-family: ui-monospace, SFMono-Regular, 'JetBrains Mono', Menlo, Consolas, monospace; font-size: 14px; font-weight: 500; border-radius: 4px; }"
+ },
+ {
+ "name": "Nav Link",
+ "kind": "nav",
+ "refersTo": "nav-link",
+ "description": "Top-nav link with default + active states. Active state in Blueprint Indigo (light) or Hi-Vis Yellow (dark).",
+ "html": "",
+ "css": ".ds-nav { display: inline-flex; gap: 4px; padding: 4px; } .ds-nav-link { display: inline-flex; align-items: center; padding: 6px 10px; font-family: Inter, ui-sans-serif, system-ui, sans-serif; font-size: 14px; font-weight: 500; color: #020817; text-decoration: none; border-radius: 4px; transition: background 150ms cubic-bezier(0.25, 1, 0.5, 1), color 150ms cubic-bezier(0.25, 1, 0.5, 1); } .ds-nav-link:hover { background: #E2E8F0; } .ds-nav-link-active { color: #0F55A6; } .ds-nav-link-active:hover { background: oklch(46% 0.14 252 / 0.1); }"
+ }
+ ],
+ "narrative": {
+ "northStar": "The Built System",
+ "overview": "Lifecycle is a tool for engineers who care about how things are built. The site is itself an artifact of that care: pixel-aligned, fast, well-typeset, with code and diagrams treated as first-class voices instead of decorative content. A skeptical platform engineer should land on this site, scan for thirty seconds, and conclude 'these people sweat the details, the product probably does too.' That conclusion is the entire job of the visual system. The system has a deliberate two-mode personality. Light mode is the blueprint: cool slate neutrals, deep indigo authority, generous negative space, the brand yellow used sparingly as a highlighter on a technical document. Dark mode is the workshop: warm near-black, a cream foreground that feels like paper under a lamp, and the brand yellow stepping forward as the primary signal. The same architecture, two atmospheres. Neither mode is the default; both are first-class.",
+ "keyCharacteristics": [
+ "Two-mode personality: light is the blueprint, dark is the workshop. Not skinning, repositioning.",
+ "Brand yellow is decorative in light, structural in dark. Its rarity in light is the point.",
+ "Type-led, not image-led. Real diagrams (React Flow) and real code (CodeHike) over stock graphics.",
+ "Flat at rest, lift on intent. Surfaces stay calm; hover and focus do the talking.",
+ "Monospace is a first-class voice, not a code-block formatting choice."
+ ],
+ "rules": [
+ {
+ "name": "The Highlighter Rule",
+ "body": "In light mode, Hi-Vis Yellow appears on no more than 5-10% of the visible surface and is reserved for moments that genuinely matter. The rest of the page belongs to slate and indigo. Yellow stops being a brand and starts being noise the moment it spreads.",
+ "section": "colors"
+ },
+ {
+ "name": "The Hue Inversion Rule",
+ "body": "Light mode and dark mode use different neutral hue families on purpose. Light is cool-slate (220 hue, low chroma). Dark is warm-coal (12-60 hue, low chroma). Do not unify them to a single grey ramp. The cool/warm split is what makes light feel like a blueprint and dark feel like a workshop.",
+ "section": "colors"
+ },
+ {
+ "name": "The No Pure Extremes Rule",
+ "body": "Never #000, never #fff. Light backgrounds tint slightly cool; dark backgrounds tint slightly warm. Pure extremes look clinical and undermine the lived-in atmosphere.",
+ "section": "colors"
+ },
+ {
+ "name": "The Mono-As-Voice Rule",
+ "body": "When body prose names a file, flag, command, env var, or schema key, it is set in mono, never quoted, never italicized, never both. Mono is how the docs speak about the system, not just how they format a code block.",
+ "section": "typography"
+ },
+ {
+ "name": "The Long-Line Floor Rule",
+ "body": "Body prose never exceeds 75 characters per line. Fixed, not advisory. Wide screens widen the gutters, not the text column.",
+ "section": "typography"
+ },
+ {
+ "name": "The Display-Once Rule",
+ "body": "Display weight (the largest, tightest setting) appears at most once per page. If a page wants two display headlines, the second one steps down to Headline. Hierarchy collapses the moment it repeats.",
+ "section": "typography"
+ },
+ {
+ "name": "The Lift-On-Intent Rule",
+ "body": "Elevation is a response, never a default. Cards, callouts, and feature tiles ship flat. They lift only when the user signals interest (hover, focus). The lift is translateY(-4px) plus shadow-lg, never one without the other. A flat card with a heavy shadow looks broken.",
+ "section": "elevation"
+ },
+ {
+ "name": "The Border-Over-Shadow Rule",
+ "body": "In dark mode, a 1px border in Workshop Graphite does the work that shadow-sm does in light mode. Shadows on dark surfaces almost always read as soot, not depth.",
+ "section": "elevation"
+ }
+ ],
+ "dos": [
+ "Do treat Hi-Vis Yellow as decorative in light mode and structural in dark mode. The mode-asymmetry is the point.",
+ "Do lead with type and information density. DevOps readers scan; reward fast eyes with crisp hierarchy and short scannable blocks.",
+ "Do set file names, flag names, command names, env vars, and schema keys in mono inline. Prose talks about the system; mono is how it names parts of it.",
+ "Do keep body prose to 65-75ch on every viewport. Wider gutters, not wider text.",
+ "Do use real diagrams (React Flow) and real code walks (CodeHike) over stock illustrations or screenshots.",
+ "Do use Drafting Line (light) / Workshop Graphite (dark) 1px borders to define surface edges. Borders carry more weight than ambient shadows in this system.",
+ "Do ship motion that respects prefers-reduced-motion. The existing keyframes (fade-up, slide-in, draw-line, pulse, shake) all need motion-safe variants.",
+ "Do verify AA contrast in both themes before shipping. The brand yellow on a white background fails, never use Hi-Vis as a body-text color in light mode.",
+ "Do keep the logo shake animation. It is the system's one sanctioned wink."
+ ],
+ "donts": [
+ "Don't ship the old-school Jekyll/MkDocs default theme (sidebar + content + zero typographic care).",
+ "Don't ship crypto / web3 neon-on-black styling: no glassmorphism cards, no animated gradient text, no high-saturation glow, no matrix hero effects.",
+ "Don't borrow the consumer GoodRx healthcare aesthetic (rounded warmth, photography-led marketing, the consumer color palette from goodrx.com).",
+ "Don't ship generic enterprise SaaS patterns: no stock isometric illustrations, no gradient blobs, no hero-metric template, no identical icon-card grids beyond the existing six-card Features section.",
+ "Don't use border-left or border-right greater than 1px as a colored stripe on cards, callouts, or alerts.",
+ "Don't use background-clip: text with a gradient. No gradient headlines, ever. Emphasis goes through weight, size, or a single color shift.",
+ "Don't use #000 or #fff. All neutrals tint toward the mode's hue family.",
+ "Don't spread Hi-Vis Yellow onto more than ~10% of any light-mode screen. It is a highlighter, not a fill.",
+ "Don't put a heavy shadow on a flat card. The lift is translateY(-1) plus shadow-lg, never shadow alone.",
+ "Don't introduce a third font. Inter + system mono is the entire stack.",
+ "Don't use modal dialogs as a first thought. Exhaust inline, drawer, popover, and routed-page alternatives first.",
+ "Don't use em dashes in any UI copy. Commas, colons, semicolons, periods, parentheses.",
+ "Don't repeat the hero gradient on docs pages or as a section divider. Its rarity is what makes it land on the home."
+ ]
+ }
+}
diff --git a/DESIGN.md b/DESIGN.md
new file mode 100644
index 00000000..75cf7365
--- /dev/null
+++ b/DESIGN.md
@@ -0,0 +1,336 @@
+---
+name: Lifecycle Docs
+description: The visual system for Lifecycle's marketing home and product documentation. Built System aesthetic — sharp, dev-native, slightly irreverent.
+colors:
+ hi-vis-yellow: "#FDDB00"
+ late-sun-gold: "#FFCE2C"
+ blueprint-indigo: "#0F55A6"
+ signal-amber: "#F59E0B"
+ destructive-red: "#EF4444"
+ schematic-black: "#020817"
+ graphite-mute: "#64748B"
+ drafting-line: "#E2E8F0"
+ blueprint-paper: "#FFFFFE"
+ workshop-coal: "#0C0A09"
+ workshop-cream: "#FAFAF9"
+ workshop-graphite: "#27201F"
+ workshop-mute: "#A6A29B"
+typography:
+ display:
+ fontFamily: "'IBM Plex Sans', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
+ fontSize: "clamp(2.25rem, 5.5vw, 4.5rem)"
+ fontWeight: 700
+ lineHeight: 1.05
+ letterSpacing: "-0.02em"
+ headline:
+ fontFamily: "'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif"
+ fontSize: "clamp(1.75rem, 3vw, 2.5rem)"
+ fontWeight: 700
+ lineHeight: 1.15
+ letterSpacing: "-0.01em"
+ title:
+ fontFamily: "'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif"
+ fontSize: "1.125rem"
+ fontWeight: 600
+ lineHeight: 1.4
+ letterSpacing: "0"
+ body:
+ fontFamily: "'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif"
+ fontSize: "1rem"
+ fontWeight: 400
+ lineHeight: 1.65
+ letterSpacing: "0"
+ note: "Brand and chrome surfaces (home, nav, sidebar, TOC, components)."
+ body-docs:
+ fontFamily: "'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif"
+ fontSize: "1.0625rem"
+ fontWeight: 400
+ lineHeight: 1.7
+ letterSpacing: "-0.005em"
+ color: "hsl(var(--foreground) / 0.92)"
+ note: "Reading scale for /docs prose. Same family as body, scaled and tracked for long-form via main:not(.layout-full)."
+ label:
+ fontFamily: "'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif"
+ fontSize: "0.75rem"
+ fontWeight: 600
+ lineHeight: 1.2
+ letterSpacing: "0.06em"
+ wordmark:
+ fontFamily: "'JetBrains Mono', ui-monospace, monospace"
+ fontSize: "0.875rem"
+ fontWeight: 600
+ textTransform: "uppercase"
+ letterSpacing: "0.08em"
+ note: "Nav and footer Lifecycle wordmark only. Set in mono uppercase for a coded, set-in-stone feel."
+ mono:
+ fontFamily: "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
+ fontSize: "0.875rem"
+ fontWeight: 500
+ lineHeight: 1.55
+ letterSpacing: "0"
+ note: "Loaded explicitly via next/font/google. Used for code blocks, inline code, and the wordmark."
+rounded:
+ sm: "4px"
+ md: "6px"
+ lg: "8px"
+ xl: "12px"
+ pill: "9999px"
+spacing:
+ xs: "4px"
+ sm: "8px"
+ md: "16px"
+ lg: "24px"
+ xl: "40px"
+ 2xl: "64px"
+components:
+ button-primary:
+ backgroundColor: "{colors.blueprint-indigo}"
+ textColor: "{colors.blueprint-paper}"
+ typography: "{typography.label}"
+ rounded: "{rounded.md}"
+ padding: "0 32px"
+ height: "44px"
+ button-primary-hover:
+ backgroundColor: "{colors.blueprint-indigo}"
+ textColor: "{colors.blueprint-paper}"
+ button-outline:
+ backgroundColor: "{colors.blueprint-paper}"
+ textColor: "{colors.schematic-black}"
+ typography: "{typography.label}"
+ rounded: "{rounded.md}"
+ padding: "0 32px"
+ height: "44px"
+ button-outline-hover:
+ backgroundColor: "{colors.drafting-line}"
+ textColor: "{colors.schematic-black}"
+ feature-card:
+ backgroundColor: "{colors.blueprint-paper}"
+ textColor: "{colors.schematic-black}"
+ rounded: "{rounded.xl}"
+ padding: "24px"
+ feature-card-hover:
+ backgroundColor: "{colors.blueprint-paper}"
+ textColor: "{colors.schematic-black}"
+ input-default:
+ backgroundColor: "{colors.blueprint-paper}"
+ textColor: "{colors.schematic-black}"
+ typography: "{typography.body}"
+ rounded: "{rounded.md}"
+ padding: "0 12px"
+ height: "40px"
+ chip-tag:
+ backgroundColor: "{colors.drafting-line}"
+ textColor: "{colors.schematic-black}"
+ typography: "{typography.label}"
+ rounded: "{rounded.pill}"
+ padding: "4px 10px"
+ code-inline:
+ backgroundColor: "{colors.drafting-line}"
+ textColor: "{colors.schematic-black}"
+ typography: "{typography.mono}"
+ rounded: "{rounded.sm}"
+ padding: "2px 6px"
+ nav-link:
+ backgroundColor: "transparent"
+ textColor: "{colors.schematic-black}"
+ typography: "{typography.title}"
+ rounded: "{rounded.sm}"
+ padding: "6px 10px"
+ nav-link-active:
+ backgroundColor: "transparent"
+ textColor: "{colors.blueprint-indigo}"
+---
+
+# Design System: Lifecycle Docs
+
+## 1. Overview
+
+**Creative North Star: "The Built System"**
+
+Lifecycle is a tool for engineers who care about how things are built. The site is itself an artifact of that care: pixel-aligned, fast, well-typeset, with code and diagrams treated as first-class voices instead of decorative content. A skeptical platform engineer should land on this site, scan for thirty seconds, and conclude _"these people sweat the details — the product probably does too."_ That conclusion is the entire job of the visual system.
+
+The system has a deliberate two-mode personality. **Light mode is the blueprint:** cool slate neutrals, deep indigo authority, generous negative space, the brand yellow used sparingly as a highlighter on a technical document. **Dark mode is the workshop:** warm near-black, a cream foreground that feels like paper under a lamp, and the brand yellow stepping forward as the primary signal. The same architecture, two atmospheres. Neither mode is the default; both are first-class.
+
+What this system explicitly rejects, carried verbatim from `PRODUCT.md`: old-school Jekyll/MkDocs default themes (sidebar + content + zero design care), crypto/web3 neon-on-black (high-saturation glow, glassmorphism cards, animated gradient text), and the consumer GoodRx healthcare aesthetic (rounded warmth, photography-led marketing). Generic enterprise SaaS — stock isometric illustrations, gradient blobs, hero-metric templates, identical icon-card grids — is also out.
+
+**Key Characteristics:**
+
+- Two-mode personality: light is the blueprint, dark is the workshop. Not skinning, repositioning.
+- Brand yellow is decorative in light, structural in dark. Its rarity in light is the point.
+- Type-led, not image-led. Real diagrams (React Flow) and real code (CodeHike) over stock graphics.
+- Flat at rest, lift on intent. Surfaces stay calm; hover and focus do the talking.
+- Monospace is a first-class voice, not a code-block formatting choice.
+
+## 2. Colors: The Built-System Palette
+
+Roles split by mode. The blueprint (light) leans on cool indigo and slate; the workshop (dark) leans on warm coal and cream. Hi-Vis Yellow is the through-line that ties both modes to the brand.
+
+### Primary
+
+- **Hi-Vis Yellow** (`#FDDB00`, ~`oklch(89% 0.18 100)`, HSL `52 100% 49.6%`): The brand carrier. In **light mode** it appears almost exclusively in the hero gradient and as occasional emphasis — its scarcity is the point. In **dark mode** it is the primary action color (buttons, links, focus rings, the Nextra primary hue). Treat it like high-visibility paint: it earns attention because it is rationed.
+- **Late Sun Gold** (`#FFCE2C`, ~`oklch(87% 0.17 89)`, HSL `46 100% 58.6%`): Hi-Vis's gradient partner. Used only in the hero background gradient (`from-primary-brand-default to-primary-brand-gold`) and never alone.
+
+### Secondary
+
+- **Blueprint Indigo** (`#0F55A6`, ~`oklch(46% 0.14 252)`, HSL `212 83.4% 35.5%`): The primary action color in **light mode**. Carries the hero accent on the home (`text-primary` on "that grow with you"), primary buttons, link underlines, and the focus ring. In dark mode it recedes; Hi-Vis takes over.
+
+### Tertiary
+
+- **Signal Amber** (`#F59E0B`): Reserved for system-level callouts (warnings, in-progress states, "experimental" badges). Distinct enough from Hi-Vis to never be confused with the brand. Use sparingly.
+- **Destructive Red** (`#EF4444`, HSL `0 84.2% 60.2%`): Errors, destruction confirmations, the heart icon in the footer. Never decorative.
+
+### Neutral — Blueprint (light mode)
+
+- **Schematic Black** (`#020817`, HSL `222.2 84% 4.9%`): Body text, headings, foreground icons. Cool-tipped, not pure black.
+- **Graphite Mute** (`#64748B`, HSL `215.4 16.3% 46.9%`): Secondary text, captions, sub-headings.
+- **Drafting Line** (`#E2E8F0`, HSL `214.3 31.8% 91.4%`): Borders, dividers, input strokes, code-inline backgrounds.
+- **Blueprint Paper** (`#FFFFFE`, HSL `0 0% 100%`): Background and card surface. Tinted toward the cool family by 1% to keep it from being clinical pure-white.
+
+### Neutral — Workshop (dark mode)
+
+- **Workshop Coal** (`#0C0A09`, HSL `20 14.3% 4.1%`): Background. Warm-tipped near-black, never `#000`. Reads like a desk lamp turned low, not a void.
+- **Workshop Graphite** (`#27201F`, HSL `12 6.5% 15.1%`): Cards, secondary surfaces, borders, dividers, input strokes. One note above Coal.
+- **Workshop Mute** (`#A6A29B`, HSL `24 5.4% 63.9%`): Secondary text and captions.
+- **Workshop Cream** (`#FAFAF9`, HSL `60 9.1% 97.8%`): Body text and headings. Warm, paper-like, never pure white.
+
+### Named Rules
+
+**The Highlighter Rule.** In light mode, Hi-Vis Yellow appears on no more than 5–10% of the visible surface and is reserved for moments that genuinely matter: the hero gradient, "new" badges, occasional text emphasis, callout headers. The rest of the page belongs to slate and indigo. Yellow stops being a brand and starts being noise the moment it spreads.
+
+**The Hue Inversion Rule.** Light mode and dark mode use different neutral hue families on purpose. Light is `cool-slate` (220° hue, low chroma). Dark is `warm-coal` (12–60° hue, low chroma). Do not unify them to a single grey ramp. The cool/warm split is what makes light feel like a blueprint and dark feel like a workshop.
+
+**The No Pure Extremes Rule.** Never `#000`, never `#fff`. Light backgrounds tint slightly cool; dark backgrounds tint slightly warm. Pure extremes look clinical and undermine the lived-in atmosphere.
+
+## 3. Typography
+
+The stack is two explicitly-loaded fonts via `next/font/google`. No third face, ever.
+
+**IBM Plex Sans** is the entire sans layer: home, nav, sidebar, TOC, marketing copy, component labels, docs prose. Plex has engineered DNA with slight humanist warmth, the right register for a system that wants to read as built rather than designed. Long-form prose in `/docs` runs at a slightly larger reading scale (1.0625rem / 1.7 / `-0.005em`) scoped via `article.nextra-content main:not(.layout-full)`; everything else uses the standard 1rem body.
+
+**JetBrains Mono** carries everything coded: code blocks, inline code, command flags, env vars, schema keys, and the `Lifecycle` wordmark in nav and footer (set uppercase with `0.08em` tracking).
+
+### Hierarchy
+
+- **Display** (Plex 700, `clamp(2.25rem, 5.5vw, 4.5rem)`, line-height 1.05, `-0.02em` tracking): Hero headlines on the home. One per page, maximum.
+- **Headline** (Plex 700, `clamp(1.75rem, 3vw, 2.5rem)`, line-height 1.15, `-0.01em` tracking): Section openers in marketing pages.
+- **Title** (Plex 600, `1.125rem`, line-height 1.4): Component headings inside cards, sidebar group labels, callout titles.
+- **Body — brand** (Plex 400, `1rem`, line-height 1.65): Marketing prose on the home and any non-docs surface.
+- **Body — docs** (Plex 400, `1.0625rem`, line-height 1.7, `-0.005em` tracking, color at 92% foreground): All long-form prose in `/docs`. Maximum line length 75ch. Headings inside docs: h1 700/`-0.024em`/1.15, h2 600/`-0.018em`/1.25, h3-h4 600/`-0.018em`/1.3.
+- **Label** (Plex 600, `0.75rem`, `0.06em` tracking): Tag chips, `[NEW]` / `[BETA]` badges, button text, table headers, the few uppercase moments allowed.
+- **Wordmark** (JetBrains Mono 600, `0.875rem` uppercase, `0.08em` tracking): Nav and footer `Lifecycle` only. The mono uppercase setting reads as a label etched into the chrome, not a brand name in disguise.
+- **Mono** (JetBrains Mono 500, `0.875rem`, line-height 1.55): Inline code, file paths, command flags, environment variable names, schema keys. Treated as a structural voice in the documentation.
+
+### Named Rules
+
+**The Mono-As-Voice Rule.** When body prose names a file, flag, command, env var, or schema key, it is set in mono — never quoted, never italicized, never both. Mono is how the docs _speak_ about the system, not just how they format a code block.
+
+**The Long-Line Floor Rule.** Body prose never exceeds 75 characters per line. Fixed, not advisory. Wide screens widen the gutters, not the text column.
+
+**The Display-Once Rule.** Display weight (the largest, tightest setting) appears at most once per page. If a page wants two display headlines, the second one steps down to Headline. Hierarchy collapses the moment it repeats.
+
+## 4. Elevation
+
+The system is **flat at rest, lift on intent**. Surfaces sit at a single z-plane by default; hover, focus, and selection are when elevation appears. This matches the existing `FeatureCard` pattern in the codebase and reinforces "tactile and snappy" without crossing into Material-style ambient shadows everywhere.
+
+### Shadow Vocabulary
+
+- **`shadow-sm`** (`box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05)`): The default for cards. Almost imperceptible in light mode, completely invisible in dark mode. The job is reassurance, not depth.
+- **`shadow-lg`** (`box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)`): Hover state for cards, dropdowns, popovers, tooltips. Always paired with a `-translate-y-1` micro-lift; shadow alone reads as a Bootstrap-era box.
+- **No "shadow-xl"** is used anywhere. If a surface needs more elevation than `shadow-lg`, it should be a different surface (modal, popover) — not a louder shadow.
+
+### Named Rules
+
+**The Lift-On-Intent Rule.** Elevation is a response, never a default. Cards, callouts, and feature tiles ship flat. They lift only when the user signals interest (hover, focus). The lift is `transform: translateY(-4px)` plus `shadow-lg`, never one without the other. A flat card with a heavy shadow looks broken.
+
+**The Border-Over-Shadow Rule.** In dark mode, a `1px` border in `Workshop Graphite` does the work that `shadow-sm` does in light mode. Shadows on dark surfaces almost always read as soot, not depth.
+
+## 5. Components
+
+The library is shadcn/ui (Radix primitives) extended for docs. Below is the on-tone behavior — not a re-spec of every prop.
+
+### Buttons
+
+- **Shape:** Rounded corners (`rounded-md`, 6px). Not pills, not square. Small radius keeps them on the "sharp" side of friendly.
+- **Primary** (light mode): Blueprint Indigo background, paper foreground, `h-11` (44px) on `lg` size, `0 32px` horizontal padding. Hover: opacity-90, no color shift. Focus: 2px Blueprint Indigo ring with 2px offset.
+- **Primary** (dark mode): Hi-Vis Yellow background, Workshop Coal foreground. Yellow is structural in dark; this is its main canvas.
+- **Outline:** Drafting Line border, paper background, Schematic Black text. Hover: Drafting Line background fill. Used as the secondary CTA next to Primary (e.g. "View on GitHub" next to "Get Started").
+- **Ghost / Link:** Used in nav. Underline-on-hover for link variant; subtle accent background for ghost.
+
+### Feature Cards
+
+The signature surface on the home `/`. Defines the hover personality of the whole site.
+
+- **Shape:** `rounded-xl` (12px). Larger radius than buttons by design — the card is the object, the button is the action.
+- **At rest:** `bg-card`, `1px border` in Drafting Line (light) or Workshop Graphite (dark), no shadow.
+- **Hover:** Border shifts to Blueprint Indigo at 50% opacity, `shadow-lg` appears, the entire card lifts `-translate-y-1` over 300ms ease-out. The icon container background brightens from `primary/10` to `primary/20`.
+- **Internal padding:** 24px (`p-6`). The icon-tile is 48×48 with 8px radius, sitting top-left.
+- **Anti-pattern guard:** never group more than 6 of these in one viewport. If a section needs more than 6, the pattern is wrong (use a list, a table, or a tabbed interface).
+
+### Cards (generic shadcn ``)
+
+- **Shape:** `rounded-lg` (8px) — one step tighter than Feature Cards.
+- **Background:** `bg-card`, identical to surface; the card's edge comes from a `1px` border, not a fill contrast.
+- **Internal padding:** 24px header, 24px content, 24px footer (`p-6`).
+- **Shadow:** `shadow-sm` at rest. No hover lift unless the card is interactive; static cards stay flat.
+
+### Inputs / Fields
+
+- **Style:** `1px` border in Drafting Line / Workshop Graphite, paper background, `rounded-md` (6px), 40px height, 12px horizontal padding.
+- **Focus:** 2px ring in Blueprint Indigo (light) or Hi-Vis Yellow (dark) with 2px offset. The ring sits _outside_ the border, not replacing it.
+- **Placeholder:** Graphite Mute / Workshop Mute, never the same color as filled-input text.
+
+### Chips / Tags
+
+Used for doc tags (`core`, `lifecycle`, `intro`, etc.) and inline `[NEW]` / `[BETA]` badges.
+
+- **Style:** Pill (`rounded-full`), Drafting Line / Workshop Graphite background, Schematic Black / Workshop Cream text.
+- **Padding:** 4px vertical, 10px horizontal.
+- **Typography:** Label setting (12px, 600, 0.06em tracking).
+
+### Navigation
+
+- **Top nav (Nextra):** Logo + section links + theme toggle + Discord icon. Logo has a custom `shake` animation on hover (`logo-shake` class) — keep it; it is the system's one allowed wink.
+- **Sidebar (Nextra):** `defaultMenuCollapseLevel: 2`, `defaultOpen: false`. Active link is Blueprint Indigo / Hi-Vis Yellow. Inactive links are Schematic Black / Workshop Cream. Hover background is a subtle accent tint.
+- **Right TOC (Nextra):** `float: true` with `extraContent` for related-tag content. "Back to top" uses Title typography.
+
+### Code Surfaces
+
+The single most important content type in `/docs`. Treat with care.
+
+- **Inline code:** Drafting Line / Workshop Graphite background, mono typography, `rounded-sm` (4px), 2px vertical / 6px horizontal padding. No border.
+- **Block code (CodeHike):** Full-width within the prose column, `rounded-lg` (8px), system-monospace, syntax tokens via CodeHike's own theme. Always show the language label as a chip in the top-right.
+- **Diagrams (React Flow):** Transparent backgrounds (`background-color: transparent !important` is in `globals.css` for a reason — keep). The diagram inherits the page's atmosphere instead of imposing its own.
+
+### Signature Component: The Hero Gradient + Grid
+
+The home page's defining visual: a yellow→gold gradient masked into a radial fade, overlaid with a sparse grid pattern (72×56 tile grid, four highlighted squares at fixed coordinates). This is the strongest brand signature in the system. It appears once, on `/`. **Do not replicate it on docs pages, landing-style sections, or as a section divider.** Its rarity is what makes it land.
+
+## 6. Do's and Don'ts
+
+### Do:
+
+- **Do** treat Hi-Vis Yellow as decorative in light mode and structural in dark mode. The mode-asymmetry is the point.
+- **Do** lead with type and information density. DevOps readers scan; reward fast eyes with crisp hierarchy and short scannable blocks.
+- **Do** set file names, flag names, command names, env vars, and schema keys in mono inline. Prose talks about the system; mono is how it names parts of it.
+- **Do** keep body prose to 65–75ch on every viewport. Wider gutters, not wider text.
+- **Do** use real diagrams (React Flow) and real code walks (CodeHike) over stock illustrations or screenshots. The codebase already has these; lean into them.
+- **Do** use Drafting Line (light) / Workshop Graphite (dark) `1px` borders to define surface edges. Borders carry more weight than ambient shadows in this system.
+- **Do** ship motion that respects `prefers-reduced-motion`. The existing keyframes (`fade-up`, `slide-in-*`, `draw-line`, `pulse`, `shake`) all need motion-safe variants.
+- **Do** verify AA contrast in both themes before shipping. The brand yellow on a white background fails — never use Hi-Vis as a body-text color in light mode.
+- **Do** keep the logo `shake` animation. It is the system's one sanctioned wink.
+
+### Don't:
+
+- **Don't** ship the **old-school Jekyll/MkDocs default theme** (sidebar + content + zero typographic care). Carried directly from `PRODUCT.md` as an anti-reference.
+- **Don't** ship **crypto / web3 neon-on-black** styling: no glassmorphism cards, no animated gradient text, no high-saturation glow, no "matrix" hero effects. From `PRODUCT.md`, repeated by name.
+- **Don't** borrow the **consumer GoodRx healthcare aesthetic** (rounded warmth, photography-led marketing, the consumer color palette from goodrx.com). From `PRODUCT.md`, repeated by name.
+- **Don't** ship **generic enterprise SaaS** patterns: no stock isometric illustrations, no gradient blobs, no hero-metric template (big number + small label + supporting stats), no identical icon-card grids beyond the existing six-card Features section.
+- **Don't** use `border-left` or `border-right` greater than 1px as a colored stripe on cards, callouts, or alerts. Replace with a full border, a leading icon/number, or a tinted background.
+- **Don't** use `background-clip: text` with a gradient. No gradient headlines, ever. Emphasis goes through weight, size, or a single color shift.
+- **Don't** use `#000` or `#fff`. All neutrals tint toward the mode's hue family.
+- **Don't** spread Hi-Vis Yellow onto more than ~10% of any light-mode screen. It is a highlighter, not a fill.
+- **Don't** put a heavy shadow on a flat card. The lift is `translate-y-1` _plus_ `shadow-lg`, never shadow alone.
+- **Don't** introduce a third font. The stack is IBM Plex Sans (everything not coded) + JetBrains Mono (code and wordmark). Two faces, every surface. Adding a serif display, a "designer" font, or a second sans for "variety" undercuts the Built System voice.
+- **Don't** use modal dialogs as a first thought. For a docs site this is almost always laziness — exhaust inline, drawer, popover, and routed-page alternatives first.
+- **Don't** use em dashes in any UI copy. Commas, colons, semicolons, periods, parentheses. Also not `--`.
+- **Don't** repeat the hero gradient on docs pages or as a section divider. Its rarity is what makes it land on `/`.
diff --git a/PRODUCT.md b/PRODUCT.md
new file mode 100644
index 00000000..e145b44e
--- /dev/null
+++ b/PRODUCT.md
@@ -0,0 +1,60 @@
+# Product
+
+## Register
+
+brand
+
+This site is a split surface: the home page (`/`) is brand-led marketing, and `/docs/*` is utility-led product. PRODUCT.md carries `brand` as the default because the home is the high-stakes first impression and the docs reading experience benefits from brand-level craft (typography, atmosphere, editorial pacing). For deep utility surfaces inside `/docs/*` (tables, schema reference, troubleshooting), override per task with `product`.
+
+## Users
+
+Two audiences in one site:
+
+- **Decision makers — Platform / DevOps engineers.** They land on the home page or "What is Lifecycle?" while evaluating tools. They are skeptical, scan-first, terminal-adjacent, and reading on a 27-inch monitor in a dim office or a laptop on a flight. Their job: decide in under five minutes whether Lifecycle is worth a deeper look, then forward a link to a teammate.
+- **Day-to-day consumers — App developers.** They land in `/docs/*` from a Google search or an internal Slack link, mid-task, with a specific question. Their job: find the exact recipe / flag / config / troubleshooting note, copy it, and get back to their PR. They do not want narrative; they want the answer.
+
+The site must serve both without compromising either: the home convinces, the docs deliver.
+
+## Product Purpose
+
+Documentation and marketing site for **Lifecycle** — an open-source ephemeral environments orchestrator that turns every GitHub pull request into a fully-functional preview environment. Lifecycle is licensed Apache 2.0 and maintained by GoodRx OSS.
+
+The site exists to:
+
+1. Convince a new visitor — in under a minute — that Lifecycle replaces brittle shared dev/staging environments with per-PR isolation that cleans up after itself.
+2. Get an evaluator from "what is this?" → "I have it running on a sample repo" with the fewest dead-ends possible.
+3. Be the canonical answer-source for existing operators and consumers (config, schema, troubleshooting, recipes).
+4. Carry the OSS community: visible paths to GitHub (stars, contribution) and Discord (community), without making the home feel like a star-farming page.
+
+Success looks like: a DevOps lead reads the home, opens the demo iframe, skims one feature page, and either installs or sends the link to their platform team. A developer searching "lifecycle environment variable templating" lands directly on the relevant docs page and copies the exact snippet they need within seconds.
+
+## Brand Personality
+
+**Sharp, dev-native, slightly irreverent.**
+
+- **Voice:** Direct. Technical without being dry. Explains hard things without dumbing them down. Earns trust by being precise about what Lifecycle does and does not do.
+- **Tone:** Confident but not corporate. Comfortable with code, monospace, and shell snippets as first-class content. Allowed to wink — emoji in a `## A Developer's Story` heading is on-brand; corporate marketing copy is not.
+- **Emotional goals:** A reader should feel "this was built by engineers who care", not "this was wrapped by a marketing team". The site itself should feel like a small piece of evidence that the product is well-made.
+
+## Anti-references
+
+- **Old-school Jekyll / MkDocs / read-the-docs default theme.** Sidebar + content + zero design care. The OSS docs we are not.
+- **Crypto / web3 neon-on-black.** No high-saturation glow, no glassmorphism cards, no animated gradient text, no "matrix" hero.
+- **Consumer GoodRx healthcare aesthetic (goodrx.com).** Do not borrow the parent brand's consumer color palette, rounded warmth, or photography style. Lifecycle is OSS infra, not a healthcare app.
+- **Generic enterprise SaaS** (implied — guard against drift). No stock isometric illustrations, no gradient blobs, no "hero metric + 3 supporting stats" template, no identical icon-card grids.
+
+## Design Principles
+
+1. **Practice what you preach.** Lifecycle is a tool for engineers who care about craft. The docs themselves are evidence — pixel-aligned, fast, well-typeset. Sloppy docs would undercut the pitch.
+2. **Two doors, one welcome.** The home funnels visitors to two endings — _install_ (read → run) and _community_ (GitHub star + Discord). Both must be visible from the home without competing for the same pixel or feeling like a star-farming bar.
+3. **Density rewards the scanner.** DevOps readers scan before they read. Reward fast eyes with information-dense layouts, real code, real diagrams. Avoid hero-paragraph filler and stock-illustration negative space.
+4. **Show the system, don't describe it.** The codebase already ships React Flow diagrams and CodeHike code walks — lean into them. A live diagram of "PR → environment → cleanup" beats a paragraph every time.
+5. **Sharpness over softness.** Personality is sharp/dev-native, so the interface is too: crisp edges where appropriate, monospace as a first-class voice, code blocks treated as content not as ornament. Avoid rounded "consumer warmth".
+
+## Accessibility & Inclusion
+
+- **Target:** WCAG 2.1 AA. Treat as a hard floor, not a ceiling.
+- **Color:** All text and code blocks must meet AA contrast in both light and dark themes. Brand yellow is decorative — never the carrier of meaning.
+- **Keyboard:** Every interactive element (theme toggle, sidebar, code-tabs, accordions, React Flow controls, copy-code buttons) must be reachable and operable via keyboard with a visible focus ring.
+- **Motion:** Respect `prefers-reduced-motion`. Hero/feature animations must have a non-animated fallback. The existing custom keyframes (`fade-up`, `slide-in-*`, `draw-line`, logo `shake`) all need motion-safe variants.
+- **Content:** Code samples must be selectable and copyable, not images of code. Diagrams (React Flow) must have a textual description nearby for screen-reader users.
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 04170027..d2f3084c 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -51,5 +51,11 @@ export default [
version: "detect",
},
},
+ rules: {
+ "react/no-unknown-property": [
+ "error",
+ { ignore: ["jsx", "global"] },
+ ],
+ },
},
];
diff --git a/src/components/home/bg/index.tsx b/src/components/home/bg/index.tsx
index 0b0284bb..fdbde06c 100644
--- a/src/components/home/bg/index.tsx
+++ b/src/components/home/bg/index.tsx
@@ -15,34 +15,22 @@
*/
import { GridPattern } from "@/components/home/bg/grid";
+import { GridPulses } from "@/components/home/bg/pulses";
export const Bg = () => {
return (
-
-
-
-
-
-
-
+
+
+
);
};
diff --git a/src/components/home/bg/pulses.tsx b/src/components/home/bg/pulses.tsx
new file mode 100644
index 00000000..87ff9e9c
--- /dev/null
+++ b/src/components/home/bg/pulses.tsx
@@ -0,0 +1,140 @@
+/**
+ * Copyright 2025 GoodRx, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use client";
+
+import { motion } from "framer-motion";
+
+const easeOutQuart = [0.25, 1, 0.5, 1] as const;
+const cell = 56;
+
+type Pulse = {
+ axis: "x" | "y";
+ lane: number;
+ length: number;
+ duration: number;
+ delay: number;
+ repeatDelay: number;
+ reverse?: boolean;
+};
+
+const pulses: Pulse[] = [
+ {
+ axis: "x",
+ lane: 4,
+ length: 168,
+ duration: 5.6,
+ delay: 0.0,
+ repeatDelay: 4.4,
+ },
+ {
+ axis: "x",
+ lane: 8,
+ length: 112,
+ duration: 4.0,
+ delay: 2.4,
+ repeatDelay: 5.6,
+ reverse: true,
+ },
+ {
+ axis: "x",
+ lane: 11,
+ length: 96,
+ duration: 3.6,
+ delay: 1.4,
+ repeatDelay: 5.0,
+ },
+ {
+ axis: "y",
+ lane: 6,
+ length: 140,
+ duration: 5.4,
+ delay: 0.6,
+ repeatDelay: 4.8,
+ },
+ {
+ axis: "y",
+ lane: 14,
+ length: 112,
+ duration: 4.2,
+ delay: 3.0,
+ repeatDelay: 5.8,
+ },
+ {
+ axis: "y",
+ lane: 19,
+ length: 84,
+ duration: 3.8,
+ delay: 4.0,
+ repeatDelay: 6.0,
+ reverse: true,
+ },
+];
+
+const gradientFor = (axis: "x" | "y", reverse: boolean) => {
+ const angle =
+ axis === "x" ? (reverse ? "270deg" : "90deg") : reverse ? "0deg" : "180deg";
+ return `linear-gradient(${angle}, transparent 0%, hsl(var(--primary) / 0.05) 30%, hsl(var(--primary) / 0.55) 75%, hsl(var(--primary)) 96%, transparent 100%)`;
+};
+
+export function GridPulses() {
+ return (
+
+ );
+}
+
+export default GridPulses;
diff --git a/src/components/home/capabilities/index.tsx b/src/components/home/capabilities/index.tsx
new file mode 100644
index 00000000..00cf52dc
--- /dev/null
+++ b/src/components/home/capabilities/index.tsx
@@ -0,0 +1,267 @@
+/**
+ * Copyright 2025 GoodRx, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use client";
+
+import { useState } from "react";
+import { motion, type Variants } from "framer-motion";
+import { Check, Copy } from "lucide-react";
+import { cn } from "@/lib/utils";
+
+const easeOutQuart = [0.25, 1, 0.5, 1] as const;
+
+type CopyState = "idle" | "copied" | "failed";
+
+const lifecycleYaml = `version: "1.0.0"
+
+environment:
+ defaultServices:
+ - name: "api"
+ - name: "postgres"
+
+services:
+ - name: "api"
+ github:
+ repository: "acme/api"
+ branchName: "main"
+ docker:
+ defaultTag: "main"
+ app:
+ dockerfilePath: "Dockerfile"
+ ports: [8080]
+ env:
+ DATABASE_URL: "postgresql://app:pw@{{{postgres_internalHostname}}}:5432/appdb"
+
+ - name: "postgres"
+ docker:
+ dockerImage: "postgres"
+ defaultTag: "15-alpine"
+ ports: [5432]
+ env:
+ POSTGRES_USER: "app"
+ POSTGRES_PASSWORD: "pw"
+ POSTGRES_DB: "appdb"`;
+
+const headerStagger: Variants = {
+ hidden: {},
+ visible: { transition: { staggerChildren: 0.08, delayChildren: 0.05 } },
+};
+
+const headerItem: Variants = {
+ hidden: { opacity: 0, y: 10 },
+ visible: {
+ opacity: 1,
+ y: 0,
+ transition: { duration: 0.6, ease: easeOutQuart },
+ },
+};
+
+const capabilities = [
+ {
+ label: "Auto-deploy on pull request",
+ body: "Push, get an environment. No imperative steps.",
+ },
+ {
+ label: "Cross-repo composition",
+ body: "Compose services from multiple repos in one env.",
+ },
+ {
+ label: "Automatic cleanup",
+ body: "Merge or close › environment is reclaimed.",
+ },
+ {
+ label: "Webhooks & automation",
+ body: "Hook env events into your existing CI/CD.",
+ },
+ {
+ label: "Mission-control comments",
+ body: "URLs, status, logs delivered into the pull request.",
+ },
+ {
+ label: "Debug from chat",
+ body: "Triage build and deploy failures in a chat-driven agent session.",
+ },
+];
+
+export function Capabilities() {
+ const [copyState, setCopyState] = useState("idle");
+
+ const handleCopy = async () => {
+ try {
+ await navigator.clipboard.writeText(lifecycleYaml);
+ setCopyState("copied");
+ setTimeout(() => setCopyState("idle"), 1800);
+ } catch {
+ setCopyState("failed");
+ setTimeout(() => setCopyState("idle"), 2400);
+ }
+ };
+
+ return (
+
+
+
+
+
+ {"// what it does"}
+
+
+ Connected multi-service.
+
+ From one config.
+
+
+
+ Most preview tools start a container. Lifecycle starts the topology:
+ frontend, APIs, queues, databases. Each on ephemeral DNS, wired like
+ prod.
+
+
+
+
+
+
+
+ Self-hosted on your Kubernetes · brings its own controller · no SaaS
+ lock-in
+
+
+
+ );
+}
+
+export default Capabilities;
diff --git a/src/components/home/contrast/index.tsx b/src/components/home/contrast/index.tsx
new file mode 100644
index 00000000..1c7080fb
--- /dev/null
+++ b/src/components/home/contrast/index.tsx
@@ -0,0 +1,171 @@
+/**
+ * Copyright 2025 GoodRx, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use client";
+
+import { motion, type Variants } from "framer-motion";
+import { Check, X } from "lucide-react";
+
+const easeOutQuart = [0.25, 1, 0.5, 1] as const;
+
+const without = [
+ "One shared staging. One queue at a time.",
+ "Local works. Staging doesn't. The drift is the norm.",
+ "Twelve services, wired by hand, for one PR.",
+ "Old envs linger. Costs creep. Cleanup is manual.",
+];
+
+const with_ = [
+ "Each pull request opens its own isolated environment.",
+ "Run the exact branch, against the exact dependencies.",
+ "Multi-service topology builds itself from one config.",
+ "Merge or close. The env is gone. No janitor needed.",
+];
+
+const headerVariants: Variants = {
+ hidden: { opacity: 0, y: 10 },
+ visible: {
+ opacity: 1,
+ y: 0,
+ transition: { duration: 0.6, ease: easeOutQuart },
+ },
+};
+
+const columnVariants = (delay: number): Variants => ({
+ hidden: {},
+ visible: {
+ transition: {
+ delayChildren: delay,
+ staggerChildren: 0.07,
+ },
+ },
+});
+
+const itemVariants: Variants = {
+ hidden: { opacity: 0, y: 6 },
+ visible: {
+ opacity: 1,
+ y: 0,
+ transition: { duration: 0.45, ease: easeOutQuart },
+ },
+};
+
+export function Contrast() {
+ return (
+
+
+
+
{"// the shift"}
+
+ Stop sharing one staging.
+
+
+ Start shipping in parallel.
+
+
+
+ );
+}
+
+export default Contrast;
diff --git a/src/components/home/features/FeatureCard.tsx b/src/components/home/features/FeatureCard.tsx
deleted file mode 100644
index c9b49976..00000000
--- a/src/components/home/features/FeatureCard.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-/**
- * Copyright 2025 GoodRx, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-"use client";
-
-import { motion } from "framer-motion";
-import type { Feature } from "./types";
-
-interface FeatureCardProps {
- feature: Feature;
- index: number;
-}
-
-export function FeatureCard({ feature, index }: FeatureCardProps) {
- const Icon = feature.icon;
-
- return (
-
-
-
-
-
- {feature.title}
-
-
- {feature.description}
-
-
- );
-}
diff --git a/src/components/home/features/data.ts b/src/components/home/features/data.ts
deleted file mode 100644
index f19a5614..00000000
--- a/src/components/home/features/data.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-/**
- * Copyright 2025 GoodRx, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import {
- GitPullRequest,
- Network,
- Trash2,
- GitFork,
- Webhook,
- MessageSquare,
-} from "lucide-react";
-import type { Feature } from "./types";
-
-export const features: Feature[] = [
- {
- id: "auto-deploy",
- title: "Auto-deploy on PR",
- description:
- "Every pull request automatically gets its own isolated environment. Simple config setup.",
- icon: GitPullRequest,
- },
- {
- id: "multi-service",
- title: "Connected Multi-Service",
- description:
- "Spin up your entire stack - frontend, backend, databases - all connected and working together.",
- icon: Network,
- },
- {
- id: "auto-cleanup",
- title: "Automatic Cleanup",
- description:
- "Environments are automatically torn down when PRs are merged or closed. No resource waste.",
- icon: Trash2,
- },
- {
- id: "cross-repo",
- title: "Cross-Repo Composition",
- description:
- "Test changes across multiple repositories in a single unified environment.",
- icon: GitFork,
- },
- {
- id: "webhooks",
- title: "Webhooks & Automation",
- description:
- "Integrate with your existing CI/CD pipelines and trigger custom workflows on environment events.",
- icon: Webhook,
- },
- {
- id: "mission-control",
- title: "Mission Control Comments",
- description:
- "Get environment URLs, status updates, and deployment logs directly in your PR comments.",
- icon: MessageSquare,
- },
-];
diff --git a/src/components/home/features/index.tsx b/src/components/home/features/index.tsx
deleted file mode 100644
index b8e4d905..00000000
--- a/src/components/home/features/index.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-/**
- * Copyright 2025 GoodRx, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-"use client";
-
-import { motion } from "framer-motion";
-import { FeatureCard } from "./FeatureCard";
-import { features } from "./data";
-
-export function Features() {
- return (
-
-
-
-
- Everything you need for ephemeral environments
-
-
- Lifecycle provides all the tools to create, manage, and scale your
- development environments automatically.
-
-
-
-
- {features.map((feature, index) => (
-
- ))}
-
-
-
- );
-}
-
-export { FeatureCard } from "./FeatureCard";
-export { features } from "./data";
-export type { Feature } from "./types";
diff --git a/src/components/home/features/types.ts b/src/components/home/features/types.ts
deleted file mode 100644
index 72c2bdb1..00000000
--- a/src/components/home/features/types.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * Copyright 2025 GoodRx, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import type { LucideIcon } from "lucide-react";
-
-export interface Feature {
- id: string;
- title: string;
- description: string;
- icon: LucideIcon;
-}
diff --git a/src/components/home/hero/HeroContent.tsx b/src/components/home/hero/HeroContent.tsx
index 3a002f09..a60c8ae8 100644
--- a/src/components/home/hero/HeroContent.tsx
+++ b/src/components/home/hero/HeroContent.tsx
@@ -17,46 +17,133 @@
"use client";
import Link from "next/link";
+import { useState } from "react";
import { motion } from "framer-motion";
-import { ArrowRight, Github } from "lucide-react";
+import { ArrowRight, Check, Copy, Github } from "lucide-react";
import { buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
+const installCommand = "git clone https://github.com/GoodRxOSS/lifecycle";
+const easeOutQuart = [0.25, 1, 0.5, 1] as const;
+
+type CopyState = "idle" | "copied" | "failed";
+
export function HeroContent() {
+ const [copyState, setCopyState] = useState("idle");
+
+ const handleCopy = async () => {
+ try {
+ await navigator.clipboard.writeText(installCommand);
+ setCopyState("copied");
+ setTimeout(() => setCopyState("idle"), 1800);
+ } catch {
+ setCopyState("failed");
+ setTimeout(() => setCopyState("idle"), 2400);
+ }
+ };
+
return (
-
+
+
+
+ Apache 2.0 · Ephemeral environments · GoodRx OSS
+
+
- Enterprise-grade ephemeral environments{" "}
- that grow with you
+ Every pull request gets a{" "}
+ real environment.
- Instantly spin up connected multi-service development environments from
- any pull request. Review, test, and iterate faster than ever before.
+ A multi-service env per pull request. Builds itself. Tears itself down
+ on merge.
+
+ {step.description}
+
+
);
}
diff --git a/src/components/home/how-it-works/data.ts b/src/components/home/how-it-works/data.ts
index 9e258562..4cbf98cf 100644
--- a/src/components/home/how-it-works/data.ts
+++ b/src/components/home/how-it-works/data.ts
@@ -21,33 +21,33 @@ export const steps: Step[] = [
{
id: "open-pr",
number: 1,
- title: "Open a Pull Request",
+ title: "Open a pull request",
description:
- "Push your code and open a pull request as you normally would. Lifecycle detects the PR automatically.",
+ "Push your branch. Lifecycle picks it up the moment the pull request opens.",
icon: GitPullRequest,
},
{
id: "provision",
number: 2,
- title: "Environment Provisioned",
+ title: "Lifecycle builds it",
description:
- "Lifecycle spins up a complete, isolated environment with all your services configured and connected.",
+ "Every service builds, deploys, and wires together from one config.",
icon: Rocket,
},
{
id: "access",
number: 3,
- title: "Access Your Environment",
+ title: "Test on a real URL",
description:
- "Get a unique URL posted to your PR. Share it with teammates for testing and review.",
+ "A unique URL lands in the pull request. Share with reviewers, QA, designers.",
icon: Globe,
},
{
id: "merge",
number: 4,
- title: "Merge and Cleanup",
+ title: "Merge, it’s gone",
description:
- "Merge your PR when ready. Lifecycle automatically tears down the environment, freeing up resources.",
+ "Close or merge. The env tears itself down. No stale envs, no orphan costs.",
icon: GitMerge,
},
];
diff --git a/src/components/home/how-it-works/index.tsx b/src/components/home/how-it-works/index.tsx
index 21ea34f4..aa70ac08 100644
--- a/src/components/home/how-it-works/index.tsx
+++ b/src/components/home/how-it-works/index.tsx
@@ -16,39 +16,31 @@
"use client";
-import { motion } from "framer-motion";
import { Step } from "./Step";
import { steps } from "./data";
export function HowItWorks() {
return (
-
-
-
-
- How it works
+
+
+
+
{"// the path"}
+
+ From pull request to live URL in four steps.
-
- From PR to production-like environment in seconds. No configuration
- headaches, no DevOps bottlenecks.
+
+ No tickets, no provisioning calls, no cleanup chores.
);
diff --git a/src/components/home/index.tsx b/src/components/home/index.tsx
index 167bcf87..9c53ee4d 100644
--- a/src/components/home/index.tsx
+++ b/src/components/home/index.tsx
@@ -19,7 +19,8 @@ import { ServicesFlow as Services, Static } from "@/components/home/flows";
export { Main } from "@/components/home/main";
export { Bg, Services, Static };
-// New homepage sections
export { Hero } from "@/components/home/hero";
-export { Features } from "@/components/home/features";
+export { Contrast } from "@/components/home/contrast";
export { HowItWorks } from "@/components/home/how-it-works";
+export { Capabilities } from "@/components/home/capabilities";
+export { TwoDoors } from "@/components/home/two-doors";
diff --git a/src/components/home/two-doors/index.tsx b/src/components/home/two-doors/index.tsx
new file mode 100644
index 00000000..3e9325d3
--- /dev/null
+++ b/src/components/home/two-doors/index.tsx
@@ -0,0 +1,155 @@
+/**
+ * Copyright 2025 GoodRx, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use client";
+
+import Link from "next/link";
+import { ArrowRight, BookOpen, Github } from "lucide-react";
+import { FaDiscord } from "react-icons/fa";
+import { motion, type Variants } from "framer-motion";
+import { buttonVariants } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
+
+const easeOutQuart = [0.25, 1, 0.5, 1] as const;
+
+const sectionVariants: Variants = {
+ hidden: {},
+ visible: { transition: { staggerChildren: 0.12, delayChildren: 0.05 } },
+};
+
+const blockVariants: Variants = {
+ hidden: { opacity: 0, y: 12 },
+ visible: {
+ opacity: 1,
+ y: 0,
+ transition: { duration: 0.6, ease: easeOutQuart },
+ },
+};
+
+export function TwoDoors() {
+ return (
+
+
+
+
{"// pick a door"}
+
+ Run it, or build it with us.
+
+
+
+
+
+
01 · run it
+
+ Get it running on a sample repo.
+
+
+ Clone, follow the guide, watch your first pull request spin up its
+ own env in under ten minutes.
+
+
+
+ );
+}
+
+export default TwoDoors;
diff --git a/src/components/site-footer/index.tsx b/src/components/site-footer/index.tsx
new file mode 100644
index 00000000..27f1abd8
--- /dev/null
+++ b/src/components/site-footer/index.tsx
@@ -0,0 +1,101 @@
+/**
+ * Copyright 2025 GoodRx, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+"use client";
+
+import Image from "next/image";
+import Link from "next/link";
+import { ArrowUp } from "lucide-react";
+
+const navLinks: ReadonlyArray<{
+ href: string;
+ label: string;
+ external?: boolean;
+}> = [
+ { href: "/docs", label: "Docs" },
+ {
+ href: "https://github.com/GoodRxOSS/lifecycle",
+ label: "GitHub",
+ external: true,
+ },
+ {
+ href: "https://discord.gg/M5fhHJuEX8",
+ label: "Discord",
+ external: true,
+ },
+ {
+ href: "https://github.com/GoodRxOSS/lifecycle/blob/main/LICENSE",
+ label: "License",
+ external: true,
+ },
+];
+
+const handleBackToTop = () => {
+ if (typeof window === "undefined") return;
+ window.scrollTo({ top: 0, behavior: "smooth" });
+};
+
+export function SiteFooter() {
+ return (
+
+
+
+
+
+ Lifecycle
+
+
+
+
+
+
+ );
+}
+
+export default SiteFooter;
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index 512d1888..b9cb4a73 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -16,11 +16,20 @@
import { useEffect, useState } from "react";
import type { AppProps } from "next/app";
+import { IBM_Plex_Sans, JetBrains_Mono } from "next/font/google";
import { useConfig } from "nextra-theme-docs";
+import { MotionConfig } from "framer-motion";
import TagContent from "@/components/tags";
import "../styles/globals.css";
import "@xyflow/react/dist/style.css";
+const ibmPlexSans = IBM_Plex_Sans({
+ subsets: ["latin"],
+ weight: ["400", "500", "600", "700"],
+ display: "swap",
+});
+const jetbrainsMono = JetBrains_Mono({ subsets: ["latin"], display: "swap" });
+
export default function App({ Component, pageProps }: AppProps) {
const [isClient, setIsClient] = useState(false);
const { frontMatter } = useConfig();
@@ -41,9 +50,15 @@ export default function App({ Component, pageProps }: AppProps) {
}, []);
return (
- <>
+
+
{frontMatter?.tags?.length > 0 && }
- >
+
);
}
diff --git a/src/pages/_meta.ts b/src/pages/_meta.ts
index 64975ec4..29881f57 100644
--- a/src/pages/_meta.ts
+++ b/src/pages/_meta.ts
@@ -19,7 +19,7 @@ export default {
"type": "page"
},
"index": {
- "title": "Lifecycle",
+ "title": "Lifecycle · every pull request gets a real environment",
"display": "hidden",
"theme": {
"layout": "full"
diff --git a/src/pages/docs/_meta.ts b/src/pages/docs/_meta.ts
index 12ff43a6..4de5d8d9 100644
--- a/src/pages/docs/_meta.ts
+++ b/src/pages/docs/_meta.ts
@@ -15,6 +15,10 @@
*/
export default {
+ "index": {
+ "title": "What is Lifecycle?",
+ "display": "hidden"
+ },
"what-is-lifecycle": {
"title": "What is Lifecycle?"
},
diff --git a/src/pages/docs/index.mdx b/src/pages/docs/index.mdx
new file mode 100644
index 00000000..3e833c6f
--- /dev/null
+++ b/src/pages/docs/index.mdx
@@ -0,0 +1,12 @@
+---
+title: What is Lifecycle?
+description: Lifecycle is your effortless way to test and create ephemeral environments
+tags:
+ - core
+ - lifecycle
+ - intro
+---
+
+import WhatIsLifecycle from "./what-is-lifecycle.mdx";
+
+
diff --git a/src/pages/docs/setup/prerequisites.mdx b/src/pages/docs/setup/prerequisites.mdx
index a74a9b4f..5a275021 100644
--- a/src/pages/docs/setup/prerequisites.mdx
+++ b/src/pages/docs/setup/prerequisites.mdx
@@ -69,44 +69,47 @@ _Update your domain's DNS records with NS records provided by Google Cloud DNS.
**[AWS Route 53](https://aws.amazon.com/route53/)**: Amazon's scalable DNS web
service designed to route end users to Internet applications.
- **Manual Setup**:
+**Manual Setup**:
+
+- Authenticate with AWS CLI using the role/usr you desire.
+- Ensure you have [your domain provisioned to accept wildcards](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/CreatingNewSubdomain.html); eg `*.lifecycle..com`
- - Authenticate with AWS CLI using the role/usr you desire.
- - Ensure you have [your domain provisioned to accept wildcards](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/CreatingNewSubdomain.html); eg `*.lifecycle..com`
-
- **CLI Setup**:
-
- ```sh
- aws configure
- ```
-
- ```sh
- aws route53 change-resource-record-sets --hosted-zone-id --change-batch '{
- "Comment": "CREATE wildcard for ",
- "Changes": [
- {
- "Action": "CREATE",
- "ResourceRecordSet": {
- "Name": "..com",
- "Type": "A",
- "TTL": 300,
- "ResourceRecords": [
- {
- "Value": "*********"
- }
- ]
- }
+**CLI Setup**:
+
+```sh
+aws configure
+```
+
+```sh
+aws route53 change-resource-record-sets --hosted-zone-id --change-batch '{
+ "Comment": "CREATE wildcard for ",
+ "Changes": [
+ {
+ "Action": "CREATE",
+ "ResourceRecordSet": {
+ "Name": "..com",
+ "Type": "A",
+ "TTL": 300,
+ "ResourceRecords": [
+ {
+ "Value": "*********"
+ }
+ ]
}
- ]
- }'
- ```
+ }
+ ]
+}'
+```
- If you want to use Cloudflare as your primary DNS provider and manage your DNS records on Cloudflare, your domain should be using a full setup.
- This means that you are using Cloudflare for your authoritative DNS nameservers.
- Follow the steps [here](https://developers.cloudflare.com/dns/zone-setups/full-setup/setup/) to setup a public DNS zone in Cloudflare.
+ If you want to use Cloudflare as your primary DNS provider and manage your DNS
+ records on Cloudflare, your domain should be using a full setup. This means
+ that you are using Cloudflare for your authoritative DNS nameservers. Follow
+ the steps
+ [here](https://developers.cloudflare.com/dns/zone-setups/full-setup/setup/) to
+ setup a public DNS zone in Cloudflare.
diff --git a/src/pages/index.mdx b/src/pages/index.mdx
index 43c541b0..f5cc4618 100644
--- a/src/pages/index.mdx
+++ b/src/pages/index.mdx
@@ -1,12 +1,14 @@
---
-title: Lifecycle
-description: Enterprise-grade ephemeral environments that grow with you
+title: Lifecycle · every pull request gets a real environment
+description: Each pull request gets a connected multi-service preview. Builds itself, runs on its own URL, tears down on merge. Apache 2.0, maintained by GoodRx OSS.
---
import {
Hero,
- Features,
+ Contrast,
HowItWorks,
+ Capabilities,
+ TwoDoors,
Bg,
} from "@lifecycle-docs/components/home";
@@ -14,6 +16,10 @@ import {
-
+
+
+
+
+
diff --git a/src/styles/globals.css b/src/styles/globals.css
index 2ff3297e..ed8539a8 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -88,10 +88,24 @@
@apply border-border;
}
body {
- @apply bg-background text-foreground;
+ @apply bg-background font-sans text-foreground;
font-feature-settings:
"rlig" 1,
- "calt" 1;
+ "calt" 1,
+ "ss01" 1;
+ font-variant-ligatures: contextual;
+ }
+ code,
+ kbd,
+ pre,
+ samp {
+ @apply font-mono;
+ }
+}
+
+@layer components {
+ .kicker {
+ @apply font-mono uppercase text-label;
}
}
@@ -100,6 +114,71 @@
background-color: hsl(var(--background));
}
+article.nextra-content main:not(.layout-full) {
+ font-size: 1.0625rem;
+ line-height: 1.7;
+ letter-spacing: -0.005em;
+ color: hsl(var(--foreground) / 0.92);
+}
+
+article.nextra-content main:not(.layout-full) :is(p, ul, ol, blockquote) {
+ max-width: 72ch;
+}
+
+article.nextra-content main:not(.layout-full) :is(p, li) {
+ text-wrap: pretty;
+}
+
+article.nextra-content main:not(.layout-full) :is(h1, h2, h3, h4, h5, h6) {
+ color: hsl(var(--foreground));
+ letter-spacing: -0.018em;
+ text-wrap: balance;
+}
+
+article.nextra-content main:not(.layout-full) h1 {
+ font-weight: 700;
+ letter-spacing: -0.024em;
+ line-height: 1.15;
+}
+
+article.nextra-content main:not(.layout-full) h2 {
+ font-weight: 600;
+ line-height: 1.25;
+}
+
+article.nextra-content main:not(.layout-full) :is(h3, h4) {
+ font-weight: 600;
+ line-height: 1.3;
+}
+
+article.nextra-content main:not(.layout-full) :is(h2, h3, h4) {
+ max-width: 60ch;
+}
+
+article.nextra-content main:not(.layout-full) :is(table, pre, .nextra-code-block) {
+ max-width: none;
+}
+
+article.nextra-content main:not(.layout-full) :is(table, td, th) {
+ font-variant-numeric: tabular-nums;
+}
+
+article.nextra-content main:not(.layout-full) :is(code, kbd, samp) {
+ font-size: 0.92em;
+ letter-spacing: 0;
+}
+
+article.nextra-content main:not(.layout-full) :not(pre) > code {
+ padding: 0.1em 0.35em;
+ border-radius: 0.25rem;
+ background: hsl(var(--muted) / 0.6);
+}
+
+article.nextra-content main:not(.layout-full) a {
+ text-underline-offset: 0.2em;
+ text-decoration-thickness: 1px;
+}
+
.react-flow__viewport {
transform: scale(1) !important;
}
@@ -117,19 +196,12 @@
display: none;
}
-@keyframes shake {
- 0%, 100% { transform: translate(0, 0); }
- 10% { transform: translate(-3px, -2px); }
- 20% { transform: translate(3px, 2px); }
- 30% { transform: translate(-3px, -1px); }
- 40% { transform: translate(3px, 1px); }
- 50% { transform: translate(-2px, -2px); }
- 60% { transform: translate(2px, 2px); }
- 70% { transform: translate(-2px, -1px); }
- 80% { transform: translate(2px, 1px); }
- 90% { transform: translate(-1px, -1px); }
+.nextra-nav-container a:not(:has(.logo-image)),
+.nextra-nav-container button {
+ transition: color 200ms cubic-bezier(0.25, 1, 0.5, 1);
}
-.logo-shake:hover .logo-image {
- animation: shake 0.8s ease-in-out;
+.nextra-nav-container a:not(:has(.logo-image)):hover,
+.nextra-nav-container button:hover {
+ color: hsl(var(--primary));
}
diff --git a/src/theme.config.tsx b/src/theme.config.tsx
index 94457fc3..8f91ddb0 100644
--- a/src/theme.config.tsx
+++ b/src/theme.config.tsx
@@ -15,26 +15,28 @@
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
-import React, { ReactElement, useState, useEffect } from "react";
-import { useConfig, useTheme } from "nextra-theme-docs";
+import React, { ReactElement } from "react";
+import { useConfig } from "nextra-theme-docs";
import { FaDiscord as Discord } from "react-icons/fa";
-import { Moon, Sun, Github, Heart } from "lucide-react";
import TagContent from "@/components/tags";
import { Archived } from "@/components/archived";
import { Separator } from "@/components/ui/separator";
import { Code } from "@/components";
+import { SiteFooter } from "@/components/site-footer";
import Image from "next/image";
const logo = (
-