From 680cf5ecd011fc90c34f25d960be727303d8cac1 Mon Sep 17 00:00:00 2001 From: Fernando Paladini Date: Sun, 28 Jun 2026 00:23:40 -0300 Subject: [PATCH] docs: add SpecGov README and Pages site --- .github/workflows/pages.yml | 34 ++ .specgov.yml | 4 + .specs/features/specgov-core/spec.md | 1 + .specs/features/specgov-core/tasks.md | 5 +- .specs/project/ROADMAP.md | 1 + .specs/project/STATE.md | 2 + README.md | 256 +++++++++++-- docs/assets/specgov-report.svg | 23 ++ docs/index.html | 327 ++++++++++++++++ docs/script.js | 84 +++++ docs/styles.css | 522 ++++++++++++++++++++++++++ 11 files changed, 1216 insertions(+), 43 deletions(-) create mode 100644 .github/workflows/pages.yml create mode 100644 docs/assets/specgov-report.svg create mode 100644 docs/index.html create mode 100644 docs/script.js create mode 100644 docs/styles.css diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..0a0611e --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,34 @@ +name: GitHub Pages + +on: + push: + branches: + - main + paths: + - docs/** + - .github/workflows/pages.yml + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/configure-pages@v5 + - uses: actions/upload-pages-artifact@v4 + with: + path: docs + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/.specgov.yml b/.specgov.yml index 84dec76..a6cf9dd 100644 --- a/.specgov.yml +++ b/.specgov.yml @@ -6,6 +6,10 @@ artifacts: kind: documentation owner: maintainers status: active + - path: "docs/**/*" + kind: documentation + owner: maintainers + status: active - path: ".specs/**/*.md" kind: specification owner: maintainers diff --git a/.specs/features/specgov-core/spec.md b/.specs/features/specgov-core/spec.md index 2d05968..f788cf8 100644 --- a/.specs/features/specgov-core/spec.md +++ b/.specs/features/specgov-core/spec.md @@ -87,3 +87,4 @@ Spec-driven development creates useful artifacts, but those artifacts drift when - [x] `npm run lint` passes. - [x] `npm run typecheck` passes. - [x] README quickstart commands are locally verified. +- [x] GitHub Pages documentation site is available from `docs/`. diff --git a/.specs/features/specgov-core/tasks.md b/.specs/features/specgov-core/tasks.md index 17e81dc..1ad9fb8 100644 --- a/.specs/features/specgov-core/tasks.md +++ b/.specs/features/specgov-core/tasks.md @@ -99,10 +99,11 @@ T4 + T5 + T6 -> T7 -> T8 ### T8: Validate and Publish -**What**: Run release checks, commit atomically, create public GitHub repo, push, and verify remote. +**What**: Run release checks, commit atomically, create public GitHub repo, add +README and GitHub Pages adoption docs, push, and verify remote. **Where**: repository **Depends on**: T7 **Requirement**: SGOV-05 **Tests**: full **Gate**: `npm test`, `npm run build`, `npm run lint`, `npm run typecheck` -**Status**: In Progress +**Status**: Done diff --git a/.specs/project/ROADMAP.md b/.specs/project/ROADMAP.md index 585057c..b80fc2c 100644 --- a/.specs/project/ROADMAP.md +++ b/.specs/project/ROADMAP.md @@ -18,6 +18,7 @@ - Example manifests for docs-only, ADR-heavy, and spec-framework repos. - README quickstart. +- GitHub Pages product site for global project adoption. - Contribution and security docs. ## Later diff --git a/.specs/project/STATE.md b/.specs/project/STATE.md index fe86d9d..84fb81f 100644 --- a/.specs/project/STATE.md +++ b/.specs/project/STATE.md @@ -18,6 +18,8 @@ - The v1 smoke test should include both advisory warnings and strict failures so users can trust the opt-in enforcement mode before adopting it. - GitHub Actions now warns that Node 20 actions are forced to Node 24, so SpecGov's own Action and CI target Node 24 while the CLI package can still support Node 20+. - GitHub's official checkout and setup-node v5 tags exist and avoid the Node 20 runtime warning emitted by v4. +- Public project education lives in the README and the static GitHub Pages site + under `docs/`. The Pages workflow deploys the `docs` directory from `main`. ## Deferred Ideas diff --git a/README.md b/README.md index 5605c75..a8c5307 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,91 @@ # SpecGov +[![CI](https://github.com/paladini/specgov/actions/workflows/ci.yml/badge.svg)](https://github.com/paladini/specgov/actions/workflows/ci.yml) +[![MIT License](https://img.shields.io/badge/license-MIT-0f766e.svg)](LICENSE) +[![Docs](https://img.shields.io/badge/docs-GitHub%20Pages-2f6f4e.svg)](https://paladini.github.io/specgov/) + +Govern the specs your code is supposed to honor. + SpecGov is a framework-agnostic governance layer for spec-driven development in -Git repositories. It does not create a new spec format. Instead, it helps teams -keep existing requirements, ADRs, product docs, tasks, and framework-specific -spec folders aligned with code changes. +Git repositories. It does not invent a new spec format. It lets you declare the +requirements, ADRs, product docs, `.specs` folders, Kiro specs, Spec Kit plans, +or custom artifacts that already define your system, then checks whether code +changes keep those artifacts in the loop. -Version 0.1 is deterministic by design: no API keys, no LLM dependency, and no -vendor-specific workflow required. +The core is deterministic by design: no API keys, no hosted service, no model +calls, and no vendor lock-in. You can run it locally, in CI, or as a GitHub +Action. -## What It Does +## Why SpecGov exists -- Discovers governed artifacts from a `.specgov.yml` manifest. -- Validates lifecycle metadata such as `active`, `superseded`, and `archived`. -- Checks whether code changes touched the related spec artifacts. -- Generates a trace index for humans, CI, and future AI auditors. -- Runs as a TypeScript CLI or GitHub Action. -- Defaults to advisory mode so projects can adopt it gradually. +Spec-driven development works until Git quietly merges implementation changes +that bypass the spec layer. Over time, reviewers stop trusting requirements, +ADRs become archaeology, and AI-assisted changes become harder to audit. -## Quick Start +SpecGov gives teams a small, explicit contract: -Install dependencies and build from source: +- Which files are governed artifacts? +- Which code paths depend on which artifacts? +- Did this pull request update the right spec, doc, ADR, or plan? +- Which artifacts are stale, orphaned, superseded, or missing ownership? +- Can a human or future AI auditor reconstruct the trace later? -```bash -npm install -npm run build -``` +## What SpecGov checks + +| Capability | What it does | +| ------------------ | ---------------------------------------------------------------- | +| Artifact discovery | Finds governed docs, ADRs, specs, and requirements from globs. | +| Lifecycle metadata | Reads optional YAML frontmatter such as `status` and `owner`. | +| PR impact checks | Flags code changes that do not touch mapped spec artifacts. | +| Unmapped code | Finds changed files outside your declared code-to-spec map. | +| Trace index | Emits JSON linking artifacts, mappings, and matched files. | +| Drift report | Reports stale, empty, orphaned, or superseded artifacts. | +| GitHub Action | Runs the same deterministic check inside pull request workflows. | + +## Installation -Create a manifest: +SpecGov is currently pre-release. Until the first npm package and version tag +are published, install it from source: ```bash -npx specgov init +git clone https://github.com/paladini/specgov.git +cd specgov +npm ci +npm run build +npm link ``` -Scan governed artifacts: +After `npm link`, the `specgov` command is available on your machine: ```bash -npx specgov scan +specgov --help ``` -Check a pull request diff: +You can also run the local build directly: ```bash -npx specgov check-pr --base-ref origin/main --head-ref HEAD +node dist/cli.js --help ``` -Generate a trace index: +## Quick start + +From the repository you want to govern: ```bash -npx specgov trace --out .specgov.trace.json +specgov init +specgov scan +specgov check-pr --changed-file src/auth/session.ts +specgov trace --out .specgov.trace.json +specgov drift ``` +`specgov init` creates `.specgov.yml`. Start in `advisory` mode so teams can +see findings without blocking merges, then switch selected repositories or +paths to `strict` when the mapping is trusted. + ## Manifest -SpecGov uses a small YAML manifest: +SpecGov uses one YAML manifest: ```yaml version: 1 @@ -74,30 +107,88 @@ mappings: specs: - "docs/auth/**" - "adr/auth/**" - - code: "src/payments/**" - specs: "docs/payments/**" + - ".specs/features/auth/**" + description: Authentication behavior must stay aligned with its specs. rules: require_spec_impact_for_code_changes: true require_lifecycle_status: false require_owner_for_active_specs: false stale_after_days: 180 + +ignore: + - "node_modules/**" + - "dist/**" + - ".git/**" ``` -Each governed artifact can optionally include YAML frontmatter: +### Governed artifacts + +Each entry in `artifacts` tells SpecGov which files belong to your spec layer. +Use any folder convention you already have: + +- `docs/**/*.md` for product or engineering docs. +- `adr/**/*.md` for architectural decisions. +- `.specs/**/*.md` for TLC Spec Driven or custom specs. +- `.kiro/specs/**/*.md` for Kiro-style spec folders. +- `specs/**/*.md` for Spec Kit or other repository-local plans. + +### Code-to-spec mappings + +Each entry in `mappings` connects implementation paths to the artifacts that +must move with them. If `src/auth/**` changes and no mapped artifact changes, +SpecGov reports `SPEC_IMPACT_MISSING`. + +When `require_spec_impact_for_code_changes` is enabled, SpecGov also reports +`CODE_CHANGE_UNMAPPED` for changed files that are not covered by any mapping, +are not governed artifacts, and are not ignored. + +### Lifecycle frontmatter + +Governed Markdown files can include optional lifecycle metadata: ```markdown --- status: active -owner: engineering +owner: platform last_verified: 2026-06-27 --- -# Checkout Flow +# Authentication Session Contract ``` +Supported statuses are `draft`, `active`, `superseded`, `deprecated`, and +`archived`. Superseded artifacts can declare `superseded_by` so readers know +where the current source of truth moved. + +## Commands + +| Command | Purpose | Common use | +| ------------------ | ---------------------------------------------------- | ---------------------- | +| `specgov init` | Create a starter `.specgov.yml`. | First-time setup. | +| `specgov scan` | Discover governed artifacts and lifecycle findings. | Local audit. | +| `specgov check-pr` | Compare changed files against code-to-spec mappings. | Pull request checks. | +| `specgov trace` | Generate a machine-readable trace index. | Automation and audits. | +| `specgov drift` | Report stale, empty, orphaned, or superseded specs. | Maintenance reviews. | + +All report commands default to Markdown output. Use `--format json` for +automation: + +```bash +specgov scan --format json +specgov check-pr --format json --changed-file src/payments/checkout.ts +``` + +Exit codes: + +- `0`: pass, or warning in `advisory` mode. +- `1`: governance failure in `strict` mode. +- `2`: runtime or configuration error. + ## GitHub Action +Add SpecGov to pull requests with a workflow like this: + ```yaml name: SpecGov @@ -111,32 +202,115 @@ jobs: - uses: actions/checkout@v5 with: fetch-depth: 0 - - uses: paladini/specgov@v0.1.0 + + - uses: paladini/specgov@main with: mode: advisory base-ref: ${{ github.event.pull_request.base.sha }} head-ref: ${{ github.event.pull_request.head.sha }} ``` -Use `mode: strict` when you want governance findings to fail the check. +Use `paladini/specgov@main` only while the project is pre-release. After a +version tag exists, pin the Action to that tag. + +Use `mode: strict` when governance findings should block the pull request. + +### Action inputs + +| Input | Default | Description | +| --------------- | -------------- | -------------------------------------------------------- | +| `config` | `.specgov.yml` | Path to the manifest. | +| `mode` | `advisory` | `advisory` reports warnings; `strict` fails on warnings. | +| `base-ref` | unset | Base git ref for pull request comparison. | +| `head-ref` | unset | Head git ref for pull request comparison. | +| `output-format` | `markdown` | Report format for logs and `report-json`. | +| `changed-files` | unset | Newline-delimited file list when you provide the diff. | + +### Action outputs + +| Output | Description | +| ------------- | ----------------------------------------------- | +| `status` | `pass`, `warn`, `fail`, or `error`. | +| `report-json` | Serialized `SpecGovReport` for downstream jobs. | + +## Adoption recipes + +The `examples/` folder includes starter manifests: + +| Repository style | Example | +| ----------------- | ------------------------------------------------------------------------------------ | +| Docs-only | [`examples/docs-only/.specgov.yml`](examples/docs-only/.specgov.yml) | +| ADR-heavy | [`examples/adr-heavy/.specgov.yml`](examples/adr-heavy/.specgov.yml) | +| Framework folders | [`examples/framework-folders/.specgov.yml`](examples/framework-folders/.specgov.yml) | + +A practical rollout usually looks like this: + +1. Run `specgov init`. +2. Add one or two high-value mappings, not the whole repository. +3. Run `specgov scan` and fix obvious empty globs. +4. Add the GitHub Action in `advisory` mode. +5. Review warnings in a few pull requests. +6. Tighten high-confidence areas with `mode: strict`. +7. Schedule `specgov drift` as a periodic maintenance check. + +## Report example -## How This Differs From SpecTrace +```markdown +# SpecGov check-pr report + +Status: **warn** +Mode: `advisory` +Findings: 1 (0 errors, 1 warnings, 0 info) + +## Changed Files + +- `src/auth/session.ts` + +## Findings + +- **WARNING SPEC_IMPACT_MISSING**: Code changed under src/auth/** without a + related spec artifact change. + - Related: `src/auth/session.ts`, `docs/auth/**`, `adr/auth/**` + - Suggestion: Update a mapped spec artifact or run in advisory mode until + this mapping is ready to enforce. +``` -SpecTrace for AI Coding verifies whether a specific AI-assisted code change +## How SpecGov differs from SpecTrace + +SpecTrace for AI Coding verifies whether a specific AI-assisted change satisfies explicit requirements and evidence maps. SpecGov operates one layer -higher: it governs spec artifacts across Git workflows regardless of author, -framework, or whether AI was involved. +higher: it governs living spec artifacts across Git workflows regardless of +author, framework, or whether AI was involved. + +They work well together: + +- Use SpecGov to keep the repository's source-of-truth artifacts aligned. +- Use SpecTrace to audit the evidence behind a specific implementation change. ## Development ```bash +npm ci npm test npm run build npm run lint npm run typecheck +npm run format:check ``` -## Status +SpecGov uses TLC Spec Driven internally. Public behavior changes should update +the relevant files under `.specs/`, tests, and README examples in the same pull +request. + +## Security and privacy + +SpecGov runs locally or in your CI runner. Version 0.1 does not call external +services, require API keys, or send repository contents to a model. See +[`SECURITY.md`](SECURITY.md) for vulnerability reporting. + +## Project links -SpecGov is early OSS. The v1 core is intentionally small: deterministic checks -first, AI audit later as an optional plugin. +- Website: +- Repository: +- Roadmap: [`.specs/project/ROADMAP.md`](.specs/project/ROADMAP.md) +- Contribution guide: [`CONTRIBUTING.md`](CONTRIBUTING.md) diff --git a/docs/assets/specgov-report.svg b/docs/assets/specgov-report.svg new file mode 100644 index 0000000..b9d379f --- /dev/null +++ b/docs/assets/specgov-report.svg @@ -0,0 +1,23 @@ + + SpecGov check-pr report + A terminal-style SpecGov report showing advisory mode with one missing spec impact warning. + + + + + + + specgov check-pr + + # SpecGov check-pr report + Status: + + warn + Mode: advisory + Findings: 1 (0 errors, 1 warnings, 0 info) + Changed Files + - src/auth/session.ts + Finding + WARNING SPEC_IMPACT_MISSING + Code changed without a related spec artifact change. + diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..bef1457 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,327 @@ + + + + + + + + + + + + SpecGov | Spec governance for Git repositories + + + + + + + +
+
+ SpecGov pull request report showing an advisory warning for a code change without related spec impact. +
+

Deterministic spec governance for Git

+

+ Govern the specs your code is supposed to honor. +

+

+ SpecGov keeps requirements, ADRs, product docs, and spec folders + aligned with pull requests without API keys, hosted services, or + model calls. +

+ +
+
+ +
+
+ No new spec format + Bring your existing docs, ADRs, `.specs`, Kiro, or Spec Kit + files. +
+
+ No secrets + Runs locally and in CI with deterministic checks. +
+
+ No surprise blocking + Start advisory, then opt into strict enforcement. +
+
+ +
+
+

The drift problem

+

Specs fail when Git can bypass them.

+
+
+

+ Modern teams write requirements, ADRs, implementation plans, and + AI-facing specs, then review code in a separate workflow. SpecGov + connects those layers so reviewers can see when a pull request + changes implementation without updating the artifacts that explain + the intended behavior. +

+

+ It is intentionally small: a YAML manifest, deterministic file + discovery, code-to-spec mappings, lifecycle metadata, Markdown + reports, JSON trace output, and a GitHub Action wrapper. +

+
+
+ +
+
+

Mechanics

+

+ A manifest turns your repo into a governed map. +

+
+
+
+ 01 +

Declare artifacts

+

+ Mark docs, ADRs, requirements, and spec folders as governed + artifacts. +

+
+
+ 02 +

Map code paths

+

+ Connect implementation directories to the artifacts that must move + with them. +

+
+
+ 03 +

Check pull requests

+

+ Report missing spec impact, unmapped code, lifecycle gaps, and + drift. +

+
+
+
+ +
+
+

Quick start

+

+ Install from source, then govern any repo. +

+

+ SpecGov is pre-release. Until the first npm package and version tag + are published, use the source build. +

+
+
+
Install
+
git clone https://github.com/paladini/specgov.git
+cd specgov
+npm ci
+npm run build
+npm link
+
Use in your repository
+
specgov init
+specgov scan
+specgov check-pr --changed-file src/auth/session.ts
+specgov trace --out .specgov.trace.json
+specgov drift
+
+
+ +
+
+

Manifest

+

Adopt with one `.specgov.yml` file.

+

+ Start with one high-value mapping. Expand gradually after the first + advisory findings make sense to reviewers. +

+
+
+
version: 1
+mode: advisory
+
+artifacts:
+  - path: "docs/**/*.md"
+    kind: documentation
+    owner: docs
+  - path: "adr/**/*.md"
+    kind: decision
+    owner: architecture
+
+mappings:
+  - code: "src/auth/**"
+    specs:
+      - "docs/auth/**"
+      - "adr/auth/**"
+
+rules:
+  require_spec_impact_for_code_changes: true
+  stale_after_days: 180
+
+
+ +
+
+

Commands

+

+ Five commands cover setup, review, and maintenance. +

+
+
+
+ specgov init + Create a starter manifest. +
+
+ specgov scan + Discover governed artifacts and lifecycle findings. +
+
+ specgov check-pr + Compare changed files against code-to-spec mappings. +
+
+ specgov trace + Write machine-readable trace JSON. +
+
+ specgov drift + Report stale, empty, orphaned, or superseded artifacts. +
+
+
+ +
+
+

GitHub Action

+

Put governance in the pull request.

+

+ Advisory mode gives teams visibility without blocking adoption. + Strict mode turns warnings into a failing check. +

+
+
+
name: SpecGov
+
+on:
+  pull_request:
+
+jobs:
+  specgov:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v5
+        with:
+          fetch-depth: 0
+      - uses: paladini/specgov@main
+        with:
+          mode: advisory
+          base-ref: ${{ github.event.pull_request.base.sha }}
+          head-ref: ${{ github.event.pull_request.head.sha }}
+
+
+ +
+
+

Adoption paths

+

Use your existing repository style.

+
+
+
+

Docs-only

+

Map `src/**` to `docs/**` for product or engineering docs.

+ + View example + +
+
+

ADR-heavy

+

+ Require code areas to move with matching ADR and product docs. +

+ + View example + +
+
+

Framework folders

+

Govern `.specs`, `.kiro/specs`, or custom framework folders.

+ + View example + +
+
+
+
+ +
+
+ SpecGov + Framework-neutral governance for living specs. +
+ GitHub repository +
+ + + + diff --git a/docs/script.js b/docs/script.js new file mode 100644 index 0000000..47f88cc --- /dev/null +++ b/docs/script.js @@ -0,0 +1,84 @@ +/* global document, navigator, window */ + +const blocks = document.querySelectorAll(".copy-block"); + +for (const block of blocks) { + const shell = document.createElement("div"); + shell.className = "copy-shell"; + block.before(shell); + shell.append(block); + + const button = document.createElement("button"); + button.className = "copy-button"; + button.type = "button"; + button.textContent = "Copy"; + button.setAttribute("aria-label", "Copy code block"); + + button.addEventListener("click", async () => { + const code = block.querySelector("code"); + const text = code?.textContent ?? ""; + const copied = await copyText(text.trim()); + if (copied) { + showTemporaryStatus(button, "Copied"); + } else if (selectCode(code)) { + showTemporaryStatus(button, "Selected"); + } else { + button.textContent = "Select"; + } + }); + + shell.append(button); +} + +async function copyText(text) { + try { + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(text); + return true; + } + } catch { + return fallbackCopy(text); + } + + return fallbackCopy(text); +} + +function fallbackCopy(text) { + const textarea = document.createElement("textarea"); + textarea.value = text; + textarea.setAttribute("readonly", ""); + textarea.style.position = "fixed"; + textarea.style.left = "-9999px"; + document.body.append(textarea); + textarea.select(); + + try { + return document.execCommand("copy"); + } finally { + textarea.remove(); + } +} + +function selectCode(code) { + if (!code) { + return false; + } + + const selection = window.getSelection(); + if (!selection) { + return false; + } + + const range = document.createRange(); + range.selectNodeContents(code); + selection.removeAllRanges(); + selection.addRange(range); + return true; +} + +function showTemporaryStatus(button, status) { + button.textContent = status; + window.setTimeout(() => { + button.textContent = "Copy"; + }, 1400); +} diff --git a/docs/styles.css b/docs/styles.css new file mode 100644 index 0000000..c51d9a2 --- /dev/null +++ b/docs/styles.css @@ -0,0 +1,522 @@ +:root { + color-scheme: light; + --page: oklch(98% 0.012 125); + --paper: oklch(99.2% 0.006 125); + --ink: oklch(19% 0.028 150); + --muted: oklch(42% 0.032 155); + --line: oklch(84% 0.036 145); + --green: oklch(55% 0.15 150); + --green-dark: oklch(31% 0.1 150); + --blue: oklch(45% 0.13 245); + --coral: oklch(59% 0.17 30); + --shadow: 0 24px 60px color-mix(in oklch, var(--ink) 12%, transparent); + --radius: 8px; + --space-xs: clamp(0.5rem, 0.45rem + 0.2vw, 0.75rem); + --space-sm: clamp(0.75rem, 0.65rem + 0.4vw, 1rem); + --space-md: clamp(1rem, 0.8rem + 0.8vw, 1.5rem); + --space-lg: clamp(1.5rem, 1.05rem + 1.8vw, 3rem); + --space-xl: clamp(3rem, 1.9rem + 4.5vw, 6rem); + --max: 1180px; + font-family: "Aptos", "Segoe UI", "Helvetica Neue", Arial, sans-serif; + font-kerning: normal; +} + +* { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + background: + linear-gradient( + 90deg, + color-mix(in oklch, var(--line) 28%, transparent) 1px, + transparent 1px + ) + 0 0 / 72px 72px, + linear-gradient( + color-mix(in oklch, var(--line) 26%, transparent) 1px, + transparent 1px + ) + 0 0 / 72px 72px, + var(--page); + color: var(--ink); + font-size: 1rem; + line-height: 1.6; +} + +a { + color: inherit; +} + +code, +pre { + font-family: + "Cascadia Mono", "SFMono-Regular", Consolas, "Liberation Mono", monospace; +} + +.skip-link { + position: fixed; + left: 1rem; + top: 1rem; + z-index: 20; + transform: translateY(-160%); + background: var(--ink); + color: var(--paper); + padding: 0.75rem 1rem; + border-radius: var(--radius); +} + +.skip-link:focus { + transform: translateY(0); +} + +.site-header { + position: sticky; + top: 0; + z-index: 10; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0.85rem max(1rem, env(safe-area-inset-left)); + border-bottom: 1px solid color-mix(in oklch, var(--line) 72%, transparent); + background: color-mix(in oklch, var(--paper) 88%, transparent); + backdrop-filter: blur(16px); +} + +.brand { + display: inline-flex; + align-items: center; + gap: 0.7rem; + color: var(--ink); + font-weight: 800; + text-decoration: none; +} + +.brand-mark { + display: inline-grid; + width: 2.1rem; + height: 2.1rem; + place-items: center; + border: 1px solid var(--ink); + background: var(--green); + color: var(--paper); + font-size: 0.78rem; + letter-spacing: 0; +} + +.nav-links { + display: none; + align-items: center; + gap: 1.2rem; + color: var(--muted); + font-size: 0.92rem; + font-weight: 650; +} + +.nav-links a { + text-decoration: none; +} + +.nav-links a:hover, +.nav-links a:focus-visible { + color: var(--green-dark); +} + +.hero { + position: relative; + min-height: calc(100svh - 4rem); + display: grid; + align-items: center; + overflow: hidden; + padding: clamp(2.4rem, 5vw, 5rem) 1rem clamp(2.5rem, 5vw, 5rem); + border-bottom: 1px solid var(--line); +} + +.hero::after { + content: ""; + position: absolute; + inset: auto 0 0; + height: 32%; + background: linear-gradient( + to top, + color-mix(in oklch, var(--page) 94%, transparent), + transparent + ); + pointer-events: none; +} + +.hero-visual { + position: absolute; + right: max(-30rem, -42vw); + top: clamp(5rem, 12vh, 9rem); + width: min(980px, 118vw); + max-width: none; + filter: drop-shadow(var(--shadow)); + opacity: 0.88; +} + +.hero-copy { + position: relative; + z-index: 1; + width: min(720px, 100%); + margin-inline: auto; + padding-right: clamp(0rem, 22vw, 18rem); +} + +.eyebrow { + margin: 0 0 0.9rem; + color: var(--green-dark); + font-size: 0.78rem; + font-weight: 850; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +h1, +h2, +h3, +p { + margin-top: 0; +} + +h1 { + max-width: 15ch; + margin-bottom: 1.25rem; + font-size: clamp(2.8rem, 5.6vw, 5.2rem); + line-height: 0.98; + letter-spacing: 0; +} + +h2 { + max-width: 14ch; + margin-bottom: 1rem; + font-size: clamp(2.1rem, 5vw, 4.8rem); + line-height: 1; + letter-spacing: 0; +} + +h3 { + margin-bottom: 0.55rem; + font-size: 1.22rem; + line-height: 1.2; +} + +.lede { + max-width: 58ch; + margin-bottom: 1.5rem; + color: color-mix(in oklch, var(--ink) 84%, var(--green-dark)); + font-size: clamp(1.12rem, 1.5vw, 1.35rem); + line-height: 1.55; +} + +.hero-actions { + display: flex; + flex-wrap: wrap; + gap: 0.8rem; +} + +.button { + display: inline-flex; + min-height: 44px; + align-items: center; + justify-content: center; + padding: 0.75rem 1rem; + border: 1px solid var(--ink); + border-radius: var(--radius); + font-weight: 800; + text-decoration: none; + transition: + transform 160ms ease, + background-color 160ms ease; +} + +.button:hover, +.button:focus-visible { + transform: translateY(-2px); +} + +.button.primary { + background: var(--ink); + color: var(--paper); +} + +.button.secondary { + background: color-mix(in oklch, var(--paper) 86%, transparent); + color: var(--ink); +} + +.signal-band { + display: grid; + gap: 1px; + max-width: var(--max); + margin: -3rem auto var(--space-xl); + padding-inline: 1rem; +} + +.signal-band div { + display: grid; + gap: 0.3rem; + padding: 1.1rem; + border: 1px solid var(--line); + background: var(--paper); + box-shadow: 0 10px 30px color-mix(in oklch, var(--ink) 7%, transparent); +} + +.signal-band strong { + font-size: 0.95rem; +} + +.signal-band span { + color: var(--muted); +} + +.section { + max-width: var(--max); + margin-inline: auto; + padding: var(--space-xl) 1rem; +} + +.section-heading { + display: grid; + gap: 0.4rem; + margin-bottom: var(--space-lg); +} + +.section-heading p:not(.eyebrow) { + max-width: 56ch; + color: var(--muted); +} + +.section-heading.compact { + align-self: start; + margin-bottom: 0; +} + +.two-column { + display: grid; + gap: var(--space-md); + color: var(--muted); + font-size: 1.08rem; +} + +.flow-grid, +.recipe-grid { + display: grid; + gap: 1rem; +} + +.flow-step, +.recipe { + border: 1px solid var(--line); + border-radius: var(--radius); + background: var(--paper); + padding: clamp(1rem, 2.4vw, 1.7rem); +} + +.step-number { + display: inline-block; + margin-bottom: 1.25rem; + color: var(--coral); + font-weight: 900; + font-variant-numeric: tabular-nums; +} + +.flow-step p, +.recipe p { + margin-bottom: 0; + color: var(--muted); +} + +.split-section { + display: grid; + gap: var(--space-lg); + align-items: start; +} + +.code-panel { + border: 1px solid color-mix(in oklch, var(--ink) 88%, var(--green)); + border-radius: var(--radius); + background: oklch(17% 0.026 155); + color: oklch(95% 0.018 130); + overflow: hidden; + box-shadow: var(--shadow); +} + +.panel-title { + padding: 0.78rem 1rem 0; + color: oklch(74% 0.13 150); + font-size: 0.76rem; + font-weight: 850; + letter-spacing: 0.07em; + text-transform: uppercase; +} + +pre { + margin: 0; + overflow-x: auto; + padding: 1rem; + font-size: 0.9rem; + line-height: 1.55; +} + +.copy-shell { + position: relative; +} + +.copy-button { + position: absolute; + top: 0.65rem; + right: 0.65rem; + min-height: 36px; + border: 1px solid color-mix(in oklch, var(--paper) 32%, transparent); + border-radius: 6px; + background: color-mix(in oklch, var(--paper) 8%, transparent); + color: oklch(96% 0.015 130); + cursor: pointer; + font: inherit; + font-size: 0.78rem; + font-weight: 800; +} + +.command-list { + display: grid; + border: 1px solid var(--line); + border-radius: var(--radius); + background: var(--paper); +} + +.command-row { + display: grid; + gap: 0.5rem; + padding: 1rem; + border-bottom: 1px solid var(--line); +} + +.command-row:last-child { + border-bottom: 0; +} + +.command-row code { + color: var(--green-dark); + font-weight: 850; +} + +.command-row span { + color: var(--muted); +} + +.recipe { + display: grid; + gap: 0.7rem; +} + +.recipe a { + color: var(--blue); + font-weight: 850; +} + +.site-footer { + display: flex; + flex-direction: column; + gap: 1rem; + max-width: var(--max); + margin-inline: auto; + padding: var(--space-lg) 1rem; + border-top: 1px solid var(--line); + color: var(--muted); +} + +.site-footer div { + display: grid; + gap: 0.2rem; +} + +.site-footer strong { + color: var(--ink); +} + +.site-footer a { + color: var(--green-dark); + font-weight: 850; +} + +@media (min-width: 720px) { + .site-header { + padding-inline: 1.5rem; + } + + .nav-links { + display: flex; + } + + .hero { + padding-inline: 1.5rem; + } + + .hero-copy { + margin-left: max(1rem, calc((100vw - var(--max)) / 2)); + margin-right: 0; + } + + .hero-visual { + right: max(-36rem, calc((100vw - var(--max)) / 2 - 21rem)); + width: min(900px, 70vw); + } + + .signal-band { + grid-template-columns: repeat(3, 1fr); + } + + .two-column, + .split-section { + grid-template-columns: minmax(0, 0.9fr) minmax(340px, 1.1fr); + } + + .flow-grid, + .recipe-grid { + grid-template-columns: repeat(3, 1fr); + } + + .command-row { + grid-template-columns: minmax(220px, 0.55fr) minmax(0, 1fr); + align-items: center; + } + + .site-footer { + flex-direction: row; + align-items: center; + justify-content: space-between; + } +} + +@media (max-width: 719px) { + .hero-visual { + right: -34rem; + opacity: 0.14; + } + + .hero-copy { + padding-right: 0; + } + + h1 { + max-width: 10.5ch; + font-size: clamp(2.75rem, 13vw, 3.6rem); + line-height: 0.98; + } +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + scroll-behavior: auto !important; + transition: none !important; + } +}