Migrate site from Bun to Astro (#130)

* feat(site): scaffold Astro migration, convert 3 pages

Phase 1+2 of the Astro migration:

- Astro v6.2.1 installed, srcDir: 'site', static output to build/
- Shared layout: Base.astro (head, fonts, meta, slots), Header.astro
  (star count in one place: 23k), Footer.astro
- CSS moved from public/css/ to site/styles/ (9 files, @import chains
  resolve via Vite)
- Three pages converted: privacy, cases/neo-mirai, live-mode
  (all return 200 on astro dev)

Remaining: designing, slop, homepage, content collections (docs),
JS migration, server/index.js deletion, build.js cleanup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(site): migrate all 6 main pages to Astro

Converts the remaining pages:
- designing/index.html → site/pages/designing/index.astro (551 lines)
- slop/index.html → site/pages/slop/index.astro (909 lines)
- index.html → site/pages/index.astro (1278 lines, the homepage)

Base.astro gains OG meta tag props, before-header/after-header
slots (for grain overlay and section nav), and configurable mainId.

Homepage uses link tags to public/css/ instead of frontmatter CSS
imports to avoid esbuild choking on :has() in main.css. Curly
braces inside <code> elements (CSS snippets in changelog) escaped
with HTML entities to prevent Astro JSX expression parsing.

All 6 pages return 200 on astro dev. Branch: feat/astro-migration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(site): content collections for docs and tutorials

Replaces the 1532-line build-sub-pages.js generator with Astro v6
content collections:

- 24 skill editorial files move to site/content/skills/
- 4 tutorial files move to site/content/tutorials/
- site/content.config.ts defines both collections with glob loaders
- site/pages/docs/[...slug].astro reads skills collection + command
  metadata from source/skills/ at build time
- site/pages/docs/index.astro renders the command grid grouped by
  category (create, evaluate, refine, simplify, harden, system)
- site/pages/tutorials/ mirrors the pattern with ordered index
- Doc.astro layout provides sidebar nav, breadcrumbs, and related-
  command chips from the COMMAND_RELATIONSHIPS data
- Category/relationship data extracted to site/data/sub-pages-data.ts

All 15 tested pages return 200: 6 main pages + 5 docs + 2 tutorials
+ 2 index pages. The old generator is not yet deleted (Task #6).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(site): move JS source from public/js/ to site/scripts/

Moves all 49 JS files (app.js + 48 in js/) into site/scripts/.
Vite now processes them through its module bundler instead of
serving them raw from public/.

app.js import paths updated from ./js/X to ./X (the js/ nesting
is gone since app.js now lives alongside the subdirectories).

Homepage and live-mode page switch from <script is:inline src="/app.js">
to Vite-processed <script> imports, so tree-shaking, bundling,
and minification happen automatically at build time.

public/js/ still exists for now (cleanup in Task #6) and the
generated/counts.js build output path needs updating there too.
@paper-design/shaders added to npm dependencies (was missing).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(site): delete old Bun server, generator, and duplicated files

Cleanup after the Astro migration:

Deleted:
- server/index.js (233 lines, replaced by `astro dev`)
- scripts/build-sub-pages.js (1532 lines, replaced by content collections)
- scripts/lib/render-page.js (247 lines, replaced by Base.astro layout)
- content/site/partials/header.html (replaced by Header.astro component)
- public/index.html, privacy.html, designing/, live-mode/, cases/
  (replaced by .astro pages in site/pages/)
- public/css/ (moved to site/styles/)
- public/js/ old source files (moved to site/scripts/)
- public/app.js (moved to site/scripts/app.js)

Kept in public/:
- antipattern-examples/ (standalone HTML demos, not Astro pages)
- antipattern-images/, assets/, neo-mirai/ (static assets)
- js/detect-antipatterns-browser.js (referenced by antipattern examples)
- js/generated/counts.js (build output from scripts/build.js)
- _data/api/ (generated API data, now written to public/ so Astro
  passes it through to build/)

Updated:
- astro.config.mjs: added redirects (skills->docs, cheatsheet->docs,
  gallery->slop, neon-mirai->neo-mirai, etc.)
- package.json: dev->astro dev, build->build:skills+build:site,
  preview->astro preview
- scripts/build.js: removed buildStaticSite(), generateSubPages(),
  static-asset copying. API data writes to public/_data/ instead of
  build/_data/. Site-header validator is a no-op (shared component).
  Em-dash validator scans site/components + site/layouts, not pages
  (pages contain content from other sources like detector descriptions).
- .gitignore: removed public/slop/ entry

Tests: 186/186 pass. Skills build: clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(site): fix redirect config for Astro compatibility

Move the dynamic /skills/:id -> /docs/:id redirect to public/_redirects
(Cloudflare Pages native format) since Astro's redirect config can't
handle dynamic routes that don't match existing page patterns.

Remove duplicate trailing-slash redirect entries that caused warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(site): switch remaining pages from /css/ link tags to frontmatter imports

Doc.astro, docs/index, tutorials/index, and tutorials/[slug] were
still using <link href="/css/sub-pages.css"> which pointed at the
deleted public/css/ directory. Switched to frontmatter CSS imports
(import '../../styles/sub-pages.css') which Vite resolves from
site/styles/.

Homepage also switches from link tags to frontmatter imports for
main.css and sub-pages.css — the esbuild error that originally
forced the link-tag workaround was caused by unescaped curly braces
in the HTML content (since fixed), not by the CSS itself.

All pages verified visually in Chrome: homepage hero, foundation
grid, docs index (card grid with categories), docs detail (sidebar +
editorial content + visual mockups), designing (core loop diagram),
privacy, tutorials. Header renders with 23k stars on every page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(site): fix edge-to-edge sections, broken API paths, CSS links

Three fixes:

1. Homepage sections sat on the viewport edge because Base.astro's
   <main> lacked the site-content class (provides max-width + padding).
   Added mainClass prop to Base.astro; homepage sets mainClass="site-content".

2. "Failed to load commands" because app.js fetched /api/commands
   which only existed in the old Bun server's routing. Updated to
   fetch from /_data/api/commands.json (the static JSON files that
   build:skills writes to public/_data/).

3. CSS reference fix (previous commit was incomplete): Doc.astro,
   docs/index, tutorials pages all used <link href="/css/sub-pages.css">
   pointing at deleted public/css/. Switched to frontmatter imports.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(site): add sidebar to docs index page

The docs index was using Base.astro directly without the skills-layout
grid, so it rendered without a sidebar. Added the same sidebar structure
from Doc.astro (category-grouped command list) and wrapped the content
in the skills-layout grid.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(site): extract footer CSS to shared file, import in Base.astro

Footer was unstyled on sub-pages because footer CSS lived only in
main.css (loaded by the homepage) not in sub-pages.css. Extracted
the 95 lines of footer rules into site/styles/footer.css and
imported it in Base.astro so every page gets footer styles regardless
of which page-specific CSS it loads.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(demos): move landing-demo into repo, add as slop specimens

Moves ~/code/landing-demo/ into demos/landing-demo/ (without
node_modules or the redundant .claude/.agents skill copies — the
repo root's skill is found by walking up). PRODUCT.md, DESIGN.md,
DESIGN.json, PROMPT.md, and SCRIPT.md stay in place so running
Claude from demos/landing-demo/ picks up the project context.

Also copies both pages as slop specimens to public/antipattern-examples/
with the detector script baked in:
- new-slop-2026.html (Fraunces + warm cream editorial monoculture)
- old-slop-2022.html (purple gradient + glassmorphism + neon glow)

These can be linked from the slop page gallery alongside the
existing 11 synthetic specimens.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(slop): replace single demo iframe with Then vs Now comparison

The "See it" section (01) on the slop page now shows two side-by-side
browser frames: 2022 slop (purple gradients, glassmorphism, neon glow)
and 2026 slop (Fraunces, warm cream, editorial restraint). Both run
the detector overlay live — hover either to see which rules fire.

Replaces the single visual-mode-demo.html iframe. Responsive: stacks
vertically on viewports below 900px.

Caption: "Same engine, different decade, both flagged."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(slop): switch to single-frame era toggle, center the section

Replaces the side-by-side dual-iframe layout with a single large
frame and a segmented 2022/2026 toggle. Clicking the toggle swaps
which iframe is visible (both pre-loaded, instant switch). Browser
chrome title updates to match the active era.

Centers the lede text and toggle above the frame for visual
cohesion with the full-width iframe below.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(slop): left-align See It section, toggle inline with lede

Moves the era toggle to the right of the lede paragraph using a
flex row (align-items: flex-end). Left-aligned text + right-docked
toggle matches the rest of the page's flow instead of standing out
as a centered island. Stacks vertically on narrow viewports.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(slop): left-align iframe, remove max-width and auto margin

The visual-mode-preview had max-width: 1040px + margin: 0 auto
which centered it within the column. Override both in the
.slop-then-now context so the frame fills the full content width
flush with the text above. Caption left-aligned to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(site): update star count to 24k (24,062)

One file, one edit. The Astro migration working as intended.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(build): regenerate pnpm-lock.yaml for astro + shaders deps

Cloudflare Pages uses pnpm with frozen-lockfile. The lockfile was
stale after adding astro, @astrojs/cloudflare, and
@paper-design/shaders via npm.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(build): resolve 3 bugbot review issues

1. Restore public/slop/ to .gitignore — prevents accidental legacy
   generator output from conflicting with the Astro page.

2. Move astro and @paper-design/shaders to devDependencies — these
   are site-build tools, not CLI runtime deps. Removes @astrojs/cloudflare
   entirely (unused; static output mode needs no adapter).

3. Fix Astro wiping build:skills output — CF config (_headers,
   _redirects, _routes.json) and API data now write to public/ so
   Astro copies them through. Dist ZIPs copy to build/_data/dist/
   as a post-build step (after Astro finishes).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(build): merge duplicate devDependencies, use npx for astro CLI

The previous commit created a second devDependencies key in
package.json. JSON doesn't support duplicate keys — pnpm ignored
the first block (with astro), so `astro build` wasn't found.

Merged astro and @paper-design/shaders into the existing
devDependencies block. Changed `astro build/dev/preview` to
`npx astro build/dev/preview` so pnpm finds the local binary
on Cloudflare Pages (which doesn't add node_modules/.bin to PATH
by default).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(demos): remove private demo script and prompt from public repo

SCRIPT.md contained a detailed conference talk script with personal
delivery strategies, rehearsed Q&A answers, and venue details.
PROMPT.md contained the origin brief for the demo page. Neither
belongs in a public repo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(build): gitignore generated public/ artifacts, consolidate redirects

1. Generated files written to public/ by build:skills (API data,
   CF config, browser detector, counts.js) are now gitignored.
   Prevents noisy diffs and merge conflicts from committed build
   artifacts.

2. Removed duplicate redirects from astro.config.mjs. All redirects
   now live in one place: the _redirects file generated by
   scripts/build.js (which Cloudflare Pages processes natively).
   Eliminates the dual-maintenance risk where the two sources
   could drift apart.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-05-02 11:29:10 -07:00
committed by GitHub
co-authored by Claude Opus 4.7
parent a312da5ec7
commit b8f09c8142
124 changed files with 7658 additions and 3307 deletions
+10 -1
View File
@@ -58,13 +58,21 @@ extension/detector/
evals/
tests/evals-v2/
# Generated sub-pages (built from source/skills + content/site at build time)
# Generated sub-pages (legacy, now replaced by Astro content collections)
public/docs/
public/anti-patterns/
public/tutorials/
public/visual-mode/
public/slop/
# Build artifacts written to public/ so Astro copies them to build/
public/_data/
public/_headers
public/_redirects
public/_routes.json
public/js/detect-antipatterns-browser.js
public/js/generated/
# Note: harness skill directories (.claude/skills/, .cursor/skills/, etc.)
# are intentionally tracked. npx skills reads them from this repo at install
# time, and they enable clean submodule use. Run `bun run build` to refresh
@@ -73,3 +81,4 @@ public/slop/
# Codex CLI consumes `.agents/skills/`; `.codex/` is not used. Ignore it so
# local artifacts or old trees are never committed.
.codex/
.astro/
+15
View File
@@ -0,0 +1,15 @@
import { defineConfig } from 'astro/config';
export default defineConfig({
srcDir: './site',
output: 'static',
build: {
format: 'directory',
},
outDir: './build',
vite: {
build: {
assetsInlineLimit: 0,
},
},
});
+281
View File
@@ -0,0 +1,281 @@
{
"schemaVersion": 2,
"generatedAt": "2026-04-29T00:00:00Z",
"title": "Design System: Lumina",
"extensions": {
"colorMeta": {
"cream": {
"role": "neutral",
"displayName": "Cream",
"canonical": "oklch(96.5% 0.012 80)",
"tonalRamp": [
"oklch(15% 0.012 80)",
"oklch(25% 0.012 80)",
"oklch(35% 0.012 80)",
"oklch(50% 0.012 80)",
"oklch(65% 0.012 80)",
"oklch(80% 0.012 80)",
"oklch(90% 0.012 80)",
"oklch(96.5% 0.012 80)"
]
},
"cream-warm": {
"role": "neutral",
"displayName": "Cream Warm",
"canonical": "oklch(92% 0.020 75)",
"tonalRamp": [
"oklch(15% 0.020 75)",
"oklch(25% 0.020 75)",
"oklch(35% 0.020 75)",
"oklch(50% 0.020 75)",
"oklch(65% 0.020 75)",
"oklch(78% 0.020 75)",
"oklch(85% 0.020 75)",
"oklch(92% 0.020 75)"
]
},
"peach": {
"role": "neutral",
"displayName": "Peach",
"canonical": "oklch(91% 0.040 60)",
"tonalRamp": [
"oklch(20% 0.040 60)",
"oklch(30% 0.060 60)",
"oklch(45% 0.080 60)",
"oklch(60% 0.080 60)",
"oklch(72% 0.060 60)",
"oklch(82% 0.050 60)",
"oklch(88% 0.045 60)",
"oklch(91% 0.040 60)"
]
},
"line": {
"role": "neutral",
"displayName": "Line (Hairline Border)",
"canonical": "oklch(89% 0.018 75)",
"tonalRamp": [
"oklch(15% 0.018 75)",
"oklch(25% 0.018 75)",
"oklch(40% 0.018 75)",
"oklch(55% 0.018 75)",
"oklch(70% 0.018 75)",
"oklch(80% 0.018 75)",
"oklch(85% 0.018 75)",
"oklch(89% 0.018 75)"
]
},
"ink": {
"role": "neutral",
"displayName": "Ink",
"canonical": "oklch(15% 0.010 60)",
"tonalRamp": [
"oklch(15% 0.010 60)",
"oklch(22% 0.010 60)",
"oklch(30% 0.010 60)",
"oklch(40% 0.010 60)",
"oklch(55% 0.010 60)",
"oklch(70% 0.010 60)",
"oklch(85% 0.010 60)",
"oklch(94% 0.010 60)"
]
},
"soft": {
"role": "neutral",
"displayName": "Soft",
"canonical": "oklch(40% 0.012 60)",
"tonalRamp": [
"oklch(15% 0.012 60)",
"oklch(25% 0.012 60)",
"oklch(35% 0.012 60)",
"oklch(40% 0.012 60)",
"oklch(55% 0.012 60)",
"oklch(70% 0.012 60)",
"oklch(82% 0.012 60)",
"oklch(92% 0.012 60)"
]
},
"accent": {
"role": "primary",
"displayName": "Burnt Orange Accent",
"canonical": "oklch(60% 0.150 40)",
"tonalRamp": [
"oklch(15% 0.060 40)",
"oklch(25% 0.090 40)",
"oklch(35% 0.120 40)",
"oklch(48% 0.150 40)",
"oklch(60% 0.150 40)",
"oklch(72% 0.130 40)",
"oklch(85% 0.080 40)",
"oklch(95% 0.040 40)"
]
},
"accent-deep": {
"role": "primary",
"displayName": "Accent Deep",
"canonical": "oklch(50% 0.150 38)",
"tonalRamp": [
"oklch(15% 0.060 38)",
"oklch(25% 0.090 38)",
"oklch(35% 0.120 38)",
"oklch(50% 0.150 38)",
"oklch(62% 0.140 38)",
"oklch(75% 0.110 38)",
"oklch(86% 0.070 38)",
"oklch(95% 0.035 38)"
]
}
},
"typographyMeta": {
"display": {
"displayName": "Display",
"purpose": "Hero headlines only. Fraunces, weight 400, optical-sized for large display."
},
"headline": {
"displayName": "Headline",
"purpose": "Section headlines (the features-head h2, cta-section h2)."
},
"title": {
"displayName": "Title",
"purpose": "Card headings inside the feature grid."
},
"lede": {
"displayName": "Lede",
"purpose": "The supporting paragraph that sits below a hero headline."
},
"body": {
"displayName": "Body",
"purpose": "Default paragraph copy. Cap line length at 6575ch."
},
"label": {
"displayName": "Label",
"purpose": "The eyebrow chip and any small uppercase labels."
}
},
"shadows": [],
"motion": [
{
"name": "ease-button",
"value": "ease",
"duration": "150ms",
"purpose": "Default easing for button hover transforms."
},
{
"name": "ease-card",
"value": "ease",
"duration": "300ms",
"purpose": "Card hover transition (currently unused but reserved)."
}
],
"breakpoints": [
{ "name": "container", "value": "1180px" },
{ "name": "logo-strip", "value": "1100px" }
]
},
"components": [
{
"name": "Primary Button",
"kind": "button",
"refersTo": "button-primary",
"description": "The default CTA. Ink background, cream text, fully rounded.",
"html": "<a href=\"#\" class=\"ds-btn-primary\">Start free trial</a>",
"css": ".ds-btn-primary { display: inline-flex; align-items: center; gap: 8px; padding: 14px 28px; border-radius: 999px; background: #1f1a15; color: #faf6ef; font-family: 'Inter', system-ui, sans-serif; font-weight: 500; font-size: 15px; text-decoration: none; transition: transform 150ms ease; } .ds-btn-primary:hover { transform: translateY(-1px); }"
},
{
"name": "Ghost Button",
"kind": "button",
"refersTo": "button-ghost",
"description": "Secondary CTA, always paired with the primary.",
"html": "<a href=\"#\" class=\"ds-btn-ghost\">Watch demo</a>",
"css": ".ds-btn-ghost { display: inline-flex; align-items: center; gap: 8px; padding: 14px 28px; border-radius: 999px; background: transparent; color: #1f1a15; border: 1px solid #1f1a15; font-family: 'Inter', system-ui, sans-serif; font-weight: 500; font-size: 15px; text-decoration: none; transition: transform 150ms ease; } .ds-btn-ghost:hover { transform: translateY(-1px); }"
},
{
"name": "Nav Pill",
"kind": "button",
"refersTo": "nav-pill",
"description": "Compact primary CTA used in the nav. Smaller padding than the full button.",
"html": "<a href=\"#\" class=\"ds-nav-pill\">Get started</a>",
"css": ".ds-nav-pill { display: inline-flex; align-items: center; padding: 9px 18px; border-radius: 999px; background: #1f1a15; color: #faf6ef; font-family: 'Inter', system-ui, sans-serif; font-weight: 500; font-size: 14px; text-decoration: none; }"
},
{
"name": "Eyebrow Chip",
"kind": "chip",
"description": "The small uppercase label sitting above the hero headline. Pure typography, no background.",
"html": "<div class=\"ds-eyebrow\">AI-native workflows</div>",
"css": ".ds-eyebrow { display: inline-block; font-family: 'Inter', system-ui, sans-serif; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.16em; color: #a8431f; }"
},
{
"name": "Feature Card",
"kind": "card",
"refersTo": "card",
"description": "Tonal-layered card on cream-warm with hairline border. Flat, no shadow.",
"html": "<div class=\"ds-card\">\n <div class=\"ds-card-icon\">⚡</div>\n <h3 class=\"ds-card-title\">Lightning Fast</h3>\n <p class=\"ds-card-body\">Move from idea to production in minutes.</p>\n</div>",
"css": ".ds-card { background: #f4ebdc; border: 1px solid #e6dccb; border-radius: 20px; padding: 40px 32px; text-align: center; max-width: 280px; } .ds-card-icon { width: 56px; height: 56px; border-radius: 14px; background: #faf6ef; border: 1px solid #e6dccb; display: inline-flex; align-items: center; justify-content: center; font-size: 28px; margin-bottom: 24px; } .ds-card-title { font-family: 'Fraunces', Georgia, serif; font-weight: 500; font-size: 22px; letter-spacing: -0.01em; margin: 0 0 12px; color: #1f1a15; } .ds-card-body { font-family: 'Inter', system-ui, sans-serif; font-size: 15px; color: #5b4f44; margin: 0; line-height: 1.55; }"
},
{
"name": "Hero Headline",
"kind": "custom",
"description": "The display-scale headline with the One-Italic Rule applied: italic accent on a single word.",
"html": "<h1 class=\"ds-hero-h1\">Build faster with <em>intelligent</em> workflows</h1>",
"css": ".ds-hero-h1 { font-family: 'Fraunces', Georgia, serif; font-weight: 400; font-size: clamp(48px, 7vw, 88px); line-height: 1.05; letter-spacing: -0.02em; color: #1f1a15; max-width: 720px; margin: 0; } .ds-hero-h1 em { font-style: italic; color: #a8431f; }"
},
{
"name": "Logo Strip Wordmark",
"kind": "custom",
"description": "The italic Fraunces wordmarks in the logo row — the exception to the One-Italic Rule.",
"html": "<span class=\"ds-logo-mark\">Northwind</span>",
"css": ".ds-logo-mark { font-family: 'Fraunces', Georgia, serif; font-style: italic; font-size: 22px; color: #5b4f44; }"
}
],
"narrative": {
"northStar": "Editorial confidence in warm light.",
"overview": "The system reads as a printed magazine spread, transposed to a screen. Cream paper as the page surface, ink-dark headlines in Fraunces, restrained pacing carried by whitespace. Density is mid. The accent (a burnt orange warming toward the lower stop of the cream-to-peach hero gradient) appears sparingly: on the eyebrow chip, the logo dot, and the italic emphasis inside the hero headline. Nowhere else.\n\nWhat the system explicitly rejects, per PRODUCT.md anti-references: glassmorphism, dark mode with neon glow, gradient text, side-stripe accents on cards, and the broader \"Fraunces-cream-peach SaaS template\" that this very page exemplifies. The DESIGN.md documents the current visual reality so the live-mode design panel can render it accurately. The brand intent is to diffuse away from it.",
"keyCharacteristics": [
"Warm-paper palette anchored on cream (#faf6ef)",
"Display in Fraunces, body in Inter",
"Editorial-leaning layout vocabulary",
"Rounded throughout (14px, 20px, 999px)",
"Flat: no shadows, depth via tonal layering"
],
"rules": [
{
"name": "The Cream-Family Rule",
"body": "Every neutral surface tints toward the brand hue. No pure white anywhere, no pure black, no untinted gray. The eye should never read this page as 'default browser.'",
"section": "colors"
},
{
"name": "The 10% Accent Rule",
"body": "The burnt orange covers no more than 10% of any rendered surface. Its rarity is the point.",
"section": "colors"
},
{
"name": "The One-Italic Rule",
"body": "Italic appears exactly once per page: on a single emphasized word inside the hero headline. Nowhere else. The logo strip's italic Fraunces wordmarks are the exception that proves it (wordmark, not running italic).",
"section": "typography"
},
{
"name": "The No-Gradient-Text Rule",
"body": "Type is solid color, always. The hero's cream-to-peach gradient is a section background, never a typographic effect.",
"section": "typography"
},
{
"name": "The Flat-By-Default Rule",
"body": "Surfaces are flat at rest. Hover lift uses transform: translateY(-1px), never a shadow. Glassmorphism, neon glow, and elevation halos are absent by design.",
"section": "elevation"
}
],
"dos": [
"Do keep the burnt-orange accent under 10% of any visible surface; it's a typographic accent and a logo dot, not a button.",
"Do use Fraunces for display and Inter for body; respect the One-Italic Rule.",
"Do carry depth via tonal layering and hairline borders, not shadows.",
"Do tint every neutral toward the cream hue. Reject pure white and pure gray.",
"Do keep buttons fully rounded (999px) and cards moderately rounded (20px); the contrast is intentional."
],
"donts": [
"Don't add box-shadows to surfaces. The system is flat by default; hover lift uses transform, not shadow.",
"Don't introduce gradient text or background-clip: text. The hero gradient is a section background, never a typographic effect.",
"Don't add glassmorphism, neon glow, dark mode by default, or side-stripe colored borders. All banned in PRODUCT.md anti-references.",
"Don't introduce a fourth color outside the cream / ink / orange family without an explicit reason recorded in PRODUCT.md.",
"Don't drift into the broader 'Fraunces-cream-peach SaaS template' the page already exemplifies. The PRODUCT.md anti-references this aesthetic; departure-mode variants should diffuse away from it."
]
}
}
+207
View File
@@ -0,0 +1,207 @@
---
name: Lumina
description: Editorial-warm landing page for an AI-native workflow tool.
colors:
cream: "#faf6ef"
cream-warm: "#f4ebdc"
peach: "#f6dfcb"
line: "#e6dccb"
ink: "#1f1a15"
soft: "#5b4f44"
accent: "#c8552b"
accent-deep: "#a8431f"
typography:
display:
fontFamily: "Fraunces, Georgia, serif"
fontSize: "clamp(3rem, 7vw, 5.5rem)"
fontWeight: 400
lineHeight: 1.05
letterSpacing: "-0.02em"
headline:
fontFamily: "Fraunces, Georgia, serif"
fontSize: "clamp(2.25rem, 4.5vw, 3.25rem)"
fontWeight: 400
lineHeight: 1.1
letterSpacing: "-0.02em"
title:
fontFamily: "Fraunces, Georgia, serif"
fontSize: "1.375rem"
fontWeight: 500
lineHeight: 1.3
letterSpacing: "-0.01em"
body:
fontFamily: "Inter, system-ui, sans-serif"
fontSize: "1rem"
fontWeight: 400
lineHeight: 1.55
letterSpacing: "normal"
lede:
fontFamily: "Inter, system-ui, sans-serif"
fontSize: "1.25rem"
fontWeight: 400
lineHeight: 1.55
letterSpacing: "normal"
label:
fontFamily: "Inter, system-ui, sans-serif"
fontSize: "0.75rem"
fontWeight: 600
lineHeight: 1.4
letterSpacing: "0.16em"
rounded:
card: "20px"
icon: "14px"
pill: "999px"
spacing:
xs: "8px"
sm: "16px"
md: "24px"
lg: "32px"
xl: "56px"
2xl: "80px"
3xl: "120px"
components:
button-primary:
backgroundColor: "{colors.ink}"
textColor: "{colors.cream}"
rounded: "{rounded.pill}"
padding: "14px 28px"
button-ghost:
backgroundColor: "transparent"
textColor: "{colors.ink}"
rounded: "{rounded.pill}"
padding: "14px 28px"
nav-pill:
backgroundColor: "{colors.ink}"
textColor: "{colors.cream}"
rounded: "{rounded.pill}"
padding: "9px 18px"
card:
backgroundColor: "{colors.cream-warm}"
textColor: "{colors.soft}"
rounded: "{rounded.card}"
padding: "40px 32px"
icon-tile:
backgroundColor: "{colors.cream}"
rounded: "{rounded.icon}"
size: "56px"
---
# Design System: Lumina
## 1. Overview
**Creative North Star: "Editorial confidence in warm light."**
The system reads as a printed magazine spread, transposed to a screen. Cream paper as the page surface, ink-dark headlines in Fraunces, restrained pacing carried by whitespace. Density is mid. The accent (a burnt orange warming toward the lower stop of the cream-to-peach hero gradient) appears sparingly: on the eyebrow chip, the logo dot, and the italic emphasis inside the hero headline. Nowhere else.
What the system explicitly rejects, per PRODUCT.md anti-references: glassmorphism, dark mode with neon glow, gradient text, side-stripe accents on cards, and the broader "Fraunces-cream-peach SaaS template" that this very page exemplifies. The DESIGN.md documents the current visual reality so the live-mode design panel can render it accurately. The brand intent is to diffuse away from it.
**Key Characteristics:**
- Warm-paper palette anchored on cream (#faf6ef)
- Display in Fraunces, body in Inter
- Editorial-leaning layout vocabulary
- Rounded throughout (14px, 20px, 999px)
- Flat: no shadows, depth via tonal layering
## 2. Colors
A single warm-cream family carrying both surface and neutral text, with one muted-orange accent that earns its rare appearances.
### Primary
- **Burnt Orange Accent** (#c8552b): the eyebrow chip's color, the logo dot, the italic emphasis inside the hero headline. Decorative; never a surface, never a button background.
- **Accent Deep** (#a8431f): the darker variant for hover and emphasis states; same hue, a step deeper.
### Neutral
- **Cream** (#faf6ef): the page surface, the nav background (translucent), and the bottom of the hero gradient.
- **Cream Warm** (#f4ebdc): the feature card surface; one shade darker than cream, anchors the card group as a contained set.
- **Peach** (#f6dfcb): the warmer stop of the hero gradient. Atmospheric, not structural.
- **Ink** (#1f1a15): primary body text; primary-button background; the dark CTA section's surface.
- **Soft** (#5b4f44): secondary text; captions; footer copy; nav link rest state.
- **Line** (#e6dccb): hairline borders on cards, the nav, the footer, and the icon tiles.
### Named Rules
**The Cream-Family Rule.** Every neutral surface tints toward the brand hue. No pure white anywhere, no pure black, no untinted gray. The eye should never read this page as "default browser."
**The 10% Accent Rule.** The burnt orange covers no more than 10% of any rendered surface. Its rarity is the point.
## 3. Typography
**Display Font:** Fraunces (Georgia fallback, serif)
**Body Font:** Inter (system-ui fallback, sans-serif)
**Character:** A magazine-cover serif for the headlines and brand mark, paired with a refined sans for everything else. The italic Fraunces inside `<em>` is the system's only italic, used exactly once per page on a single word inside the hero headline.
### Hierarchy
- **Display** (Fraunces, clamp(48px, 7vw, 88px), weight 400, line-height 1.05, letter-spacing -0.02em): hero headlines only.
- **Headline** (Fraunces, clamp(36px, 4.5vw, 52px), weight 400, line-height 1.1): section headlines.
- **Title** (Fraunces, 22px, weight 500, letter-spacing -0.01em): card headings.
- **Lede** (Inter, 20px, weight 400, line-height 1.55): the supporting paragraph below a hero headline.
- **Body** (Inter, 1518px, weight 400, line-height 1.55): default paragraph copy. Cap line length at 6575ch.
- **Label** (Inter, 12px, weight 600, letter-spacing 0.16em, uppercase): the eyebrow chip and any small uppercase labels.
### Named Rules
**The One-Italic Rule.** Italic appears exactly once per page: on a single emphasized word inside the hero headline. Nowhere else. The logo strip's italic Fraunces wordmarks are the exception that proves it (wordmark, not running italic).
**The No-Gradient-Text Rule.** Type is solid color, always. The hero's cream-to-peach gradient is a section background, never a typographic effect.
## 4. Elevation
Flat. No shadows on cards, buttons, surfaces, or any rendered element. Depth is conveyed by three things and only those:
- **Tonal layering**: cream (page) sits below cream-warm (cards), which sit below ink (the dark CTA section).
- **Hairline borders**: 1px line color (`--line`) on every contained surface.
- **Sticky-nav backdrop blur**: the only blur in the system, marking the nav as living above content.
### Named Rules
**The Flat-By-Default Rule.** Surfaces are flat at rest. Hover lift uses `transform: translateY(-1px)`, never a shadow. Glassmorphism, neon glow, and elevation halos are absent by design.
## 5. Components
### Buttons
- **Shape:** fully rounded (border-radius: 999px). The full-size variant is 14px 28px padding; the smaller nav pill is 9px 18px.
- **Primary** (`.btn-primary`, `.pill`): background ink, text cream. The primary CTA on every section.
- **Ghost** (`.btn-ghost`): transparent background, ink 1px border, ink text. Secondary CTA, always paired with primary.
- **Hover:** translateY(-1px), 150ms ease.
- **Inverted Primary** (`.cta-section .btn-primary`): background cream, text ink. Used because the dark CTA section reverses the surface contrast.
### Cards (Feature Tiles)
- **Corner Style:** 20px rounded (less than pill, more than container).
- **Background:** cream-warm (`--cream-warm`), one shade darker than the surrounding cream surface.
- **Border:** 1px line color.
- **Internal Padding:** 40px 32px (generous; cards breathe).
- **Shadow:** none, see Elevation.
### Icon Tile (inside Cards)
- **Size:** 56×56px.
- **Background:** cream (`--cream`).
- **Border:** 1px line color.
- **Corner Style:** 14px rounded.
- **Position:** centered above the card heading, 24px bottom margin.
### Eyebrow Chip
- **Style:** uppercase Inter, 12px, weight 600, letter-spacing 0.16em, color accent-deep (#a8431f). No background or border — pure typographic label.
- **Position:** standalone above the hero `<h1>`, with 32px bottom margin.
### Navigation
- **Background:** translucent cream (`rgba(250, 246, 239, 0.85)`) with `backdrop-filter: blur(10px)`. Sticky to the top.
- **Border:** 1px line color at the bottom edge.
- **Links:** 14px Inter, weight 400, color soft (rest), color ink (hover). The trailing pill CTA uses the nav-pill component.
### Logo Strip
- **Items:** Fraunces italic, 22px, color soft. Six wordmarks, justified across a single row, separated by hairline borders top and bottom.
## 6. Do's and Don'ts
### Do:
- **Do** keep the burnt-orange accent under 10% of any visible surface; it's a typographic accent and a logo dot, not a button.
- **Do** use Fraunces for display and Inter for body; respect the One-Italic Rule.
- **Do** carry depth via tonal layering and hairline borders, not shadows.
- **Do** tint every neutral toward the cream hue. Reject pure white and pure gray.
- **Do** keep buttons fully rounded (999px) and cards moderately rounded (20px); the contrast is intentional.
### Don't:
- **Don't** add box-shadows to surfaces. The system is flat by default; hover lift uses transform, not shadow.
- **Don't** introduce gradient text or `background-clip: text`. The hero gradient is a section background, never a typographic effect.
- **Don't** add glassmorphism, neon glow, dark mode by default, or side-stripe colored borders — all banned in PRODUCT.md anti-references.
- **Don't** introduce a fourth color outside the cream / ink / orange family without an explicit reason recorded in PRODUCT.md.
- **Don't** drift into the broader "Fraunces-cream-peach SaaS template" the page already exemplifies. The PRODUCT.md anti-references this aesthetic; departure-mode variants should diffuse away from it.
+39
View File
@@ -0,0 +1,39 @@
# Lumina
## Register
brand
## Users
Senior product builders at Series A through C software companies: founding engineers, design-leaning PMs, and tech-fluent founders. They evaluate tools fast, distrust marketing-speak, and have spent the last twelve months watching every SaaS landing page converge on the same warm-cream-and-Fraunces template. They will close the tab on anything that pattern-matches to "AI startup default" within two seconds.
## Product Purpose
Lumina is an AI-native workflow tool for product teams. The marketing site exists to make taste-aware buyers stop, read, and remember. Success is not "high conversion." Success is "shared in a group chat with the message: have you seen this yet."
## Brand Personality
Three-word personality: **specific, earned, unmistakable**. The voice is closer to a well-written engineering blog post than a pitch deck. Claims are concrete and verifiable. Adjectives are rationed. The brand has a point of view and trusts the reader's intelligence.
## Anti-references
Explicitly avoid the patterns that have become the new monoculture:
- **The Fraunces-cream-peach SaaS template.** Warm cream backgrounds, large italic Fraunces headlines, soft burnt-orange accent, gentle peach gradients. This was novel in early 2025 and is now everywhere. The current `index.html` is itself an example. Diffuse away from it.
- **Three-icon feature tile rows.** Icon-above-h3-above-paragraph, repeated three or four times across equal-width cards, generic single-word feature names ("Lightning Fast", "Enterprise Secure", "Built to Scale"). The flattest possible expression of "we have features."
- **Hero superlatives.** "All-in-one platform", "in record time", "powered by AI, designed for humans", "trusted by 10,000+ teams worldwide". B2B SaaS mad-libs.
- **Soft-everything aesthetic.** Rounded corners on every element, soft shadows on every surface, polite center-aligned spacing, no edges, no opinions.
- **Decorative gradient text and glass-blur backdrops.** Both already banned by the shared design laws; restated here because they're still the first thing models reach for on brand surfaces.
## Design Principles
1. **Distinctiveness over polish.** Polish is table stakes; standing out from the SaaS-default sea is the job. If the page could plausibly be running on twenty other companies' domains, it has failed regardless of how clean it looks.
2. **Specific over generic.** Real numbers, real product screenshots, real customer names, real claims with evidence. No abstract feature taxonomies.
3. **Earned attention, not decorative attention.** Type weight, scale, and rhythm carry hierarchy. Animation, gradients, and effects are reserved for moments that genuinely warrant them.
4. **Commitment over compromise.** Confidence shows up as restraint OR as full commitment, never as a hedged middle. A timid centered-stack with polite shadows is the failure mode this brand exists to avoid.
5. **Confidence without volume.** The strongest brands speak quietly when quiet is the right register, and loudly when loud is. Insecurity is what splits the difference.
## Accessibility & Inclusion
WCAG 2.2 AA minimum across body text, headings, and interactive controls. Honor `prefers-reduced-motion` for any animation. Color is never the sole carrier of meaning. Tap targets ≥44px on mobile. Tested with screen readers; reading order matches visual order.
+350
View File
@@ -0,0 +1,350 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Lumina — The future of intelligent workflows</title>
<link rel="icon" href="data:," />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<style>
:root {
--bg: #0a0a14;
--bg-elev: #14141f;
--text: #ffffff;
--text-muted: rgba(255, 255, 255, 0.6);
--border: rgba(255, 255, 255, 0.08);
--purple: #8b5cf6;
--violet: #7c3aed;
--pink: #ec4899;
--cyan: #06b6d4;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
font-family: 'Inter', system-ui, sans-serif;
color: var(--text);
background: var(--bg);
-webkit-font-smoothing: antialiased;
line-height: 1.6;
overflow-x: hidden;
}
/* Nav */
.nav {
position: sticky; top: 0; z-index: 10;
backdrop-filter: blur(24px);
background: rgba(10, 10, 20, 0.6);
border-bottom: 1px solid var(--border);
}
.nav-inner {
max-width: 1180px; margin: 0 auto;
padding: 18px 32px;
display: flex; align-items: center; gap: 40px;
}
.logo {
font-weight: 700; font-size: 22px;
letter-spacing: -0.02em;
background: linear-gradient(135deg, var(--purple), var(--pink));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.nav-links { display: flex; gap: 28px; font-size: 14px; }
.nav-links a { color: var(--text-muted); text-decoration: none; }
.nav-cta { margin-left: auto; display: flex; gap: 16px; align-items: center; }
.nav-cta a { font-size: 14px; color: var(--text-muted); text-decoration: none; }
.pill {
background: linear-gradient(135deg, var(--purple), var(--pink));
color: #fff;
padding: 9px 18px; border-radius: 999px;
font-weight: 600; font-size: 14px;
box-shadow: 0 0 32px rgba(139, 92, 246, 0.5);
}
/* Hero */
.hero {
padding: 140px 32px 160px;
text-align: center;
position: relative;
overflow: hidden;
}
.hero::before {
content: '';
position: absolute;
top: -200px; left: 50%;
transform: translateX(-50%);
width: 800px; height: 800px;
background: radial-gradient(circle, rgba(139, 92, 246, 0.4) 0%, transparent 70%);
filter: blur(80px);
z-index: 0;
}
.hero::after {
content: '';
position: absolute;
top: 100px; right: 5%;
width: 400px; height: 400px;
background: radial-gradient(circle, rgba(6, 182, 212, 0.3) 0%, transparent 70%);
filter: blur(80px);
z-index: 0;
}
.hero > * { position: relative; z-index: 1; }
.eyebrow {
display: inline-block;
padding: 6px 14px;
border-radius: 999px;
background: rgba(139, 92, 246, 0.15);
border: 1px solid rgba(139, 92, 246, 0.3);
color: var(--purple);
text-transform: uppercase;
letter-spacing: 0.12em;
font-size: 12px;
font-weight: 600;
margin-bottom: 32px;
}
.h1 {
font-weight: 800;
font-size: clamp(48px, 7vw, 88px);
line-height: 1.05;
letter-spacing: -0.03em;
max-width: 920px;
margin: 0 auto 28px;
background: linear-gradient(135deg, #ffffff 0%, #c4b5fd 50%, var(--pink) 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.lede {
font-size: 20px;
color: var(--text-muted);
max-width: 620px;
margin: 0 auto 44px;
}
.ctas { display: flex; gap: 16px; justify-content: center; margin-bottom: 48px; }
.btn {
padding: 14px 32px;
border-radius: 12px;
font-weight: 600;
font-size: 15px;
text-decoration: none;
display: inline-flex; align-items: center; gap: 8px;
transition: all 0.2s ease;
border: 0;
}
.btn-primary {
background: linear-gradient(135deg, var(--purple), var(--pink));
color: #fff;
box-shadow: 0 0 40px rgba(139, 92, 246, 0.5), 0 4px 16px rgba(139, 92, 246, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 0 60px rgba(139, 92, 246, 0.7);
}
.btn-ghost {
color: #fff;
border: 1px solid var(--border);
background: rgba(255, 255, 255, 0.04);
backdrop-filter: blur(12px);
}
.trust { font-size: 13px; color: var(--text-muted); }
.trust strong { color: #fff; font-weight: 600; }
/* Logo strip */
.logos {
padding: 56px 32px;
border-top: 1px solid var(--border);
border-bottom: 1px solid var(--border);
}
.logos-inner {
max-width: 1100px; margin: 0 auto;
display: flex; justify-content: space-between; align-items: center;
flex-wrap: wrap; gap: 32px;
font-weight: 600; font-size: 18px; color: var(--text-muted);
opacity: 0.6;
}
/* Features */
.features {
padding: 120px 32px;
max-width: 1180px;
margin: 0 auto;
position: relative;
}
.features-head {
text-align: center;
max-width: 720px;
margin: 0 auto 80px;
}
.features-head h2 {
font-weight: 800;
font-size: clamp(36px, 4.5vw, 52px);
line-height: 1.1;
letter-spacing: -0.03em;
margin: 0 0 20px;
background: linear-gradient(135deg, #fff, #c4b5fd);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.features-head p { font-size: 18px; color: var(--text-muted); margin: 0; }
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}
.card {
background: rgba(255, 255, 255, 0.03);
backdrop-filter: blur(24px);
border: 1px solid var(--border);
border-radius: 24px;
padding: 40px 32px;
text-align: center;
transition: all 0.3s ease;
}
.card:hover {
border-color: rgba(139, 92, 246, 0.3);
background: rgba(139, 92, 246, 0.04);
transform: translateY(-4px);
}
.icon {
width: 56px; height: 56px;
border-radius: 16px;
background: linear-gradient(135deg, var(--purple), var(--pink));
display: inline-flex; align-items: center; justify-content: center;
font-size: 28px;
margin-bottom: 24px;
box-shadow: 0 0 32px rgba(139, 92, 246, 0.4);
}
.card h3 {
font-weight: 700;
font-size: 22px;
margin: 0 0 12px;
letter-spacing: -0.02em;
color: #fff;
}
.card p {
font-size: 15px;
color: var(--text-muted);
margin: 0;
}
/* CTA */
.cta-section {
padding: 120px 32px;
text-align: center;
position: relative;
overflow: hidden;
}
.cta-section::before {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(ellipse at center, rgba(139, 92, 246, 0.2) 0%, transparent 70%);
}
.cta-section > * { position: relative; }
.cta-section h2 {
font-weight: 800;
font-size: clamp(36px, 5vw, 60px);
line-height: 1.1;
letter-spacing: -0.03em;
margin: 0 0 20px;
max-width: 720px;
margin-left: auto; margin-right: auto;
background: linear-gradient(135deg, #fff, #c4b5fd, var(--pink));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.cta-section p {
font-size: 18px;
color: var(--text-muted);
margin: 0 auto 40px;
max-width: 540px;
}
/* Footer */
.footer {
padding: 40px 32px;
text-align: center;
font-size: 13px;
color: var(--text-muted);
border-top: 1px solid var(--border);
}
</style>
</head>
<body>
<nav class="nav">
<div class="nav-inner">
<div class="logo">Lumina ✨</div>
<div class="nav-links">
<a href="#">Product</a>
<a href="#">Solutions</a>
<a href="#">Pricing</a>
<a href="#">Customers</a>
<a href="#">Docs</a>
</div>
<div class="nav-cta">
<a href="#">Sign in</a>
<a href="#" class="pill">Get started →</a>
</div>
</div>
</nav>
<section class="hero">
<div class="eyebrow">🚀 Powered by AI</div>
<h1 class="h1">Build faster with intelligent workflows</h1>
<p class="lede">The all-in-one platform that helps modern teams ship better products in record time. Powered by AI, designed for humans.</p>
<div class="ctas">
<a href="#" class="btn btn-primary">Join the waitlist →</a>
<a href="#" class="btn btn-ghost">Watch demo</a>
</div>
<div class="trust">Backed by <strong>top-tier investors</strong></div>
</section>
<section class="logos">
<div class="logos-inner">
<span>Northwind</span>
<span>Halcyon</span>
<span>Meridian</span>
<span>Fieldnote</span>
<span>Atlas &amp; Co.</span>
<span>Verity</span>
</div>
</section>
<section class="features">
<div class="features-head">
<h2>Everything you need to ship faster</h2>
<p>One platform. Endless possibilities. Built for the future.</p>
</div>
<div class="grid">
<div class="card">
<div class="icon"></div>
<h3>Lightning Fast</h3>
<p>Move from idea to production in minutes, not months. Our AI handles the heavy lifting so you can focus on what matters.</p>
</div>
<div class="card">
<div class="icon">🔒</div>
<h3>Enterprise Secure</h3>
<p>SOC 2 compliant infrastructure with end-to-end encryption. Your data is safe, always.</p>
</div>
<div class="card">
<div class="icon">📈</div>
<h3>Built to Scale</h3>
<p>From your first prototype to millions of users, Lumina grows with your team without breaking a sweat.</p>
</div>
</div>
</section>
<section class="cta-section">
<h2>Ready to transform how your team builds?</h2>
<p>Join thousands of teams already shipping faster with Lumina.</p>
<a href="#" class="btn btn-primary">Get early access →</a>
</section>
<footer class="footer">
© 2022 Lumina, Inc. Powered by AI in San Francisco.
</footer>
</body>
</html>
+309
View File
@@ -0,0 +1,309 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Lumina — Intelligent workflows for modern product teams</title>
<link rel="icon" href="data:," />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300;9..144,400;9..144,500;9..144,600;9..144,700&family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
<style>
:root {
--cream: #faf6ef;
--cream-warm: #f4ebdc;
--peach: #f6dfcb;
--ink: #1f1a15;
--soft: #5b4f44;
--line: #e6dccb;
--accent: #c8552b;
--accent-deep: #a8431f;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
font-family: 'Inter', system-ui, sans-serif;
color: var(--ink);
background: var(--cream);
-webkit-font-smoothing: antialiased;
line-height: 1.55;
}
.serif { font-family: 'Fraunces', Georgia, serif; font-optical-sizing: auto; }
/* Nav */
.nav {
position: sticky; top: 0; z-index: 10;
backdrop-filter: blur(10px);
background: rgba(250, 246, 239, 0.85);
border-bottom: 1px solid var(--line);
}
.nav-inner {
max-width: 1180px; margin: 0 auto;
padding: 18px 32px;
display: flex; align-items: center; gap: 40px;
}
.logo {
font-family: 'Fraunces', Georgia, serif;
font-weight: 600; font-size: 22px;
letter-spacing: -0.01em;
}
.logo-dot {
display: inline-block; width: 8px; height: 8px;
background: var(--accent); border-radius: 50%;
margin-right: 8px; vertical-align: middle;
}
.nav-links { display: flex; gap: 28px; font-size: 14px; }
.nav-links a { color: var(--soft); text-decoration: none; }
.nav-cta { margin-left: auto; display: flex; gap: 16px; align-items: center; }
.nav-cta a:not(.pill) { font-size: 14px; color: var(--soft); text-decoration: none; }
.pill {
background: var(--ink); color: var(--cream);
padding: 9px 18px; border-radius: 999px;
font-weight: 500; font-size: 14px;
}
/* Hero */
.hero {
background: linear-gradient(180deg, var(--cream) 0%, var(--peach) 100%);
padding: 120px 32px 140px;
text-align: center;
position: relative;
overflow: hidden;
}
.eyebrow {
text-transform: uppercase;
letter-spacing: 0.16em;
font-size: 12px;
font-weight: 600;
color: var(--accent-deep);
margin-bottom: 32px;
}
.h1 {
font-family: 'Fraunces', Georgia, serif;
font-weight: 400;
font-size: clamp(48px, 7vw, 88px);
line-height: 1.05;
letter-spacing: -0.02em;
max-width: 920px;
margin: 0 auto 28px;
}
.h1 em { font-style: italic; color: var(--accent-deep); }
.lede {
font-size: 20px;
color: var(--soft);
max-width: 620px;
margin: 0 auto 44px;
}
.ctas { display: flex; gap: 16px; justify-content: center; margin-bottom: 48px; }
.btn {
padding: 14px 28px;
border-radius: 999px;
font-weight: 500;
font-size: 15px;
text-decoration: none;
display: inline-flex; align-items: center; gap: 8px;
transition: transform 0.15s ease;
}
.btn-primary { background: var(--ink); color: var(--cream); }
.btn-ghost { color: var(--ink); border: 1px solid var(--ink); }
.btn:hover { transform: translateY(-1px); }
.trust { font-size: 13px; color: var(--soft); }
.trust strong { color: var(--ink); font-weight: 600; }
/* Logo strip */
.logos {
padding: 56px 32px;
border-top: 1px solid var(--line);
border-bottom: 1px solid var(--line);
background: var(--cream);
}
.logos-inner {
max-width: 1100px; margin: 0 auto;
display: flex; justify-content: space-between; align-items: center;
flex-wrap: wrap; gap: 32px;
font-family: 'Fraunces', Georgia, serif;
font-style: italic; font-size: 22px; color: var(--soft);
}
/* Features */
.features {
padding: 120px 32px;
max-width: 1180px;
margin: 0 auto;
}
.features-head {
text-align: center;
max-width: 720px;
margin: 0 auto 80px;
}
.features-head h2 {
font-family: 'Fraunces', Georgia, serif;
font-weight: 400;
font-size: clamp(36px, 4.5vw, 52px);
line-height: 1.1;
letter-spacing: -0.02em;
margin: 0 0 20px;
}
.features-head p { font-size: 18px; color: var(--soft); margin: 0; }
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 32px;
}
.card {
background: var(--cream-warm);
border: 1px solid var(--line);
border-radius: 20px;
padding: 40px 32px;
text-align: center;
}
.icon {
width: 56px; height: 56px;
border-radius: 14px;
background: var(--cream);
border: 1px solid var(--line);
display: inline-flex; align-items: center; justify-content: center;
font-size: 28px;
margin-bottom: 24px;
}
.card h3 {
font-family: 'Fraunces', Georgia, serif;
font-weight: 500;
font-size: 22px;
margin: 0 0 12px;
letter-spacing: -0.01em;
}
.card p {
font-size: 15px;
color: var(--soft);
margin: 0;
}
/* CTA */
.cta-section {
background: var(--ink);
color: var(--cream);
padding: 120px 32px;
text-align: center;
}
.cta-section h2 {
font-family: 'Fraunces', Georgia, serif;
font-weight: 400;
font-size: clamp(36px, 5vw, 60px);
line-height: 1.1;
letter-spacing: -0.02em;
margin: 0 0 20px;
max-width: 720px;
margin-left: auto; margin-right: auto;
}
.cta-section p {
font-size: 18px;
color: rgba(250, 246, 239, 0.7);
margin: 0 auto 40px;
max-width: 540px;
}
.cta-section .btn-primary {
background: var(--cream);
color: var(--ink);
}
/* Footer */
.footer {
padding: 40px 32px;
text-align: center;
font-size: 13px;
color: var(--soft);
border-top: 1px solid var(--line);
background: var(--cream);
}
</style>
</head>
<body>
<nav class="nav">
<div class="nav-inner">
<div class="logo"><span class="logo-dot"></span>Lumina</div>
<div class="nav-links">
<a href="#">Product</a>
<a href="#">Solutions</a>
<a href="#">Pricing</a>
<a href="#">Customers</a>
<a href="#">Docs</a>
</div>
<div class="nav-cta">
<a href="#">Sign in</a>
<a href="#" class="pill">Get started</a>
</div>
</div>
</nav>
<section class="hero">
<div class="eyebrow">AI-native workflows</div>
<h1 class="h1">Build faster with <em>intelligent</em> workflows for modern teams</h1>
<p class="lede">The all-in-one platform that helps product teams ship better software in record time. Powered by AI, designed for humans.</p>
<div class="ctas">
<a href="#" class="btn btn-primary">Start free trial →</a>
<a href="#" class="btn btn-ghost">Watch demo</a>
</div>
<div class="trust">Trusted by <strong>10,000+</strong> forward-thinking teams worldwide</div>
</section>
<section class="logos">
<div class="logos-inner">
<span>Northwind</span>
<span>Halcyon</span>
<span>Meridian</span>
<span>Fieldnote</span>
<span>Atlas &amp; Co.</span>
<span>Verity</span>
</div>
</section>
<section class="features">
<div class="features-head">
<h2>Everything you need to ship great products</h2>
<p>One platform. Endless possibilities. Built for teams who care about craft.</p>
</div>
<div class="grid">
<div class="card">
<div class="icon"></div>
<h3>Lightning Fast</h3>
<p>Move from idea to production in minutes, not months. Our AI handles the heavy lifting so you can focus on what matters.</p>
</div>
<div class="card">
<div class="icon">🔒</div>
<h3>Enterprise Secure</h3>
<p>SOC 2 compliant infrastructure with end-to-end encryption. Your data is safe, always.</p>
</div>
<div class="card">
<div class="icon">📈</div>
<h3>Built to Scale</h3>
<p>From your first prototype to millions of users — Lumina grows with your team without breaking a sweat.</p>
</div>
</div>
</section>
<section class="cta-section">
<h2>Ready to transform how your team builds?</h2>
<p>Join thousands of teams already shipping faster with Lumina.</p>
<a href="#" class="btn btn-primary">Start your free trial →</a>
</section>
<footer class="footer">
© 2026 Lumina, Inc. — Crafted with care in San Francisco.
</footer>
</body>
</html>
+14
View File
@@ -0,0 +1,14 @@
{
"name": "landing-demo",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^8.0.0"
}
}
+7 -3
View File
@@ -41,13 +41,15 @@
"LICENSE"
],
"scripts": {
"build": "bun run scripts/build.js",
"build:skills": "bun run scripts/build.js",
"build:site": "npx astro build",
"build": "bun run build:skills && bun run build:site && cp -R dist build/_data/dist",
"build:browser": "node scripts/build-browser-detector.js",
"build:extension": "node scripts/build-extension.js",
"clean": "rm -rf dist build",
"rebuild": "bun run clean && bun run build",
"dev": "bun run server/index.js",
"preview": "bun run build && wrangler pages dev",
"dev": "npx astro dev",
"preview": "bun run build && npx astro preview",
"deploy": "bun run build && wrangler pages deploy build/",
"test": "bun test tests/build.test.js tests/detect-antipatterns.test.js tests/windows-path-fix.test.js && node --test tests/detect-antipatterns-fixtures.test.mjs && node --test tests/detect-antipatterns-browser.test.mjs && node --test tests/cleanup-deprecated.test.mjs && node --test tests/live-wrap.test.mjs && node --test tests/live-accept.test.mjs && node --test tests/live-inject.test.mjs && node --test tests/live-server.test.mjs && node --test tests/live-browser-regression.test.mjs && node --test tests/framework-fixtures.test.mjs",
"test:live-e2e": "node --test --test-timeout=600000 tests/live-e2e.test.mjs",
@@ -67,6 +69,8 @@
"puppeteer": "^24.42.0"
},
"devDependencies": {
"astro": "^6.2.1",
"@paper-design/shaders": "^0.0.76",
"@ai-sdk/anthropic": "^3.0.71",
"@ai-sdk/openai": "^3.0.53",
"@anthropic-ai/claude-agent-sdk": "^0.2.119",
+2327 -7
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,310 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Lumina — Intelligent workflows for modern product teams</title>
<link rel="icon" href="data:," />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300;9..144,400;9..144,500;9..144,600;9..144,700&family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
<style>
:root {
--cream: #faf6ef;
--cream-warm: #f4ebdc;
--peach: #f6dfcb;
--ink: #1f1a15;
--soft: #5b4f44;
--line: #e6dccb;
--accent: #c8552b;
--accent-deep: #a8431f;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
font-family: 'Inter', system-ui, sans-serif;
color: var(--ink);
background: var(--cream);
-webkit-font-smoothing: antialiased;
line-height: 1.55;
}
.serif { font-family: 'Fraunces', Georgia, serif; font-optical-sizing: auto; }
/* Nav */
.nav {
position: sticky; top: 0; z-index: 10;
backdrop-filter: blur(10px);
background: rgba(250, 246, 239, 0.85);
border-bottom: 1px solid var(--line);
}
.nav-inner {
max-width: 1180px; margin: 0 auto;
padding: 18px 32px;
display: flex; align-items: center; gap: 40px;
}
.logo {
font-family: 'Fraunces', Georgia, serif;
font-weight: 600; font-size: 22px;
letter-spacing: -0.01em;
}
.logo-dot {
display: inline-block; width: 8px; height: 8px;
background: var(--accent); border-radius: 50%;
margin-right: 8px; vertical-align: middle;
}
.nav-links { display: flex; gap: 28px; font-size: 14px; }
.nav-links a { color: var(--soft); text-decoration: none; }
.nav-cta { margin-left: auto; display: flex; gap: 16px; align-items: center; }
.nav-cta a:not(.pill) { font-size: 14px; color: var(--soft); text-decoration: none; }
.pill {
background: var(--ink); color: var(--cream);
padding: 9px 18px; border-radius: 999px;
font-weight: 500; font-size: 14px;
}
/* Hero */
.hero {
background: linear-gradient(180deg, var(--cream) 0%, var(--peach) 100%);
padding: 120px 32px 140px;
text-align: center;
position: relative;
overflow: hidden;
}
.eyebrow {
text-transform: uppercase;
letter-spacing: 0.16em;
font-size: 12px;
font-weight: 600;
color: var(--accent-deep);
margin-bottom: 32px;
}
.h1 {
font-family: 'Fraunces', Georgia, serif;
font-weight: 400;
font-size: clamp(48px, 7vw, 88px);
line-height: 1.05;
letter-spacing: -0.02em;
max-width: 920px;
margin: 0 auto 28px;
}
.h1 em { font-style: italic; color: var(--accent-deep); }
.lede {
font-size: 20px;
color: var(--soft);
max-width: 620px;
margin: 0 auto 44px;
}
.ctas { display: flex; gap: 16px; justify-content: center; margin-bottom: 48px; }
.btn {
padding: 14px 28px;
border-radius: 999px;
font-weight: 500;
font-size: 15px;
text-decoration: none;
display: inline-flex; align-items: center; gap: 8px;
transition: transform 0.15s ease;
}
.btn-primary { background: var(--ink); color: var(--cream); }
.btn-ghost { color: var(--ink); border: 1px solid var(--ink); }
.btn:hover { transform: translateY(-1px); }
.trust { font-size: 13px; color: var(--soft); }
.trust strong { color: var(--ink); font-weight: 600; }
/* Logo strip */
.logos {
padding: 56px 32px;
border-top: 1px solid var(--line);
border-bottom: 1px solid var(--line);
background: var(--cream);
}
.logos-inner {
max-width: 1100px; margin: 0 auto;
display: flex; justify-content: space-between; align-items: center;
flex-wrap: wrap; gap: 32px;
font-family: 'Fraunces', Georgia, serif;
font-style: italic; font-size: 22px; color: var(--soft);
}
/* Features */
.features {
padding: 120px 32px;
max-width: 1180px;
margin: 0 auto;
}
.features-head {
text-align: center;
max-width: 720px;
margin: 0 auto 80px;
}
.features-head h2 {
font-family: 'Fraunces', Georgia, serif;
font-weight: 400;
font-size: clamp(36px, 4.5vw, 52px);
line-height: 1.1;
letter-spacing: -0.02em;
margin: 0 0 20px;
}
.features-head p { font-size: 18px; color: var(--soft); margin: 0; }
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 32px;
}
.card {
background: var(--cream-warm);
border: 1px solid var(--line);
border-radius: 20px;
padding: 40px 32px;
text-align: center;
}
.icon {
width: 56px; height: 56px;
border-radius: 14px;
background: var(--cream);
border: 1px solid var(--line);
display: inline-flex; align-items: center; justify-content: center;
font-size: 28px;
margin-bottom: 24px;
}
.card h3 {
font-family: 'Fraunces', Georgia, serif;
font-weight: 500;
font-size: 22px;
margin: 0 0 12px;
letter-spacing: -0.01em;
}
.card p {
font-size: 15px;
color: var(--soft);
margin: 0;
}
/* CTA */
.cta-section {
background: var(--ink);
color: var(--cream);
padding: 120px 32px;
text-align: center;
}
.cta-section h2 {
font-family: 'Fraunces', Georgia, serif;
font-weight: 400;
font-size: clamp(36px, 5vw, 60px);
line-height: 1.1;
letter-spacing: -0.02em;
margin: 0 0 20px;
max-width: 720px;
margin-left: auto; margin-right: auto;
}
.cta-section p {
font-size: 18px;
color: rgba(250, 246, 239, 0.7);
margin: 0 auto 40px;
max-width: 540px;
}
.cta-section .btn-primary {
background: var(--cream);
color: var(--ink);
}
/* Footer */
.footer {
padding: 40px 32px;
text-align: center;
font-size: 13px;
color: var(--soft);
border-top: 1px solid var(--line);
background: var(--cream);
}
</style>
</head>
<body>
<nav class="nav">
<div class="nav-inner">
<div class="logo"><span class="logo-dot"></span>Lumina</div>
<div class="nav-links">
<a href="#">Product</a>
<a href="#">Solutions</a>
<a href="#">Pricing</a>
<a href="#">Customers</a>
<a href="#">Docs</a>
</div>
<div class="nav-cta">
<a href="#">Sign in</a>
<a href="#" class="pill">Get started</a>
</div>
</div>
</nav>
<section class="hero">
<div class="eyebrow">AI-native workflows</div>
<h1 class="h1">Build faster with <em>intelligent</em> workflows for modern teams</h1>
<p class="lede">The all-in-one platform that helps product teams ship better software in record time. Powered by AI, designed for humans.</p>
<div class="ctas">
<a href="#" class="btn btn-primary">Start free trial →</a>
<a href="#" class="btn btn-ghost">Watch demo</a>
</div>
<div class="trust">Trusted by <strong>10,000+</strong> forward-thinking teams worldwide</div>
</section>
<section class="logos">
<div class="logos-inner">
<span>Northwind</span>
<span>Halcyon</span>
<span>Meridian</span>
<span>Fieldnote</span>
<span>Atlas &amp; Co.</span>
<span>Verity</span>
</div>
</section>
<section class="features">
<div class="features-head">
<h2>Everything you need to ship great products</h2>
<p>One platform. Endless possibilities. Built for teams who care about craft.</p>
</div>
<div class="grid">
<div class="card">
<div class="icon"></div>
<h3>Lightning Fast</h3>
<p>Move from idea to production in minutes, not months. Our AI handles the heavy lifting so you can focus on what matters.</p>
</div>
<div class="card">
<div class="icon">🔒</div>
<h3>Enterprise Secure</h3>
<p>SOC 2 compliant infrastructure with end-to-end encryption. Your data is safe, always.</p>
</div>
<div class="card">
<div class="icon">📈</div>
<h3>Built to Scale</h3>
<p>From your first prototype to millions of users — Lumina grows with your team without breaking a sweat.</p>
</div>
</div>
</section>
<section class="cta-section">
<h2>Ready to transform how your team builds?</h2>
<p>Join thousands of teams already shipping faster with Lumina.</p>
<a href="#" class="btn btn-primary">Start your free trial →</a>
</section>
<footer class="footer">
© 2026 Lumina, Inc. — Crafted with care in San Francisco.
</footer>
<script src="/js/detect-antipatterns-browser.js"></script>
</body>
</html>
@@ -0,0 +1,351 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Lumina — The future of intelligent workflows</title>
<link rel="icon" href="data:," />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<style>
:root {
--bg: #0a0a14;
--bg-elev: #14141f;
--text: #ffffff;
--text-muted: rgba(255, 255, 255, 0.6);
--border: rgba(255, 255, 255, 0.08);
--purple: #8b5cf6;
--violet: #7c3aed;
--pink: #ec4899;
--cyan: #06b6d4;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
font-family: 'Inter', system-ui, sans-serif;
color: var(--text);
background: var(--bg);
-webkit-font-smoothing: antialiased;
line-height: 1.6;
overflow-x: hidden;
}
/* Nav */
.nav {
position: sticky; top: 0; z-index: 10;
backdrop-filter: blur(24px);
background: rgba(10, 10, 20, 0.6);
border-bottom: 1px solid var(--border);
}
.nav-inner {
max-width: 1180px; margin: 0 auto;
padding: 18px 32px;
display: flex; align-items: center; gap: 40px;
}
.logo {
font-weight: 700; font-size: 22px;
letter-spacing: -0.02em;
background: linear-gradient(135deg, var(--purple), var(--pink));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.nav-links { display: flex; gap: 28px; font-size: 14px; }
.nav-links a { color: var(--text-muted); text-decoration: none; }
.nav-cta { margin-left: auto; display: flex; gap: 16px; align-items: center; }
.nav-cta a { font-size: 14px; color: var(--text-muted); text-decoration: none; }
.pill {
background: linear-gradient(135deg, var(--purple), var(--pink));
color: #fff;
padding: 9px 18px; border-radius: 999px;
font-weight: 600; font-size: 14px;
box-shadow: 0 0 32px rgba(139, 92, 246, 0.5);
}
/* Hero */
.hero {
padding: 140px 32px 160px;
text-align: center;
position: relative;
overflow: hidden;
}
.hero::before {
content: '';
position: absolute;
top: -200px; left: 50%;
transform: translateX(-50%);
width: 800px; height: 800px;
background: radial-gradient(circle, rgba(139, 92, 246, 0.4) 0%, transparent 70%);
filter: blur(80px);
z-index: 0;
}
.hero::after {
content: '';
position: absolute;
top: 100px; right: 5%;
width: 400px; height: 400px;
background: radial-gradient(circle, rgba(6, 182, 212, 0.3) 0%, transparent 70%);
filter: blur(80px);
z-index: 0;
}
.hero > * { position: relative; z-index: 1; }
.eyebrow {
display: inline-block;
padding: 6px 14px;
border-radius: 999px;
background: rgba(139, 92, 246, 0.15);
border: 1px solid rgba(139, 92, 246, 0.3);
color: var(--purple);
text-transform: uppercase;
letter-spacing: 0.12em;
font-size: 12px;
font-weight: 600;
margin-bottom: 32px;
}
.h1 {
font-weight: 800;
font-size: clamp(48px, 7vw, 88px);
line-height: 1.05;
letter-spacing: -0.03em;
max-width: 920px;
margin: 0 auto 28px;
background: linear-gradient(135deg, #ffffff 0%, #c4b5fd 50%, var(--pink) 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.lede {
font-size: 20px;
color: var(--text-muted);
max-width: 620px;
margin: 0 auto 44px;
}
.ctas { display: flex; gap: 16px; justify-content: center; margin-bottom: 48px; }
.btn {
padding: 14px 32px;
border-radius: 12px;
font-weight: 600;
font-size: 15px;
text-decoration: none;
display: inline-flex; align-items: center; gap: 8px;
transition: all 0.2s ease;
border: 0;
}
.btn-primary {
background: linear-gradient(135deg, var(--purple), var(--pink));
color: #fff;
box-shadow: 0 0 40px rgba(139, 92, 246, 0.5), 0 4px 16px rgba(139, 92, 246, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 0 60px rgba(139, 92, 246, 0.7);
}
.btn-ghost {
color: #fff;
border: 1px solid var(--border);
background: rgba(255, 255, 255, 0.04);
backdrop-filter: blur(12px);
}
.trust { font-size: 13px; color: var(--text-muted); }
.trust strong { color: #fff; font-weight: 600; }
/* Logo strip */
.logos {
padding: 56px 32px;
border-top: 1px solid var(--border);
border-bottom: 1px solid var(--border);
}
.logos-inner {
max-width: 1100px; margin: 0 auto;
display: flex; justify-content: space-between; align-items: center;
flex-wrap: wrap; gap: 32px;
font-weight: 600; font-size: 18px; color: var(--text-muted);
opacity: 0.6;
}
/* Features */
.features {
padding: 120px 32px;
max-width: 1180px;
margin: 0 auto;
position: relative;
}
.features-head {
text-align: center;
max-width: 720px;
margin: 0 auto 80px;
}
.features-head h2 {
font-weight: 800;
font-size: clamp(36px, 4.5vw, 52px);
line-height: 1.1;
letter-spacing: -0.03em;
margin: 0 0 20px;
background: linear-gradient(135deg, #fff, #c4b5fd);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.features-head p { font-size: 18px; color: var(--text-muted); margin: 0; }
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}
.card {
background: rgba(255, 255, 255, 0.03);
backdrop-filter: blur(24px);
border: 1px solid var(--border);
border-radius: 24px;
padding: 40px 32px;
text-align: center;
transition: all 0.3s ease;
}
.card:hover {
border-color: rgba(139, 92, 246, 0.3);
background: rgba(139, 92, 246, 0.04);
transform: translateY(-4px);
}
.icon {
width: 56px; height: 56px;
border-radius: 16px;
background: linear-gradient(135deg, var(--purple), var(--pink));
display: inline-flex; align-items: center; justify-content: center;
font-size: 28px;
margin-bottom: 24px;
box-shadow: 0 0 32px rgba(139, 92, 246, 0.4);
}
.card h3 {
font-weight: 700;
font-size: 22px;
margin: 0 0 12px;
letter-spacing: -0.02em;
color: #fff;
}
.card p {
font-size: 15px;
color: var(--text-muted);
margin: 0;
}
/* CTA */
.cta-section {
padding: 120px 32px;
text-align: center;
position: relative;
overflow: hidden;
}
.cta-section::before {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(ellipse at center, rgba(139, 92, 246, 0.2) 0%, transparent 70%);
}
.cta-section > * { position: relative; }
.cta-section h2 {
font-weight: 800;
font-size: clamp(36px, 5vw, 60px);
line-height: 1.1;
letter-spacing: -0.03em;
margin: 0 0 20px;
max-width: 720px;
margin-left: auto; margin-right: auto;
background: linear-gradient(135deg, #fff, #c4b5fd, var(--pink));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.cta-section p {
font-size: 18px;
color: var(--text-muted);
margin: 0 auto 40px;
max-width: 540px;
}
/* Footer */
.footer {
padding: 40px 32px;
text-align: center;
font-size: 13px;
color: var(--text-muted);
border-top: 1px solid var(--border);
}
</style>
</head>
<body>
<nav class="nav">
<div class="nav-inner">
<div class="logo">Lumina ✨</div>
<div class="nav-links">
<a href="#">Product</a>
<a href="#">Solutions</a>
<a href="#">Pricing</a>
<a href="#">Customers</a>
<a href="#">Docs</a>
</div>
<div class="nav-cta">
<a href="#">Sign in</a>
<a href="#" class="pill">Get started →</a>
</div>
</div>
</nav>
<section class="hero">
<div class="eyebrow">🚀 Powered by AI</div>
<h1 class="h1">Build faster with intelligent workflows</h1>
<p class="lede">The all-in-one platform that helps modern teams ship better products in record time. Powered by AI, designed for humans.</p>
<div class="ctas">
<a href="#" class="btn btn-primary">Join the waitlist →</a>
<a href="#" class="btn btn-ghost">Watch demo</a>
</div>
<div class="trust">Backed by <strong>top-tier investors</strong></div>
</section>
<section class="logos">
<div class="logos-inner">
<span>Northwind</span>
<span>Halcyon</span>
<span>Meridian</span>
<span>Fieldnote</span>
<span>Atlas &amp; Co.</span>
<span>Verity</span>
</div>
</section>
<section class="features">
<div class="features-head">
<h2>Everything you need to ship faster</h2>
<p>One platform. Endless possibilities. Built for the future.</p>
</div>
<div class="grid">
<div class="card">
<div class="icon"></div>
<h3>Lightning Fast</h3>
<p>Move from idea to production in minutes, not months. Our AI handles the heavy lifting so you can focus on what matters.</p>
</div>
<div class="card">
<div class="icon">🔒</div>
<h3>Enterprise Secure</h3>
<p>SOC 2 compliant infrastructure with end-to-end encryption. Your data is safe, always.</p>
</div>
<div class="card">
<div class="icon">📈</div>
<h3>Built to Scale</h3>
<p>From your first prototype to millions of users, Lumina grows with your team without breaking a sweat.</p>
</div>
</div>
</section>
<section class="cta-section">
<h2>Ready to transform how your team builds?</h2>
<p>Join thousands of teams already shipping faster with Lumina.</p>
<a href="#" class="btn btn-primary">Get early access →</a>
</section>
<footer class="footer">
© 2022 Lumina, Inc. Powered by AI in San Francisco.
</footer>
<script src="/js/detect-antipatterns-browser.js"></script>
</body>
</html>
-133
View File
@@ -1,133 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Neo Mirai case study | Impeccable</title>
<meta name="description" content="How Impeccable turned generated brand and hi-fi references into the shipped Neo Mirai conference website.">
<meta name="theme-color" content="#fafafa">
<link rel="canonical" href="https://impeccable.style/cases/neo-mirai">
<link rel="icon" type="image/svg+xml" href="../../favicon.svg">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,600;1,400&family=Instrument+Sans:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../../css/sub-pages.css">
</head>
<body class="sub-page neon-case-page">
<a href="#main" class="skip-link">Skip to content</a>
<header class="site-header" data-site-header>
<a href="/" class="site-header-brand" aria-label="Impeccable home">
<svg class="site-header-brand-logo" viewBox="0 0 32 32" aria-hidden="true"><rect width="32" height="32" rx="6" fill="#1a1a1a"/><text x="16" y="24" font-family="system-ui, -apple-system, sans-serif" font-size="22" font-weight="500" fill="#f5f3ef" text-anchor="middle">/</text></svg>
<span class="site-header-brand-name">Impeccable</span>
</a>
<div class="site-header-right">
<nav class="site-header-nav" aria-label="Primary">
<a href="/" data-nav="home">Home</a>
<a href="/designing" data-nav="designing">Designing</a>
<a href="/docs" data-nav="docs">Docs</a>
<a href="/slop" data-nav="slop">Slop</a>
<a href="/live-mode" data-nav="live">Live</a>
</nav>
<a href="https://github.com/pbakaus/impeccable" class="site-header-github" target="_blank" rel="noopener" aria-label="Impeccable on GitHub, 23k stars">
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/></svg>
<span class="site-header-github-label">23k</span>
<svg class="site-header-github-star" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2l2.76 6.36L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l7.24-.91L12 2z"/></svg>
</a>
</div>
</header>
<main id="main" class="neon-case">
<section class="neon-case-hero">
<div class="neon-case-hero-copy">
<a class="neon-case-back" href="/designing#start">Designing with Impeccable</a>
<span class="neon-case-eyebrow">Case study</span>
<h1>Neo Mirai: generated mock to shipped page.</h1>
<p>A retro-futurist AI design conference became a real static site through the full Impeccable loop: visual reference, brand direction, implementation, asset regeneration, responsive fixes, animation polish, and browser verification.</p>
<div class="neon-case-actions">
<a class="neon-case-primary" href="/neo-mirai/">Open the live build</a>
<a class="neon-case-secondary" href="/docs/craft">Read the craft docs</a>
</div>
</div>
<a class="neon-case-hero-shot" href="/neo-mirai/" aria-label="Open the Neo Mirai live build">
<img src="../../assets/cases/neo-mirai/live-fold.png" alt="The shipped Neo Mirai conference website hero and agenda, showing a warm retro-futurist Tokyo skyline and large NEO MIRAI headline." width="1440" height="1100">
</a>
</section>
<section class="neon-case-strip" aria-label="From generated references to shipped website">
<figure>
<span>01 &middot; Visualize</span>
<img src="../../assets/openai_image_2_hifi.jpg" alt="Generated hi-fi website mock for the Neo Mirai conference page." loading="lazy" width="864" height="1821">
<figcaption>Hi-fi north star. A concrete composition to build toward, not a paragraph to interpret.</figcaption>
</figure>
<figure>
<span>02 &middot; Shape</span>
<img src="../../assets/openai_image_2_brand.jpg" alt="Generated Neo Mirai brand toolkit plate with palette, typography, symbols, and application mockups." loading="lazy" width="1536" height="1024">
<figcaption>Brand toolkit. Identity, palette, type, image language, and motion direction before code.</figcaption>
</figure>
<figure>
<span>03 &middot; Ship</span>
<a href="/neo-mirai/" aria-label="Open the Neo Mirai live build">
<img src="../../assets/cases/neo-mirai/live-page.png" alt="Full-page screenshot of the implemented Neo Mirai website." loading="lazy" width="1440" height="3013">
</a>
<figcaption>Implemented page. Semantic HTML, responsive layout, regenerated assets, real states, and polish.</figcaption>
</figure>
</section>
<section class="neon-case-body">
<div class="neon-case-column">
<span class="neon-case-section-label">What changed</span>
<h2>The mock did not become a screenshot. It became a system.</h2>
</div>
<div class="neon-case-notes">
<article>
<h3>Composition matching</h3>
<p>The build preserved the mock's asymmetric rhythm: full-bleed hero artwork, dark agenda block, orange manifesto band, drifting installation grid, and structured ticket field.</p>
</article>
<article>
<h3>Asset regeneration</h3>
<p>Image-native pieces stayed image-native. The manifesto city, speaker portraits, pine overlay, and supporting illustrations were regenerated or isolated where raster detail mattered.</p>
</article>
<article>
<h3>Browser iteration</h3>
<p>The page was tested in the browser after each pass. Overlaps, bad crops, active nav state, speaker carousel behavior, mobile heights, and large-viewport balance were fixed visually.</p>
</article>
</div>
</section>
<section class="neon-case-details" aria-label="Mock and live page comparison">
<figure>
<img src="../../assets/openai_image_2_hifi.jpg" alt="Generated hi-fi Neo Mirai page mock." loading="lazy" width="864" height="1821">
<figcaption><span>North-star mock</span> The reference image preserved the intended rhythm: full-bleed hero, dark agenda, speaker carousel, installation field, manifesto, and tickets.</figcaption>
</figure>
<figure>
<a href="/neo-mirai/" aria-label="Open the Neo Mirai live build">
<img src="../../assets/cases/neo-mirai/live-page.png" alt="Full-page screenshot of the implemented Neo Mirai website." loading="lazy" width="1440" height="3013">
</a>
<figcaption><span>Live build</span> The shipped page keeps the same visual ambition while becoming responsive markup, real links, carousel controls, hover states, and browser-tested layout.</figcaption>
</figure>
</section>
<section class="neon-case-command">
<div>
<span class="neon-case-section-label">Reproduce the loop</span>
<h2>Use craft when the output has to feel designed.</h2>
<p><code>/impeccable craft</code> is the right command when a feature needs shaping, visual direction, implementation, and browser iteration in one run.</p>
</div>
<div class="code-block-wrap"><pre class="code-block"><code>/impeccable craft retro-futurist AI design conference website</code></pre><button class="code-block-copy" type="button" data-copy="/impeccable craft retro-futurist AI design conference website" aria-label="Copy to clipboard"></button></div>
</section>
</main>
<script>
document.addEventListener('click', (event) => {
const button = event.target.closest('[data-copy]');
if (!button) return;
const text = button.getAttribute('data-copy');
if (!text) return;
navigator.clipboard.writeText(text).then(() => {
button.classList.add('is-copied');
setTimeout(() => button.classList.remove('is-copied'), 1500);
}).catch(() => {});
});
</script>
</body>
</html>
-396
View File
@@ -1,396 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Live Mode | Impeccable</title>
<meta name="description" content="Iterate on UI in the browser. Pick an element, drop a comment, get three production-quality variants, accept one, and it writes back to source. /impeccable live.">
<meta name="theme-color" content="#fafafa">
<link rel="canonical" href="https://impeccable.style/live-mode">
<link rel="icon" type="image/svg+xml" href="../favicon.svg">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,600;1,400&family=Instrument+Sans:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../css/sub-pages.css">
<link rel="stylesheet" href="../css/live-mode.css">
<script type="module">
import { initLiveDemo } from "../js/components/live-demo.js";
document.addEventListener("DOMContentLoaded", initLiveDemo);
</script>
</head>
<body class="sub-page live-mode-page-body">
<a href="#main" class="skip-link">Skip to content</a>
<!-- site-header v1 -->
<header class="site-header" data-site-header>
<a href="/" class="site-header-brand" aria-label="Impeccable home">
<svg class="site-header-brand-logo" viewBox="0 0 32 32" aria-hidden="true"><rect width="32" height="32" rx="6" fill="#1a1a1a"/><text x="16" y="24" font-family="system-ui, -apple-system, sans-serif" font-size="22" font-weight="500" fill="#f5f3ef" text-anchor="middle">/</text></svg>
<span class="site-header-brand-name">Impeccable</span>
</a>
<div class="site-header-right">
<nav class="site-header-nav" aria-label="Primary">
<a href="/" data-nav="home">Home</a>
<a href="/designing" data-nav="designing">Designing</a>
<a href="/docs" data-nav="docs">Docs</a>
<a href="/slop" data-nav="slop">Slop</a>
<a href="/live-mode" data-nav="live" aria-current="page">Live</a>
</nav>
<a href="https://github.com/pbakaus/impeccable" class="site-header-github" target="_blank" rel="noopener" aria-label="Impeccable on GitHub, 23k stars">
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/></svg>
<span class="site-header-github-label">23k</span>
<svg class="site-header-github-star" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2l2.76 6.36L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l7.24-.91L12 2z"/></svg>
</a>
</div>
</header>
<main id="main">
<div class="live-mode-page">
<header class="live-mode-page-header">
<p class="live-mode-page-eyebrow">New in v3.0 <span class="live-mode-page-eyebrow-badge">Alpha</span></p>
<h1 class="live-mode-page-title">Live Mode</h1>
<p class="live-mode-page-lede">Pick any element in the browser. Drop a comment or a stroke. Three production-quality variants swap in via your framework's HMR. Accept the one you want and it writes back to source.</p>
<p class="live-mode-page-alpha-note"><strong>Why alpha:</strong> Live Mode works end-to-end and is ready to try, but it still needs more testing against real-world repos and framework configs. Expect rough edges on uncommon setups, and please report what breaks.</p>
<div class="live-mode-start" aria-label="Start live mode command">
<span class="live-mode-start-prompt">$</span>
<code class="live-mode-start-cmd">/impeccable live</code>
<button class="live-mode-start-copy" type="button" aria-label="Copy command" data-copy="/impeccable live">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"></path>
</svg>
</button>
</div>
</header>
<section class="live-mode-demo-wrap" aria-label="Live Mode interactive demo">
<div class="live-demo" id="live-demo" aria-label="Live Mode interactive demo loop">
<div class="live-demo-frame-col"><div class="live-demo-frame">
<div class="live-demo-chrome">
<span class="live-demo-dot"></span>
<span class="live-demo-dot"></span>
<span class="live-demo-dot"></span>
<span class="live-demo-url">localhost:3000</span>
</div>
<div class="live-demo-stage">
<div class="live-demo-skeleton" aria-hidden="true">
<div class="live-demo-skel-nav">
<span class="live-demo-skel-logo"></span>
<span class="live-demo-skel-link"></span>
<span class="live-demo-skel-link"></span>
<span class="live-demo-skel-link"></span>
<span class="live-demo-skel-cta"></span>
</div>
<div class="live-demo-skel-heading"></div>
<div class="live-demo-skel-line"></div>
<div class="live-demo-skel-line live-demo-skel-line--short"></div>
</div>
<div class="live-demo-target" data-demo-target>
<div class="live-demo-variant is-active" data-variant="original">
<div class="live-demo-card live-demo-card--plain">
<span class="live-demo-card-kicker">Newsletter</span>
<h3>Subscribe for updates</h3>
<p>Monthly-ish design notes.</p>
<button type="button">Subscribe</button>
</div>
</div>
<div class="live-demo-variant" data-variant="1">
<div class="live-demo-card live-demo-card--v1">
<span class="live-demo-card-kicker">No. 04</span>
<h3>Letters, <em>occasionally</em>.</h3>
<p>A postcard from the editor, about once a month. No tracking pixels, no "just checking in."</p>
<button type="button">Send me one</button>
</div>
</div>
<div class="live-demo-variant" data-variant="2">
<div class="live-demo-card live-demo-card--v2">
<div class="live-demo-card-stamp"></div>
<span class="live-demo-card-kicker">Dispatch</span>
<h3>Design&nbsp;notes, <br>every&nbsp;other<br>Thursday.</h3>
<button type="button">Join the list →</button>
</div>
</div>
<div class="live-demo-variant" data-variant="3">
<div class="live-demo-card live-demo-card--v3">
<div class="live-demo-card-sticker"><span>&star;</span><span>&star;</span><span>&star;</span></div>
<span class="live-demo-card-kicker">Field Notes</span>
<h3>A monthly letter, for people who still read email for pleasure.</h3>
<button type="button">Receive the letter <span aria-hidden="true"></span></button>
</div>
</div>
</div>
<div class="live-demo-outline" data-demo-outline aria-hidden="true"></div>
<div class="live-demo-annotations" data-demo-annotations aria-hidden="true">
<svg class="live-demo-stroke" viewBox="0 0 300 60" preserveAspectRatio="none" aria-hidden="true">
<path d="M 10,40 Q 60,10 110,38 T 210,32 T 290,20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" pathLength="1"/>
</svg>
<div class="live-demo-comment">more playful</div>
</div>
<div class="live-demo-ctx" data-demo-ctx data-phase="hidden">
<div class="live-demo-ctx-row live-demo-ctx-row--configure">
<button type="button" class="live-demo-ctx-pill" data-demo-ctx-pill>
<span data-demo-cmd-name>delight</span>
<span class="live-demo-ctx-pill-caret" aria-hidden="true"></span>
</button>
<span class="live-demo-ctx-input" data-demo-input>
<span data-demo-input-text></span><span class="live-demo-ctx-caret"></span>
</span>
<button type="button" class="live-demo-ctx-count">×3</button>
<button type="button" class="live-demo-ctx-go" data-demo-go>Go <span aria-hidden="true"></span></button>
</div>
<div class="live-demo-ctx-row live-demo-ctx-row--generating">
<span class="live-demo-ctx-spinner" aria-hidden="true"></span>
<span>Generating variants…</span>
</div>
<div class="live-demo-ctx-row live-demo-ctx-row--cycling">
<button type="button" class="live-demo-ctx-nav" aria-label="Previous variant"></button>
<span class="live-demo-ctx-counter" data-demo-counter>1 / 3</span>
<button type="button" class="live-demo-ctx-nav" aria-label="Next variant"></button>
<span class="live-demo-ctx-divider"></span>
<button type="button" class="live-demo-ctx-discard" aria-label="Discard"></button>
<button type="button" class="live-demo-ctx-accept" data-demo-accept>Accept</button>
</div>
<div class="live-demo-ctx-row live-demo-ctx-row--accepted">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
<span>Variant 3 written to source</span>
</div>
</div>
<div class="live-demo-cursor" data-demo-cursor aria-hidden="true">
<svg width="18" height="22" viewBox="0 0 18 22" fill="none">
<path d="M1 1 L1 17 L5 13 L8 20 L11 19 L7.5 12 L13 12 Z" fill="#111" stroke="#fff" stroke-width="1.2" stroke-linejoin="round"/>
</svg>
</div>
</div>
<div class="live-demo-gbar" data-demo-gbar>
<span class="live-demo-gbar-brand">/</span>
<button type="button" class="live-demo-gbar-btn is-active" data-demo-gbar-pick>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="22" y1="12" x2="18" y2="12"/><line x1="6" y1="12" x2="2" y2="12"/><line x1="12" y1="6" x2="12" y2="2"/><line x1="12" y1="22" x2="12" y2="18"/></svg>
<span>Pick</span>
</button>
<button type="button" class="live-demo-gbar-btn">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
</button>
<button type="button" class="live-demo-gbar-btn">
<span class="live-demo-gbar-dmd" aria-hidden="true"><span></span><span></span><span></span><span></span></span>
</button>
<span class="live-demo-gbar-divider"></span>
<button type="button" class="live-demo-gbar-x" aria-label="Exit live mode"></button>
</div>
</div>
</div>
</div>
<p class="live-mode-demo-caption">Click the frame or scroll it into view to start the loop. Respects <code>prefers-reduced-motion</code>.</p>
</section>
<section class="live-mode-stages" aria-label="What happens in a live mode session">
<h2 class="live-mode-stages-title">What happens, in three moves</h2>
<div class="live-mode-stages-grid">
<article class="live-mode-stage">
<span class="live-mode-stage-num">01 &middot; Pick</span>
<h3 class="live-mode-stage-name">Point at what bugs you</h3>
<p class="live-mode-stage-desc">Click any element on your running dev server. Add a comment pin where the issue lives. Draw a stroke through the bit you want to change. Or just type "more playful".</p>
<div class="live-mode-stage-viz">
<div class="docs-viz-picker-row" style="min-height:72px;padding:10px">
<div class="docs-viz-picker-target" style="font-size:12px;padding:6px 12px">
Newsletter card
<span class="docs-viz-picker-pin" style="width:18px;height:18px;font-size:9px">1</span>
</div>
</div>
</div>
</article>
<article class="live-mode-stage">
<span class="live-mode-stage-num">02 &middot; Generate</span>
<h3 class="live-mode-stage-name">Three genuinely different takes</h3>
<p class="live-mode-stage-desc">Variants anchor to different archetypes, not three riffs on color. Each one explores a different primary axis: hierarchy, typography, density, layout, or palette strategy.</p>
<div class="live-mode-stage-viz">
<div class="docs-viz-variants" style="width:100%;gap:4px">
<div class="docs-viz-variant docs-viz-variant--v1" style="min-height:44px;padding:6px"><span class="docs-viz-variant-kicker" style="font-size:8px">No.04</span></div>
<div class="docs-viz-variant docs-viz-variant--v2 is-active" style="min-height:44px;padding:6px"><span class="docs-viz-variant-kicker" style="font-size:8px">Dispatch</span></div>
<div class="docs-viz-variant docs-viz-variant--v3" style="min-height:44px;padding:6px"><span class="docs-viz-variant-kicker" style="font-size:8px">Field</span></div>
</div>
</div>
</article>
<article class="live-mode-stage">
<span class="live-mode-stage-num">03 &middot; Accept</span>
<h3 class="live-mode-stage-name">Lands in real source</h3>
<p class="live-mode-stage-desc">The accepted variant replaces the picked element in your source file. CSS consolidates into your real stylesheet, not inline. Discard all three and the original stays.</p>
<div class="live-mode-stage-viz">
<span class="docs-viz-accept-pill">Variant 2 written to source</span>
</div>
</article>
</div>
</section>
<section class="live-mode-pathways" aria-label="Where to go next">
<h2 class="live-mode-pathways-title">Where next</h2>
<div class="live-mode-pathways-grid">
<a class="live-mode-pathway" href="/tutorials/iterate-live">
<span class="live-mode-pathway-kind">Tutorial</span>
<h3 class="live-mode-pathway-title">Walk it step by step</h3>
<p class="live-mode-pathway-desc">A ten-minute walkthrough from first run to accepted variant. Covers CSP patching, the picker actions, and the fallback flow for generated files.</p>
<span class="live-mode-pathway-cta">Open the tutorial &rarr;</span>
</a>
<a class="live-mode-pathway" href="/docs/live">
<span class="live-mode-pathway-kind">Reference</span>
<h3 class="live-mode-pathway-title">Full command reference</h3>
<p class="live-mode-pathway-desc">Everything your AI harness reads when <code>/impeccable live</code> runs: the poll loop, the wrap/accept helpers, the CSP templates, and every event shape.</p>
<span class="live-mode-pathway-cta">Read the reference &rarr;</span>
</a>
<a class="live-mode-pathway" href="/#downloads">
<span class="live-mode-pathway-kind">Install</span>
<h3 class="live-mode-pathway-title">Get Impeccable set up</h3>
<p class="live-mode-pathway-desc">Install the skill and CLI once, then run <code>/impeccable live</code> from your AI harness. Works with Claude Code, Cursor, Codex, Gemini, and more.</p>
<span class="live-mode-pathway-cta">See the install steps &rarr;</span>
</a>
</div>
</section>
<section class="live-mode-frameworks" aria-label="Supported frameworks">
<span class="live-mode-frameworks-label">Supported dev servers</span>
<ul class="live-mode-frameworks-list">
<li>Vite</li>
<li>Next.js (incl. monorepos)</li>
<li>SvelteKit</li>
<li>Astro</li>
<li>Nuxt</li>
<li>Bun</li>
<li>Plain static HTML</li>
</ul>
</section>
</div>
</main>
<script>
// Copy buttons on rendered code blocks
document.addEventListener('click', (e) => {
const btn = e.target.closest('[data-copy]');
if (!btn) return;
const text = btn.getAttribute('data-copy');
if (!text) return;
navigator.clipboard.writeText(text).then(() => {
btn.classList.add('is-copied');
setTimeout(() => btn.classList.remove('is-copied'), 1500);
}).catch(() => {});
});
// Mobile sidebar toggle (shown on narrow viewports, hidden on desktop).
document.addEventListener('click', (e) => {
const toggle = e.target.closest('.skills-sidebar-toggle');
if (!toggle) return;
const expanded = toggle.getAttribute('aria-expanded') === 'true';
toggle.setAttribute('aria-expanded', String(!expanded));
});
// Before/after split-compare: drag on touch, hover OR drag on mouse.
// Pointer events attach to the padded .split-comparison wrapper so
// there is a ~20px invisible buffer around the visible box. The
// divider only snaps back when the pointer leaves that outer buffer.
(function initSplitCompare() {
const wrappers = document.querySelectorAll('.split-comparison');
if (wrappers.length === 0) return;
const hasHover = matchMedia('(hover: hover)').matches;
const DEFAULT_POSITION = 50;
for (const wrapper of wrappers) {
const container = wrapper.querySelector('.split-container');
const splitAfter = wrapper.querySelector('.split-after');
const splitDivider = wrapper.querySelector('.split-divider');
if (!container || !splitAfter || !splitDivider) continue;
const tanAngle = Math.tan(10 * Math.PI / 180);
let skewOffset = 8;
const recalcSkew = () => {
const r = container.getBoundingClientRect();
if (r.width > 0 && r.height > 0) {
skewOffset = 50 * r.height * tanAngle / r.width;
}
};
recalcSkew();
window.addEventListener('resize', recalcSkew, { passive: true });
let targetX = DEFAULT_POSITION;
let currentX = DEFAULT_POSITION;
let rafId = null;
const paint = (pct) => {
const x = Math.max(-skewOffset, Math.min(100 + skewOffset, pct));
splitAfter.style.clipPath =
`polygon(${x + skewOffset}% 0%, 100% 0%, 100% 100%, ${x - skewOffset}% 100%)`;
splitDivider.style.left = `${x}%`;
};
const step = () => {
currentX += (targetX - currentX) * 0.2;
if (Math.abs(targetX - currentX) < 0.1) {
currentX = targetX;
rafId = null;
} else {
rafId = requestAnimationFrame(step);
}
paint(currentX);
};
const setTarget = (pct) => {
targetX = pct;
if (rafId === null) rafId = requestAnimationFrame(step);
};
paint(DEFAULT_POSITION);
// Percentage is always relative to the VISIBLE .split-container,
// not the padded .split-comparison wrapper. The pointer event
// target is the wrapper but the clip-path math uses the inner box.
const pctFromClientX = (clientX) => {
const rect = container.getBoundingClientRect();
return ((clientX - rect.left) / rect.width) * 100;
};
let hovering = false;
let dragging = false;
wrapper.addEventListener('pointerenter', (e) => {
if (hasHover && e.pointerType === 'mouse') {
hovering = true;
}
});
wrapper.addEventListener('pointerdown', (e) => {
dragging = true;
wrapper.setPointerCapture(e.pointerId);
setTarget(pctFromClientX(e.clientX));
});
wrapper.addEventListener('pointermove', (e) => {
if (dragging || hovering) {
setTarget(pctFromClientX(e.clientX));
}
});
const endDrag = (e) => {
if (dragging) {
dragging = false;
try { wrapper.releasePointerCapture(e.pointerId); } catch {}
}
};
wrapper.addEventListener('pointerup', endDrag);
wrapper.addEventListener('pointercancel', endDrag);
wrapper.addEventListener('pointerleave', (e) => {
endDrag(e);
if (hovering) {
hovering = false;
setTarget(DEFAULT_POSITION);
}
});
}
})();
</script>
</body>
</html>
-80
View File
@@ -1,80 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Privacy Policy - Impeccable</title>
<meta name="robots" content="noindex">
<link rel="icon" type="image/svg+xml" href="./favicon.svg">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,600&family=Instrument+Sans:wght@400;500;600&family=Space+Grotesk:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="./css/sub-pages.css">
<style>
.privacy-content { max-width: 680px; margin: 0 auto; padding: 2rem 1.5rem 4rem; }
.privacy-content h1 { font-family: var(--font-display); font-size: 2.25rem; margin-bottom: 0.5rem; letter-spacing: -0.01em; }
.privacy-content h2 { font-size: 1.125rem; margin-top: 2rem; font-weight: 600; }
.privacy-content p, .privacy-content ul { color: var(--color-charcoal); margin-top: 0.5rem; }
.privacy-content ul { padding-left: 1.25rem; }
.privacy-content li { margin-top: 0.25rem; }
.privacy-content a { color: var(--color-accent); text-decoration: underline; text-underline-offset: 3px; }
.privacy-content a:hover { color: var(--color-accent-hover); }
.privacy-content .updated { color: var(--color-ash); font-size: 0.875rem; margin-bottom: 2rem; font-style: italic; }
</style>
</head>
<body class="sub-page">
<!-- site-header v1 -->
<header class="site-header" data-site-header>
<a href="/" class="site-header-brand" aria-label="Impeccable home">
<svg class="site-header-brand-logo" viewBox="0 0 32 32" aria-hidden="true"><rect width="32" height="32" rx="6" fill="#1a1a1a"/><text x="16" y="24" font-family="system-ui, -apple-system, sans-serif" font-size="22" font-weight="500" fill="#f5f3ef" text-anchor="middle">/</text></svg>
<span class="site-header-brand-name">Impeccable</span>
</a>
<div class="site-header-right">
<nav class="site-header-nav" aria-label="Primary">
<a href="/" data-nav="home">Home</a>
<a href="/docs" data-nav="docs">Docs</a>
<a href="/slop" data-nav="slop">Slop</a>
</nav>
<a href="https://github.com/pbakaus/impeccable" class="site-header-github" target="_blank" rel="noopener" aria-label="Impeccable on GitHub, 23k stars">
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/></svg>
<span class="site-header-github-label">23k</span>
<svg class="site-header-github-star" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2l2.76 6.36L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l7.24-.91L12 2z"/></svg>
</a>
</div>
</header>
<main class="privacy-content">
<h1>Privacy Policy</h1>
<p class="updated">Last updated: April 6, 2026</p>
<h2>What Impeccable is</h2>
<p>Impeccable is an open-source collection of agent skills (text files) that run locally in your AI coding tool. The skills themselves collect no data, make no network requests, and have no analytics.</p>
<h2>Website analytics</h2>
<p>The Impeccable website (<a href="https://impeccable.style">impeccable.style</a>) uses Google Analytics to understand traffic patterns (page views, referrers, country). No personal information is collected beyond what Google Analytics provides by default. No cookies are used for advertising.</p>
<h2>Downloads</h2>
<p>When you download a skill bundle from the website, we log the download event (which bundle, timestamp) for usage statistics. No personal information is attached to these logs.</p>
<h2>Claude Code Plugin</h2>
<p>When installed as a Claude Code plugin, Impeccable runs entirely within your local Claude Code session. No data is sent to Impeccable's servers. Anthropic's own privacy policy governs the Claude Code application itself.</p>
<h2>Chrome Extension</h2>
<p>The Impeccable Chrome DevTools extension runs entirely in your browser. All anti-pattern detection happens locally on the page you are inspecting. No page content, URLs, or detection results are ever sent to any external server.</p>
<p>The extension stores your rule preferences (which detections are enabled or disabled) using Chrome's built-in sync storage (<code>chrome.storage.sync</code>), which syncs settings across your Chrome instances via your Google account. No other data is stored or transmitted.</p>
<p>The extension requests the following permissions:</p>
<ul>
<li><strong>activeTab / scripting</strong> - to inject the detector script into the page you are inspecting</li>
<li><strong>storage</strong> - to save your rule preferences</li>
<li><strong>webNavigation</strong> - to re-scan automatically when you navigate to a new page</li>
<li><strong>Host permissions (all URLs)</strong> - so the detector can run on any website you choose to inspect</li>
</ul>
<h2>GitHub</h2>
<p>The source code is hosted on GitHub. Interactions with the repository (issues, pull requests, stars) are governed by <a href="https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement">GitHub's privacy policy</a>.</p>
<h2>Contact</h2>
<p>Questions about this policy? Open an issue on <a href="https://github.com/pbakaus/impeccable">GitHub</a> or reach out to <a href="https://x.com/pbakaus">@pbakaus</a>.</p>
</main>
</body>
</html>
File diff suppressed because it is too large Load Diff
+30 -68
View File
@@ -21,7 +21,8 @@ import { fileURLToPath } from 'url';
import { readSourceFiles, readPatterns, stashPerProjectArtifacts, restorePerProjectArtifacts } from './lib/utils.js';
import { createTransformer, PROVIDERS } from './lib/transformers/index.js';
import { createAllZips } from './lib/zip.js';
import { generateSubPages } from './build-sub-pages.js';
// Sub-page generation is now handled by Astro content collections.
// import { generateSubPages } from './build-sub-pages.js';
/**
* Generate authoritative counts from source data and write to public/js/generated/counts.js.
@@ -137,12 +138,10 @@ function validateSkillFrontmatter(skills) {
function validateNoEmDashes(rootDir) {
const targets = [
'content/site',
'public/index.html',
'public/privacy.html',
'scripts/build-sub-pages.js',
'scripts/lib/sub-pages-data.js',
'site/components',
'site/layouts',
];
const extensions = new Set(['.html', '.md', '.js', '.mjs', '.css']);
const extensions = new Set(['.html', '.md', '.js', '.mjs', '.css', '.astro']);
const emDashPatterns = [/—/g, /&mdash;/gi, /&#8212;/gi, /&#x2014;/gi];
let errors = 0;
@@ -188,30 +187,13 @@ function validateNoEmDashes(rootDir) {
*
* Returns the number of validation errors. Build fails if > 0.
*/
function validateSiteHeader(rootDir) {
const pages = [
'public/index.html',
'public/privacy.html',
];
const marker = '<!-- site-header v1 -->';
let errors = 0;
for (const rel of pages) {
const full = path.join(rootDir, rel);
if (!fs.existsSync(full)) {
console.error(`${rel} is missing`);
errors++;
continue;
}
const src = fs.readFileSync(full, 'utf-8');
if (!src.includes(marker)) {
console.error(`${rel} is missing the shared site header marker '${marker}'`);
errors++;
}
}
if (errors === 0) {
console.log(`✓ Validated site header on ${pages.length} hand-authored pages`);
}
return errors;
function validateSiteHeader(_rootDir) {
// With Astro, the shared header is a component (site/components/Header.astro).
// There's nothing to validate per-page — the component is imported by Base.astro
// and rendered identically everywhere. This function is kept as a no-op so the
// call site doesn't need to change.
console.log('✓ Site header is a shared Astro component (no per-page validation needed)');
return 0;
}
/**
@@ -236,11 +218,10 @@ const __dirname = path.dirname(__filename);
const ROOT_DIR = path.resolve(__dirname, '..');
const DIST_DIR = path.join(ROOT_DIR, 'dist');
/**
* Build static site using Bun's HTML bundler
* Bun's HTML loader resolves <link rel="stylesheet"> and inlines CSS @imports.
*/
async function buildStaticSite(extraEntrypoints = []) {
// buildStaticSite (Bun HTML bundler) removed — now handled by Astro.
// Placeholder so the line-number-based edits below don't shift.
async function _REMOVED() {
const entrypoints = [
path.join(ROOT_DIR, 'public', 'index.html'),
path.join(ROOT_DIR, 'public', 'privacy.html'),
@@ -563,43 +544,21 @@ function generateCFConfig(buildDir) {
async function build() {
console.log('🔨 Building cross-provider design skills...\n');
// Pre-generate sub-pages (docs, slop, tutorials, live-mode, designing) from source
console.log('📝 Generating sub-pages...');
const { files: subPageFiles } = await generateSubPages(ROOT_DIR);
console.log(`✓ Generated ${subPageFiles.length} sub-page(s)\n`);
// Sub-page generation, HTML bundling, and static-asset copying are now
// handled by Astro (bun run build:site). This script focuses on skills,
// API data, and Cloudflare config.
const casePageFiles = [
path.join(ROOT_DIR, 'public', 'cases', 'neo-mirai', 'index.html'),
path.join(ROOT_DIR, 'public', 'neo-mirai', 'index.html'),
].filter((pagePath) => fs.existsSync(pagePath));
// Bundle HTML, JS, and CSS with Bun (including generated sub-pages)
await buildStaticSite([...subPageFiles, ...casePageFiles]);
// Copy root-level static assets that need stable (unhashed) URLs
const staticAssets = ['og-image.jpg', 'robots.txt', 'sitemap.xml', 'favicon.svg', 'apple-touch-icon.png'];
const buildDir = path.join(ROOT_DIR, 'build');
for (const asset of staticAssets) {
const src = path.join(ROOT_DIR, 'public', asset);
if (fs.existsSync(src)) {
fs.copyFileSync(src, path.join(buildDir, asset));
}
}
// Copy antipattern examples (self-contained HTML, not Bun entrypoints)
const examplesDir = path.join(ROOT_DIR, 'public', 'antipattern-examples');
if (fs.existsSync(examplesDir)) {
copyDirSync(examplesDir, path.join(buildDir, 'antipattern-examples'));
}
// Copy browser detector script (referenced by antipattern examples at /js/...)
// Copy browser detector to public/js/ so the antipattern examples can
// reference it (Astro serves public/ as-is).
const detectorSrc = path.join(ROOT_DIR, 'src', 'detect-antipatterns-browser.js');
if (fs.existsSync(detectorSrc)) {
const jsDir = path.join(buildDir, 'js');
const jsDir = path.join(ROOT_DIR, 'public', 'js');
fs.mkdirSync(jsDir, { recursive: true });
fs.copyFileSync(detectorSrc, path.join(jsDir, 'detect-antipatterns-browser.js'));
}
const buildDir = path.join(ROOT_DIR, 'build');
// Read source files (unified skills architecture)
const { skills } = readSourceFiles(ROOT_DIR);
const patterns = readPatterns(ROOT_DIR);
@@ -628,9 +587,12 @@ async function build() {
await createAllZips(DIST_DIR);
// Generate static API data and Cloudflare Pages config
generateApiData(buildDir, skills, patterns);
copyDistToBuild(DIST_DIR, buildDir);
generateCFConfig(buildDir);
// Write API data and CF config to public/ so Astro copies them to build/.
// Astro wipes build/ before writing, so anything written directly to build/
// during build:skills would be destroyed when build:site runs.
const publicDir = path.join(ROOT_DIR, 'public');
generateApiData(publicDir, skills, patterns);
generateCFConfig(publicDir);
// Copy all provider outputs to project root for local testing.
// `.codex/` is intentionally excluded: Codex no longer consumes that layout; keep
-247
View File
@@ -1,247 +0,0 @@
/**
* Page template wrapper for generated sub-pages.
*
* Reads the shared site header partial once and wraps content bodies with
* a minimal HTML scaffold that imports tokens.css + sub-pages.css.
*
* Used by scripts/build-sub-pages.js (wired up in commit 3).
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT_DIR = path.resolve(__dirname, '..', '..');
const HEADER_PARTIAL = path.join(ROOT_DIR, 'content', 'site', 'partials', 'header.html');
let cachedHeader = null;
/**
* Read the shared site header partial.
* Cached after first read.
*/
export function readHeaderPartial() {
if (cachedHeader === null) {
cachedHeader = fs.readFileSync(HEADER_PARTIAL, 'utf8').trim();
}
return cachedHeader;
}
/**
* Mark a nav item as current by adding aria-current="page" and removing
* the default nav href state. Matches on `data-nav="{activeNav}"`.
*
* @param {string} headerHtml
* @param {string} activeNav - one of: home, designing, docs, slop, live, github
* @returns {string}
*/
export function applyActiveNav(headerHtml, activeNav) {
if (!activeNav) return headerHtml;
return headerHtml.replace(
new RegExp(`data-nav="${activeNav}"`, 'g'),
`data-nav="${activeNav}" aria-current="page"`,
);
}
/**
* Wrap body HTML in a full page shell.
*
* @param {object} opts
* @param {string} opts.title - <title> text
* @param {string} opts.description - meta description
* @param {string} opts.bodyHtml - main content HTML (will be placed inside <main>)
* @param {string} [opts.activeNav] - which nav item to mark current
* @param {string} [opts.canonicalPath] - relative URL path for <link rel="canonical">
* @param {string} [opts.extraHead] - raw HTML to inject into <head>
* @param {string} [opts.bodyClass] - optional class on <body>
* @param {number} [opts.assetDepth] - how many `..` to prepend for Bun's HTML loader to resolve on-disk paths. 1 = page is one dir deep under public/ (e.g. public/skills/polish.html). Defaults to 1.
* @returns {string} full HTML document
*/
export function renderPage({
title,
description,
bodyHtml,
activeNav,
canonicalPath,
extraHead = '',
bodyClass = 'sub-page',
assetDepth = 1,
}) {
const header = applyActiveNav(readHeaderPartial(), activeNav);
const safeTitle = escapeHtml(title);
const safeDesc = escapeAttr(description || '');
const canonical = canonicalPath
? `<link rel="canonical" href="https://impeccable.style${canonicalPath}">`
: '';
// Relative prefix for on-disk resolution by Bun's HTML loader.
// Bun rewrites these to hashed absolute URLs at build time, so runtime
// serving works regardless of the request path.
const rel = assetDepth > 0 ? '../'.repeat(assetDepth) : './';
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${safeTitle}</title>
<meta name="description" content="${safeDesc}">
<meta name="theme-color" content="#fafafa">
${canonical}
<link rel="icon" type="image/svg+xml" href="${rel}favicon.svg">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,600;1,400&family=Instrument+Sans:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="${rel}css/sub-pages.css">
${extraHead}
</head>
<body class="${bodyClass}">
<a href="#main" class="skip-link">Skip to content</a>
${header}
<main id="main">
${bodyHtml}
</main>
<script>
// Copy buttons on rendered code blocks
document.addEventListener('click', (e) => {
const btn = e.target.closest('[data-copy]');
if (!btn) return;
const text = btn.getAttribute('data-copy');
if (!text) return;
navigator.clipboard.writeText(text).then(() => {
btn.classList.add('is-copied');
setTimeout(() => btn.classList.remove('is-copied'), 1500);
}).catch(() => {});
});
// Mobile sidebar toggle (shown on narrow viewports, hidden on desktop).
document.addEventListener('click', (e) => {
const toggle = e.target.closest('.skills-sidebar-toggle');
if (!toggle) return;
const expanded = toggle.getAttribute('aria-expanded') === 'true';
toggle.setAttribute('aria-expanded', String(!expanded));
});
// Before/after split-compare: drag on touch, hover OR drag on mouse.
// Pointer events attach to the padded .split-comparison wrapper so
// there is a ~20px invisible buffer around the visible box. The
// divider only snaps back when the pointer leaves that outer buffer.
(function initSplitCompare() {
const wrappers = document.querySelectorAll('.split-comparison');
if (wrappers.length === 0) return;
const hasHover = matchMedia('(hover: hover)').matches;
const DEFAULT_POSITION = 50;
for (const wrapper of wrappers) {
const container = wrapper.querySelector('.split-container');
const splitAfter = wrapper.querySelector('.split-after');
const splitDivider = wrapper.querySelector('.split-divider');
if (!container || !splitAfter || !splitDivider) continue;
const tanAngle = Math.tan(10 * Math.PI / 180);
let skewOffset = 8;
const recalcSkew = () => {
const r = container.getBoundingClientRect();
if (r.width > 0 && r.height > 0) {
skewOffset = 50 * r.height * tanAngle / r.width;
}
};
recalcSkew();
window.addEventListener('resize', recalcSkew, { passive: true });
let targetX = DEFAULT_POSITION;
let currentX = DEFAULT_POSITION;
let rafId = null;
const paint = (pct) => {
const x = Math.max(-skewOffset, Math.min(100 + skewOffset, pct));
splitAfter.style.clipPath =
\`polygon(\${x + skewOffset}% 0%, 100% 0%, 100% 100%, \${x - skewOffset}% 100%)\`;
splitDivider.style.left = \`\${x}%\`;
};
const step = () => {
currentX += (targetX - currentX) * 0.2;
if (Math.abs(targetX - currentX) < 0.1) {
currentX = targetX;
rafId = null;
} else {
rafId = requestAnimationFrame(step);
}
paint(currentX);
};
const setTarget = (pct) => {
targetX = pct;
if (rafId === null) rafId = requestAnimationFrame(step);
};
paint(DEFAULT_POSITION);
// Percentage is always relative to the VISIBLE .split-container,
// not the padded .split-comparison wrapper. The pointer event
// target is the wrapper but the clip-path math uses the inner box.
const pctFromClientX = (clientX) => {
const rect = container.getBoundingClientRect();
return ((clientX - rect.left) / rect.width) * 100;
};
let hovering = false;
let dragging = false;
wrapper.addEventListener('pointerenter', (e) => {
if (hasHover && e.pointerType === 'mouse') {
hovering = true;
}
});
wrapper.addEventListener('pointerdown', (e) => {
dragging = true;
wrapper.setPointerCapture(e.pointerId);
setTarget(pctFromClientX(e.clientX));
});
wrapper.addEventListener('pointermove', (e) => {
if (dragging || hovering) {
setTarget(pctFromClientX(e.clientX));
}
});
const endDrag = (e) => {
if (dragging) {
dragging = false;
try { wrapper.releasePointerCapture(e.pointerId); } catch {}
}
};
wrapper.addEventListener('pointerup', endDrag);
wrapper.addEventListener('pointercancel', endDrag);
wrapper.addEventListener('pointerleave', (e) => {
endDrag(e);
if (hovering) {
hovering = false;
setTarget(DEFAULT_POSITION);
}
});
}
})();
</script>
</body>
</html>
`;
}
function escapeHtml(str) {
return String(str || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function escapeAttr(str) {
return String(str || '').replace(/"/g, '&quot;');
}
-233
View File
@@ -1,233 +0,0 @@
import { serve, file } from "bun";
import path from "node:path";
import { fileURLToPath } from "node:url";
import homepage from "../public/index.html";
import privacy from "../public/privacy.html";
import {
getSkills,
getCommands,
getCommandSource,
getPatterns,
handleFileDownload,
handleBundleDownload
} from "./lib/api-handlers.js";
import { generateSubPages } from "../scripts/build-sub-pages.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const ROOT_DIR = path.resolve(__dirname, "..");
// Pre-generate sub-pages so dev + prod share the same output shape.
console.log("📝 Generating sub-pages for dev server...");
const { files: subPageFiles } = await generateSubPages(ROOT_DIR);
console.log(`✓ Generated ${subPageFiles.length} sub-page(s)`);
// Helper: serve a generated HTML file by absolute path, 404 if missing.
async function serveGenerated(pagePath) {
const f = file(pagePath);
if (!(await f.exists())) return new Response("Not Found", { status: 404 });
return new Response(f, {
headers: {
"Content-Type": "text/html;charset=utf-8",
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
},
});
}
const server = serve({
port: process.env.PORT || 3000,
routes: {
"/": homepage,
"/privacy": privacy,
// Legacy URL redirects (kept stable for external links and existing users).
"/cheatsheet": Response.redirect("/docs", 301),
"/gallery": Response.redirect("/slop#try-it-live", 301),
"/skills": Response.redirect("/docs", 301),
"/skills/:id": (req) => Response.redirect(`/docs/${req.params.id}`, 301),
"/anti-patterns": Response.redirect("/slop#catalog", 301),
"/visual-mode": Response.redirect("/slop#see-it", 301),
// Generated sub-pages — served directly from the pre-generated files
"/docs": () => serveGenerated(path.join(ROOT_DIR, "public/docs/index.html")),
"/docs/": () => serveGenerated(path.join(ROOT_DIR, "public/docs/index.html")),
"/docs/:id": (req) => {
const id = req.params.id.replace(/[^a-z0-9-]/gi, "");
return serveGenerated(path.join(ROOT_DIR, `public/docs/${id}.html`));
},
"/slop": () => serveGenerated(path.join(ROOT_DIR, "public/slop/index.html")),
"/slop/": () => serveGenerated(path.join(ROOT_DIR, "public/slop/index.html")),
"/live-mode": () => serveGenerated(path.join(ROOT_DIR, "public/live-mode/index.html")),
"/live-mode/": () => serveGenerated(path.join(ROOT_DIR, "public/live-mode/index.html")),
"/designing": () => serveGenerated(path.join(ROOT_DIR, "public/designing/index.html")),
"/designing/": () => serveGenerated(path.join(ROOT_DIR, "public/designing/index.html")),
"/neo-mirai": () => Response.redirect("/neo-mirai/", 302),
"/neo-mirai/": () => serveGenerated(path.join(ROOT_DIR, "public/neo-mirai/index.html")),
"/neon-mirai": () => Response.redirect("/neo-mirai/", 301),
"/neon-mirai/": () => Response.redirect("/neo-mirai/", 301),
"/cases/neon-mirai": () => Response.redirect("/cases/neo-mirai", 301),
"/cases/neon-mirai/": () => Response.redirect("/cases/neo-mirai", 301),
"/cases/:slug": (req) => {
const slug = req.params.slug.replace(/[^a-z0-9-]/gi, "");
return serveGenerated(path.join(ROOT_DIR, `public/cases/${slug}/index.html`));
},
"/cases/:slug/": (req) => {
const slug = req.params.slug.replace(/[^a-z0-9-]/gi, "");
return serveGenerated(path.join(ROOT_DIR, `public/cases/${slug}/index.html`));
},
"/tutorials": () => serveGenerated(path.join(ROOT_DIR, "public/tutorials/index.html")),
"/tutorials/": () => serveGenerated(path.join(ROOT_DIR, "public/tutorials/index.html")),
"/tutorials/:slug": (req) => {
const slug = req.params.slug.replace(/[^a-z0-9-]/gi, "");
return serveGenerated(path.join(ROOT_DIR, `public/tutorials/${slug}.html`));
},
// Static assets - all public subdirectories
"/assets/*": async (req) => {
const url = new URL(req.url);
if (url.pathname.includes('..')) return new Response("Bad Request", { status: 400 });
const filePath = `./public${url.pathname}`;
const assetFile = file(filePath);
if (await assetFile.exists()) {
return new Response(assetFile, {
headers: { "X-Content-Type-Options": "nosniff", "X-Frame-Options": "DENY" }
});
}
return new Response("Not Found", { status: 404 });
},
"/css/*": async (req) => {
const url = new URL(req.url);
if (url.pathname.includes('..')) return new Response("Bad Request", { status: 400 });
const filePath = `./public${url.pathname}`;
const assetFile = file(filePath);
if (await assetFile.exists()) {
return new Response(assetFile, {
headers: { "Content-Type": "text/css", "X-Content-Type-Options": "nosniff", "X-Frame-Options": "DENY" }
});
}
return new Response("Not Found", { status: 404 });
},
"/js/*": async (req) => {
const url = new URL(req.url);
if (url.pathname.includes('..')) return new Response("Bad Request", { status: 400 });
// Check public/js/ first, then fall back to built artifacts
const headers = { "Content-Type": "application/javascript", "X-Content-Type-Options": "nosniff", "X-Frame-Options": "DENY" };
const publicFile = file(`./public${url.pathname}`);
if (await publicFile.exists()) return new Response(publicFile, { headers });
// Browser detector served from impeccable package
if (url.pathname === '/js/detect-antipatterns-browser.js') {
const pkgFile = file('./src/detect-antipatterns-browser.js');
if (await pkgFile.exists()) return new Response(pkgFile, { headers });
}
return new Response("Not Found", { status: 404 });
},
// Test fixtures (for browser visual testing)
"/fixtures/*": async (req) => {
const url = new URL(req.url);
if (url.pathname.includes('..')) return new Response("Bad Request", { status: 400 });
const filePath = `./tests${url.pathname}`;
const assetFile = file(filePath);
if (await assetFile.exists()) {
const ext = url.pathname.split('.').pop();
const types = { html: 'text/html', css: 'text/css', js: 'application/javascript' };
return new Response(assetFile, {
headers: { "Content-Type": types[ext] || "application/octet-stream", "X-Content-Type-Options": "nosniff" }
});
}
return new Response("Not Found", { status: 404 });
},
"/antipattern-images/*": async (req) => {
const url = new URL(req.url);
if (url.pathname.includes('..')) return new Response("Bad Request", { status: 400 });
const filePath = `./public${url.pathname}`;
const assetFile = file(filePath);
if (await assetFile.exists()) {
return new Response(assetFile, {
headers: { "X-Content-Type-Options": "nosniff" }
});
}
return new Response("Not Found", { status: 404 });
},
"/antipattern-examples/*": async (req) => {
const url = new URL(req.url);
if (url.pathname.includes('..')) return new Response("Bad Request", { status: 400 });
const filePath = `./public${url.pathname}`;
const assetFile = file(filePath);
if (await assetFile.exists()) {
return new Response(assetFile, {
headers: { "Content-Type": "text/html", "X-Content-Type-Options": "nosniff", "X-Frame-Options": "SAMEORIGIN" }
});
}
return new Response("Not Found", { status: 404 });
},
// API: Get all skills
"/api/skills": {
async GET() {
const skills = await getSkills();
return Response.json(skills);
},
},
// API: Get all commands
"/api/commands": {
async GET() {
const commands = await getCommands();
return Response.json(commands);
},
},
// API: Get patterns and antipatterns
"/api/patterns": {
async GET() {
const patterns = await getPatterns();
return Response.json(patterns);
},
},
// API: Get command source content
"/api/command-source/:id": async (req) => {
const { id } = req.params;
const result = await getCommandSource(id);
if (result && result.error) {
return Response.json({ error: result.error }, { status: result.status });
}
if (!result) {
return Response.json({ error: "Command not found" }, { status: 404 });
}
return Response.json({ content: result });
},
// API: Download individual file
"/api/download/:type/:provider/:id": async (req) => {
const { type, provider, id } = req.params;
return handleFileDownload(type, provider, id);
},
// API: Download provider bundle ZIP
"/api/download/bundle/:provider": async (req) => {
const { provider } = req.params;
return handleBundleDownload(provider);
},
},
// Serve root-level static files (og-image.png, favicon, robots.txt, etc.)
fetch(req) {
const url = new URL(req.url);
if (url.pathname.includes('..')) {
return new Response("Bad Request", { status: 400 });
}
const filePath = `./public${url.pathname}`;
const staticFile = file(filePath);
if (staticFile.size > 0) {
return new Response(staticFile);
}
return new Response("Not Found", { status: 404 });
},
development: process.env.NODE_ENV !== "production",
});
console.log(`🎨 impeccable.style running at ${server.url}`);
-223
View File
@@ -1,223 +0,0 @@
import { readdir, readFile } from "fs/promises";
import { basename, join, dirname } from "path";
import { existsSync } from "fs";
import { fileURLToPath } from "url";
import { readPatterns, parseFrontmatter } from "../../scripts/lib/utils.js";
import { FILE_DOWNLOAD_PROVIDER_CONFIG_DIRS } from "../../lib/download-providers.js";
import {
isAllowedBundleProvider,
isAllowedFileProvider,
isAllowedType,
isValidId,
sanitizeFilename
} from "./validation.js";
// Get project root directory (works in both Node.js and Bun, including Vercel)
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const PROJECT_ROOT = join(__dirname, "..", "..");
// Helper to read file content (works in both Node.js and Bun)
async function readFileContent(filePath) {
return readFile(filePath, "utf-8");
}
// Read all skills from source/skills/ subdirectories
export async function getSkills() {
const skillsDir = join(PROJECT_ROOT, "source", "skills");
const entries = await readdir(skillsDir, { withFileTypes: true });
const skills = [];
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const skillMdPath = join(skillsDir, entry.name, "SKILL.md");
if (!existsSync(skillMdPath)) continue;
const content = await readFileContent(skillMdPath);
const { frontmatter } = parseFrontmatter(content);
skills.push({
id: entry.name,
name: frontmatter.name || entry.name,
description: frontmatter.description || "No description available",
userInvocable: frontmatter['user-invocable'] === true || frontmatter['user-invocable'] === 'true',
});
}
return skills;
}
// Read a short tagline for a command from its editorial file
// (content/site/skills/<id>.md). Returns null if the file or tagline is
// missing. Taglines are used by UI surfaces that need a human-friendly
// one-liner; `description` stays optimized for auto-trigger matching.
async function readCommandTagline(id) {
const editorialPath = join(PROJECT_ROOT, "content/site/skills", `${id}.md`);
if (!existsSync(editorialPath)) return null;
try {
const raw = await readFileContent(editorialPath);
const match = raw.match(/^---\n([\s\S]*?)\n---/);
if (!match) return null;
const taglineMatch = match[1].match(/tagline:\s*"([^"]+)"/);
return taglineMatch ? taglineMatch[1] : null;
} catch {
return null;
}
}
// Read commands. After the v3.0 consolidation, commands are sub-commands of
// /impeccable. Read them from command-metadata.json and include the root
// impeccable skill itself so UI surfaces (cheatsheet, magazine spread) can
// list them.
export async function getCommands() {
const allSkills = await getSkills();
const metadataPath = join(PROJECT_ROOT, "source/skills/impeccable/scripts/command-metadata.json");
const commands = [];
const impeccable = allSkills.find(s => s.name === "impeccable");
if (impeccable) {
commands.push({
id: "impeccable",
name: "impeccable",
description: impeccable.description,
tagline: await readCommandTagline("impeccable"),
userInvocable: true,
});
}
if (existsSync(metadataPath)) {
try {
const raw = await readFileContent(metadataPath);
const metadata = JSON.parse(raw);
for (const [id, meta] of Object.entries(metadata)) {
commands.push({
id,
name: id,
description: meta.description,
tagline: await readCommandTagline(id),
userInvocable: true,
});
}
} catch (error) {
console.error("Error reading command metadata:", error);
}
}
// Fallback: return just user-invocable skills if no metadata
if (commands.length === 0) {
return allSkills.filter(s => s.userInvocable);
}
return commands;
}
// Get command/skill source content
export async function getCommandSource(id) {
if (!isValidId(id)) {
return { error: "Invalid command ID", status: 400 };
}
const skillPath = join(PROJECT_ROOT, "source", "skills", id, "SKILL.md");
try {
if (!existsSync(skillPath)) {
return null;
}
const content = await readFileContent(skillPath);
return content;
} catch (error) {
console.error("Error reading skill source:", error);
return null;
}
}
// Get the appropriate file path for a provider
export function getFilePath(type, provider, id) {
const distDir = join(PROJECT_ROOT, "dist");
const configDir = FILE_DOWNLOAD_PROVIDER_CONFIG_DIRS[provider];
if (!configDir) return null;
// Everything is a skill now
if (type === "skill" || type === "command") {
return join(distDir, provider, configDir, "skills", id, "SKILL.md");
}
return null;
}
// Handle individual file download
export async function handleFileDownload(type, provider, id) {
if (!isAllowedType(type)) {
return new Response("Invalid type", { status: 400 });
}
if (!isAllowedFileProvider(provider)) {
return new Response("Invalid provider", { status: 400 });
}
if (!isValidId(id)) {
return new Response("Invalid file ID", { status: 400 });
}
const filePath = getFilePath(type, provider, id);
if (!filePath) {
return new Response("Invalid provider", { status: 400 });
}
try {
if (!existsSync(filePath)) {
return new Response("File not found", { status: 404 });
}
const content = await readFile(filePath);
const fileName = sanitizeFilename(basename(filePath));
return new Response(content, {
headers: {
"Content-Type": "application/octet-stream",
"Content-Disposition": `attachment; filename="${fileName}"`,
},
});
} catch (error) {
console.error("Error downloading file:", error);
return new Response("Error downloading file", { status: 500 });
}
}
// Extract patterns from SKILL.md using the shared utility
export async function getPatterns() {
try {
return readPatterns(PROJECT_ROOT);
} catch (error) {
console.error("Error reading patterns:", error);
return { patterns: [], antipatterns: [] };
}
}
// Handle bundle download
export async function handleBundleDownload(provider) {
if (!isAllowedBundleProvider(provider)) {
return new Response("Invalid provider", { status: 400 });
}
const distDir = join(PROJECT_ROOT, "dist");
const zipPath = join(distDir, `${provider}.zip`);
try {
if (!existsSync(zipPath)) {
return new Response("Bundle not found", { status: 404 });
}
const content = await readFile(zipPath);
const safeProvider = sanitizeFilename(provider);
return new Response(content, {
headers: {
"Content-Type": "application/zip",
"Content-Disposition": `attachment; filename="impeccable-style-${safeProvider}.zip"`,
},
});
} catch (error) {
console.error("Error downloading bundle:", error);
return new Response("Error downloading bundle", { status: 500 });
}
}
-40
View File
@@ -1,40 +0,0 @@
// Shared validation helpers for input sanitization
import {
BUNDLE_DOWNLOAD_PROVIDERS,
DOWNLOAD_PROVIDERS,
FILE_DOWNLOAD_PROVIDERS,
} from '../../lib/download-providers.js';
// Only allow alphanumeric, hyphens, and underscores in IDs
export const VALID_ID = /^[a-zA-Z0-9_-]+$/;
export const ALLOWED_PROVIDERS = DOWNLOAD_PROVIDERS;
export const ALLOWED_FILE_PROVIDERS = FILE_DOWNLOAD_PROVIDERS;
export const ALLOWED_BUNDLE_PROVIDERS = BUNDLE_DOWNLOAD_PROVIDERS;
export const ALLOWED_TYPES = ['skill', 'command'];
export function isValidId(id) {
return typeof id === 'string' && VALID_ID.test(id);
}
export function isAllowedProvider(provider) {
return ALLOWED_PROVIDERS.includes(provider);
}
export function isAllowedFileProvider(provider) {
return ALLOWED_FILE_PROVIDERS.includes(provider);
}
export function isAllowedBundleProvider(provider) {
return ALLOWED_BUNDLE_PROVIDERS.includes(provider);
}
export function isAllowedType(type) {
return ALLOWED_TYPES.includes(type);
}
// Sanitize a filename for use in Content-Disposition headers
export function sanitizeFilename(filename) {
return filename.replace(/[^a-zA-Z0-9._-]/g, '');
}
+29
View File
@@ -0,0 +1,29 @@
---
---
<footer class="site-footer">
<div class="footer-row">
<span class="footer-logo">Impeccable</span>
<nav class="footer-links" aria-label="Footer">
<a href="/designing">Designing</a>
<a href="/docs">Docs</a>
<a href="/slop">Slop</a>
<a href="/live-mode">Live Mode</a>
<a href="/privacy">Privacy</a>
<a href="https://github.com/pbakaus/impeccable">GitHub</a>
</nav>
<div class="footer-credit">
<span>Created by <a href="https://x.com/pbakaus" target="_blank" rel="noopener">Paul Bakaus</a></span>
<a href="https://x.com/pbakaus" class="footer-social-link" aria-label="Follow on X" target="_blank" rel="noopener">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
</svg>
</a>
<a href="https://linkedin.com/in/paulbakaus" class="footer-social-link" aria-label="Connect on LinkedIn" target="_blank" rel="noopener">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
</svg>
</a>
</div>
</div>
</footer>
@@ -1,4 +1,10 @@
<!-- site-header v1 -->
---
interface Props {
activeNav?: 'home' | 'designing' | 'docs' | 'slop' | 'live';
}
const { activeNav } = Astro.props;
---
<header class="site-header" data-site-header>
<a href="/" class="site-header-brand" aria-label="Impeccable home">
<svg class="site-header-brand-logo" viewBox="0 0 32 32" aria-hidden="true"><rect width="32" height="32" rx="6" fill="#1a1a1a"/><text x="16" y="24" font-family="system-ui, -apple-system, sans-serif" font-size="22" font-weight="500" fill="#f5f3ef" text-anchor="middle">/</text></svg>
@@ -6,15 +12,15 @@
</a>
<div class="site-header-right">
<nav class="site-header-nav" aria-label="Primary">
<a href="/" data-nav="home">Home</a>
<a href="/designing" data-nav="designing">Designing</a>
<a href="/docs" data-nav="docs">Docs</a>
<a href="/slop" data-nav="slop">Slop</a>
<a href="/live-mode" data-nav="live">Live</a>
<a href="/" data-nav="home" aria-current={activeNav === 'home' ? 'page' : undefined}>Home</a>
<a href="/designing" data-nav="designing" aria-current={activeNav === 'designing' ? 'page' : undefined}>Designing</a>
<a href="/docs" data-nav="docs" aria-current={activeNav === 'docs' ? 'page' : undefined}>Docs</a>
<a href="/slop" data-nav="slop" aria-current={activeNav === 'slop' ? 'page' : undefined}>Slop</a>
<a href="/live-mode" data-nav="live" aria-current={activeNav === 'live' ? 'page' : undefined}>Live</a>
</nav>
<a href="https://github.com/pbakaus/impeccable" class="site-header-github" target="_blank" rel="noopener" aria-label="Impeccable on GitHub, 23k stars">
<a href="https://github.com/pbakaus/impeccable" class="site-header-github" target="_blank" rel="noopener" aria-label="Impeccable on GitHub, 24k stars">
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/></svg>
<span class="site-header-github-label">23k</span>
<span class="site-header-github-label">24k</span>
<svg class="site-header-github-star" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2l2.76 6.36L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l7.24-.91L12 2z"/></svg>
</a>
</div>
+21
View File
@@ -0,0 +1,21 @@
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const skills = defineCollection({
loader: glob({ pattern: '**/*.md', base: './site/content/skills' }),
schema: z.object({
tagline: z.string(),
}),
});
const tutorials = defineCollection({
loader: glob({ pattern: '**/*.md', base: './site/content/tutorials' }),
schema: z.object({
title: z.string(),
tagline: z.string(),
order: z.number(),
description: z.string(),
}),
});
export const collections = { skills, tutorials };
+40
View File
@@ -0,0 +1,40 @@
---
tagline: "Make designs work across screens, devices, and contexts without amputating features."
---
## When to use it
`/impeccable adapt` is for taking a design built for one context and making it work in another. Mobile from desktop, tablet from mobile, print from web, embedded from standalone, email from dashboard. Reach for it when the source design is solid but falls apart at other breakpoints, on touch, or in a different container.
Not for building responsive from scratch. For that, start with `/impeccable` and shape the layout responsive-first. Adapt is for the "we never thought about mobile" backfill.
## How it works
The skill works through four dimensions of contextual fit:
1. **Breakpoints and fluid layout**: collapse multi-column to single, adjust clamp ranges, introduce new breakpoints where the design genuinely breaks.
2. **Touch targets**: minimum 44px hit areas, sufficient spacing between adjacent targets, larger tap zones than visual bounds where needed.
3. **Navigation patterns**: desktop sidebars become mobile bottom nav or slide-outs, dense toolbars collapse into menus, hover states get touch equivalents.
4. **Content priority**: decide what must be visible, what can collapse into disclosures, what can be removed entirely for that context.
The non-negotiable rule: adapt, do not amputate. Critical functionality cannot disappear on mobile just because it is inconvenient. Find a way to fit it, redesign the interaction, or reconsider whether it was really critical on desktop.
## Try it
```
/impeccable adapt the settings page for mobile
```
Expected changes:
- Three-column grid becomes single column with section headers acting as sticky dividers
- Sidebar nav moves to a horizontal scroller above the content
- Toggles gain 8px vertical padding so they meet 44px touch targets
- Inline help text moves to tap-to-reveal, not hover
- The "Danger zone" section expands fully on mobile instead of collapsing, because it contains irreversible actions and we want users to see them clearly
## Pitfalls
- **Amputating features.** If the mobile version hides things the desktop version can do, that is a regression, not an adaptation. Fight for the feature.
- **Treating mobile as "smaller desktop".** Mobile is a different context: thumbs, interruption, short sessions. Adapt to the context, not to the viewport width.
- **Skipping `/impeccable harden` afterward.** Responsive layouts reveal edge cases. Run hardening after adapt to catch the ones that only show up at 320px.
+42
View File
@@ -0,0 +1,42 @@
---
tagline: "Purposeful motion that conveys state, not decoration."
---
## When to use it
`/impeccable animate` is for interfaces that feel lifeless, where state changes are instant and jarring, where loading just pops in, where the user never quite trusts that their click registered. Use it to add the small motions that communicate what is happening: entrances, exits, feedback, transitions between states.
Do not use it to add bounces or elastic springs for the sake of energy. That is decoration, and this skill will not give it to you.
## How it works
The skill identifies static moments that would benefit from motion, then applies them with strict discipline:
1. **Entrances and exits**: elements appear and leave with 200 to 300ms fades plus subtle Y or scale, never layout properties.
2. **State feedback**: hover, active, focus, loading, success all communicate via motion instead of sudden swaps.
3. **Transitions between views**: shared-element transitions where it makes sense, fade-through otherwise.
4. **Progress and loading**: skeleton screens, determinate bars, motion that says "still working".
5. **Reduced motion**: every animation has a `prefers-reduced-motion` fallback.
Easing is always exponential (ease-out-quart, quint, or expo) because real objects decelerate smoothly. No bounce, no elastic, no linear for anything except progress indicators.
The skill animates `transform` and `opacity` only. If you find yourself animating `width`, `height`, `top`, or `left`, it is doing the wrong thing. Use `grid-template-rows` for height transitions.
## Try it
```
/impeccable animate the sign-up flow
```
Typical additions:
- Email input gets a focus glow on focus-visible (opacity + shadow, 180ms)
- Submit button shows a spinner inside itself on loading state, not a separate spinner next to it
- Success screen enters with opacity + translateY(8px), 260ms, ease-out-quart
- Error message slides down with grid-template-rows (not height), 220ms
- `@media (prefers-reduced-motion: reduce)` fallback for every transition
## Pitfalls
- **Asking for "more animation".** Animate is not a dial. It adds where motion communicates, not everywhere.
- **Removing the reduced-motion fallbacks.** The skill adds them automatically. Non-negotiable for accessibility.
+99
View File
@@ -0,0 +1,99 @@
---
tagline: "Five-dimension technical quality check with P0 to P3 severity."
---
<div class="docs-viz-hero">
<div class="docs-viz-report">
<div class="docs-viz-report-head">
<div>
<div class="docs-viz-report-title">/impeccable audit the checkout flow</div>
<div class="docs-viz-report-target">src/checkout/**</div>
</div>
<div class="docs-viz-report-score">
<span class="docs-viz-report-score-num">2.6</span>
<span class="docs-viz-report-score-out">/ 4</span>
</div>
</div>
<div class="docs-viz-report-dims">
<div class="docs-viz-report-dim">
<span class="docs-viz-report-dim-name">Accessibility</span>
<span class="docs-viz-report-dim-bar"><span class="docs-viz-report-dim-fill docs-viz-report-dim-fill--fail" style="width:50%"></span></span>
<span class="docs-viz-report-dim-score">2 / 4</span>
</div>
<div class="docs-viz-report-dim">
<span class="docs-viz-report-dim-name">Performance</span>
<span class="docs-viz-report-dim-bar"><span class="docs-viz-report-dim-fill" style="width:75%"></span></span>
<span class="docs-viz-report-dim-score">3 / 4</span>
</div>
<div class="docs-viz-report-dim">
<span class="docs-viz-report-dim-name">Theming</span>
<span class="docs-viz-report-dim-bar"><span class="docs-viz-report-dim-fill docs-viz-report-dim-fill--warn" style="width:62%"></span></span>
<span class="docs-viz-report-dim-score">2.5 / 4</span>
</div>
<div class="docs-viz-report-dim">
<span class="docs-viz-report-dim-name">Responsive</span>
<span class="docs-viz-report-dim-bar"><span class="docs-viz-report-dim-fill" style="width:75%"></span></span>
<span class="docs-viz-report-dim-score">3 / 4</span>
</div>
<div class="docs-viz-report-dim">
<span class="docs-viz-report-dim-name">Anti-patterns</span>
<span class="docs-viz-report-dim-bar"><span class="docs-viz-report-dim-fill docs-viz-report-dim-fill--warn" style="width:70%"></span></span>
<span class="docs-viz-report-dim-score">2.8 / 4</span>
</div>
</div>
<div class="docs-viz-report-issues">
<span class="docs-viz-report-sev docs-viz-report-sev--p0">P0<span class="docs-viz-report-sev-n">2</span></span>
<span class="docs-viz-report-sev docs-viz-report-sev--p1">P1<span class="docs-viz-report-sev-n">5</span></span>
<span class="docs-viz-report-sev docs-viz-report-sev--p2">P2<span class="docs-viz-report-sev-n">8</span></span>
<span class="docs-viz-report-sev docs-viz-report-sev--p3">P3<span class="docs-viz-report-sev-n">14</span></span>
</div>
</div>
<p class="docs-viz-caption">Five dimensions scored 0 to 4, each finding tagged P0 (blocks release) to P3 (polish). Audit documents; it doesn't fix. Route the findings into <code>/impeccable harden</code>, <code>/impeccable polish</code>, or <code>/impeccable optimize</code>.</p>
</div>
## When to use it
`/impeccable audit` is the technical counterpart to `/impeccable critique`. Where `/impeccable critique` asks "does this feel right", `/impeccable audit` asks "does this hold up". It runs accessibility, performance, theming, responsive design, and anti-pattern checks against the implementation, scores each dimension 0 to 4, and produces a plan with P0 to P3 severity ratings.
Use it before shipping, during a quality sprint, or whenever a tech lead says "we should really look at accessibility".
## How it works
The skill scans your code across five dimensions:
1. **Accessibility**: WCAG contrast, ARIA, keyboard nav, semantic HTML, form labels.
2. **Performance**: layout thrashing, expensive animations, missing lazy loading, bundle weight.
3. **Theming**: hard-coded colors, dark mode coverage, token consistency.
4. **Responsive**: breakpoint behavior, touch targets, mobile viewport handling.
5. **Anti-patterns**: the same deterministic 25 checks the detector runs.
Each dimension gets a 0 to 4 score. Each finding gets a severity: P0 blocks the release, P1 should fix this sprint, P2 is next cycle, P3 is polish. You get back a single document you can paste into a ticket tracker.
Audit does not fix anything. It documents. Route the findings to `/impeccable polish`, `/impeccable harden`, or `/impeccable optimize` depending on the category.
## Try it
```
/impeccable audit the checkout flow
```
Expected output:
```
Accessibility: 2/4 (partial)
P0: Missing form labels on 4 inputs
P1: Contrast 3.1:1 on disabled button state
P2: No visible focus indicator on custom dropdown
Performance: 3/4 (good)
P1: Hero image not lazy-loaded (340KB)
...
```
Hand the P0s to `/impeccable harden`, the theming and typography P1s to `/impeccable typeset` and `/impeccable polish`, the rest to `/impeccable polish`.
## Pitfalls
- **Confusing it with `/impeccable critique`.** Audit is implementation quality. Critique is design quality. Run both for a full picture.
- **Fixing P3s before P0s.** The severity scale exists for a reason. Start at the top.
- **Skipping the dimensions you think are fine.** Theming and responsive are the ones most people assume are fine until they are not.
+40
View File
@@ -0,0 +1,40 @@
---
tagline: "Push safe designs toward impact without sliding into chaos."
---
## When to use it
Reach for `/impeccable bolder` when the interface looks like every other interface. Generic sans, medium weights, soft shadows, modest accent color, reasonable spacing, forgettable. The design is not wrong, it is just safe. Use bolder when a project can handle presence and the current state is not bringing any.
Do not use it on dashboards people stare at for hours. Boldness earns its place on marketing pages, hero moments, and content features. Not in operator tools.
## How it works
The skill amplifies four axes without breaking usability:
1. **Scale**: display type gets pushed to clamp(3rem, 6vw, 6rem) or beyond. Headlines that fill the viewport, not hedge it.
2. **Weight contrast**: light 300 against heavy 800 instead of medium against regular. Real tension, not a shrug.
3. **Color commitment**: the accent color shows up at full strength, not diluted. Backgrounds can take a stance (ink, accent, cream) instead of all-paper.
4. **Compositional confidence**: asymmetry, off-grid, pullquotes, hanging punctuation, scale jumps. The layout has a voice.
The skill does not add more. It amplifies what is already there. If the design has three colors, bolder does not add a fourth, it commits harder to the three.
## Try it
```
/impeccable bolder the landing page hero
```
Expected changes:
- Hero heading from 3rem to clamp(3.5rem, 7vw, 6.5rem), display font, weight 700
- Subhead from regular to italic at 1.5rem, pulled 8px left of the heading for optical alignment
- Background switches from paper to a cream-to-paper gradient, creating a warmer container
- CTA button fills, drops shadow removed, border radius reduced, hover state inverts colors
- Supporting image pushed slightly off-grid with a negative top margin, creating asymmetry
## Pitfalls
- **Running it on the wrong page.** Product dashboards, settings, and forms should not be bold. They should be legible. Use `/impeccable layout` or `/impeccable polish` instead.
- **Confusing bold with loud.** Bold means committed and confident. Loud means shouting. Bolder is the former. If the result feels aggressive, follow up with `/impeccable quieter`.
- **Pairing it with `/impeccable delight` in the same pass.** Delight works best against a stable visual baseline. Bold first, stabilize, then delight.
+42
View File
@@ -0,0 +1,42 @@
---
tagline: "Rewrite confusing UX copy so interfaces explain themselves."
---
## When to use it
`/impeccable clarify` is for interface text that makes people stop and think. Confusing labels, ambiguous button copy, error messages that blame the user, tooltips that repeat the label, empty states that say nothing useful. Use it when the problem is not the layout or the color, it is the words.
Good triggers: "users do not understand this field", "the error message is not helpful", "I cannot write good button copy", "this tooltip is a waste".
## How it works
The skill rewrites text across the surfaces where most UX copy problems live:
1. **Labels and field hints**: direct, specific, say what is expected.
2. **Button copy**: verb-first, describes the outcome, not the action. "Save changes" not "OK".
3. **Error messages**: explain what went wrong, whose fault it is, and what to do next. Never blame the user.
4. **Empty states**: orient the user, explain why the state is empty, offer a next step.
5. **Tooltips and helper text**: add information the label cannot carry, never restate it.
6. **Confirmation dialogs**: name the consequences, not the action.
The skill uses the audience and mental state from `PRODUCT.md` to tune voice. Technical audience gets precise language. Consumer audience gets plain speech. Rushed users get short text. Anxious users (payment, delete) get reassurance.
## Try it
```
/impeccable clarify the billing form
```
Before and after, typical:
- Label "Billing address" → "Address on your card"
- Placeholder "Enter your VAT ID" → "VAT ID (optional, for business)"
- Error "Invalid input" → "This card number is 15 digits. You entered 14."
- Button "Submit" → "Charge $29 and subscribe"
- Empty state "No transactions yet" → "Your first charge will show up here after your first order."
## Pitfalls
- **Writing cleverer, not clearer.** Clarify is not for voice upgrades. If the copy is already clear, do not reach for this skill. Use `/impeccable delight` instead when you want personality.
- **Skipping the audience question.** Clarify needs to know who is reading. If `PRODUCT.md` does not specify audience technical level, the rewrites will be generic.
- **Running clarify on marketing copy.** Clarify is for functional UX text: labels, errors, instructions. Marketing copy needs a different set of moves and a human writer.
+38
View File
@@ -0,0 +1,38 @@
---
tagline: "Add strategic color to monochrome interfaces without going garish."
---
## When to use it
`/impeccable colorize` is the counterweight to "everything is gray". Dashboards that read as a beige wall, forms with no accent, content pages that could be any SaaS product. Reach for it when the interface is functional but emotionally flat, and you want warmth without tipping into the AI color palette (purple-to-pink, cyan neon, dark mode glow).
## How it works
The skill starts by reading your brand color if one exists, then decides where color earns its place:
1. **Primary action** gets the strongest expression of the brand hue.
2. **Secondary accents** get muted or tinted variants, not a second full color.
3. **Neutrals** get tinted toward the brand hue at low chroma (around 0.005 to 0.01), which is nearly invisible per pixel but creates subconscious cohesion.
4. **Content categories** get a limited, intentional accent system, not a rainbow.
Importantly, it uses OKLCH rather than HSL so that equal lightness steps look equal. As lightness moves toward the extremes, chroma drops automatically. This is how you get color that feels considered instead of computed.
## Try it
```
/impeccable colorize the dashboard
```
Expected diff:
- Brand color moved from a hardcoded hex to `--color-accent: oklch(62% 0.18 240)`
- Neutrals tinted with 0.007 chroma toward the brand hue
- Primary button gets the full accent, secondary buttons get ink/mist
- Chart series uses 3 distinct hues, all at matched lightness so no series visually dominates
- Empty state illustration picks up a soft accent wash
## Pitfalls
- **Running it without a brand hue.** Colorize needs a starting point. If `PRODUCT.md` does not specify one, it will ask. Do not let it pick from the AI color palette defaults.
- **Expecting it to fix the AI color palette problem.** If your design already has purple gradients and cyan neon, you need `/impeccable quieter` first, then colorize can rebuild.
- **Using it on already-colorful interfaces.** That is a `/impeccable quieter` job. Colorize adds, it does not subtract.
+68
View File
@@ -0,0 +1,68 @@
---
tagline: "Shape the design, then build it, all in one flow."
---
<div class="docs-viz-hero">
<div class="docs-viz-flow">
<div class="docs-viz-flow-step">
<span class="docs-viz-flow-num">01</span>
<span class="docs-viz-flow-name">Shape</span>
<span class="docs-viz-flow-hint">Discovery interview. Purpose, users, constraints, direction.</span>
</div>
<div class="docs-viz-flow-step">
<span class="docs-viz-flow-num">02</span>
<span class="docs-viz-flow-name">Load references</span>
<span class="docs-viz-flow-hint">Spatial, typography, motion, color, interaction.</span>
</div>
<div class="docs-viz-flow-step">
<span class="docs-viz-flow-num">03</span>
<span class="docs-viz-flow-name">Build</span>
<span class="docs-viz-flow-hint">Structure, hierarchy, type, color, states, motion, responsive.</span>
</div>
<div class="docs-viz-flow-step docs-viz-flow-step--accent">
<span class="docs-viz-flow-num">04</span>
<span class="docs-viz-flow-name">Iterate visually</span>
<span class="docs-viz-flow-hint">Check in browser, refine until it matches the brief.</span>
</div>
</div>
<p class="docs-viz-caption">Every phase is non-skippable. The discovery step is where most AI output fails: by the time code exists, the thinking is locked in.</p>
</div>
## When to use it
`/impeccable craft` is the end-to-end build command. Give it a feature description and it runs the whole pipeline: structured discovery, reference loading, implementation, visual iteration. Use it when you are starting a new feature from zero and want the whole workflow in one invocation.
Reach for it when:
- **You are building a new feature and want the full flow.** You do not want to manage the steps yourself.
- **You know what you are building but not how it should look.** The discovery phase forces the design thinking before implementation locks it in.
- **You want visual iteration by default.** `craft` checks the result in a browser and refines until the polish is high, instead of shipping the first working version.
If you only want the thinking without the code, use `/impeccable shape` standalone. If you already have a clear vision and just want to build, call `/impeccable` directly with your feature description. `craft` sits in between: structured, complete, opinionated.
## How it works
`craft` runs four phases in order:
1. **Shape the design.** Runs `/impeccable shape` internally: a short discovery conversation about purpose, users, content, constraints, and goals. The output is a design brief you can read and push back on.
2. **Load references.** Based on the brief, pulls in the right reference files (spatial, typography, motion, color, interaction, responsive, UX writing) so the model has the relevant principles loaded before it starts coding.
3. **Build.** Implements the feature in a deliberate order: structure first, then spacing and hierarchy, then type and color, then states, then motion, then responsive. Every decision traces back to the brief.
4. **Visual iteration.** Opens the result in a browser, checks it against the brief and the anti-pattern catalog, and refines until it matches the intent. This step is critical. The first working version is never the shipped version.
The discovery phase is non-skippable and that is the point. Most AI-generated UIs fail because nobody asked what the user was trying to accomplish before the model started writing JSX. `craft` inverts that.
## Try it
```
/impeccable craft a pricing page for a developer tool
```
Expect a 5 to 10 question discovery interview first. Questions about your audience, the product's personality, the emotional tone you want, anti-references, and constraints. Then a design brief. Then implementation, with the browser checked at each stage. Expect multiple iteration rounds in the visual polish phase.
The whole run is longer than a typical command because it includes the thinking, the building, and the refining. That is the trade: more upfront structure, less cleanup afterwards.
## Pitfalls
- **Using it for small changes.** `craft` is for new features, not touch-ups. For existing code, reach for `/impeccable polish`, `/impeccable critique`, or a specific refinement command instead.
- **Rushing the discovery phase.** The interview feels slow compared to "just start coding". It is not. Answering the questions carefully produces a sharper brief, which produces a sharper build, which produces fewer rewrites.
- **Skipping the visual iteration.** The phase exists for a reason. The gap between "technically works" and "feels right" is closed with visual polish, not code review. Let it run.
+109
View File
@@ -0,0 +1,109 @@
---
tagline: "A design review with scoring, persona tests, and automated detection."
---
<div class="docs-viz-hero">
<div class="docs-viz-critique">
<div class="docs-viz-critique-head">
<div class="docs-viz-critique-verdict">
<span class="docs-viz-critique-verdict-label">AI slop verdict</span>
<span class="docs-viz-critique-verdict-value">FAIL</span>
</div>
<span class="docs-viz-report-target">gradient-text &middot; ai-color-palette &middot; nested-cards</span>
</div>
<div class="docs-viz-critique-cols">
<div>
<div class="docs-viz-critique-col-title">Heuristics (Nielsen)</div>
<div class="docs-viz-critique-heuristics">
<div class="docs-viz-critique-heur">
<span>Visibility of status</span>
<span class="docs-viz-critique-heur-score docs-viz-critique-heur-score--good">3</span>
</div>
<div class="docs-viz-critique-heur">
<span>Match with real world</span>
<span class="docs-viz-critique-heur-score docs-viz-critique-heur-score--ok">2</span>
</div>
<div class="docs-viz-critique-heur">
<span>Consistency & standards</span>
<span class="docs-viz-critique-heur-score docs-viz-critique-heur-score--ok">2</span>
</div>
<div class="docs-viz-critique-heur">
<span>Error prevention</span>
<span class="docs-viz-critique-heur-score docs-viz-critique-heur-score--good">3</span>
</div>
<div class="docs-viz-critique-heur">
<span>Recognition over recall</span>
<span class="docs-viz-critique-heur-score docs-viz-critique-heur-score--bad">1</span>
</div>
</div>
</div>
<div>
<div class="docs-viz-critique-col-title">Personas</div>
<div class="docs-viz-critique-personas">
<div class="docs-viz-critique-persona">
<div>
<span class="docs-viz-critique-persona-name">The evaluator</span>
<span class="docs-viz-critique-persona-note">Comparing us to two alternatives on a Tuesday evening.</span>
</div>
<span class="docs-viz-critique-persona-score">2 / 4</span>
</div>
<div class="docs-viz-critique-persona">
<div>
<span class="docs-viz-critique-persona-name">The returning user</span>
<span class="docs-viz-critique-persona-note">Knows the product, on mobile, in a hurry.</span>
</div>
<span class="docs-viz-critique-persona-score">3 / 4</span>
</div>
<div class="docs-viz-critique-persona">
<div>
<span class="docs-viz-critique-persona-name">The skeptic</span>
<span class="docs-viz-critique-persona-note">Has seen every SaaS landing and is bored.</span>
</div>
<span class="docs-viz-critique-persona-score">1 / 4</span>
</div>
</div>
</div>
</div>
</div>
<p class="docs-viz-caption">The two passes (LLM design review plus the deterministic detector) merge into one prioritized list. What's working, what to fix, and the provocative questions worth answering before shipping.</p>
</div>
## When to use it
Reach for `/impeccable critique` when you want an honest second opinion on something you already built. Not "does it work" but "is it any good". The skill scores your interface against Nielsen's 10 heuristics, runs cognitive load checks, tests through persona lenses, and cross-references an automated detector for 25 concrete anti-patterns.
Use it when a page is functionally done and you want to know if it reads as intentional or as AI slop.
## How it works
`/impeccable critique` runs two independent assessments in parallel so they do not bias each other.
The first is an **LLM design review**: the model reads your source, visually inspects the live page if browser automation is available, and walks the impeccable skill's full DO/DON'T catalog. It scores Nielsen's heuristics, counts cognitive load failures, traces the emotional journey through the flow, and flags AI slop.
The second is an **automated detector** (`npx impeccable detect`) that deterministically finds gradient text, purple palettes, side-tab borders, nested cards, line length problems, and the other visible fingerprints of generic AI output.
The two reports merge into one prioritized list: what is working, the three to five things that need fixing, and the provocative questions worth answering before shipping.
## Try it
Point it at a page:
```
/impeccable critique the homepage hero
```
You get back a scored report. Typical shape:
- **AI slop verdict**: pass / fail with the specific tells
- **Heuristic scores**: 10 numbers, 0 to 4
- **Cognitive load**: failure count out of 8
- **Priority issues**: three to five items, each with what, why, and fix
- **Questions to answer**: the ones the interface itself cannot decide for you
From there, pair with `/impeccable polish` or `/impeccable distill` to act on the fixes.
## Pitfalls
- **Running it on incomplete work.** Critique is for finished pages. An empty state with three TODOs will score badly because it is not done, not because it is bad.
- **Ignoring the questions at the end.** They are usually the highest-leverage fixes.
- **Treating the heuristic scores as a grade.** They are diagnostic, not evaluative. A 3/4 on a heuristic that matters less for your context is fine.
+42
View File
@@ -0,0 +1,42 @@
---
tagline: "Small moments of personality that turn functional into memorable."
---
## When to use it
`/impeccable delight` is for interfaces that work but do not feel like anything. Use it when the core experience is solid and you want to add the small human touches that make people remember it: a considered empty state, a loading message with a point of view, a success animation that feels earned, a microcopy moment that makes someone smile.
It is a finishing skill. Never the first thing you run on a new build.
## How it works
The skill hunts for delight opportunities in the places most designers skip:
1. **Empty states**: instead of "No items yet", something with personality appropriate to the brand.
2. **Loading and waiting moments**: the best products turn waits into content.
3. **Success feedback**: a moment of celebration when something worth celebrating happens.
4. **Microcopy**: button labels, tooltips, error messages, placeholder text. Tiny copy with taste.
5. **Easter eggs and secondary states**: things users discover that reward paying attention.
The skill reads the brand tone from `PRODUCT.md`. A serious analytics tool gets serious delight (dry, precise, a little clever). A playful consumer app gets more overt personality. It does not force humor where humor is wrong for the audience.
The rule is: every delight moment must still work perfectly if you delete the delight. Nothing depends on the smile.
## Try it
```
/impeccable delight the first-run experience
```
Expected additions:
- Empty dashboard replaces "No data yet" with "Your dashboard is quiet. Let's fix that." plus a single-action CTA.
- Initial sync gets a 3-state loading message that advances: "Finding your accounts... / Pulling the last 30 days... / Making it look good...".
- First successful action triggers a one-time toast with a tiny celebratory moment. After that, just a quiet checkmark.
- Help tooltip on the tricky field has a voice that sounds like a person wrote it.
## Pitfalls
- **Forcing humor.** Not every brand is playful. If the brand voice in `PRODUCT.md` is "clinical and precise", delight adds clever restraint, not jokes.
- **Over-decorating.** One moment of delight is memorable. Twenty becomes noise. The skill is conservative on purpose.
- **Running delight before polish.** Polish fixes what is wrong. Delight adds what is missing. In that order.
+44
View File
@@ -0,0 +1,44 @@
---
tagline: "Ruthless subtraction. Strip designs to their essence."
---
## When to use it
`/impeccable distill` removes what should not be there. Competing buttons, redundant information, decorative borders, three fonts where one works, six navigation items where three belong. Use it when an interface feels cluttered, busy, or like it is trying to do too much at once.
Reach for it after `/impeccable critique` flags "cognitive load" or "visual noise", or any time a page has grown by accretion and no one has done the editing.
## How it works
The skill starts from one question: what is the single job this interface is trying to do? Everything that does not help that job is on the chopping block.
It works in two passes:
1. **Assess the complexity sources**. Too many elements, excessive variation, information overload, visual noise, confusing hierarchy, feature creep. Name each one.
2. **Edit ruthlessly**. Remove what is not essential. Combine what can be combined. Hide what can wait. Consolidate variation into a single treatment. Commit to a single visual language.
The principle: simplicity is not about fewer features. It is about fewer obstacles between users and their goals. Every element on the page has to justify its existence.
## Try it
```
/impeccable distill this dashboard
```
Before: four card styles, three button variants, two header treatments, a sidebar with 14 items grouped into 5 sections.
After a `/distill` pass, typical changes:
- Collapse the four card styles into one
- Pick one button variant, demote the others to text links
- Unify the headers
- Group the sidebar into 3 sections, not 5
- Hide advanced options behind a disclosure
Fewer things. Each one clearer.
## Pitfalls
- **Confusing distill with delete.** Distill removes obstacles. It does not remove features users need. If a user relies on something daily, find a way to keep it quietly, not a way to cut it.
- **Running it too early.** If the feature is still growing, distilling it now means distilling the same thing again next week. Wait until the shape is stable.
- **Expecting it to replace hierarchy work.** Sometimes the right fix is not removing things, it is arranging them. Reach for `/impeccable layout` when the problem is layout, not quantity.
+114
View File
@@ -0,0 +1,114 @@
---
tagline: "Generate a spec-compliant DESIGN.md that captures your visual system so every AI agent stays on-brand."
---
<div class="docs-viz-hero">
<div class="docs-viz-file">
<div class="docs-viz-file-header">
<span class="docs-viz-file-name">DESIGN.md</span>
<span class="docs-viz-file-status">Google Stitch format</span>
</div>
<div class="docs-viz-designmd-section">
<div class="docs-viz-designmd-head">
<span class="docs-viz-designmd-num">01</span>
<span class="docs-viz-designmd-title">Overview</span>
</div>
<p class="docs-viz-designmd-note">Creative North Star: <em>"The Editorial Sanctuary."</em> Quiet type, generous air, one committed accent.</p>
</div>
<div class="docs-viz-designmd-section">
<div class="docs-viz-designmd-head">
<span class="docs-viz-designmd-num">02</span>
<span class="docs-viz-designmd-title">Colors</span>
</div>
<div class="docs-viz-designmd-swatches" aria-hidden="true">
<span class="docs-viz-designmd-swatch" style="background:#1a1a1a"></span>
<span class="docs-viz-designmd-swatch" style="background:#f5f3ef"></span>
<span class="docs-viz-designmd-swatch" style="background:oklch(60% 0.22 30)"></span>
<span class="docs-viz-designmd-swatch" style="background:oklch(90% 0.02 30)"></span>
</div>
</div>
<div class="docs-viz-designmd-section">
<div class="docs-viz-designmd-head">
<span class="docs-viz-designmd-num">03</span>
<span class="docs-viz-designmd-title">Typography</span>
</div>
<div class="docs-viz-designmd-type">
<span class="docs-viz-designmd-type-display">Aa</span>
<span class="docs-viz-designmd-type-body">Cormorant Garamond &middot; Instrument Sans</span>
</div>
</div>
<div class="docs-viz-designmd-section">
<div class="docs-viz-designmd-head">
<span class="docs-viz-designmd-num">04</span>
<span class="docs-viz-designmd-title">Elevation</span>
</div>
<p class="docs-viz-designmd-note">Flat by default. Shadows appear only as a response to state.</p>
</div>
<div class="docs-viz-designmd-section">
<div class="docs-viz-designmd-head">
<span class="docs-viz-designmd-num">05</span>
<span class="docs-viz-designmd-title">Components</span>
</div>
<div class="docs-viz-designmd-comps" aria-hidden="true">
<span class="docs-viz-designmd-btn">Subscribe</span>
<span class="docs-viz-designmd-chip">filter</span>
<span class="docs-viz-designmd-card">card</span>
</div>
</div>
<div class="docs-viz-designmd-section">
<div class="docs-viz-designmd-head">
<span class="docs-viz-designmd-num">06</span>
<span class="docs-viz-designmd-title">Do's and Don'ts</span>
</div>
<div class="docs-viz-designmd-rules">
<span class="docs-viz-designmd-do">Tint neutrals toward the accent hue.</span>
<span class="docs-viz-designmd-dont">Gradient text for emphasis.</span>
</div>
</div>
</div>
<p class="docs-viz-caption">The six sections are fixed, in a fixed order, with fixed names. Alongside, <code>DESIGN.json</code> ships as a machine-readable sidecar for the Live Mode design panel.</p>
</div>
## When to use it
Run `/impeccable document` once you have enough of a visual system to document: colors, typography, at least a button and a card. The command scans your codebase, extracts the tokens and component patterns it finds, and writes a `DESIGN.md` at the project root that follows the [Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/), six sections in a fixed order, interoperable with every other DESIGN.md-aware tool.
Reach for it when:
- **You just ran `/impeccable teach`** and `PRODUCT.md` now exists. Document is the matching visual-side file.
- **A command nudged you toward it.** Live, craft, and polish all read DESIGN.md. If it is missing, the skill suggests running document first.
- **The design has drifted** from an older DESIGN.md and the file no longer describes the live system.
- **Before a large redesign**, to capture current state as a reference for the next direction.
For projects with no code yet (fresh `teach` run, nothing built), there is a seed mode: `/impeccable document --seed` asks five quick strategic questions (color strategy, type direction, motion energy, references, anti-references) and writes a scaffold. Re-run in scan mode once there is code.
## How it works
The scan pass finds design assets in priority order: CSS custom properties, Tailwind config, CSS-in-JS themes, design token files, component source, the global stylesheet, and finally computed styles from the live rendered output if a browser is available. It auto-extracts everything it can, then asks one grouped question for the parts that need creative input: the **Creative North Star** (a single named metaphor for the whole system, like "The Editorial Sanctuary"), descriptive color names, the elevation philosophy, and the component character.
Output is a DESIGN.md with exactly six sections: Overview, Colors, Typography, Elevation, Components, Do's and Don'ts. Headers are fixed character-for-character so the file is parseable by other tools. Alongside it, `DESIGN.json` is written as a machine-readable sidecar. That sidecar is what the live-mode design panel uses to render *this project's* actual button, input, nav, and card tiles instead of a generic approximation.
Every other command reads DESIGN.md on invocation. Variants, polishes, audits, and new features inherit the visual system without being told.
## Try it
```
/impeccable document
```
On a project with tokens already defined, this takes about two minutes: the scan finds your palette and type stack, you pick a North Star from 2 or 3 options, confirm descriptive color names ("Deep Muted Teal-Navy", not "blue-800"), and the file lands at the project root.
On a fresh project:
```
/impeccable document --seed
```
Five questions, about five minutes. The file is a scaffold, marked with a `<!-- SEED -->` comment so it is honest about what it is. Re-run without the flag once you have implemented tokens.
## Pitfalls
- **Running it too early.** On a project with no implemented tokens, seed mode is right. Do not fabricate a full spec the code cannot back up. A fake DESIGN.md is worse than no DESIGN.md.
- **Treating DESIGN.md as documentation for humans only.** It is primarily for the AI. Every other command reads it. The format's forcefulness ("never", "always", Named Rules) is intentional.
- **Adding a Layout / Motion / Responsive top-level section.** The spec has six sections, in a fixed order, with fixed names. Fold layout or motion content into Overview (philosophy-level rules) or Components (per-component behavior).
- **Overwriting an existing DESIGN.md silently.** Document always confirms first. If you want to start fresh, rename the existing file out of the way or explicitly tell the skill to overwrite.
+64
View File
@@ -0,0 +1,64 @@
---
tagline: "Pull reusable components, tokens, and patterns into the design system."
---
<div class="docs-viz-hero">
<div class="docs-viz-flow">
<div class="docs-viz-flow-step">
<span class="docs-viz-flow-num">01</span>
<span class="docs-viz-flow-name">Discover drift</span>
<span class="docs-viz-flow-hint">Repeated hex values, button variants, spacing scales, text styles.</span>
</div>
<div class="docs-viz-flow-step">
<span class="docs-viz-flow-num">02</span>
<span class="docs-viz-flow-name">Propose primitives</span>
<span class="docs-viz-flow-hint">Token names, component APIs with variant + size, text styles.</span>
</div>
<div class="docs-viz-flow-step docs-viz-flow-step--accent">
<span class="docs-viz-flow-num">03</span>
<span class="docs-viz-flow-name">Migrate call sites</span>
<span class="docs-viz-flow-hint">Replace duplicated CSS with the new primitives. No orphan code left behind.</span>
</div>
</div>
<p class="docs-viz-caption">The skill only extracts what's used three or more times with the same intent. Two usages are not a pattern, and migration always happens in the same pass.</p>
</div>
## When to use it
`/impeccable extract` is for the moment your codebase has accidentally become a design system. Repeated button styles in 12 places. Three variants of the same card. Hex colors scattered throughout. Hand-rolled spacing that accidentally matches a scale. Reach for it when you want to consolidate this drift into reusable primitives.
Use it after a product has shipped enough features to reveal the patterns. Premature extraction creates abstractions that do not match reality.
## How it works
The skill discovers the design system structure first, then identifies extraction opportunities:
1. **Tokens**: find repeated literal values (colors, spacing, radii, shadows, font sizes). Propose token names, add to the token system, replace usages.
2. **Components**: find UI patterns that repeat with minor variation (buttons, cards, inputs, modals). Extract into a single component with variants, migrate callers.
3. **Composition patterns**: find layout or interaction patterns that repeat (form rows, toolbar groups, empty states). Extract into composition primitives.
4. **Type styles**: find repeated font-size + weight + line-height combinations. Extract into text styles.
5. **Animation patterns**: find repeated easing, duration, or keyframe combinations. Extract into motion tokens.
The skill is cautious. It only extracts things used three or more times, with the same intent. It never extracts "because it might be reused later". Premature abstraction is worse than duplication.
## Try it
```
/impeccable extract the button styles
```
Expected output:
- Found 14 button instances across 8 files
- 4 distinct variants: primary (filled accent), secondary (bordered), ghost (text-only), destructive (filled red)
- All 4 variants use the same size scale (small, default, large)
- Extracted into `<Button variant="primary" size="default">` with token-driven styles
- Migrated 14 call sites, removed ~180 lines of duplicated CSS
- Added 3 missing tokens: `--button-radius`, `--button-padding-y`, `--button-padding-x`
## Pitfalls
- **Extracting too early.** Two usages are not a pattern. Three might be. Wait until the pattern is obvious.
- **Over-generalizing.** The extracted component should match the current use cases closely, not anticipate every possible future one. You can always add variants later.
- **Forgetting the migration.** Extraction without migration leaves the old duplicated code around and creates a third way of doing the same thing. Always migrate in the same pass.
- **Extracting things that differ in intent.** Two buttons that look similar but serve different purposes (primary action vs link styled as button) should probably stay separate.
+44
View File
@@ -0,0 +1,44 @@
---
tagline: "Make interfaces production-ready. Edge cases, i18n, error states, overflow."
---
## When to use it
`/impeccable harden` is for the day your interface meets reality. Real user data is messy: names that are 60 characters long, product titles in German, prices in the billions, 500 errors, offline modes, right-to-left text. Designs that only work with perfect data are not production-ready.
Reach for it before launch, before opening to a new market, or any time a bug report starts with "our user had a really long name and". For first-run flows, empty-state activation, and onboarding design, reach for `/impeccable onboard` instead.
## How it works
The skill works through four dimensions of real-world resilience:
1. **Text and data extremes**. Long text, short text, special characters, emoji, RTL, numbers in the billions, 1000-item lists.
2. **Error scenarios**. Network failures, API 4xx/5xx, validation errors, permission errors, rate limits, concurrent operations.
3. **Internationalization**. Long translations (German is often 30% longer than English), RTL languages, date and number formats, currency symbols, character sets.
4. **Device and context**. Touch targets, offline behavior, slow connections, low-power mode.
For each dimension it identifies the failure mode, then applies the concrete fix: overflow handling, informative error UI, i18n-safe layouts, pluralization, sensible fallbacks.
## Try it
Start with one page and one dimension:
```
/impeccable harden the user profile page for long names
```
Expected output:
- `.user-name` now has `text-overflow: ellipsis` with a tooltip for the full value
- `.bio` switched from fixed height to `max-height` with a "show more" disclosure
- Added an empty state for users with no bio
- Added a skeleton loader for the async avatar fetch
- Tested at name lengths 1, 20, 60, 200 characters
Run it per-page, not all at once. The first run is the biggest; subsequent runs find fewer issues as patterns solidify.
## Pitfalls
- **Waiting for a bug report.** Harden is preventative. If you find yourself fixing the same class of bug twice, run `/impeccable harden` across the feature.
- **Treating error and empty states as an afterthought.** Most hardening work is error and empty state UI. Budget time for it, not just a `catch` block.
- **Skipping i18n because "we are English-only for now".** i18n-safe layouts are still better layouts. Flexible containers, proper text wrapping, generous line-height. None of that hurts English.
+74
View File
@@ -0,0 +1,74 @@
---
tagline: "The design intelligence behind every command."
---
## When to use it
`/impeccable` is the home command. Call it directly when you want freeform design work with the full guidebook loaded, without picking a specialized command. It is the fallback you reach for when none of the 23 specialists (`audit`, `polish`, `critique`, and the rest) map cleanly onto what you are trying to do.
Reach for `/impeccable` directly when:
- **You are not sure which command fits.** Describe what you want in plain English and let the skill pick the right approach.
- **The work spans multiple disciplines.** "Redo this hero section" touches layout, type, color, and motion. One command cannot own that.
- **You want the full design intelligence without constraints.** Every reference file loaded, every anti-pattern checked, no pre-set workflow.
For structured flows, reach for the specialized commands in the sidebar. Run `/impeccable teach` first on any new project to establish PRODUCT.md and DESIGN.md. `/impeccable craft` chains a discovery interview into a full build with live visual iteration. `/impeccable shape` produces a design brief without touching code. `/impeccable live` gives you a browser picker with three variants per element. The evaluation and refinement commands (`audit`, `critique`, `polish`, `typeset`, `layout`, `colorize`, and the rest) each own a specific slice of the work.
## How it works
Most AI-generated UIs fail the same way: generic fonts, purple gradients, card grids on card grids, glassmorphism everywhere. `/impeccable` gives your AI a strong point of view. It loads an opinionated design handbook plus a long list of anti-patterns, then pushes the model to commit to a specific aesthetic direction before writing a single line of code.
Two files at your project root shape everything the skill does:
- **`PRODUCT.md`** carries register (brand vs product), target users, brand personality, anti-references, design principles. Answers "who, what, why".
- **`DESIGN.md`** carries colors, typography, elevation, components, do's and don'ts, in the six-section Google Stitch format. Answers "how it looks".
Every command reads both files before generating. **Register** is the load-bearing switch. Brand (marketing, landing, portfolio, where design IS the product) and product (app UI, dashboards, tools, where design SERVES the product) have different defaults for type, motion, color, and density. Specifying it once in PRODUCT.md means `/impeccable typeset` will not push editorial-magazine fonts on a dashboard, and will not push product-fluent defaults on a campaign page. See the [brand vs product tutorial](/tutorials/brand-vs-product) for how the two diverge.
On first use in a project, the skill runs the `teach` flow automatically: a short interview that writes PRODUCT.md and then delegates to `/impeccable document` for DESIGN.md. Future commands read the files without asking again.
## Try it
```
/impeccable redo this hero section
```
```
/impeccable build me a pricing page for a developer tool
```
Both prompts are vague on purpose. `/impeccable` will pick a strong aesthetic direction consistent with your register, commit to non-default fonts, avoid the AI color palette, and make the kind of specific choices that a designer would make. No command name to pick first, no step-by-step workflow to follow.
For visual iteration in the browser rather than chat:
```
/impeccable live
```
Pick any element on your running dev server. Drop a comment or stroke. Get three production-quality variants hot-swapped in via HMR. Accept the one you want and it writes back to source.
## Pin commands back as shortcuts
v3.0 consolidated 18 standalone skills into a single `/impeccable` with 23 sub-commands. If you miss the short form of a specific command, pin it back:
```
/impeccable pin critique
```
From now on, `/critique` invokes `/impeccable critique` directly. It writes a lightweight redirect skill that delegates to the parent, so updates to the skill flow through without re-pinning.
Useful pins to try:
- `/impeccable pin polish` for final-pass work
- `/impeccable pin audit` for deterministic a11y/perf checks
- `/impeccable pin live` for the browser iteration flow
- `/impeccable pin critique` for design review
To remove: `/impeccable unpin critique`. Pins live as directories prefixed with `i-` in your harness skills folder (`.claude/skills/i-critique/`, `.cursor/skills/i-critique/`, etc.), so you can also delete them manually.
## Pitfalls
- **Treating it like a style guide.** It is an opinionated design partner, not a linter. The defaults exist to raise the floor, not to overrule your judgment. If you have a real reason to push back (brand guideline, accessibility constraint, user research), push back and explain why. The skill will work with you. What produces worse output is ignoring the opinion without a reason.
- **Expecting it to fix existing code.** `/impeccable` is for creation. For refinement, reach for `/impeccable polish`, `/impeccable distill`, or `/impeccable critique` instead.
- **Running it before `teach` has had a chance to save context.** On a fresh project it will interview you mid-flight, which is fine but slower. Running `/impeccable teach` explicitly as your very first command is a tiny bit smoother.
- **Skipping the register question.** Brand and product defaults diverge enough that running on the wrong register produces subtly off output. If `PRODUCT.md` has no `## Register` field (legacy), run `/impeccable teach` to add it.
+41
View File
@@ -0,0 +1,41 @@
---
tagline: "Fix layout, spacing, and visual rhythm."
---
## When to use it
`/impeccable layout` is for pages where nothing is technically wrong but nothing is breathing either. Equal padding everywhere, monotonous card grids, content that runs edge to edge, hierarchy that relies on size alone. Reach for it when a layout "feels off" and you cannot articulate why.
Good triggers: "everything feels crowded", "it reads like a wall", "I do not know where to look first".
## How it works
The skill runs through five layout dimensions:
1. **Spacing**: is the spacing scale consistent or are there random 13px gaps, are related elements grouped tightly with generous space between groups, is there any rhythm at all.
2. **Visual hierarchy**: does the eye land on the primary action within 2 seconds, is the hierarchy doing real work or is everything shouting.
3. **Grid and structure**: is there an underlying grid or is the layout random, are elements aligned to baselines.
4. **Rhythm**: does the page alternate between tight and generous spacing, or is everything uniform.
5. **Density**: is the layout cramped or is it wasteful, does density match the content type.
Fixes usually involve rebuilding the spacing scale, introducing asymmetry, collapsing monotonous grids into a mixed layout with hero and supporting elements, and giving the primary action real space.
## Try it
```
/impeccable layout the settings page
```
Typical changes:
- Spacing scale unified to 8 / 16 / 24 / 48 / 96px
- Section breaks at 48px, row gaps at 16px, form field groups at 8px
- Primary actions pulled out of the form flow with 32px buffer
- Decorative borders removed, replaced with spacing-driven grouping
- Sidebar and main column proportions rebalanced (280 / flex vs 25 / 75)
## Pitfalls
- **Confusing arrange with distill.** If the problem is too many things, run `/impeccable distill` first. Layout is for arranging what is already the right set.
- **Expecting it to rescue a broken grid.** If the page has no grid at all, arrange will build one. Just know that the diff is going to be larger than you expect.
- **Ignoring the hierarchy verdict.** If arrange says "nothing is primary", no amount of spacing work fixes that. You need a content decision, not a layout tweak.
+86
View File
@@ -0,0 +1,86 @@
---
tagline: "Iterate on UI in the browser. Pick an element, drop a comment, get three variants. Accept one and it writes to source."
---
<div class="docs-live-callout">
<span class="docs-live-callout-icon" aria-hidden="true">▸</span>
<span class="docs-live-callout-text">See it in action, with the animated demo, at <a href="/live-mode">/live-mode</a>. This page is the reference for what your AI harness reads when the command runs.</span>
</div>
<div class="docs-live-callout">
<span class="docs-live-callout-icon" aria-hidden="true">▸</span>
<span class="docs-live-callout-text"><strong>Status: alpha.</strong> Live Mode works end-to-end and is ready to try, but it still needs more testing against real-world repos and framework configs. Expect rough edges on uncommon setups, and please report what breaks.</span>
</div>
<div class="docs-viz-hero docs-viz-hero--plain">
<div class="docs-viz-live-frame">
<div class="docs-viz-live-chrome">
<span class="docs-viz-live-dot"></span>
<span class="docs-viz-live-dot"></span>
<span class="docs-viz-live-dot"></span>
<span class="docs-viz-live-url">localhost:3000</span>
</div>
<div class="docs-viz-live-stage docs-viz-live-stage--tall">
<div class="docs-viz-live-target">
<span class="docs-viz-live-kicker">No. 04</span>
<h3 class="docs-viz-live-title">Letters, <em>occasionally</em>.</h3>
<p class="docs-viz-live-body">A postcard from the editor, about once a month. No tracking pixels, no "just checking in."</p>
<button class="docs-viz-live-btn" type="button">Send me one</button>
</div>
<div class="docs-viz-live-outline" aria-hidden="true"></div>
<div class="docs-viz-live-ctx" aria-hidden="true">
<button class="docs-viz-live-ctx-nav" type="button" aria-label="Previous"></button>
<span class="docs-viz-live-ctx-counter">2 / 3</span>
<button class="docs-viz-live-ctx-nav" type="button" aria-label="Next"></button>
<span class="docs-viz-live-ctx-divider"></span>
<button class="docs-viz-live-ctx-accept" type="button">Accept</button>
</div>
<div class="docs-viz-live-gbar" aria-hidden="true">
<span class="docs-viz-live-gbar-brand">/</span>
<span class="docs-viz-live-gbar-btn is-active">Pick</span>
<span class="docs-viz-live-gbar-divider"></span>
<span class="docs-viz-live-gbar-x">✕</span>
</div>
</div>
</div>
<p class="docs-viz-caption">Live Mode mid-cycle: the picker outlines the element you chose, the context bar shows which variant you're on, and the global bar stays pinned to the bottom. Accept on this one writes Variant 2 back to source.</p>
</div>
## When to use it
Reach for `/impeccable live` when you want to iterate on something visually the way you would in a design tool, but keep production code as the output. The canvas-like flow of Figma without the round trip to an implementation step.
Use it for:
- **Exploring directions on a real element.** A hero section, a newsletter card, a pricing tier. Three genuinely different takes, side by side, on the actual page with the actual context.
- **Polishing a piece of UI that is almost right.** You know what feels off but cannot quite say it. Pick the element, scribble "more playful" or draw a stroke through the bit that bugs you, hit Go.
- **A quick A/B between two directions your team is debating.** Generate variants, accept nothing, walk away. The point was the comparison.
It is NOT for new greenfield features (reach for `/impeccable craft`) or whole-page redesigns (reach for `/impeccable` or a specialized refine command).
## How it works
One command brings up a picker overlay on top of your running dev server. You pick any element. A small context bar appears next to it. Type a freeform description or pick one of the action chips (`bolder`, `quieter`, `distill`, `polish`, `typeset`, `colorize`, `layout`, `animate`, `delight`, `overdrive`). Optionally drop comment pins or draw strokes directly on the element first, and the skill reads those as intent.
Hit Go. Three **production-quality variants** get generated, each anchored to a genuinely different design archetype (not three riffs on color) and hot-swapped into the page via your framework's HMR. Cycle through them with arrow keys. Accept one and the variant is written back to source. Discard all three and the original stays.
It supports Vite, Next.js (including monorepos), SvelteKit, Astro, Nuxt, and plain static HTML. If your dev server has a strict Content Security Policy, the first-run setup detects it and offers a one-time, dev-only patch so the picker can load. `DESIGN.md` wins on visual decisions, `PRODUCT.md` wins on voice: if you have both, variants stay on-brand without being told.
## Try it
```
/impeccable live
```
Open your dev server URL, pick the newsletter signup card, click the `delight` chip, hit Go. You will get three variants that vary across personality dimensions (a stamp-and-postcard feel, a typographic-surprise version, an illustrated-accent one), not three riffs on the same treatment.
Or pick a hero, type "more editorial, less SaaS", hit Go. The three variants anchor to different editorial archetypes (broadsheet masthead, catalog-style spec rows, oversized-glyph poster) rather than three shades of the same idea.
Stop live mode when you are done: say "stop live mode", close the tab, or hit the exit button on the picker bar.
## Pitfalls
- **Running it on a page that is still half-written.** Live variant generation needs context. If the element has placeholder copy, generic Lorem ipsum, or pre-stylesheet default formatting, variants will reflect that. Fill the content first.
- **Expecting it to make macro decisions.** Live mode iterates on a single picked element. For "redo the entire pricing page", reach for `/impeccable` or `/impeccable craft` instead.
- **Ignoring the fallback messages.** If the element lives in a generated file (a compiled template, a build output), the picker says so explicitly and offers to route the accept into true source. Do not force the accept into the generated file: the next build will wipe it.
- **Running it without PRODUCT.md or DESIGN.md when you care about brand fit.** Live will still generate, but the variants will lean toward generic defaults. Run `/impeccable teach` and `/impeccable document` first if the result needs to sound like your product.
+40
View File
@@ -0,0 +1,40 @@
---
tagline: "Design first-run experiences, empty states, and paths to value."
---
## When to use it
`/impeccable onboard` is for the moments that decide whether a new user sticks around: the first screen, the empty state, the setup flow, the product tour, the "what do I do now" gap. Reach for it when activation is weak, when new users drop off before reaching value, or when your product has empty states that say "no items yet" and stop there.
## How it works
The command starts from one question: what is the aha moment, and how fast can a new user get there. Every design decision points at that moment.
It works across the surfaces that shape first impressions:
1. **First-run experience**: the moments immediately after sign-up. Should the user see a tour, a blank canvas, a filled example, or nothing at all. Pick the approach that matches the product.
2. **Empty states**: every zero-data screen gets oriented. Where am I, why is this empty, what do I do next, what will it look like once it is full.
3. **Setup and installation**: required configuration is minimized, defaults are smart, each step explains why it matters.
4. **Progressive disclosure**: advanced features stay out of the way until they are earned.
5. **Activation events**: the moment a user first experiences the core value is instrumented and celebrated, quietly.
The command resists two common failure modes: over-tutorialized onboarding where users click through a carousel before they can touch anything, and zero-onboarding where users are dropped into an empty app and expected to figure it out.
## Try it
```
/impeccable onboard the editor
```
Typical output:
- First-run: replaces empty editor with a filled example document the user can modify. Cancel button discards the example, edit replaces the content with the user's work.
- Empty state on document list: "No documents yet. Create your first, or import from Notion, Google Docs, or Markdown."
- Setup: reduced from 6 required fields to 1 (workspace name). Everything else has a smart default and can be edited later in settings.
- Activation: the first time a user saves a document, a quiet toast says "Saved. Your work is in the cloud now." One-time, not repeated.
## Pitfalls
- **Adding a product tour as the default answer.** Most products do not need a tour. They need a better first screen. Tours are a crutch.
- **Designing onboarding without defining the aha moment.** If you cannot say in one sentence what the user should feel in the first 60 seconds, go back to `/impeccable shape` first.
- **Running onboard on a broken flow.** Fix the flow first. Onboarding cannot rescue a product where the core action is broken.
+56
View File
@@ -0,0 +1,56 @@
---
tagline: "Diagnose and fix UI performance from LCP to bundle size."
---
## When to use it
`/impeccable optimize` is for interfaces that feel slow. First paint takes forever, scrolling janks, images pop in late, interactions feel laggy, the bundle ships 800KB of JavaScript. Use it when the Web Vitals are bad or when users are complaining that things are sluggish.
Do not use it as premature optimization. If LCP is 1.1s and INP is 80ms, stop. The design work matters more.
## How it works
The skill works through five perf dimensions:
1. **Loading and Web Vitals**: LCP, INP, CLS. Identify what is blocking the first paint, what is delaying interaction, what is shifting layout.
2. **Rendering**: unnecessary re-renders, missing memoization, expensive reconciliation, layout thrash in loops.
3. **Animations**: is anything animating layout properties, are transforms and opacity the only thing touched, does `will-change` help or hurt here.
4. **Images and assets**: lazy loading, responsive images (`srcset`, `sizes`), modern formats (WebP, AVIF), dimensions set to prevent CLS.
5. **Bundle size**: unused imports, oversized dependencies, missing code-splitting, dead code.
The skill measures before and after. Every fix gets quantified. If a change does not move a metric, it gets rolled back.
## Try it
```
/impeccable optimize the homepage
```
Expected shape:
```
LCP: 3.2s → 1.4s
- Hero image preloaded (-800ms)
- Removed render-blocking font stylesheet (-240ms)
- Deferred analytics script (-180ms)
INP: 240ms → 90ms
- Debounced scroll handler
- Memoized expensive list render
- Removed synchronous layout read in event loop
CLS: 0.18 → 0.02
- Set dimensions on hero image and logo
- Reserved space for async header badge
Bundle: 340KB → 180KB
- Removed unused lodash import (52KB)
- Code-split the playground route (78KB)
- Dropped deprecated icon set (30KB)
```
## Pitfalls
- **Optimizing before measuring.** Without baseline metrics, you cannot tell what helped. Run `/impeccable optimize` with specific Web Vitals numbers, not vibes.
- **Chasing tiny wins.** A 20ms improvement in INP that takes a week is rarely worth it. Optimize has diminishing returns; know when to stop.
- **Forgetting to re-measure after every change.** The build could have made things worse in a way the skill did not predict. Verify.
+30
View File
@@ -0,0 +1,30 @@
---
tagline: "Push an interface past conventional limits. Shaders, physics, 60fps, cinematic transitions."
---
## When to use it
`/impeccable overdrive` is for the moments where you want to impress. A hero that uses WebGL. A table that handles a million rows. A dialog that morphs out of its trigger element. A form that validates in real-time with streaming feedback. A page transition that feels cinematic. Use it when the project budget allows for technical ambition and the outcome needs to feel extraordinary.
Do not use it on operator tools, dashboards, or anything where reliability beats spectacle. Overdrive burns complexity for effect, and that trade-off is only worth it on moments that matter.
## How it works
The skill picks one moment to make extraordinary and commits to it, rather than spreading effort across the whole interface. It then reaches for techniques most AI-generated UIs never touch: WebGL shaders, spring physics, Scroll Timeline, View Transitions, canvas animation, GPU-accelerated filters. Everything is budgeted, profiled, and tested at 60fps, with reduced-motion fallbacks baked in.
Overdrive output is announced with `──── ⚡ OVERDRIVE ────` so you know you are entering a more ambitious mode. Expect larger diffs, new dependencies, and implementation depth beyond what other skills produce.
## Try it
```
/impeccable overdrive the landing hero
```
One concrete run might replace a static hero with a WebGL shader background driven by mouse position, a display headline that reveals with a mask on scroll using the Scroll Timeline API, and a View Transition on the CTA that morphs into the next page. Plus a reduced-motion fallback that swaps all of it for a clean static composition.
## Pitfalls
- **Using it everywhere.** Overdrive works because it is rare. If every page has cinematic moments, none of them are cinematic.
- **Shipping without reduced-motion fallbacks.** Non-negotiable. Overdrive adds them automatically; do not remove them.
- **Ignoring performance.** Extraordinary moments still need to hit 60fps. If the effect drops frames, cut it or optimize. Slow spectacle is worse than simple done well.
- **Running overdrive before the base interface is solid.** Spectacle on a broken foundation reads as distraction, not delight.
+46
View File
@@ -0,0 +1,46 @@
---
tagline: "The meticulous final pass between good and great."
---
## When to use it
`/impeccable polish` is the last thing you run before shipping. It hunts down the small details that separate a shipped feature from a polished one: half-pixel misalignments, inconsistent spacing, forgotten focus states, loading transitions that flash, copy that drifts in tone. It also aligns the feature with your design system, replacing hard-coded values with tokens, swapping custom components for shared ones, and fixing any drift from established patterns.
Reach for it when the feature is functionally complete, nothing is broken, and something still feels off. Also reach for it when a feature has drifted from the design system and needs to be pulled back in line.
## How it works
Polish starts by discovering the design system (tokens, spacing scale, shared components), then works methodically across six dimensions:
1. **Visual alignment and spacing**: pixel-perfect grid adherence, consistent spacing scale, optical alignment on icons.
2. **Typography**: hierarchy consistency, line length, widows and orphans, kerning on headlines.
3. **Color and contrast**: token usage, theme parity, WCAG ratios, focus indicators.
4. **Interaction states**: hover, focus, active, disabled, loading, error, success. Every state accounted for.
5. **Transitions and motion**: smooth easing, no layout jank, respect for `prefers-reduced-motion`.
6. **Copy**: consistent voice, correct tense, no placeholder strings, no stray TODOs.
The skill is explicit about one thing: polish is the last step, not the first. If the feature is not functionally complete, polishing it is wasted work.
## Try it
```
/impeccable polish the pricing page
```
A healthy run looks like:
```
Visual alignment: fixed 3 off-grid elements (8px baseline)
Typography: tightened h1 kerning, fixed widow on testimonial
Interaction: added hover state on FAQ items, focus ring on email input
Motion: softened modal entrance, added reduced-motion fallback
Copy: removed one "Lorem ipsum" stray, aligned button voice
```
Five small fixes, no rewrites. That is the shape of a good polish pass.
## Pitfalls
- **Polishing work that is not done.** If there are TODOs in the code, you are not ready. Run `/impeccable polish` on finished features only.
- **Treating polish as redesign.** Polish refines what exists. If you find yourself rearchitecting a layout, you needed `/impeccable critique` or `/impeccable layout` instead.
- **Running `/impeccable polish` without `/impeccable audit` first.** Polish catches feel-based issues. Audit catches measurable ones. Use both.
+40
View File
@@ -0,0 +1,40 @@
---
tagline: "Tone down designs that are shouting without losing their intent."
---
## When to use it
`/impeccable quieter` is the counterweight to `/impeccable bolder`. Reach for it when an interface is visually aggressive, overstimulating, or trying to do too many things at full volume. Neon on dark, gradient text everywhere, 6 accent colors, everything animated, 20px shadows. Use quieter when the design needs to breathe and you want refinement without losing the point of view.
Also useful after `/impeccable bolder` goes a little too far.
## How it works
The skill works by reduction across four axes:
1. **Color**: desaturate, lower chroma in OKLCH, pull accents back to a single primary plus muted support. No more than two intentional colors.
2. **Contrast**: soften extreme darks and lights, pull the range in. Backgrounds move from pure white and pure black to paper and ink.
3. **Decoration**: remove shadows that are not doing work, drop borders that are not carrying structure, retire gradients that exist for energy rather than hierarchy.
4. **Motion and effect**: slow animations down, remove anything that auto-plays, drop parallax and blur unless they serve readability.
The skill preserves the design's intent. If the original had a point of view, the quieter version has the same point of view with more confidence. Refinement, not neutralization.
## Try it
```
/impeccable quieter the pricing page
```
Typical diff:
- Gradient text on the price removed, replaced with solid ink at one weight heavier
- Three accent colors reduced to one (magenta), the other two become neutral variants
- Card shadows reduced from `0 20px 40px rgba(0,0,0,0.2)` to `0 1px 0 var(--color-mist)` (a hairline)
- Background switches from dark gradient to paper with a subtle cream wash at the top
- Hero animation from 1.2s easeOut with 3 staggered elements to a single 260ms fade-in
## Pitfalls
- **Over-applying.** Quieter can strip personality if you run it on something that was already measured. Use it when the design is too loud, not when it is correctly assertive.
- **Confusing quieter with distill.** Quieter reduces intensity. Distill removes elements. They are different moves.
- **Running it in response to a critique that says "too busy".** Busy usually means too many things, not too loud. Try `/impeccable distill` first.
+73
View File
@@ -0,0 +1,73 @@
---
tagline: "Think before you build. Produce a design brief through discovery, not guesswork."
---
<div class="docs-viz-hero">
<div class="docs-viz-file">
<div class="docs-viz-file-header">
<span class="docs-viz-file-name">brief.md</span>
<span class="docs-viz-file-status">Output of /impeccable shape</span>
</div>
<div class="docs-viz-file-body">
<div class="docs-viz-file-row">
<span class="docs-viz-file-k">Purpose</span>
<span class="docs-viz-file-v">Let committed subscribers change what they get without losing them to unsubscribe.</span>
</div>
<div class="docs-viz-file-row">
<span class="docs-viz-file-k">User</span>
<span class="docs-viz-file-v">Rushed, on mobile, mid-meeting. Reading fast, low patience.</span>
</div>
<div class="docs-viz-file-row">
<span class="docs-viz-file-k">Content</span>
<span class="docs-viz-file-v">4 digest types, 2 cadences, one opt-out-all at the bottom.</span>
</div>
<div class="docs-viz-file-row">
<span class="docs-viz-file-k">Feeling</span>
<span class="docs-viz-file-v">Calm, trustworthy, no dark patterns.</span>
</div>
<div class="docs-viz-file-row">
<span class="docs-viz-file-k">Constraints</span>
<span class="docs-viz-file-v">Mobile-first. WCAG AA contrast. One column, no modals.</span>
</div>
</div>
<div class="docs-viz-file-footer">Hand it to <code>/impeccable</code>, <code>/impeccable craft</code>, or any implementation flow.</div>
</div>
<p class="docs-viz-caption">A shape brief is a compass, not a spec. It captures intent, not UI. Implementation skills read it before writing a line of code.</p>
</div>
## When to use it
`/impeccable shape` is where a feature starts. Before anyone writes code, before anyone argues about the hero treatment, before anyone picks a font. Use it to force a discovery conversation about purpose, users, content, and constraints, then capture the answers as a design brief the implementation skills can lean on.
Reach for it whenever a feature is about to start, a ticket is vague, or you catch yourself writing JSX to figure out what the product should be.
## How it works
Most AI-generated UIs fail not because of bad code, but because of skipped thinking. The model jumps to "here is a card grid" without asking "what is the user trying to accomplish". `/impeccable shape` inverts that order.
The skill runs a structured discovery interview in conversation. It will not write code during this phase. The questions cover:
- **Purpose and context**: what the feature is for, who uses it, what state of mind they are in
- **Content and data**: what is displayed, realistic ranges, edge cases, what is dynamic
- **Design goals**: the single most important thing, the intended feeling, reference examples
- **Constraints**: technical, content, accessibility, localization
You answer naturally. The skill asks follow-ups, not a form. At the end it produces a design brief: a structured artifact you can hand to `/impeccable` or any other implementation skill.
Note: if you want the full flow (discovery interview, then straight into building), use `/impeccable craft` instead. It runs `/impeccable shape` internally, then continues into implementation with visual iteration. `/impeccable shape` standalone is for when you want just the brief, so you can take it to whatever implementation approach you prefer.
## Try it
```
/impeccable shape a daily digest email preferences page
```
Expect a 5 to 10 question conversation. The skill asks things like "who is the person opening this, and are they already committed or still curious" and "what happens when the user has unsubscribed from everything, do we hide the feature or show something". You answer, and a brief materializes.
From there you can hand the brief to `/impeccable`, `/impeccable polish`, or any other skill. Or just use it as a reference while you build by hand.
## Pitfalls
- **Skipping it because it feels slow.** The interview is maybe 5 minutes. The rewrites you avoid are measured in hours.
- **Treating the brief as a spec.** It is a compass, not a checklist. It captures intent, not UI.
- **Answering with "standard" or "normal".** Specificity is the whole point. If a user is "rushed, on mobile, between meetings", say so. That changes everything downstream.
+70
View File
@@ -0,0 +1,70 @@
---
tagline: "Teach Impeccable who your product is for, once per project."
---
<div class="docs-viz-hero">
<div class="docs-viz-file">
<div class="docs-viz-file-header">
<span class="docs-viz-file-name">PRODUCT.md</span>
<span class="docs-viz-file-status">Loaded on every command</span>
</div>
<div class="docs-viz-file-body">
<div class="docs-viz-file-row">
<span class="docs-viz-file-k">Register</span>
<span class="docs-viz-file-v">Product. Design serves the task.</span>
</div>
<div class="docs-viz-file-row">
<span class="docs-viz-file-k">Users</span>
<span class="docs-viz-file-v">SREs on call, reading fast, often in the dark.</span>
</div>
<div class="docs-viz-file-row">
<span class="docs-viz-file-k">Brand voice</span>
<span class="docs-viz-file-v">Calm, clinical, no hype.</span>
</div>
<div class="docs-viz-file-row">
<span class="docs-viz-file-k">Anti-references</span>
<span class="docs-viz-file-v">Purple gradients. Glassmorphism. "Boost your productivity."</span>
</div>
</div>
<div class="docs-viz-file-footer">Every command reads this before writing a line of code.</div>
</div>
<p class="docs-viz-caption">A finished PRODUCT.md. Strategy only: who, what, why. No colors, no fonts, no pixel values, those live in DESIGN.md.</p>
</div>
## When to use it
Run `/impeccable teach` once at the start of a project. It is the onramp. Without it, every other command will produce design that is technically competent but generically toned: stock SaaS voice, safe-default fonts, the AI color palette. With it, every command reads your answers before it generates.
Reach for it when:
- **You just installed Impeccable in a new project.** First thing to run. Other commands will nudge you toward it if you skip.
- **The project's brand direction has shifted.** New positioning, new audience, new voice. Re-run `teach` and the updated context flows through every command.
- **Another command said "no design context found"** and stopped. That is the signal: run teach, then resume.
## How it works
Teach writes two complementary files at the project root:
- **`PRODUCT.md`** is the strategic file. Register (brand or product), target users, product purpose, brand personality, anti-references, design principles, accessibility needs. Answers "who, what, why".
- **`DESIGN.md`** is the visual file. Colors, typography, elevation, components, do's and don'ts. Answers "how it looks". Written by the delegated `/impeccable document` command, which teach invokes at the end.
The flow scans the codebase first (README, package.json, components, tokens, brand assets) and forms a **register hypothesis**: brand (landing, marketing, portfolio, where design IS the product) or product (app UI, dashboards, tools, where design SERVES the product). Register is the first question, because it shapes every downstream answer: typography defaults, motion energy, color strategy, the reference set commands like `/impeccable typeset` pull from. After register, teach asks only what it could not infer: users, personality in three real words, references and anti-references, accessibility requirements.
PRODUCT.md is strategic only. No colors, no fonts, no pixel values. Those live in DESIGN.md. Keeping the two files separate is deliberate: strategy can stay stable while the visual system evolves.
## Try it
```
/impeccable teach
```
Expect a 5 to 8 minute interview. The first question is usually about register; the rest are short. Teach will quote back what it inferred from your code ("from the routes, this looks like a product surface, match?") so you are confirming, not starting from scratch.
At the end, teach offers to run `/impeccable document` for you. Say yes unless you have a specific reason to hold off. A real DESIGN.md is what keeps variants, polishes, and audits on-brand.
## Pitfalls
- **Skipping it to "just try a command quickly".** Every other command will interview you mid-flight instead. Running teach first is faster, not slower.
- **Giving generic answers.** "Modern and clean" is not useful. "Warm, mechanical, opinionated" is. Be specific. Be willing to disagree with safe defaults.
- **Treating PRODUCT.md as immutable.** The file is yours. If teach put something in there that is not quite right, edit it. Every command reads the current file.
- **Listing only adjectives for references.** Brands, products, printed objects: named, not described. "Klim Type Foundry specimen pages", not "technical and clean". Anti-references should be equally specific.
+42
View File
@@ -0,0 +1,42 @@
---
tagline: "Fix typography that feels generic, inconsistent, or accidental."
---
## When to use it
Reach for `/impeccable typeset` when the text on a page looks like default typography instead of designed typography. Muddy hierarchy, three sizes that look the same, body copy at 14px, a display font that is actually just Inter bold, headlines with no kerning attention.
Common triggers: "hierarchy feels flat", "readability is off", "fonts look generic".
## How it works
The skill assesses typography across five dimensions:
1. **Font choices**: are you using invisible defaults (Inter, Roboto, Arial, Open Sans), does the typeface match the brand, are there more than 2 to 3 families.
2. **Hierarchy**: are heading, body, and caption clearly different at a glance, is the size contrast at least 1.25x between steps, are weight contrasts legible.
3. **Sizing and scale**: is there a coherent type scale, does body text meet 16px minimum, is the scale fixed-rem for app UIs or fluid-clamp for marketing pages.
4. **Readability**: line length 45 to 75 characters, line-height tuned for font and context, contrast.
5. **Consistency**: same element uses same treatment everywhere, no one-off font-size overrides.
It then fixes what it finds: picks distinctive typefaces, builds a modular scale, widens hierarchy contrast, sets proper line length and leading.
## Try it
```
/impeccable typeset the article layout
```
Expected diff:
- Display font swapped from Inter 700 to a real display face
- Type scale rebuilt: 3rem / 2rem / 1.25rem / 1rem / 0.875rem, ratio 1.333
- Body text bumped from 14px to 16px
- Line length clamped to 68ch on the article column
- Line-height 1.6 for body, 1.1 for display
- Removed four one-off `font-size` values scattered in component styles
## Pitfalls
- **Asking for a new font without context.** Typeset will pick based on the `PRODUCT.md` brand voice. If you have not run `/impeccable teach`, the suggestion will be generic.
- **Reaching for typeset when the issue is layout.** If paragraphs are fine but the page feels cramped, you want `/impeccable layout`.
- **Expecting fluid clamp scales on app UIs.** Typeset uses fixed rem scales for app interfaces. Fluid typography is for marketing and content pages where line length varies dramatically.
+130
View File
@@ -0,0 +1,130 @@
---
title: Brand vs product, pick a register
tagline: "Two worlds, two sets of defaults. Pick the right one and every command downstream benefits."
order: 3
description: "Impeccable treats brand work (landing pages, campaigns, portfolios) and product work (app UI, dashboards, tools) as different worlds with different defaults. Learn how to pick a register and how it shapes every command that reads it."
---
## See the divergence
Same element, one register each. A newsletter signup, twice.
<div class="docs-viz-hero docs-viz-hero--plain">
<div class="docs-viz-register">
<div class="docs-viz-register-side">
<div class="docs-viz-register-label">
<span class="docs-viz-register-name">Brand</span>
<span class="docs-viz-register-lane">Editorial-magazine</span>
</div>
<div class="docs-viz-register-frame docs-viz-register-frame--brand">
<span class="docs-viz-reg-kicker">No. 04 &nbsp;·&nbsp; Dispatch</span>
<h3 class="docs-viz-reg-title">Letters, occasionally.</h3>
<p class="docs-viz-reg-body">A postcard from the editor, once a month. No tracking pixels, no "just checking in."</p>
<span class="docs-viz-reg-btn">Send me one</span>
</div>
<div class="docs-viz-register-notes">
<span>Serif display, italic display weight</span>
<span>Drenched in the primary hue</span>
<span>Monospaced kicker, editorial voice</span>
</div>
</div>
<div class="docs-viz-register-side">
<div class="docs-viz-register-label">
<span class="docs-viz-register-name">Product</span>
<span class="docs-viz-register-lane">Utility / app shell</span>
</div>
<div class="docs-viz-register-frame docs-viz-register-frame--product">
<span class="docs-viz-reg-kicker">Newsletter</span>
<h3 class="docs-viz-reg-title">Subscribe to updates</h3>
<p class="docs-viz-reg-body">Product changes and release notes, once a month. Unsubscribe at any time.</p>
<span class="docs-viz-reg-btn">Subscribe</span>
</div>
<div class="docs-viz-register-notes">
<span>Neutral sans, semibold for hierarchy</span>
<span>Restrained palette, accent only on state</span>
<span>Short, scannable, mobile-readable copy</span>
</div>
</div>
</div>
<p class="docs-viz-caption">The table below lists what's different. This is what it looks like at the pixel.</p>
</div>
## Why register matters
Every design task belongs to one of two worlds:
- **Brand** is where design IS the product. Marketing sites, landing pages, portfolios, long-form content, campaign surfaces. Distinctiveness is the bar. Fonts, motion, density, and color all push toward "this looks like nothing else in the category."
- **Product** is where design SERVES the product. App UI, admin, dashboards, tools. Earned familiarity is the bar. Fluent users of Linear, Figma, Notion, Raycast, or Stripe should trust the output on sight.
If you ask the same AI to design a dashboard and a campaign page without naming which world, you'll get the average of the two. Brand surfaces will feel too careful. Product surfaces will feel too precious. Register is how Impeccable avoids that.
Impeccable tracks register as a single field in `PRODUCT.md`:
```markdown
## Register
product
```
That is it: a bare value, `brand` or `product`. Every command that does register-sensitive work (`typeset`, `animate`, `colorize`, `layout`, `bolder`, `quieter`, `delight`) loads a different reference file based on what it finds here.
## How the two worlds diverge
This is not an exhaustive list, the full divergence lives in the `brand.md` and `product.md` reference files, but the shape of the difference:
| Dimension | Brand | Product |
|---|---|---|
| **Type lanes** | Editorial-magazine, luxury, brutalist, consumer-warm, tech-minimal, all available. Swing. | Tighter set: neutral sans + optional mono, sized for dense reading, fluid type reserved for marketing surfaces. |
| **Motion** | Choreographed entrances, scroll-driven sequences, decorative moments earn their place. | Restrained. State changes only. Animation serves feedback, not atmosphere. |
| **Color** | Full palette, Committed, or Drenched are all on the table. | Restrained by default. Accents carry meaning; color is not decoration. |
| **Density** | Whatever the narrative wants. Generous whitespace or packed rule-divided columns both valid. | Comfortable to dense. Every pixel earns its place. |
| **References** | Real-world, from the right lane. *Klim specimen pages* or *Broadsheet masthead*, not "modern SaaS". | Category best-tool. *Linear*, *Figma*, *Notion*, *Raycast*, *Stripe*. |
The same command, `/impeccable typeset`, pulls from different fonts in the two worlds. The same command, `/impeccable animate`, picks different motion vocabularies. The same command, `/impeccable layout`, assumes different density defaults. You do not re-learn the command: you answer the register question once, and the command adapts.
## Step 1. Decide or inherit
If you haven't run `/impeccable teach` yet, run it now. The first question is about register:
```
/impeccable teach
```
Teach scans your codebase first and forms a hypothesis: routes like `/`, `/pricing`, `/blog`, hero sections, scroll-driven content point toward brand. Routes like `/app`, `/dashboard`, `/settings`, forms and tables point toward product. It leads with the hypothesis rather than starting cold:
> From the codebase, this looks like a product surface, does that match your intent, or should we treat it differently?
If the project genuinely spans both (a product with a big marketing landing), teach asks which register describes the **primary** surface. Register is per-project, not per-page, but you can override it per task when needed.
## Step 2. Verify the register landed
Open `PRODUCT.md` and look for the `## Register` section. It should carry a bare value, not prose:
```markdown
## Register
brand
```
If the section is missing (you're on an older `PRODUCT.md` from pre-v3.0), re-run `/impeccable teach`. It will detect the gap and add the field without re-interviewing you on everything else.
## Step 3. Override per task when you need to
Most of the time, register is set once and forgotten. But a product project might occasionally need a single brand surface (a launch landing, an investor one-pager) without flipping the whole project.
You have two options:
- **Name it in the brief.** "`/impeccable craft a launch landing for v2, brand register for this one page.`" The skill honors the override for that task only.
- **Set a per-surface register.** If the override is lasting, add a short note in `PRODUCT.md` under an explicit section: `## Register overrides: /launch is brand.` Commands that read PRODUCT.md will respect it.
## What to try next
- Run a command that is register-sensitive and watch the divergence: `/impeccable typeset the pricing page` on a product project vs. a brand project will pick different type families, different scale ratios, and different pairings.
- Pair with [getting started](/tutorials/getting-started) if you haven't installed Impeccable yet.
- Reach for `/impeccable document` after teach to capture the visual side (colors, components) into DESIGN.md.
## Common issues
- **Register keeps slipping the wrong way.** If you set `product` but commands keep producing brand-feeling output, check that `PRODUCT.md` is at the project root and the `## Register` section has a bare value (no prose, no explanation, just the word). Commands can only read what is there.
- **The hypothesis teach formed is wrong.** Disagree in the answer. Teach is asking, not telling.
- **A project is genuinely 50/50.** Pick the primary surface, then use per-task overrides for the minority one. Trying to average the two in PRODUCT.md produces worse output than committing to one.
@@ -0,0 +1,129 @@
---
title: Critique with the visual overlay
tagline: "Use /impeccable critique plus the browser overlay to review a live page with ground truth."
order: 4
description: "Run a full design critique that combines LLM assessment, the automated detector, and a live browser overlay so you can see exactly which elements trigger which anti-patterns on the page you're looking at."
---
## What you'll build
You will run a complete design critique against a live page in your browser, with every flagged anti-pattern highlighted directly on the element that caused it. No screenshots, no guesswork, no paragraph of findings you have to map back to the code.
Total time: about ten minutes.
## Prerequisites
- Impeccable installed in your project (see [getting started](/tutorials/getting-started) if you have not).
- A harness with browser automation available (Claude Code with the Chrome extension, or similar).
- A page you want to critique, either local (`localhost:3000/pricing`) or deployed.
## Step 1. Run /impeccable critique
From your harness, run:
```
/impeccable critique the pricing page at localhost:3000/pricing
```
The skill kicks off two independent assessments in parallel. They run in separate sub-agents so one does not bias the other.
### What the LLM assessment does
The first assessment reads your source code and, if browser automation is available, opens the live page in a new tab. It walks the full impeccable skill DO/DON'T catalog and scores the page against Nielsen's 10 heuristics, the 8-item cognitive load checklist, and the brand fit from your `PRODUCT.md`.
It labels the tab it opens with `[LLM]` in the title so you can tell which one is which.
### What the automated detector does
The second assessment runs `npx impeccable detect` against the page. This is deterministic: around thirty specific pattern checks that fire or do not fire. Gradient text, purple palettes, side-tab borders, nested cards, line length problems, low contrast, tiny body text, and the rest. The [full catalog](/anti-patterns) lists every rule and which layer (CLI, browser, or LLM-only) catches it.
You get back a JSON list of every finding with its element selector, the rule that fired, and a short description.
## Step 2. Open the visual overlay
Impeccable ships with a visual mode that highlights every detected anti-pattern directly on the page. Here is what it looks like running on a deliberately-bad synthwave landing page:
<div class="tutorial-embed">
<div class="tutorial-embed-header">
<span class="tutorial-embed-dot red"></span>
<span class="tutorial-embed-dot yellow"></span>
<span class="tutorial-embed-dot green"></span>
<span class="tutorial-embed-title">Live detection overlay</span>
</div>
<iframe src="/antipattern-examples/visual-mode-demo.html" class="tutorial-embed-iframe" loading="lazy" title="Impeccable visual overlay running on a demo page"></iframe>
</div>
Every outlined element has a floating label naming the rule that fired. Hover an outline to see the full finding. This is exactly what you will see on your own page.
You have two ways to open it:
1. **[Chrome extension](https://chromewebstore.google.com/detail/impeccable/bdkgmiklpdmaojlpflclinlofgjfpabf)**: one-click activation on any page. Click the Impeccable icon in the toolbar and every anti-pattern gets highlighted instantly.
2. **Inside `/impeccable critique`**: the skill opens a browser tab labeled `[Human]` with the detector active during the browser portion of the assessment. You do not need to do anything extra.
For this tutorial, the easiest option is the Chrome extension. Install it, navigate to your pricing page, and click the Impeccable icon. You will see the overlay appear immediately on the live page.
## Step 3. Merge the two assessments
Back in your harness, `/impeccable critique` has finished and produced a combined report. It looks something like:
```
AI slop verdict: FAIL
Detected tells: gradient-text (2), ai-color-palette (1),
nested-cards (1), side-tab (3)
Heuristic scores (avg 2.8/4):
Visibility of status: 3 (good)
Match between system and real world: 2 (partial)
Consistency and standards: 2 (partial)
...
Cognitive load: 3/8 failures (moderate)
Visible options at primary decision: 6 (flag)
Decision points stacked at top: yes (flag)
Progressive disclosure: absent on advanced pricing toggles
What's working:
- Clear price hierarchy
- Strong headline
Priority issues:
1. Hero uses gradient text on the main price
Why: AI tell, reduces contrast, hurts scannability
Fix: solid ink color at one weight heavier
2. Feature comparison table has 4 nested card levels
Why: visual noise, unclear hierarchy
Fix: flatten to a table with zebra striping
Questions to answer:
- Is the free tier a real product or a funnel?
- What does a user feel when they land here from an ad vs from search?
```
## Step 4. Fix the findings
The report gives you a priority list. You can work through them one at a time, ask the model to fix them all at once, or anything in between. What matters is using the overlay to verify:
1. Keep the overlay open in one tab.
2. Make fixes in code (or ask the model to fix everything).
3. Reload. The overlay re-scans and resolved findings disappear.
This feedback loop is the reason the overlay matters. You see fixes land in real time, and you never ship a "fix" that did not actually satisfy the rule.
## Step 5. Re-run when you are done
After you have worked through the priority list, run `/impeccable critique` again. The goal is a clean AI slop verdict and at least a 3.5 average on the heuristics. Cognitive load should be below 2 failures.
If something still fires, fix it or write a suppression comment explaining why the rule does not apply in your context (the detector respects a small set of opt-out pragmas, but use them sparingly).
## What to try next
- [Iterate on the critique findings with Live Mode](/tutorials/iterate-live). Pick the element critique flagged, drop a comment, get three redirections hot-swapped in place, and write the accepted one back to source.
- `/impeccable audit the same page` to catch the implementation issues critique does not cover (accessibility, performance, theming).
- `/impeccable polish` if the critique report is clean and you want the last-mile refinement pass.
- `/impeccable distill` if critique flagged "too busy" or "cognitive load". Distill removes what should not be there.
## Common issues
- **The overlay shows no findings but critique says there are problems**. The detector catches deterministic patterns. Critique catches judgment calls. They are complementary, not redundant.
- **The LLM assessment and the detector disagree**. That is normal. The LLM is subjective. The detector is deterministic. When they disagree, look at both and make a call.
- **The overlay breaks the page layout**. Rare, but some CSS can interact with the injected overlay styles. Use the [Chrome extension](https://chromewebstore.google.com/detail/impeccable/bdkgmiklpdmaojlpflclinlofgjfpabf) for the most reliable experience, or run `npx impeccable detect` from the CLI and apply findings manually.
+106
View File
@@ -0,0 +1,106 @@
---
title: Getting started
tagline: "From zero to your first polish pass in five minutes."
order: 1
description: "Install Impeccable, run /impeccable teach once to establish project context, and run /impeccable polish on something that already exists. The fastest path to seeing what Impeccable changes about AI-generated design."
---
## What you'll build
You will end this tutorial with Impeccable installed in your project, a `PRODUCT.md` plus `DESIGN.md` pair that captures your brand, audience, and visual system, and one hand-polished page that went through a polish pass. Total time: about ten minutes.
## Prerequisites
- An AI coding harness: Claude Code, Cursor, Gemini CLI, Codex CLI, or any of the other supported tools.
- A project with at least one HTML or component file you want to improve. A fresh scaffolded landing page works fine.
## How Impeccable works
Impeccable installs as a single agent skill called `impeccable`. You access all 23 sub-commands through it:
```
/impeccable <command> <target>
```
For example: `/impeccable polish the pricing page`, or `/impeccable audit the checkout`. Type `/impeccable` alone to see the full list.
If you use a command often, pin it with `/impeccable pin <command>` to create a standalone shortcut (for example, `/impeccable pin audit` gives you `/audit` directly).
## Step 1. Install
From the root of your project, run:
```
npx skills add pbakaus/impeccable
```
This auto-detects your harness and writes the skill files to the right location (e.g., `.claude/skills/`, `.cursor/skills/`). Reload your harness and type `/`. You should see `/impeccable` in the autocomplete. Type it and the skill's argument hint will show all available commands.
## Step 2. Teach Impeccable about your project
This is the most important step. Design without context produces generic output. The `/impeccable teach` command runs a short discovery interview and writes a `PRODUCT.md` file at the root of your project.
Run:
```
/impeccable teach
```
The first question is about **register**: is this a brand surface (marketing site, landing page, portfolio, where design IS the product) or a product surface (app UI, dashboard, tools, where design SERVES the product)? Register shapes every downstream default, from type lanes to motion energy. See [brand vs product](/tutorials/brand-vs-product) for how the two diverge. Teach will form a hypothesis from your codebase and ask you to confirm, rather than starting cold.
Then a handful of shorter questions:
- **Who is this product for?** Be specific. Not "users" but "solo founders evaluating a new tool on their phone between meetings".
- **What is the brand voice in three words?** Pick real words. "Warm and mechanical and opinionated" is better than "modern and clean".
- **Any visual references?** Named brands, products, or printed objects, not adjectives. "Klim Type Foundry specimen pages", not "technical and clean".
- **Anti-references?** Things the product should explicitly not look like, equally named.
Answer in your own words. The skill writes `PRODUCT.md` with the answers. Every future command run reads it automatically.
Open `PRODUCT.md` and read what it wrote. Edit anything that does not feel right. The file is yours.
## Step 2.5. Capture the visual system
At the end of `/impeccable teach`, the skill offers to run `/impeccable document` for you. Say yes. It scans your tokens (CSS custom properties, Tailwind config, CSS-in-JS themes), extracts colors and typography, asks one grouped question for the parts that need creative input (a Creative North Star, descriptive color names), and writes a `DESIGN.md` that follows the [Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/).
On a fresh project with no tokens yet, document runs in seed mode: five quick questions about color strategy, type direction, and motion energy, and writes a scaffold you can refresh once there is code.
`PRODUCT.md` carries strategy (who, what, why). `DESIGN.md` carries visuals (colors, typography, components). Every command reads both before generating.
## Step 3. Polish something
Pick a page that already exists. An about page, a settings screen, a pricing table, anything. Run:
```
/impeccable polish the pricing page
```
The skill will walk through alignment, spacing, typography, color, interaction states, transitions, and copy. It makes targeted fixes, not a rewrite. Expect a handful of small diffs that together lift the page from "done" to "done well".
A typical polish pass looks like:
```
Visual alignment: fixed 3 off-grid elements
Typography: tightened h1 kerning, fixed widow on feature list
Color: replaced one hardcoded hex with --color-accent token
Interaction: added missing hover state on FAQ items
Motion: softened modal entrance to 220ms ease-out-quart
Copy: removed stray 'Lorem' placeholder
```
Review the diff. If something does not feel right, ask the model to explain the change. If it still does not feel right, revert it. Impeccable is opinionated but not infallible.
## What to try next
- [Iterate visually with Live Mode](/tutorials/iterate-live) opens a browser picker on your dev server, generates three production-quality variants per element, and writes the accepted one back to source.
- `/impeccable critique the landing page` runs a full design review with scoring, persona tests, and automated detection. It is the best way to find what to fix next.
- `/impeccable audit the checkout` runs accessibility, performance, theming, responsive, and anti-pattern checks against the implementation. Useful before shipping.
- `/impeccable craft a pricing page for enterprise customers` runs the full shape-then-build flow on a brand new feature.
- **Pin your favorites.** If you reach for one command constantly, `/impeccable pin audit` makes `/audit` work as a standalone shortcut without reversing the consolidation.
- `/impeccable redo this hero section` works too. Any description after `/impeccable` applies the design principles to the task.
## Common issues
- **The skill says "no design context found"**. You skipped step 2. Run `/impeccable teach` first.
- **Commands do not appear in the harness**. Reload the harness after installing. If they still do not appear, check that the installer wrote files into the expected location (`.claude/skills/`, `.cursor/skills/`, etc.) and that your harness is picking up that directory.
- **The polish pass rewrote something you liked**. Say so. Revert the change, tell the model which specific edit to undo, and continue from there.
+123
View File
@@ -0,0 +1,123 @@
---
title: Iterate on UI with Live Mode
tagline: "Pick an element, generate three variants, accept one. Canvas-like iteration without leaving your code."
order: 2
description: "Use /impeccable live to visually iterate on a real element in your dev server: pick, annotate, generate three variants, accept the one you want, and have it written back to source."
---
## What you'll build
You will use `/impeccable live` on your dev server to iterate on a single piece of UI (a hero, a card, a section) and end with one of three AI-generated variants written back to source as real code. You'll see the canvas-style picking, annotation, and three-up cycling flow.
Total time: about ten minutes. Most of that is picking what to iterate on.
## Prerequisites
- Impeccable installed (see [getting started](/tutorials/getting-started) if you have not). Run `/impeccable teach` first if you haven't yet: variants lean on `PRODUCT.md` and `DESIGN.md` for brand fit.
- A running dev server with HMR (Vite, Next.js, SvelteKit, Astro, Nuxt, Bun) OR a static HTML file open in a browser.
- A page with at least one piece of UI you'd like to iterate on. A newsletter card, a hero, a pricing tier, something small enough to hold in your head.
## Step 1. Start live mode
From your harness, run:
```
/impeccable live
```
The skill starts a small local helper server on port 8400 and injects a `<script>` tag into your dev entry file that loads the picker. If your project has a strict Content Security Policy, the first run detects it and offers a one-time, dev-only patch for `script-src` and `connect-src`. Accept the patch: it is guarded by `NODE_ENV === "development"` and you can revert any time.
Open your dev server URL (not port 8400, that's the helper server, not the app). You'll see a dark pill at the bottom of the page with **Pick** highlighted.
## Step 2. Pick an element
<div class="docs-viz-step">
<div class="docs-viz-picker-row">
<div class="docs-viz-picker-target">
<span class="docs-viz-picker-pin">1</span>
Newsletter signup
<span class="docs-viz-picker-note">more playful</span>
</div>
</div>
</div>
Click the element you want to iterate on. A picker outline appears around it, and a light context bar pops up next to the selection with a command chip on the left and a freeform text field.
A few things you can do before pressing Go:
- **Click the command chip** (default is `impeccable`, the freeform action). Pick a specific action like `bolder`, `delight`, `layout`, or `typeset` to constrain the variants along one dimension.
- **Type in the freeform field.** "More playful." "Less SaaS." "Feel like a newsletter from a magazine."
- **Drop a comment pin** by clicking anywhere on the picked element. The pin's position is load-bearing: a comment near the title is about the title, not the whole element.
- **Draw a stroke** by dragging across the element. Closed loop = "this part matters." Arrow = direction. Cross = "delete this." The skill reads strokes by shape, not by pixel content.
When the brief feels clear, hit **Go**.
## Step 3. Cycle through the three variants
<div class="docs-viz-step">
<div class="docs-viz-variants">
<div class="docs-viz-variant docs-viz-variant--v1">
<span class="docs-viz-variant-badge">1 / 3</span>
<span class="docs-viz-variant-kicker">No. 04</span>
<p class="docs-viz-variant-title">Letters, <em>occasionally</em>.</p>
<span class="docs-viz-variant-btn">Send me one</span>
</div>
<div class="docs-viz-variant docs-viz-variant--v2 is-active">
<span class="docs-viz-variant-badge">2 / 3</span>
<span class="docs-viz-variant-kicker">Dispatch</span>
<p class="docs-viz-variant-title">Design notes, <br>every other<br>Thursday.</p>
<span class="docs-viz-variant-btn">Join →</span>
</div>
<div class="docs-viz-variant docs-viz-variant--v3">
<span class="docs-viz-variant-badge">3 / 3</span>
<span class="docs-viz-variant-kicker">Field Notes</span>
<p class="docs-viz-variant-title">A monthly letter, for people who still read email.</p>
<span class="docs-viz-variant-btn">Receive ✺</span>
</div>
</div>
</div>
You'll see a spinner ("Generating variants...") and within a few seconds, three variants hot-swap into the page in place. Not a preview, the actual rendered DOM on your actual dev server with your actual context.
Use the arrow keys (or the prev / next buttons on the context bar) to cycle through them. A counter at the top right shows `1 / 3`, `2 / 3`, `3 / 3`.
The three variants are designed to be **genuinely different**, not three riffs on one idea. Freeform variants anchor to three different design archetypes (broadsheet masthead, oversized-glyph poster, catalog-style spec rows, and so on). Action-specific variants vary along the dimension the action names: `colorize` gives you three hue families, `animate` gives you three motion vocabularies, `layout` gives you three structural arrangements.
If two variants feel like they rhyme, that is the skill's "squint test" failure mode. You can tell the picker "try again, all three felt too similar" and get a fresh set.
## Step 4. Accept one
<div class="docs-viz-step" style="text-align:center">
<span class="docs-viz-accept-pill">Variant 2 written to source</span>
</div>
When you find the one you like, click **Accept** on the context bar (or press Enter). Three things happen:
1. The picked element is replaced with the accepted variant on the page.
2. The variant is written back to source: the same file your picker was injected into, or the component source if live detected a generated file during step 1.
3. If the accept touched CSS, the relevant rules are consolidated into your project's real stylesheet, not left inline.
Discard all three (press Escape) and the original stays. No trace, no commented-out leftovers.
## Step 5. Stop live mode
When you are done iterating, stop the helper:
- Say **"stop live mode"** in your harness chat, or
- Click the **×** on the picker pill, or
- Close the browser tab: the helper detects the dropped connection after eight seconds and exits cleanly.
The stop also strips the `<script>` tag from your dev entry and stops the helper server on port 8400.
## What to try next
- Run `/impeccable live` on a different page after a `/impeccable polish` pass to A/B the polished version against two more directions.
- Pair with [critique with the overlay](/tutorials/critique-with-overlay): run critique first, fix priority issues, then use live to explore redirections on the element critique flagged.
- Reach for `/impeccable craft` when you want the shape-then-build flow (a new feature end-to-end, not a single element).
## Common issues
- **The picker never appears on the page.** Either the helper did not start (look for errors in the terminal) or CSP is blocking the inject. Re-run `/impeccable live` and let it re-check CSP. If you declined the patch on first run, delete the `cspChecked` line in `.impeccable/live/config.json` and re-run.
- **"element lives in a generated file"** on Go. Live detected that the picked element is in a compiled output, not a source file. It routes the accept through a fallback path so the variant still lands in true source. Follow the hint; don't force-accept into the generated file.
- **Variants don't feel brand-aligned.** Check that `PRODUCT.md` and `DESIGN.md` exist at the project root. Without them, live leans toward generic defaults. Run `/impeccable teach` and `/impeccable document` first.
- **The helper port is in use.** Another live session left its server running. `npx impeccable live stop` releases the port.
+81
View File
@@ -0,0 +1,81 @@
/**
* Command category and relationship data for docs pages.
* Extracted from scripts/lib/sub-pages-data.js for use in Astro templates.
*/
export const SKILL_CATEGORIES: Record<string, string> = {
impeccable: 'create',
craft: 'create',
shape: 'create',
critique: 'evaluate',
audit: 'evaluate',
typeset: 'refine',
layout: 'refine',
colorize: 'refine',
animate: 'refine',
delight: 'refine',
bolder: 'refine',
quieter: 'refine',
overdrive: 'refine',
distill: 'simplify',
clarify: 'simplify',
adapt: 'simplify',
polish: 'harden',
optimize: 'harden',
harden: 'harden',
onboard: 'harden',
teach: 'system',
document: 'system',
extract: 'system',
live: 'system',
};
export const CATEGORY_ORDER = ['create', 'evaluate', 'refine', 'simplify', 'harden', 'system'];
export const CATEGORY_LABELS: Record<string, string> = {
create: 'Create',
evaluate: 'Evaluate',
refine: 'Refine',
simplify: 'Simplify',
harden: 'Harden',
system: 'System',
};
export const CATEGORY_DESCRIPTIONS: Record<string, string> = {
create: 'Build something new, from a blank page to a working feature.',
evaluate: 'Review what you have. Score it, critique it, find what to fix.',
refine: 'Improve one dimension at a time: type, layout, color, motion.',
simplify: 'Strip complexity. Remove what does not earn its place.',
harden: 'Make it production-ready. Edge cases, performance, polish.',
system: 'Setup and tooling. Design system work, extraction, organization.',
};
export const COMMAND_RELATIONSHIPS: Record<string, {
leadsTo?: string[];
pairs?: string;
combinesWith?: string[];
}> = {
craft: { combinesWith: ['shape'] },
shape: { combinesWith: ['craft'] },
audit: { leadsTo: ['harden', 'optimize', 'adapt', 'clarify'] },
critique: { leadsTo: ['polish', 'distill', 'bolder', 'quieter', 'typeset', 'layout'] },
typeset: { combinesWith: ['bolder', 'polish'] },
layout: { combinesWith: ['distill', 'adapt'] },
colorize: { combinesWith: ['bolder', 'delight'] },
animate: { combinesWith: ['delight'] },
delight: { combinesWith: ['bolder', 'animate'] },
bolder: { pairs: 'quieter' },
quieter: { pairs: 'bolder' },
overdrive: { combinesWith: ['animate', 'delight'] },
distill: { combinesWith: ['quieter', 'polish'] },
clarify: { combinesWith: ['polish', 'adapt'] },
adapt: { combinesWith: ['polish', 'clarify'] },
polish: {},
optimize: {},
harden: { combinesWith: ['optimize'] },
onboard: { combinesWith: ['clarify', 'delight'] },
teach: { combinesWith: ['document'] },
document: { combinesWith: ['teach', 'extract'] },
extract: { combinesWith: ['document'] },
live: {},
};
+92
View File
@@ -0,0 +1,92 @@
---
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
import '../styles/footer.css';
interface Props {
title: string;
description?: string;
activeNav?: 'home' | 'designing' | 'docs' | 'slop' | 'live';
canonicalPath?: string;
bodyClass?: string;
noIndex?: boolean;
mainId?: string;
mainClass?: string;
ogTitle?: string;
ogDescription?: string;
ogImage?: string;
twitterSite?: string;
twitterCreator?: string;
}
const {
title,
description = 'Design fluency for AI-assisted frontend development.',
activeNav,
canonicalPath,
bodyClass,
noIndex = false,
mainId = 'main',
mainClass,
ogTitle,
ogDescription,
ogImage,
twitterSite,
twitterCreator,
} = Astro.props;
const canonical = canonicalPath
? `https://impeccable.style${canonicalPath}`
: undefined;
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title}</title>
<meta name="description" content={description}>
<meta name="theme-color" content="#fafafa">
{noIndex && <meta name="robots" content="noindex">}
{canonical && <link rel="canonical" href={canonical}>}
{ogTitle && (
<>
<meta property="og:type" content="website">
<meta property="og:url" content={canonical || 'https://impeccable.style'}>
<meta property="og:title" content={ogTitle}>
<meta property="og:description" content={ogDescription || description}>
{ogImage && <meta property="og:image" content={ogImage}>}
</>
)}
{twitterSite && (
<>
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content={twitterSite}>
{twitterCreator && <meta name="twitter:creator" content={twitterCreator}>}
<meta name="twitter:title" content={ogTitle || title}>
<meta name="twitter:description" content={ogDescription || description}>
{ogImage && <meta name="twitter:image" content={ogImage}>}
</>
)}
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;0,700;1,300;1,400&family=Instrument+Sans:wght@400;500;600;700&family=Inter:wght@400;500;600&family=Space+Grotesk:wght@400;500;600&display=swap" rel="stylesheet">
<slot name="head" />
</head>
<body class={bodyClass}>
<a href={`#${mainId}`} class="skip-link">Skip to content</a>
<slot name="before-header" />
<Header activeNav={activeNav} />
<slot name="after-header" />
<main id={mainId} class={mainClass}>
<slot />
</main>
<Footer />
<slot name="scripts" />
</body>
</html>
+120
View File
@@ -0,0 +1,120 @@
---
import Base from './Base.astro';
import '../styles/sub-pages.css';
import {
SKILL_CATEGORIES,
CATEGORY_ORDER,
CATEGORY_LABELS,
COMMAND_RELATIONSHIPS,
} from '../data/sub-pages-data';
interface Props {
title: string;
description: string;
tagline: string;
slug: string;
category: string;
allCommands: { slug: string; category: string }[];
}
const { title, description, tagline, slug, category, allCommands } = Astro.props;
const categoryLabel = CATEGORY_LABELS[category] || category;
const relationships = COMMAND_RELATIONSHIPS[slug] || {};
const sidebarGroups: Record<string, { slug: string }[]> = {};
for (const cat of CATEGORY_ORDER) {
sidebarGroups[cat] = allCommands
.filter(c => c.category === cat)
.sort((a, b) => a.slug.localeCompare(b.slug));
}
---
<Base
title={`${title} | Impeccable`}
description={description}
activeNav="docs"
canonicalPath={`/docs/${slug}`}
bodyClass="sub-page skills-layout-page"
>
<div class="skills-layout">
<aside class="skills-sidebar" aria-label="Commands">
<button class="skills-sidebar-toggle" type="button" aria-expanded="false" aria-controls="skills-sidebar-inner">
<span class="skills-sidebar-toggle-label">Commands</span>
<svg class="skills-sidebar-toggle-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>
</button>
<div class="skills-sidebar-inner" id="skills-sidebar-inner">
<p class="skills-sidebar-label">Commands</p>
{CATEGORY_ORDER.map(cat => (
<div class="skills-sidebar-group">
<span class="skills-sidebar-category">{CATEGORY_LABELS[cat]}</span>
<ul class="skills-sidebar-list">
{sidebarGroups[cat].map(cmd => (
<li>
<a href={`/docs/${cmd.slug}`} aria-current={cmd.slug === slug ? 'page' : undefined}>
<span>{cmd.slug}</span>
</a>
</li>
))}
</ul>
</div>
))}
</div>
</aside>
<div class="skills-main">
<div class="skills-detail">
<nav class="skills-breadcrumb" aria-label="Breadcrumb">
<a href="/docs">Docs</a>
<span aria-hidden="true">/</span>
<span>{slug}</span>
</nav>
<header class="sub-page-header">
<span class="sub-page-eyebrow">{categoryLabel}</span>
<h1 class="sub-page-title">/impeccable {slug}</h1>
{tagline && <p class="sub-page-lede">{tagline}</p>}
</header>
<div class="skills-detail-body docs-body">
<slot />
</div>
{(relationships.leadsTo || relationships.pairs || relationships.combinesWith) && (
<footer class="skills-relationships">
<h2 class="skills-relationships-title">Related commands</h2>
<div class="skills-relationships-list">
{relationships.leadsTo?.map(cmd => (
<a href={`/docs/${cmd}`} class="skills-relationship-chip" data-relation="leads-to">
<span class="skills-relationship-label">leads to</span>
<span class="skills-relationship-name">{cmd}</span>
</a>
))}
{relationships.pairs && (
<a href={`/docs/${relationships.pairs}`} class="skills-relationship-chip" data-relation="pairs">
<span class="skills-relationship-label">pairs with</span>
<span class="skills-relationship-name">{relationships.pairs}</span>
</a>
)}
{relationships.combinesWith?.map(cmd => (
<a href={`/docs/${cmd}`} class="skills-relationship-chip" data-relation="combines">
<span class="skills-relationship-label">combines with</span>
<span class="skills-relationship-name">{cmd}</span>
</a>
))}
</div>
</footer>
)}
</div>
</div>
</div>
<script is:inline>
document.addEventListener('click', (e) => {
const toggle = e.target.closest('.skills-sidebar-toggle');
if (!toggle) return;
const expanded = toggle.getAttribute('aria-expanded') === 'true';
toggle.setAttribute('aria-expanded', String(!expanded));
});
</script>
</Base>
+105
View File
@@ -0,0 +1,105 @@
---
import Base from '../../layouts/Base.astro';
import '../../styles/sub-pages.css';
---
<Base
title="Neo Mirai case study | Impeccable"
description="How Impeccable turned generated brand and hi-fi references into the shipped Neo Mirai conference website."
canonicalPath="/cases/neo-mirai"
bodyClass="sub-page neon-case-page"
>
<section class="neon-case-hero">
<div class="neon-case-hero-copy">
<a class="neon-case-back" href="/designing#start">Designing with Impeccable</a>
<span class="neon-case-eyebrow">Case study</span>
<h1>Neo Mirai: generated mock to shipped page.</h1>
<p>A retro-futurist AI design conference became a real static site through the full Impeccable loop: visual reference, brand direction, implementation, asset regeneration, responsive fixes, animation polish, and browser verification.</p>
<div class="neon-case-actions">
<a class="neon-case-primary" href="/neo-mirai/">Open the live build</a>
<a class="neon-case-secondary" href="/docs/craft">Read the craft docs</a>
</div>
</div>
<a class="neon-case-hero-shot" href="/neo-mirai/" aria-label="Open the Neo Mirai live build">
<img src="/assets/cases/neo-mirai/live-fold.png" alt="The shipped Neo Mirai conference website hero and agenda, showing a warm retro-futurist Tokyo skyline and large NEO MIRAI headline." width="1440" height="1100">
</a>
</section>
<section class="neon-case-strip" aria-label="From generated references to shipped website">
<figure>
<span>01 &middot; Visualize</span>
<img src="/assets/openai_image_2_hifi.jpg" alt="Generated hi-fi website mock for the Neo Mirai conference page." loading="lazy" width="864" height="1821">
<figcaption>Hi-fi north star. A concrete composition to build toward, not a paragraph to interpret.</figcaption>
</figure>
<figure>
<span>02 &middot; Shape</span>
<img src="/assets/openai_image_2_brand.jpg" alt="Generated Neo Mirai brand toolkit plate with palette, typography, symbols, and application mockups." loading="lazy" width="1536" height="1024">
<figcaption>Brand toolkit. Identity, palette, type, image language, and motion direction before code.</figcaption>
</figure>
<figure>
<span>03 &middot; Ship</span>
<a href="/neo-mirai/" aria-label="Open the Neo Mirai live build">
<img src="/assets/cases/neo-mirai/live-page.png" alt="Full-page screenshot of the implemented Neo Mirai website." loading="lazy" width="1440" height="3013">
</a>
<figcaption>Implemented page. Semantic HTML, responsive layout, regenerated assets, real states, and polish.</figcaption>
</figure>
</section>
<section class="neon-case-body">
<div class="neon-case-column">
<span class="neon-case-section-label">What changed</span>
<h2>The mock did not become a screenshot. It became a system.</h2>
</div>
<div class="neon-case-notes">
<article>
<h3>Composition matching</h3>
<p>The build preserved the mock's asymmetric rhythm: full-bleed hero artwork, dark agenda block, orange manifesto band, drifting installation grid, and structured ticket field.</p>
</article>
<article>
<h3>Asset regeneration</h3>
<p>Image-native pieces stayed image-native. The manifesto city, speaker portraits, pine overlay, and supporting illustrations were regenerated or isolated where raster detail mattered.</p>
</article>
<article>
<h3>Browser iteration</h3>
<p>The page was tested in the browser after each pass. Overlaps, bad crops, active nav state, speaker carousel behavior, mobile heights, and large-viewport balance were fixed visually.</p>
</article>
</div>
</section>
<section class="neon-case-details" aria-label="Mock and live page comparison">
<figure>
<img src="/assets/openai_image_2_hifi.jpg" alt="Generated hi-fi Neo Mirai page mock." loading="lazy" width="864" height="1821">
<figcaption><span>North-star mock</span> The reference image preserved the intended rhythm: full-bleed hero, dark agenda, speaker carousel, installation field, manifesto, and tickets.</figcaption>
</figure>
<figure>
<a href="/neo-mirai/" aria-label="Open the Neo Mirai live build">
<img src="/assets/cases/neo-mirai/live-page.png" alt="Full-page screenshot of the implemented Neo Mirai website." loading="lazy" width="1440" height="3013">
</a>
<figcaption><span>Live build</span> The shipped page keeps the same visual ambition while becoming responsive markup, real links, carousel controls, hover states, and browser-tested layout.</figcaption>
</figure>
</section>
<section class="neon-case-command">
<div>
<span class="neon-case-section-label">Reproduce the loop</span>
<h2>Use craft when the output has to feel designed.</h2>
<p><code>/impeccable craft</code> is the right command when a feature needs shaping, visual direction, implementation, and browser iteration in one run.</p>
</div>
<div class="code-block-wrap"><pre class="code-block"><code>/impeccable craft retro-futurist AI design conference website</code></pre><button class="code-block-copy" type="button" data-copy="/impeccable craft retro-futurist AI design conference website" aria-label="Copy to clipboard"></button></div>
</section>
<script is:inline>
document.addEventListener('click', (event) => {
const button = event.target.closest('[data-copy]');
if (!button) return;
const text = button.getAttribute('data-copy');
if (!text) return;
navigator.clipboard.writeText(text).then(() => {
button.classList.add('is-copied');
setTimeout(() => button.classList.remove('is-copied'), 1500);
}).catch(() => {});
});
</script>
</Base>
@@ -1,43 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Designing with Impeccable</title>
<meta name="description" content="The core loop: start, iterate, polish, maintain. How to use Impeccable end-to-end, from a blank file to shipped feature to paid-down design debt.">
<meta name="theme-color" content="#fafafa">
<link rel="canonical" href="https://impeccable.style/designing">
<link rel="icon" type="image/svg+xml" href="../favicon.svg">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,600;1,400&family=Instrument+Sans:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../css/sub-pages.css">
</head>
<body class="sub-page designing-page-body">
<a href="#main" class="skip-link">Skip to content</a>
<!-- site-header v1 -->
<header class="site-header" data-site-header>
<a href="/" class="site-header-brand" aria-label="Impeccable home">
<svg class="site-header-brand-logo" viewBox="0 0 32 32" aria-hidden="true"><rect width="32" height="32" rx="6" fill="#1a1a1a"/><text x="16" y="24" font-family="system-ui, -apple-system, sans-serif" font-size="22" font-weight="500" fill="#f5f3ef" text-anchor="middle">/</text></svg>
<span class="site-header-brand-name">Impeccable</span>
</a>
<div class="site-header-right">
<nav class="site-header-nav" aria-label="Primary">
<a href="/" data-nav="home">Home</a>
<a href="/designing" data-nav="designing" aria-current="page">Designing</a>
<a href="/docs" data-nav="docs">Docs</a>
<a href="/slop" data-nav="slop">Slop</a>
<a href="/live-mode" data-nav="live">Live</a>
</nav>
<a href="https://github.com/pbakaus/impeccable" class="site-header-github" target="_blank" rel="noopener" aria-label="Impeccable on GitHub, 23k stars">
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/></svg>
<span class="site-header-github-label">23k</span>
<svg class="site-header-github-star" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2l2.76 6.36L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l7.24-.91L12 2z"/></svg>
</a>
</div>
</header>
<main id="main">
---
import Base from '../../layouts/Base.astro';
import '../../styles/sub-pages.css';
---
<Base
title="Designing with Impeccable"
description="The core loop: start, iterate, polish, maintain. How to use Impeccable end-to-end, from a blank file to shipped feature to paid-down design debt."
activeNav="designing"
canonicalPath="/designing"
bodyClass="sub-page designing-page-body"
>
<div class="designing-page">
<section class="designing-hero">
@@ -50,7 +22,7 @@
<div class="designing-loop-wrap" aria-label="The four-phase loop">
<span class="designing-loop-wrap-eyebrow">The core loop</span>
<div class="designing-loop">
<a class="designing-loop-node designing-loop-node--start" href="#start">
<span class="designing-loop-num">01</span>
<span class="designing-loop-name">Start</span>
@@ -90,10 +62,10 @@
<!-- Clockwise-orbiting accent dot, animates with offset-path. -->
<circle class="designing-loop-wheel-dot" cx="0" cy="0" r="2.2"/>
</svg>
<span class="designing-loop-wheel-arrow designing-loop-wheel-arrow--ne"></span>
<span class="designing-loop-wheel-arrow designing-loop-wheel-arrow--se"></span>
<span class="designing-loop-wheel-arrow designing-loop-wheel-arrow--sw"></span>
<span class="designing-loop-wheel-arrow designing-loop-wheel-arrow--nw"></span>
<span class="designing-loop-wheel-arrow designing-loop-wheel-arrow--ne">&#x2198;</span>
<span class="designing-loop-wheel-arrow designing-loop-wheel-arrow--se">&#x2199;</span>
<span class="designing-loop-wheel-arrow designing-loop-wheel-arrow--sw">&#x2196;</span>
<span class="designing-loop-wheel-arrow designing-loop-wheel-arrow--nw">&#x2197;</span>
<div class="designing-loop-wheel-center">
<span class="designing-loop-wheel-center-label">designing</span>
<span class="designing-loop-wheel-center-mark">impeccable</span>
@@ -156,7 +128,7 @@
<div class="designing-visualize-spread">
<figure class="designing-visualize-plate designing-visualize-plate--brand">
<a class="designing-visualize-plate-frame" href="/cases/neo-mirai" aria-label="Read the Neo Mirai case study">
<img src="../assets/openai_image_2_brand.jpg" alt="Auto-generated brand toolkit plate: identity lockups, colour palette, type specimens, icon system, and application mocks for a fictional AI design conference, rendered in warm earth tones." loading="lazy" width="1536" height="1024" />
<img src="/assets/openai_image_2_brand.jpg" alt="Auto-generated brand toolkit plate: identity lockups, colour palette, type specimens, icon system, and application mocks for a fictional AI design conference, rendered in warm earth tones." loading="lazy" width="1536" height="1024" />
</a>
<figcaption class="designing-visualize-plate-cap">
<span class="designing-visualize-plate-kind">Shape</span>
@@ -166,7 +138,7 @@
<figure class="designing-visualize-plate designing-visualize-plate--hifi">
<a class="designing-visualize-plate-frame" href="/cases/neo-mirai" aria-label="Read the Neo Mirai case study">
<img src="../assets/openai_image_2_hifi.jpg" alt="Auto-generated hi-fi landing-page mock: a long vertical editorial comp for a fictional Tokyo AI design conference, in warm earth tones with committed serif display type." loading="lazy" width="864" height="1821" />
<img src="/assets/openai_image_2_hifi.jpg" alt="Auto-generated hi-fi landing-page mock: a long vertical editorial comp for a fictional Tokyo AI design conference, in warm earth tones with committed serif display type." loading="lazy" width="864" height="1821" />
</a>
<figcaption class="designing-visualize-plate-cap">
<span class="designing-visualize-plate-kind">Visualize</span>
@@ -176,7 +148,7 @@
<figure class="designing-visualize-plate designing-visualize-plate--live">
<a class="designing-visualize-plate-frame" href="/neo-mirai/" aria-label="Open the Neo Mirai live site">
<img src="../assets/cases/neo-mirai/live-page.png" alt="Full-page screenshot of the implemented Neo Mirai website." loading="lazy" width="1440" height="3013" />
<img src="/assets/cases/neo-mirai/live-page.png" alt="Full-page screenshot of the implemented Neo Mirai website." loading="lazy" width="1440" height="3013" />
</a>
<figcaption class="designing-visualize-plate-cap">
<span class="designing-visualize-plate-kind">Ship</span>
@@ -230,9 +202,9 @@
</div>
<div class="docs-viz-live-outline"></div>
<div class="docs-viz-live-ctx">
<button class="docs-viz-live-ctx-nav" type="button"></button>
<button class="docs-viz-live-ctx-nav" type="button">&#x2039;</button>
<span class="docs-viz-live-ctx-counter">2 / 3</span>
<button class="docs-viz-live-ctx-nav" type="button"></button>
<button class="docs-viz-live-ctx-nav" type="button">&#x203A;</button>
<span class="docs-viz-live-ctx-divider"></span>
<button class="docs-viz-live-ctx-accept" type="button">Accept</button>
</div>
@@ -240,7 +212,7 @@
<span class="docs-viz-live-gbar-brand">/</span>
<span class="docs-viz-live-gbar-btn is-active">Pick</span>
<span class="docs-viz-live-gbar-divider"></span>
<span class="docs-viz-live-gbar-x"></span>
<span class="docs-viz-live-gbar-x">&#x2715;</span>
</div>
</div>
</div>
@@ -404,28 +376,28 @@
<div class="designing-phase-body">
<ul class="designing-avoid">
<li>
<span class="designing-avoid-x" aria-hidden="true">×</span>
<span class="designing-avoid-x" aria-hidden="true">&times;</span>
<div>
<span class="designing-avoid-title">Running both Impeccable and Anthropic's frontend-design skill</span>
<p class="designing-avoid-desc">Anthropic still promotes their skill in Claude Code, but it's been unmaintained and is now behind on recommended patterns. Run both and they collide on vocabulary, cancelling each other out. Pick one.</p>
</div>
</li>
<li>
<span class="designing-avoid-x" aria-hidden="true">×</span>
<span class="designing-avoid-x" aria-hidden="true">&times;</span>
<div>
<span class="designing-avoid-title">Pinning every command</span>
<p class="designing-avoid-desc">Pinning brings back <code>/audit</code>, <code>/polish</code>, <code>/critique</code> as shortcuts. Pin everything and you've re-exploded the <code>/</code> menu the v3.0 consolidation cleaned up. Pin the two or three you reach for daily.</p>
</div>
</li>
<li>
<span class="designing-avoid-x" aria-hidden="true">×</span>
<span class="designing-avoid-x" aria-hidden="true">&times;</span>
<div>
<span class="designing-avoid-title">Skipping <code>teach</code></span>
<p class="designing-avoid-desc">Commands still run without PRODUCT.md and DESIGN.md. They default to generic SaaS patterns. The floor is meaningfully higher with context. Run teach once; every later command benefits.</p>
</div>
</li>
<li>
<span class="designing-avoid-x" aria-hidden="true">×</span>
<span class="designing-avoid-x" aria-hidden="true">&times;</span>
<div>
<span class="designing-avoid-title">Treating it like a linter</span>
<p class="designing-avoid-desc">Impeccable is an opinionated design partner, not a validator. It has a point of view. Push back with a reason and it'll work with you. Ignore the opinion without a reason and output gets worse, not better.</p>
@@ -448,132 +420,132 @@
</a>
</nav>
</div>
</main>
<script>
// Copy buttons on rendered code blocks
document.addEventListener('click', (e) => {
const btn = e.target.closest('[data-copy]');
if (!btn) return;
const text = btn.getAttribute('data-copy');
if (!text) return;
navigator.clipboard.writeText(text).then(() => {
btn.classList.add('is-copied');
setTimeout(() => btn.classList.remove('is-copied'), 1500);
}).catch(() => {});
});
// Mobile sidebar toggle (shown on narrow viewports, hidden on desktop).
document.addEventListener('click', (e) => {
const toggle = e.target.closest('.skills-sidebar-toggle');
if (!toggle) return;
const expanded = toggle.getAttribute('aria-expanded') === 'true';
toggle.setAttribute('aria-expanded', String(!expanded));
});
<script is:inline>
// Copy buttons on rendered code blocks
document.addEventListener('click', (e) => {
const btn = e.target.closest('[data-copy]');
if (!btn) return;
const text = btn.getAttribute('data-copy');
if (!text) return;
navigator.clipboard.writeText(text).then(() => {
btn.classList.add('is-copied');
setTimeout(() => btn.classList.remove('is-copied'), 1500);
}).catch(() => {});
});
// Before/after split-compare: drag on touch, hover OR drag on mouse.
// Pointer events attach to the padded .split-comparison wrapper so
// there is a ~20px invisible buffer around the visible box. The
// divider only snaps back when the pointer leaves that outer buffer.
(function initSplitCompare() {
const wrappers = document.querySelectorAll('.split-comparison');
if (wrappers.length === 0) return;
const hasHover = matchMedia('(hover: hover)').matches;
const DEFAULT_POSITION = 50;
// Mobile sidebar toggle (shown on narrow viewports, hidden on desktop).
document.addEventListener('click', (e) => {
const toggle = e.target.closest('.skills-sidebar-toggle');
if (!toggle) return;
const expanded = toggle.getAttribute('aria-expanded') === 'true';
toggle.setAttribute('aria-expanded', String(!expanded));
});
for (const wrapper of wrappers) {
const container = wrapper.querySelector('.split-container');
const splitAfter = wrapper.querySelector('.split-after');
const splitDivider = wrapper.querySelector('.split-divider');
if (!container || !splitAfter || !splitDivider) continue;
// Before/after split-compare: drag on touch, hover OR drag on mouse.
// Pointer events attach to the padded .split-comparison wrapper so
// there is a ~20px invisible buffer around the visible box. The
// divider only snaps back when the pointer leaves that outer buffer.
(function initSplitCompare() {
const wrappers = document.querySelectorAll('.split-comparison');
if (wrappers.length === 0) return;
const hasHover = matchMedia('(hover: hover)').matches;
const DEFAULT_POSITION = 50;
const tanAngle = Math.tan(10 * Math.PI / 180);
let skewOffset = 8;
const recalcSkew = () => {
const r = container.getBoundingClientRect();
if (r.width > 0 && r.height > 0) {
skewOffset = 50 * r.height * tanAngle / r.width;
}
};
recalcSkew();
window.addEventListener('resize', recalcSkew, { passive: true });
for (const wrapper of wrappers) {
const container = wrapper.querySelector('.split-container');
const splitAfter = wrapper.querySelector('.split-after');
const splitDivider = wrapper.querySelector('.split-divider');
if (!container || !splitAfter || !splitDivider) continue;
let targetX = DEFAULT_POSITION;
let currentX = DEFAULT_POSITION;
let rafId = null;
const tanAngle = Math.tan(10 * Math.PI / 180);
let skewOffset = 8;
const recalcSkew = () => {
const r = container.getBoundingClientRect();
if (r.width > 0 && r.height > 0) {
skewOffset = 50 * r.height * tanAngle / r.width;
}
};
recalcSkew();
window.addEventListener('resize', recalcSkew, { passive: true });
const paint = (pct) => {
const x = Math.max(-skewOffset, Math.min(100 + skewOffset, pct));
splitAfter.style.clipPath =
`polygon(${x + skewOffset}% 0%, 100% 0%, 100% 100%, ${x - skewOffset}% 100%)`;
splitDivider.style.left = `${x}%`;
};
let targetX = DEFAULT_POSITION;
let currentX = DEFAULT_POSITION;
let rafId = null;
const step = () => {
currentX += (targetX - currentX) * 0.2;
if (Math.abs(targetX - currentX) < 0.1) {
currentX = targetX;
rafId = null;
} else {
rafId = requestAnimationFrame(step);
}
paint(currentX);
};
const paint = (pct) => {
const x = Math.max(-skewOffset, Math.min(100 + skewOffset, pct));
splitAfter.style.clipPath =
`polygon(${x + skewOffset}% 0%, 100% 0%, 100% 100%, ${x - skewOffset}% 100%)`;
splitDivider.style.left = `${x}%`;
};
const setTarget = (pct) => {
targetX = pct;
if (rafId === null) rafId = requestAnimationFrame(step);
};
const step = () => {
currentX += (targetX - currentX) * 0.2;
if (Math.abs(targetX - currentX) < 0.1) {
currentX = targetX;
rafId = null;
} else {
rafId = requestAnimationFrame(step);
}
paint(currentX);
};
paint(DEFAULT_POSITION);
const setTarget = (pct) => {
targetX = pct;
if (rafId === null) rafId = requestAnimationFrame(step);
};
// Percentage is always relative to the VISIBLE .split-container,
// not the padded .split-comparison wrapper. The pointer event
// target is the wrapper but the clip-path math uses the inner box.
const pctFromClientX = (clientX) => {
const rect = container.getBoundingClientRect();
return ((clientX - rect.left) / rect.width) * 100;
};
paint(DEFAULT_POSITION);
let hovering = false;
let dragging = false;
// Percentage is always relative to the VISIBLE .split-container,
// not the padded .split-comparison wrapper. The pointer event
// target is the wrapper but the clip-path math uses the inner box.
const pctFromClientX = (clientX) => {
const rect = container.getBoundingClientRect();
return ((clientX - rect.left) / rect.width) * 100;
};
wrapper.addEventListener('pointerenter', (e) => {
if (hasHover && e.pointerType === 'mouse') {
hovering = true;
}
});
let hovering = false;
let dragging = false;
wrapper.addEventListener('pointerdown', (e) => {
dragging = true;
wrapper.setPointerCapture(e.pointerId);
wrapper.addEventListener('pointerenter', (e) => {
if (hasHover && e.pointerType === 'mouse') {
hovering = true;
}
});
wrapper.addEventListener('pointerdown', (e) => {
dragging = true;
wrapper.setPointerCapture(e.pointerId);
setTarget(pctFromClientX(e.clientX));
});
wrapper.addEventListener('pointermove', (e) => {
if (dragging || hovering) {
setTarget(pctFromClientX(e.clientX));
});
}
});
wrapper.addEventListener('pointermove', (e) => {
if (dragging || hovering) {
setTarget(pctFromClientX(e.clientX));
}
});
const endDrag = (e) => {
if (dragging) {
dragging = false;
try { wrapper.releasePointerCapture(e.pointerId); } catch {}
}
};
const endDrag = (e) => {
if (dragging) {
dragging = false;
try { wrapper.releasePointerCapture(e.pointerId); } catch {}
}
};
wrapper.addEventListener('pointerup', endDrag);
wrapper.addEventListener('pointercancel', endDrag);
wrapper.addEventListener('pointerup', endDrag);
wrapper.addEventListener('pointercancel', endDrag);
wrapper.addEventListener('pointerleave', (e) => {
endDrag(e);
if (hovering) {
hovering = false;
setTarget(DEFAULT_POSITION);
}
});
}
})();
</script>
wrapper.addEventListener('pointerleave', (e) => {
endDrag(e);
if (hovering) {
hovering = false;
setTarget(DEFAULT_POSITION);
}
});
}
})();
</script>
</body>
</html>
</Base>
+44
View File
@@ -0,0 +1,44 @@
---
import { getCollection, render } from 'astro:content';
import fs from 'node:fs';
import path from 'node:path';
import Doc from '../../layouts/Doc.astro';
import { SKILL_CATEGORIES } from '../../data/sub-pages-data';
export async function getStaticPaths() {
const entries = await getCollection('skills');
const metadataPath = path.join(process.cwd(), 'source/skills/impeccable/scripts/command-metadata.json');
const commandMetadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
const allCommands = entries.map(e => ({
slug: e.id,
category: SKILL_CATEGORIES[e.id] || 'system',
}));
return entries.map(entry => ({
params: { slug: entry.id },
props: {
entry,
metadata: commandMetadata[entry.id] || { description: entry.data.tagline, argumentHint: '' },
allCommands,
},
}));
}
const { entry, metadata, allCommands } = Astro.props;
const { Content } = await render(entry);
const slug = entry.id;
const category = SKILL_CATEGORIES[slug] || 'system';
---
<Doc
title={slug}
description={metadata.description}
tagline={entry.data.tagline}
slug={slug}
category={category}
allCommands={allCommands}
>
<Content />
</Doc>
+119
View File
@@ -0,0 +1,119 @@
---
import { getCollection } from 'astro:content';
import Base from '../../layouts/Base.astro';
import '../../styles/sub-pages.css';
import {
SKILL_CATEGORIES,
CATEGORY_ORDER,
CATEGORY_LABELS,
CATEGORY_DESCRIPTIONS,
} from '../../data/sub-pages-data';
const entries = await getCollection('skills');
const grouped: Record<string, typeof entries> = {};
for (const cat of CATEGORY_ORDER) {
grouped[cat] = entries
.filter(e => SKILL_CATEGORIES[e.id] === cat)
.sort((a, b) => a.id.localeCompare(b.id));
}
const allCommands = entries.map(e => ({
slug: e.id,
category: SKILL_CATEGORIES[e.id] || 'system',
}));
const sidebarGroups: Record<string, { slug: string }[]> = {};
for (const cat of CATEGORY_ORDER) {
sidebarGroups[cat] = allCommands
.filter(c => c.category === cat)
.sort((a, b) => a.slug.localeCompare(b.slug));
}
---
<Base
title="Docs | Impeccable"
description="23 commands for design fluency. Browse the full reference for every /impeccable sub-command."
activeNav="docs"
canonicalPath="/docs"
bodyClass="sub-page skills-layout-page"
>
<div class="skills-layout">
<aside class="skills-sidebar" aria-label="Commands">
<button class="skills-sidebar-toggle" type="button" aria-expanded="false" aria-controls="skills-sidebar-inner">
<span class="skills-sidebar-toggle-label">Commands</span>
<svg class="skills-sidebar-toggle-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>
</button>
<div class="skills-sidebar-inner" id="skills-sidebar-inner">
<p class="skills-sidebar-label">Commands</p>
{CATEGORY_ORDER.map(cat => (
<div class="skills-sidebar-group">
<span class="skills-sidebar-category">{CATEGORY_LABELS[cat]}</span>
<ul class="skills-sidebar-list">
{sidebarGroups[cat].map(cmd => (
<li>
<a href={`/docs/${cmd.slug}`}>
<span>{cmd.slug}</span>
</a>
</li>
))}
</ul>
</div>
))}
</div>
</aside>
<div class="skills-main">
<div class="docs-index">
<header class="docs-index-header">
<h1 class="sub-page-title">Commands</h1>
<p class="sub-page-lede">23 commands for design fluency. One skill, six categories, from shaping a new feature to hardening it for production.</p>
</header>
{CATEGORY_ORDER.map(cat => (
<section class="docs-category-section" id={`category-${cat}`}>
<header class="docs-category-header">
<h2 class="docs-category-title">{CATEGORY_LABELS[cat]}</h2>
<p class="docs-category-desc">{CATEGORY_DESCRIPTIONS[cat]}</p>
</header>
<div class="docs-card-grid">
{grouped[cat].map(entry => (
<a href={`/docs/${entry.id}`} class="docs-command-card">
<h3 class="docs-command-name">/impeccable {entry.id}</h3>
<p class="docs-command-tagline">{entry.data.tagline}</p>
</a>
))}
</div>
</section>
))}
</div>
</div>
</div>
<script is:inline>
document.addEventListener('click', (e) => {
const toggle = e.target.closest('.skills-sidebar-toggle');
if (!toggle) return;
const expanded = toggle.getAttribute('aria-expanded') === 'true';
toggle.setAttribute('aria-expanded', String(!expanded));
});
</script>
<style>
.docs-index { padding: 2rem 0 4rem; }
.docs-index-header { margin-bottom: 3rem; }
.docs-category-section { margin-bottom: 2.5rem; }
.docs-category-header { margin-bottom: 1rem; }
.docs-category-title { font-family: var(--font-display); font-size: 1.5rem; margin: 0 0 0.25rem; }
.docs-category-desc { color: var(--color-ash); font-size: 0.9375rem; margin: 0; }
.docs-card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 1rem; }
.docs-command-card {
display: block; padding: 1.25rem; border-radius: 8px;
border: 1px solid var(--color-mist); text-decoration: none; color: inherit;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.docs-command-card:hover { border-color: var(--color-accent); box-shadow: 0 2px 8px rgba(0,0,0,0.04); }
.docs-command-name { font-family: var(--font-mono); font-size: 0.9375rem; font-weight: 600; margin: 0 0 0.375rem; }
.docs-command-tagline { font-size: 0.8125rem; color: var(--color-ash); margin: 0; line-height: 1.5; }
</style>
</Base>
+38 -101
View File
@@ -1,76 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-TEXGHC7V34"></script>
<script>
---
import Base from '../layouts/Base.astro';
import '../styles/main.css';
import '../styles/sub-pages.css';
---
<Base
title="Impeccable: The missing upgrade to Anthropic's impeccable skill"
description="1 skill, 23 commands, and curated anti-patterns for impeccable frontend design. Works with Cursor, Claude Code, Gemini CLI, and Codex CLI."
activeNav="home"
canonicalPath="/"
mainId="main-content"
mainClass="site-content"
ogTitle="Impeccable: Design skills for AI harnesses"
ogDescription="1 skill, 23 commands, and curated anti-patterns for impeccable frontend design. Works with Cursor, Claude Code, Gemini CLI, and Codex CLI."
ogImage="https://impeccable.style/og-image.jpg"
twitterSite="@pbakaus"
twitterCreator="@pbakaus"
>
<Fragment slot="head">
<script is:inline async src="https://www.googletagmanager.com/gtag/js?id=G-TEXGHC7V34"></script>
<script is:inline>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-TEXGHC7V34');
</script>
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
</Fragment>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Impeccable: The missing upgrade to Anthropic's impeccable skill</title>
<meta name="description" content="1 skill, 23 commands, and curated anti-patterns for impeccable frontend design. Works with Cursor, Claude Code, Gemini CLI, and Codex CLI.">
<meta name="theme-color" content="#fafafa">
<link rel="canonical" href="https://impeccable.style">
<!-- OpenGraph -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://impeccable.style">
<meta property="og:title" content="Impeccable: Design skills for AI harnesses">
<meta property="og:description" content="1 skill, 23 commands, and curated anti-patterns for impeccable frontend design. Works with Cursor, Claude Code, Gemini CLI, and Codex CLI.">
<meta property="og:image" content="https://impeccable.style/og-image.jpg">
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@pbakaus">
<meta name="twitter:creator" content="@pbakaus">
<meta name="twitter:title" content="Impeccable: Design skills for AI harnesses">
<meta name="twitter:description" content="1 skill, 23 commands, and curated anti-patterns for impeccable frontend design.">
<meta name="twitter:image" content="https://impeccable.style/og-image.jpg">
<link rel="icon" type="image/svg+xml" href="./favicon.svg">
<link rel="apple-touch-icon" href="./apple-touch-icon.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;0,700;1,300;1,400&family=Instrument+Sans:wght@400;500;600;700&family=Inter:wght@400;500;600&family=Space+Grotesk:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="./css/main.css">
<link rel="stylesheet" href="./css/sub-pages.css">
</head>
<body>
<!-- Skip to main content link for keyboard users -->
<a href="#main-content" class="skip-link">Skip to main content</a>
<!-- Grain Overlay -->
<Fragment slot="before-header">
<div class="grain-overlay" aria-hidden="true"></div>
</Fragment>
<!-- site-header v1 -->
<header class="site-header" data-site-header>
<a href="/" class="site-header-brand" aria-label="Impeccable home">
<svg class="site-header-brand-logo" viewBox="0 0 32 32" aria-hidden="true"><rect width="32" height="32" rx="6" fill="#1a1a1a"/><text x="16" y="24" font-family="system-ui, -apple-system, sans-serif" font-size="22" font-weight="500" fill="#f5f3ef" text-anchor="middle">/</text></svg>
<span class="site-header-brand-name">Impeccable</span>
</a>
<div class="site-header-right">
<nav class="site-header-nav" aria-label="Primary">
<a href="/" data-nav="home" aria-current="page">Home</a>
<a href="/designing" data-nav="designing">Designing</a>
<a href="/docs" data-nav="docs">Docs</a>
<a href="/slop" data-nav="slop">Slop</a>
<a href="/live-mode" data-nav="live">Live</a>
</nav>
<a href="https://github.com/pbakaus/impeccable" class="site-header-github" target="_blank" rel="noopener" aria-label="Impeccable on GitHub, 23k stars">
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/></svg>
<span class="site-header-github-label">23k</span>
<svg class="site-header-github-star" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2l2.76 6.36L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l7.24-.91L12 2z"/></svg>
</a>
</div>
</header>
<Fragment slot="after-header">
<!-- Sticky Section Nav -->
<nav class="section-nav" id="section-nav" aria-label="Page sections">
<a href="#foundation" class="section-nav-item" data-section="foundation"><span class="section-nav-num">01</span><span class="section-nav-label">Foundation</span></a>
@@ -207,8 +170,8 @@
</div>
</div>
</section>
</Fragment>
<main class="site-content" id="main-content">
<!-- 01. THE FOUNDATION - Design knowledge -->
<section class="foundation-section" id="foundation">
@@ -1000,9 +963,9 @@
</div>
<ul class="changelog-items">
<li><strong>Live mode lands valid TSX through the wrap → preview → accept → carbonize loop on Vite/Next React/TSX projects.</strong> The wrapper now keeps a single JSX-slot child instead of three adjacent siblings, so it round-trips cleanly inside <code>return (...)</code>, array <code>.map(...)</code>, and <code>asChild</code> parents like Radix's <code>&lt;DialogPrimitive.Title&gt;</code>. Carbonize stopped double-wrapping CSS in nested template literals on TSX targets, accept and discard restore the picked element at its original indent (with relative depth between lines preserved), and the screenshot overlay no longer flashes solid black on default-background pages. Closes <a href="https://github.com/pbakaus/impeccable/issues/114" target="_blank" rel="noopener">#114</a>, with thanks again to <a href="https://github.com/dergachoff" target="_blank" rel="noopener">@dergachoff</a> for the detailed bug report.</li>
<li><strong>Wrap correctly disambiguates repeated identical-class siblings.</strong> A list of <code>&lt;Card className="card"&gt;</code> rendered three times with the same classes used to land on the first one regardless of which the user picked. <code>live-wrap.mjs</code> now accepts <code>--text TEXT</code> (the picked element's <code>textContent</code>) and narrows candidates accordingly, falling back to first-match when the text is too short or doesn't appear literally in source (data-driven children like <code>{title}</code>), and erroring with <code>element_ambiguous</code> when multiple branches still match equally.</li>
<li><strong>Wrap correctly disambiguates repeated identical-class siblings.</strong> A list of <code>&lt;Card className="card"&gt;</code> rendered three times with the same classes used to land on the first one regardless of which the user picked. <code>live-wrap.mjs</code> now accepts <code>--text TEXT</code> (the picked element's <code>textContent</code>) and narrows candidates accordingly, falling back to first-match when the text is too short or doesn't appear literally in source (data-driven children like <code>&#123;title&#125;</code>), and erroring with <code>element_ambiguous</code> when multiple branches still match equally.</li>
<li><strong><code>live-inject</code> CSP-meta unwrap now byte-for-byte preserves self-closing tag whitespace.</strong> The patch+revert cycle on a <code>&lt;meta http-equiv="Content-Security-Policy" ... /&gt;</code> tag was eating the space before <code>/&gt;</code> via a double-space artifact in the marker insertion path; common Vite shapes that ship a CSP meta now round-trip cleanly.</li>
<li><strong><code>live.md</code> spec gained explicit guidance for three real authoring traps.</strong> Variant CSS must use a descendant combinator (<code>:scope &gt; .card</code>, not bare <code>:scope</code>) or it lands on the wrapper instead of the picked element. JSX <code>&lt;style&gt;</code> bodies need <code>{`...`}</code> template-literal wrapping. Aborting an in-flight session uses <code>live-poll --reply EVENT_ID error "msg"</code>, not <code>live-accept --discard</code>; the latter only mutates source while the bar stays stuck on GENERATING dots forever.</li>
<li><strong><code>live.md</code> spec gained explicit guidance for three real authoring traps.</strong> Variant CSS must use a descendant combinator (<code>:scope &gt; .card</code>, not bare <code>:scope</code>) or it lands on the wrapper instead of the picked element. JSX <code>&lt;style&gt;</code> bodies need <code>&#123;`...`&#125;</code> template-literal wrapping. Aborting an in-flight session uses <code>live-poll --reply EVENT_ID error "msg"</code>, not <code>live-accept --discard</code>; the latter only mutates source while the bar stays stuck on GENERATING dots forever.</li>
</ul>
</div>
@@ -1015,7 +978,7 @@
<li><strong><code>/impeccable craft</code> now treats approved mocks as visual contracts.</strong> The craft flow requires a mock fidelity inventory before build, maps major visible ingredients to code or assets, and flags missing hero objects, imagery, section structure, nav/CTA treatment, and distinctive motifs as blocking defects unless the user accepted the deviation.</li>
<li><strong>Image-led brand surfaces can no longer degrade into abstract panels.</strong> Travel, editorial, portfolio, venue, product showcase, entertainment, and education work now requires credible imagery, generated plates, illustrations, maps, renders, or destination scenes when the approved mock or subject matter calls for them.</li>
<li><strong><code>/impeccable craft</code> and <code>/impeccable shape</code> hardened against autonomous agents.</strong> Codex-class harnesses had started writing files before the user confirmed a design brief, treating their own summary as the brief. Setup now ships a preflight checklist (context, product, command reference, shape, image gate, mutation), and craft enforces an explicit build gate: <code>shape=pass</code> only counts when the user separately approves the brief or supplies a pre-confirmed one. Self-authored briefs and "the implementation will be semantic anyway" no longer skip the visual probe.</li>
<li><strong>Live picker plays nice with modal hosts.</strong> Inside Radix Dialog, Headless UI, vaul, and other portals that lock <code>body { pointer-events: none }</code> or attach outside-click dismissers, the picker chrome had become unclickable, and any click that did land would dismiss the host dialog. The bar, picker, params panel, annotation overlay, and design panel now defang outside-handlers at the chrome boundary and force <code>pointer-events: auto</code> on themselves. Theme detection also stopped misreading a transparent body as black. Closes <a href="https://github.com/pbakaus/impeccable/issues/113" target="_blank" rel="noopener">#113</a>, with thanks to <a href="https://github.com/dergachoff" target="_blank" rel="noopener">@dergachoff</a> for the thoughtful bug report.</li>
<li><strong>Live picker plays nice with modal hosts.</strong> Inside Radix Dialog, Headless UI, vaul, and other portals that lock <code>body &#123; pointer-events: none &#125;</code> or attach outside-click dismissers, the picker chrome had become unclickable, and any click that did land would dismiss the host dialog. The bar, picker, params panel, annotation overlay, and design panel now defang outside-handlers at the chrome boundary and force <code>pointer-events: auto</code> on themselves. Theme detection also stopped misreading a transparent body as black. Closes <a href="https://github.com/pbakaus/impeccable/issues/113" target="_blank" rel="noopener">#113</a>, with thanks to <a href="https://github.com/dergachoff" target="_blank" rel="noopener">@dergachoff</a> for the thoughtful bug report.</li>
</ul>
</div>
@@ -1310,37 +1273,11 @@
</div>
</div>
</section>
</main>
<footer class="site-footer">
<div class="footer-row">
<span class="footer-logo">Impeccable</span>
<nav class="footer-links" aria-label="Footer">
<a href="/designing">Designing</a>
<a href="/docs">Docs</a>
<a href="/slop">Slop</a>
<a href="/live-mode">Live Mode</a>
<a href="#language">Commands</a>
<a href="/privacy">Privacy</a>
<a href="https://github.com/pbakaus/impeccable">GitHub</a>
</nav>
<div class="footer-credit">
<span>Created by <a href="https://x.com/pbakaus" target="_blank" rel="noopener">Paul Bakaus</a></span>
<a href="https://x.com/pbakaus" class="footer-social-link" aria-label="Follow on X" target="_blank" rel="noopener">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
</svg>
</a>
<a href="https://linkedin.com/in/paulbakaus" class="footer-social-link" aria-label="Connect on LinkedIn" target="_blank" rel="noopener">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
</svg>
</a>
</div>
</div>
</footer>
<Fragment slot="scripts">
<script>
import '../scripts/app.js';
</script>
</Fragment>
<script type="module" src="./app.js"></script>
</body>
</html>
</Base>
+354
View File
@@ -0,0 +1,354 @@
---
import Base from '../../layouts/Base.astro';
import '../../styles/sub-pages.css';
import '../../styles/live-mode.css';
---
<Base
title="Live Mode | Impeccable"
description="Iterate on UI in the browser. Pick an element, drop a comment, get three production-quality variants, accept one, and it writes back to source. /impeccable live."
activeNav="live"
canonicalPath="/live-mode"
bodyClass="sub-page live-mode-page-body"
>
<div class="live-mode-page">
<header class="live-mode-page-header">
<p class="live-mode-page-eyebrow">New in v3.0 <span class="live-mode-page-eyebrow-badge">Alpha</span></p>
<h1 class="live-mode-page-title">Live Mode</h1>
<p class="live-mode-page-lede">Pick any element in the browser. Drop a comment or a stroke. Three production-quality variants swap in via your framework's HMR. Accept the one you want and it writes back to source.</p>
<p class="live-mode-page-alpha-note"><strong>Why alpha:</strong> Live Mode works end-to-end and is ready to try, but it still needs more testing against real-world repos and framework configs. Expect rough edges on uncommon setups, and please report what breaks.</p>
<div class="live-mode-start" aria-label="Start live mode command">
<span class="live-mode-start-prompt">$</span>
<code class="live-mode-start-cmd">/impeccable live</code>
<button class="live-mode-start-copy" type="button" aria-label="Copy command" data-copy="/impeccable live">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"></path>
</svg>
</button>
</div>
</header>
<section class="live-mode-demo-wrap" aria-label="Live Mode interactive demo">
<div class="live-demo" id="live-demo" aria-label="Live Mode interactive demo loop">
<div class="live-demo-frame-col"><div class="live-demo-frame">
<div class="live-demo-chrome">
<span class="live-demo-dot"></span>
<span class="live-demo-dot"></span>
<span class="live-demo-dot"></span>
<span class="live-demo-url">localhost:3000</span>
</div>
<div class="live-demo-stage">
<div class="live-demo-skeleton" aria-hidden="true">
<div class="live-demo-skel-nav">
<span class="live-demo-skel-logo"></span>
<span class="live-demo-skel-link"></span>
<span class="live-demo-skel-link"></span>
<span class="live-demo-skel-link"></span>
<span class="live-demo-skel-cta"></span>
</div>
<div class="live-demo-skel-heading"></div>
<div class="live-demo-skel-line"></div>
<div class="live-demo-skel-line live-demo-skel-line--short"></div>
</div>
<div class="live-demo-target" data-demo-target>
<div class="live-demo-variant is-active" data-variant="original">
<div class="live-demo-card live-demo-card--plain">
<span class="live-demo-card-kicker">Newsletter</span>
<h3>Subscribe for updates</h3>
<p>Monthly-ish design notes.</p>
<button type="button">Subscribe</button>
</div>
</div>
<div class="live-demo-variant" data-variant="1">
<div class="live-demo-card live-demo-card--v1">
<span class="live-demo-card-kicker">No. 04</span>
<h3>Letters, <em>occasionally</em>.</h3>
<p>A postcard from the editor, about once a month. No tracking pixels, no "just checking in."</p>
<button type="button">Send me one</button>
</div>
</div>
<div class="live-demo-variant" data-variant="2">
<div class="live-demo-card live-demo-card--v2">
<div class="live-demo-card-stamp">&#x261E;</div>
<span class="live-demo-card-kicker">Dispatch</span>
<h3>Design&nbsp;notes, <br>every&nbsp;other<br>Thursday.</h3>
<button type="button">Join the list &rarr;</button>
</div>
</div>
<div class="live-demo-variant" data-variant="3">
<div class="live-demo-card live-demo-card--v3">
<div class="live-demo-card-sticker"><span>&star;</span><span>&star;</span><span>&star;</span></div>
<span class="live-demo-card-kicker">Field Notes</span>
<h3>A monthly letter, for people who still read email for pleasure.</h3>
<button type="button">Receive the letter <span aria-hidden="true">&#x273A;</span></button>
</div>
</div>
</div>
<div class="live-demo-outline" data-demo-outline aria-hidden="true"></div>
<div class="live-demo-annotations" data-demo-annotations aria-hidden="true">
<svg class="live-demo-stroke" viewBox="0 0 300 60" preserveAspectRatio="none" aria-hidden="true">
<path d="M 10,40 Q 60,10 110,38 T 210,32 T 290,20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" pathLength="1"/>
</svg>
<div class="live-demo-comment">more playful</div>
</div>
<div class="live-demo-ctx" data-demo-ctx data-phase="hidden">
<div class="live-demo-ctx-row live-demo-ctx-row--configure">
<button type="button" class="live-demo-ctx-pill" data-demo-ctx-pill>
<span data-demo-cmd-name>delight</span>
<span class="live-demo-ctx-pill-caret" aria-hidden="true">&#x25BE;</span>
</button>
<span class="live-demo-ctx-input" data-demo-input>
<span data-demo-input-text></span><span class="live-demo-ctx-caret"></span>
</span>
<button type="button" class="live-demo-ctx-count">&times;3</button>
<button type="button" class="live-demo-ctx-go" data-demo-go>Go <span aria-hidden="true">&rarr;</span></button>
</div>
<div class="live-demo-ctx-row live-demo-ctx-row--generating">
<span class="live-demo-ctx-spinner" aria-hidden="true"></span>
<span>Generating variants&hellip;</span>
</div>
<div class="live-demo-ctx-row live-demo-ctx-row--cycling">
<button type="button" class="live-demo-ctx-nav" aria-label="Previous variant">&lsaquo;</button>
<span class="live-demo-ctx-counter" data-demo-counter>1 / 3</span>
<button type="button" class="live-demo-ctx-nav" aria-label="Next variant">&rsaquo;</button>
<span class="live-demo-ctx-divider"></span>
<button type="button" class="live-demo-ctx-discard" aria-label="Discard">&times;</button>
<button type="button" class="live-demo-ctx-accept" data-demo-accept>Accept</button>
</div>
<div class="live-demo-ctx-row live-demo-ctx-row--accepted">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
<span>Variant 3 written to source</span>
</div>
</div>
<div class="live-demo-cursor" data-demo-cursor aria-hidden="true">
<svg width="18" height="22" viewBox="0 0 18 22" fill="none">
<path d="M1 1 L1 17 L5 13 L8 20 L11 19 L7.5 12 L13 12 Z" fill="#111" stroke="#fff" stroke-width="1.2" stroke-linejoin="round"/>
</svg>
</div>
</div>
<div class="live-demo-gbar" data-demo-gbar>
<span class="live-demo-gbar-brand">/</span>
<button type="button" class="live-demo-gbar-btn is-active" data-demo-gbar-pick>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="22" y1="12" x2="18" y2="12"/><line x1="6" y1="12" x2="2" y2="12"/><line x1="12" y1="6" x2="12" y2="2"/><line x1="12" y1="22" x2="12" y2="18"/></svg>
<span>Pick</span>
</button>
<button type="button" class="live-demo-gbar-btn">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
</button>
<button type="button" class="live-demo-gbar-btn">
<span class="live-demo-gbar-dmd" aria-hidden="true"><span></span><span></span><span></span><span></span></span>
</button>
<span class="live-demo-gbar-divider"></span>
<button type="button" class="live-demo-gbar-x" aria-label="Exit live mode">&times;</button>
</div>
</div></div>
</div>
<p class="live-mode-demo-caption">Click the frame or scroll it into view to start the loop. Respects <code>prefers-reduced-motion</code>.</p>
</section>
<section class="live-mode-stages" aria-label="What happens in a live mode session">
<h2 class="live-mode-stages-title">What happens, in three moves</h2>
<div class="live-mode-stages-grid">
<article class="live-mode-stage">
<span class="live-mode-stage-num">01 &middot; Pick</span>
<h3 class="live-mode-stage-name">Point at what bugs you</h3>
<p class="live-mode-stage-desc">Click any element on your running dev server. Add a comment pin where the issue lives. Draw a stroke through the bit you want to change. Or just type "more playful".</p>
<div class="live-mode-stage-viz">
<div class="docs-viz-picker-row" style="min-height:72px;padding:10px">
<div class="docs-viz-picker-target" style="font-size:12px;padding:6px 12px">
Newsletter card
<span class="docs-viz-picker-pin" style="width:18px;height:18px;font-size:9px">1</span>
</div>
</div>
</div>
</article>
<article class="live-mode-stage">
<span class="live-mode-stage-num">02 &middot; Generate</span>
<h3 class="live-mode-stage-name">Three genuinely different takes</h3>
<p class="live-mode-stage-desc">Variants anchor to different archetypes, not three riffs on color. Each one explores a different primary axis: hierarchy, typography, density, layout, or palette strategy.</p>
<div class="live-mode-stage-viz">
<div class="docs-viz-variants" style="width:100%;gap:4px">
<div class="docs-viz-variant docs-viz-variant--v1" style="min-height:44px;padding:6px"><span class="docs-viz-variant-kicker" style="font-size:8px">No.04</span></div>
<div class="docs-viz-variant docs-viz-variant--v2 is-active" style="min-height:44px;padding:6px"><span class="docs-viz-variant-kicker" style="font-size:8px">Dispatch</span></div>
<div class="docs-viz-variant docs-viz-variant--v3" style="min-height:44px;padding:6px"><span class="docs-viz-variant-kicker" style="font-size:8px">Field</span></div>
</div>
</div>
</article>
<article class="live-mode-stage">
<span class="live-mode-stage-num">03 &middot; Accept</span>
<h3 class="live-mode-stage-name">Lands in real source</h3>
<p class="live-mode-stage-desc">The accepted variant replaces the picked element in your source file. CSS consolidates into your real stylesheet, not inline. Discard all three and the original stays.</p>
<div class="live-mode-stage-viz">
<span class="docs-viz-accept-pill">Variant 2 written to source</span>
</div>
</article>
</div>
</section>
<section class="live-mode-pathways" aria-label="Where to go next">
<h2 class="live-mode-pathways-title">Where next</h2>
<div class="live-mode-pathways-grid">
<a class="live-mode-pathway" href="/tutorials/iterate-live">
<span class="live-mode-pathway-kind">Tutorial</span>
<h3 class="live-mode-pathway-title">Walk it step by step</h3>
<p class="live-mode-pathway-desc">A ten-minute walkthrough from first run to accepted variant. Covers CSP patching, the picker actions, and the fallback flow for generated files.</p>
<span class="live-mode-pathway-cta">Open the tutorial &rarr;</span>
</a>
<a class="live-mode-pathway" href="/docs/live">
<span class="live-mode-pathway-kind">Reference</span>
<h3 class="live-mode-pathway-title">Full command reference</h3>
<p class="live-mode-pathway-desc">Everything your AI harness reads when <code>/impeccable live</code> runs: the poll loop, the wrap/accept helpers, the CSP templates, and every event shape.</p>
<span class="live-mode-pathway-cta">Read the reference &rarr;</span>
</a>
<a class="live-mode-pathway" href="/#downloads">
<span class="live-mode-pathway-kind">Install</span>
<h3 class="live-mode-pathway-title">Get Impeccable set up</h3>
<p class="live-mode-pathway-desc">Install the skill and CLI once, then run <code>/impeccable live</code> from your AI harness. Works with Claude Code, Cursor, Codex, Gemini, and more.</p>
<span class="live-mode-pathway-cta">See the install steps &rarr;</span>
</a>
</div>
</section>
<section class="live-mode-frameworks" aria-label="Supported frameworks">
<span class="live-mode-frameworks-label">Supported dev servers</span>
<ul class="live-mode-frameworks-list">
<li>Vite</li>
<li>Next.js (incl. monorepos)</li>
<li>SvelteKit</li>
<li>Astro</li>
<li>Nuxt</li>
<li>Bun</li>
<li>Plain static HTML</li>
</ul>
</section>
</div>
<script>
import { initLiveDemo } from "../../scripts/components/live-demo.js";
document.addEventListener("DOMContentLoaded", initLiveDemo);
</script>
<script is:inline>
document.addEventListener('click', (e) => {
const btn = e.target.closest('[data-copy]');
if (!btn) return;
const text = btn.getAttribute('data-copy');
if (!text) return;
navigator.clipboard.writeText(text).then(() => {
btn.classList.add('is-copied');
setTimeout(() => btn.classList.remove('is-copied'), 1500);
}).catch(() => {});
});
document.addEventListener('click', (e) => {
const toggle = e.target.closest('.skills-sidebar-toggle');
if (!toggle) return;
const expanded = toggle.getAttribute('aria-expanded') === 'true';
toggle.setAttribute('aria-expanded', String(!expanded));
});
(function initSplitCompare() {
const wrappers = document.querySelectorAll('.split-comparison');
if (wrappers.length === 0) return;
const hasHover = matchMedia('(hover: hover)').matches;
const DEFAULT_POSITION = 50;
for (const wrapper of wrappers) {
const container = wrapper.querySelector('.split-container');
const splitAfter = wrapper.querySelector('.split-after');
const splitDivider = wrapper.querySelector('.split-divider');
if (!container || !splitAfter || !splitDivider) continue;
const tanAngle = Math.tan(10 * Math.PI / 180);
let skewOffset = 8;
const recalcSkew = () => {
const r = container.getBoundingClientRect();
if (r.width > 0 && r.height > 0) {
skewOffset = 50 * r.height * tanAngle / r.width;
}
};
recalcSkew();
window.addEventListener('resize', recalcSkew, { passive: true });
let targetX = DEFAULT_POSITION;
let currentX = DEFAULT_POSITION;
let rafId = null;
const paint = (pct) => {
const x = Math.max(-skewOffset, Math.min(100 + skewOffset, pct));
splitAfter.style.clipPath =
`polygon(${x + skewOffset}% 0%, 100% 0%, 100% 100%, ${x - skewOffset}% 100%)`;
splitDivider.style.left = `${x}%`;
};
const step = () => {
currentX += (targetX - currentX) * 0.2;
if (Math.abs(targetX - currentX) < 0.1) {
currentX = targetX;
rafId = null;
} else {
rafId = requestAnimationFrame(step);
}
paint(currentX);
};
const setTarget = (pct) => {
targetX = pct;
if (rafId === null) rafId = requestAnimationFrame(step);
};
paint(DEFAULT_POSITION);
const pctFromClientX = (clientX) => {
const rect = container.getBoundingClientRect();
return ((clientX - rect.left) / rect.width) * 100;
};
let hovering = false;
let dragging = false;
wrapper.addEventListener('pointerenter', (e) => {
if (hasHover && e.pointerType === 'mouse') hovering = true;
});
wrapper.addEventListener('pointerdown', (e) => {
dragging = true;
wrapper.setPointerCapture(e.pointerId);
setTarget(pctFromClientX(e.clientX));
});
wrapper.addEventListener('pointermove', (e) => {
if (dragging || hovering) setTarget(pctFromClientX(e.clientX));
});
const endDrag = (e) => {
if (dragging) {
dragging = false;
try { wrapper.releasePointerCapture(e.pointerId); } catch {}
}
};
wrapper.addEventListener('pointerup', endDrag);
wrapper.addEventListener('pointercancel', endDrag);
wrapper.addEventListener('pointerleave', (e) => {
endDrag(e);
if (hovering) {
hovering = false;
setTarget(DEFAULT_POSITION);
}
});
}
})();
</script>
</Base>
+54
View File
@@ -0,0 +1,54 @@
---
import Base from '../layouts/Base.astro';
import '../styles/sub-pages.css';
---
<Base title="Privacy Policy - Impeccable" noIndex={true} bodyClass="sub-page">
<style>
.privacy-content { max-width: 680px; margin: 0 auto; padding: 2rem 1.5rem 4rem; }
.privacy-content h1 { font-family: var(--font-display); font-size: 2.25rem; margin-bottom: 0.5rem; letter-spacing: -0.01em; }
.privacy-content h2 { font-size: 1.125rem; margin-top: 2rem; font-weight: 600; }
.privacy-content p, .privacy-content ul { color: var(--color-charcoal); margin-top: 0.5rem; }
.privacy-content ul { padding-left: 1.25rem; }
.privacy-content li { margin-top: 0.25rem; }
.privacy-content a { color: var(--color-accent); text-decoration: underline; text-underline-offset: 3px; }
.privacy-content a:hover { color: var(--color-accent-hover); }
.privacy-content .updated { color: var(--color-ash); font-size: 0.875rem; margin-bottom: 2rem; font-style: italic; }
</style>
<div class="privacy-content">
<h1>Privacy Policy</h1>
<p class="updated">Last updated: April 6, 2026</p>
<h2>What Impeccable is</h2>
<p>Impeccable is an open-source collection of agent skills (text files) that run locally in your AI coding tool. The skills themselves collect no data, make no network requests, and have no analytics.</p>
<h2>Website analytics</h2>
<p>The Impeccable website (<a href="https://impeccable.style">impeccable.style</a>) uses Google Analytics to understand traffic patterns (page views, referrers, country). No personal information is collected beyond what Google Analytics provides by default. No cookies are used for advertising.</p>
<h2>Downloads</h2>
<p>When you download a skill bundle from the website, we log the download event (which bundle, timestamp) for usage statistics. No personal information is attached to these logs.</p>
<h2>Claude Code Plugin</h2>
<p>When installed as a Claude Code plugin, Impeccable runs entirely within your local Claude Code session. No data is sent to Impeccable's servers. Anthropic's own privacy policy governs the Claude Code application itself.</p>
<h2>Chrome Extension</h2>
<p>The Impeccable Chrome DevTools extension runs entirely in your browser. All anti-pattern detection happens locally on the page you are inspecting. No page content, URLs, or detection results are ever sent to any external server.</p>
<p>The extension stores your rule preferences (which detections are enabled or disabled) using Chrome's built-in sync storage (<code>chrome.storage.sync</code>), which syncs settings across your Chrome instances via your Google account. No other data is stored or transmitted.</p>
<p>The extension requests the following permissions:</p>
<ul>
<li><strong>activeTab / scripting</strong> - to inject the detector script into the page you are inspecting</li>
<li><strong>storage</strong> - to save your rule preferences</li>
<li><strong>webNavigation</strong> - to re-scan automatically when you navigate to a new page</li>
<li><strong>Host permissions (all URLs)</strong> - so the detector can run on any website you choose to inspect</li>
</ul>
<h2>GitHub</h2>
<p>The source code is hosted on GitHub. Interactions with the repository (issues, pull requests, stars) are governed by <a href="https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement">GitHub's privacy policy</a>.</p>
<h2>Contact</h2>
<p>Questions about this policy? Open an issue on <a href="https://github.com/pbakaus/impeccable">GitHub</a> or reach out to <a href="https://x.com/pbakaus">@pbakaus</a>.</p>
</div>
</Base>
@@ -1,43 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Slop | Impeccable</title>
<meta name="description" content="37 patterns that mark an interface as AI-generated, plus the live detection overlay that catches them in place. The rule catalog behind npx impeccable detect, the browser extension, and /impeccable critique.">
<meta name="theme-color" content="#fafafa">
<link rel="canonical" href="https://impeccable.style/slop">
<link rel="icon" type="image/svg+xml" href="../favicon.svg">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,600;1,400&family=Instrument+Sans:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../css/sub-pages.css">
</head>
<body class="sub-page skills-layout-page slop-page">
<a href="#main" class="skip-link">Skip to content</a>
<!-- site-header v1 -->
<header class="site-header" data-site-header>
<a href="/" class="site-header-brand" aria-label="Impeccable home">
<svg class="site-header-brand-logo" viewBox="0 0 32 32" aria-hidden="true"><rect width="32" height="32" rx="6" fill="#1a1a1a"/><text x="16" y="24" font-family="system-ui, -apple-system, sans-serif" font-size="22" font-weight="500" fill="#f5f3ef" text-anchor="middle">/</text></svg>
<span class="site-header-brand-name">Impeccable</span>
</a>
<div class="site-header-right">
<nav class="site-header-nav" aria-label="Primary">
<a href="/" data-nav="home">Home</a>
<a href="/designing" data-nav="designing">Designing</a>
<a href="/docs" data-nav="docs">Docs</a>
<a href="/slop" data-nav="slop" aria-current="page">Slop</a>
<a href="/live-mode" data-nav="live">Live</a>
</nav>
<a href="https://github.com/pbakaus/impeccable" class="site-header-github" target="_blank" rel="noopener" aria-label="Impeccable on GitHub, 23k stars">
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/></svg>
<span class="site-header-github-label">23k</span>
<svg class="site-header-github-star" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2l2.76 6.36L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l7.24-.91L12 2z"/></svg>
</a>
</div>
</header>
<main id="main">
---
import Base from '../../layouts/Base.astro';
import '../../styles/sub-pages.css';
---
<Base
title="Slop | Impeccable"
description="37 patterns that mark an interface as AI-generated, plus the live detection overlay that catches them in place. The rule catalog behind npx impeccable detect, the browser extension, and /impeccable critique."
activeNav="slop"
canonicalPath="/slop"
bodyClass="sub-page skills-layout-page slop-page"
>
<div class="skills-layout">
@@ -79,18 +51,26 @@
<p class="sub-page-lede">37 patterns that mark an interface as AI-generated, and the detection overlay that catches them in place. Watch it flag them live, try it on 11 synthetic specimens, or browse the full catalog. 25 rules run deterministically (<code>npx impeccable detect</code> or the browser extension); 12 need <a href="/docs/critique">/impeccable critique</a>'s LLM review pass.</p>
</header>
<section class="slop-section visual-mode-demo-wrap" id="see-it" aria-label="Detection overlay demo">
<section class="slop-section slop-then-now" id="see-it" aria-label="Detection overlay demo">
<h2 class="slop-section-heading"><span class="slop-section-num">01</span> See it</h2>
<div class="slop-then-now-intro">
<p class="slop-then-now-lede">Every wave of AI-generated UIs converges on a recognizable aesthetic. The detector catches both. The patterns just change.</p>
<div class="slop-era-toggle" role="tablist" aria-label="Select slop era">
<button class="slop-era-tab is-active" role="tab" aria-selected="true" data-era="2022">2022</button>
<button class="slop-era-tab" role="tab" aria-selected="false" data-era="2026">2026</button>
</div>
</div>
<div class="visual-mode-preview">
<div class="visual-mode-preview-header">
<span class="visual-mode-preview-dot red"></span>
<span class="visual-mode-preview-dot yellow"></span>
<span class="visual-mode-preview-dot green"></span>
<span class="visual-mode-preview-title">Live on a synthetic slop page</span>
<span class="visual-mode-preview-title" data-title-2022="Purple gradients, glassmorphism, neon glow" data-title-2026="Fraunces, warm cream, editorial restraint">Purple gradients, glassmorphism, neon glow</span>
</div>
<iframe src="/antipattern-examples/visual-mode-demo.html" class="visual-mode-frame" loading="lazy" title="Impeccable overlay running on a demo page"></iframe>
<iframe src="/antipattern-examples/old-slop-2022.html" class="visual-mode-frame slop-era-frame" data-era="2022" loading="lazy" title="Impeccable overlay on 2022-era AI slop"></iframe>
<iframe src="/antipattern-examples/new-slop-2026.html" class="visual-mode-frame slop-era-frame" data-era="2026" loading="lazy" title="Impeccable overlay on 2026-era AI slop" style="display:none"></iframe>
</div>
<p class="visual-mode-demo-caption">Hover or tap any outlined element to see which rule fired.</p>
<p class="visual-mode-demo-caption">Hover or tap any outlined element to see which rule fired. Toggle the era to see how the patterns shifted.</p>
</section>
<section class="slop-section visual-mode-gallery" id="try-it-live" aria-label="Try the overlay on synthetic specimens">
@@ -102,7 +82,7 @@
<a class="gallery-card" href="/antipattern-examples/purple-gradients.html">
<div class="gallery-card-thumb">
<img src="../antipattern-images/purple-gradients.png" alt="Purple Gradients Everywhere specimen" loading="lazy" width="540" height="540">
<img src="/antipattern-images/purple-gradients.png" alt="Purple Gradients Everywhere specimen" loading="lazy" width="540" height="540">
</div>
<div class="gallery-card-body">
<h3 class="gallery-card-title">Purple Gradients Everywhere</h3>
@@ -112,7 +92,7 @@
<a class="gallery-card" href="/antipattern-examples/lazy-cool.html">
<div class="gallery-card-thumb">
<img src="../antipattern-images/lazy-cool.png" alt="Lazy &quot;Cool&quot; specimen" loading="lazy" width="540" height="540">
<img src="/antipattern-images/lazy-cool.png" alt="Lazy &quot;Cool&quot; specimen" loading="lazy" width="540" height="540">
</div>
<div class="gallery-card-body">
<h3 class="gallery-card-title">Lazy &quot;Cool&quot;</h3>
@@ -122,7 +102,7 @@
<a class="gallery-card" href="/antipattern-examples/lazy-impact.html">
<div class="gallery-card-thumb">
<img src="../antipattern-images/lazy-impact.png" alt="Lazy &quot;Impact&quot; specimen" loading="lazy" width="540" height="540">
<img src="/antipattern-images/lazy-impact.png" alt="Lazy &quot;Impact&quot; specimen" loading="lazy" width="540" height="540">
</div>
<div class="gallery-card-body">
<h3 class="gallery-card-title">Lazy &quot;Impact&quot;</h3>
@@ -132,7 +112,7 @@
<a class="gallery-card" href="/antipattern-examples/thick-border-cards.html">
<div class="gallery-card-thumb">
<img src="../antipattern-images/thick-border-cards.png" alt="Side-Tab Cards specimen" loading="lazy" width="540" height="540">
<img src="/antipattern-images/thick-border-cards.png" alt="Side-Tab Cards specimen" loading="lazy" width="540" height="540">
</div>
<div class="gallery-card-body">
<h3 class="gallery-card-title">Side-Tab Cards</h3>
@@ -142,7 +122,7 @@
<a class="gallery-card" href="/antipattern-examples/cardocalypse.html">
<div class="gallery-card-thumb">
<img src="../antipattern-images/cardocalypse.png" alt="Cardocalypse specimen" loading="lazy" width="540" height="540">
<img src="/antipattern-images/cardocalypse.png" alt="Cardocalypse specimen" loading="lazy" width="540" height="540">
</div>
<div class="gallery-card-body">
<h3 class="gallery-card-title">Cardocalypse</h3>
@@ -152,7 +132,7 @@
<a class="gallery-card" href="/antipattern-examples/layout-templates.html">
<div class="gallery-card-thumb">
<img src="../antipattern-images/layout-templates.png" alt="Copy-Paste Layouts specimen" loading="lazy" width="540" height="540">
<img src="/antipattern-images/layout-templates.png" alt="Copy-Paste Layouts specimen" loading="lazy" width="540" height="540">
</div>
<div class="gallery-card-body">
<h3 class="gallery-card-title">Copy-Paste Layouts</h3>
@@ -162,7 +142,7 @@
<a class="gallery-card" href="/antipattern-examples/inter-everywhere.html">
<div class="gallery-card-thumb">
<img src="../antipattern-images/inter-everywhere.png" alt="Inter Everywhere specimen" loading="lazy" width="540" height="540">
<img src="/antipattern-images/inter-everywhere.png" alt="Inter Everywhere specimen" loading="lazy" width="540" height="540">
</div>
<div class="gallery-card-body">
<h3 class="gallery-card-title">Inter Everywhere</h3>
@@ -172,7 +152,7 @@
<a class="gallery-card" href="/antipattern-examples/massive-icons.html">
<div class="gallery-card-thumb">
<img src="../antipattern-images/massive-icons.png" alt="Massive Icons specimen" loading="lazy" width="540" height="540">
<img src="/antipattern-images/massive-icons.png" alt="Massive Icons specimen" loading="lazy" width="540" height="540">
</div>
<div class="gallery-card-body">
<h3 class="gallery-card-title">Massive Icons</h3>
@@ -182,7 +162,7 @@
<a class="gallery-card" href="/antipattern-examples/bad-contrast.html">
<div class="gallery-card-thumb">
<img src="../antipattern-images/bad-contrast.png" alt="Bad Contrast Choices specimen" loading="lazy" width="540" height="540">
<img src="/antipattern-images/bad-contrast.png" alt="Bad Contrast Choices specimen" loading="lazy" width="540" height="540">
</div>
<div class="gallery-card-body">
<h3 class="gallery-card-title">Bad Contrast Choices</h3>
@@ -192,7 +172,7 @@
<a class="gallery-card" href="/antipattern-examples/redundant-ux-writing.html">
<div class="gallery-card-thumb">
<img src="../antipattern-images/redundant-ux-writing.png" alt="Redundant UX Writing specimen" loading="lazy" width="540" height="540">
<img src="/antipattern-images/redundant-ux-writing.png" alt="Redundant UX Writing specimen" loading="lazy" width="540" height="540">
</div>
<div class="gallery-card-body">
<h3 class="gallery-card-title">Redundant UX Writing</h3>
@@ -202,7 +182,7 @@
<a class="gallery-card" href="/antipattern-examples/modal-abuse.html">
<div class="gallery-card-thumb">
<img src="../antipattern-images/modal-abuse.png" alt="Modal Abuse specimen" loading="lazy" width="540" height="540">
<img src="/antipattern-images/modal-abuse.png" alt="Modal Abuse specimen" loading="lazy" width="540" height="540">
</div>
<div class="gallery-card-body">
<h3 class="gallery-card-title">Modal Abuse</h3>
@@ -806,8 +786,8 @@
</div>
</div>
</div>
</main>
<script>
<script is:inline>
// Copy buttons on rendered code blocks
document.addEventListener('click', (e) => {
const btn = e.target.closest('[data-copy]');
@@ -828,6 +808,22 @@
toggle.setAttribute('aria-expanded', String(!expanded));
});
// Slop era toggle (2022 vs 2026)
document.addEventListener('click', (e) => {
const tab = e.target.closest('.slop-era-tab');
if (!tab) return;
const era = tab.getAttribute('data-era');
document.querySelectorAll('.slop-era-tab').forEach(t => {
t.classList.toggle('is-active', t === tab);
t.setAttribute('aria-selected', String(t === tab));
});
document.querySelectorAll('.slop-era-frame').forEach(f => {
f.style.display = f.getAttribute('data-era') === era ? '' : 'none';
});
const title = document.querySelector('.visual-mode-preview-title[data-title-2022]');
if (title) title.textContent = title.getAttribute('data-title-' + era);
});
// Before/after split-compare: drag on touch, hover OR drag on mouse.
// Pointer events attach to the padded .split-comparison wrapper so
// there is a ~20px invisible buffer around the visible box. The
@@ -933,5 +929,5 @@
}
})();
</script>
</body>
</html>
</Base>
+49
View File
@@ -0,0 +1,49 @@
---
import { getCollection, render } from 'astro:content';
import Base from '../../layouts/Base.astro';
import '../../styles/sub-pages.css';
export async function getStaticPaths() {
const entries = await getCollection('tutorials');
return entries.map(entry => ({
params: { slug: entry.id },
props: { entry },
}));
}
const { entry } = Astro.props;
const { Content } = await render(entry);
---
<Base
title={`${entry.data.title} | Impeccable`}
description={entry.data.description}
activeNav="docs"
canonicalPath={`/tutorials/${entry.id}`}
bodyClass="sub-page"
>
<div class="tutorial-page">
<nav class="skills-breadcrumb" aria-label="Breadcrumb">
<a href="/docs">Docs</a>
<span aria-hidden="true">/</span>
<a href="/tutorials">Tutorials</a>
<span aria-hidden="true">/</span>
<span>{entry.data.title}</span>
</nav>
<header class="sub-page-header">
<span class="sub-page-eyebrow">Tutorial</span>
<h1 class="sub-page-title">{entry.data.title}</h1>
{entry.data.tagline && <p class="sub-page-lede">{entry.data.tagline}</p>}
</header>
<div class="skills-detail-body docs-body tutorial-body">
<Content />
</div>
</div>
<style>
.tutorial-page { max-width: 720px; margin: 0 auto; padding: 2rem 1.5rem 4rem; }
.tutorial-body { margin-top: 2rem; }
</style>
</Base>
+51
View File
@@ -0,0 +1,51 @@
---
import { getCollection } from 'astro:content';
import Base from '../../layouts/Base.astro';
import '../../styles/sub-pages.css';
const tutorials = (await getCollection('tutorials'))
.sort((a, b) => a.data.order - b.data.order);
---
<Base
title="Tutorials | Impeccable"
description="Step-by-step guides for getting started with Impeccable, from first install to live browser iteration."
activeNav="docs"
canonicalPath="/tutorials"
bodyClass="sub-page"
>
<div class="tutorials-index">
<header class="tutorials-index-header">
<h1 class="sub-page-title">Tutorials</h1>
<p class="sub-page-lede">Step-by-step guides, from first install to live browser iteration.</p>
</header>
<div class="tutorials-list">
{tutorials.map((t, i) => (
<a href={`/tutorials/${t.id}`} class="tutorial-card">
<span class="tutorial-card-num">{String(i + 1).padStart(2, '0')}</span>
<div>
<h2 class="tutorial-card-title">{t.data.title}</h2>
<p class="tutorial-card-desc">{t.data.description}</p>
</div>
</a>
))}
</div>
</div>
<style>
.tutorials-index { max-width: 720px; margin: 0 auto; padding: 2rem 1.5rem 4rem; }
.tutorials-index-header { margin-bottom: 2rem; }
.tutorials-list { display: flex; flex-direction: column; gap: 1rem; }
.tutorial-card {
display: flex; gap: 1.25rem; align-items: flex-start;
padding: 1.25rem; border-radius: 8px; border: 1px solid var(--color-mist);
text-decoration: none; color: inherit;
transition: border-color 0.15s ease;
}
.tutorial-card:hover { border-color: var(--color-accent); }
.tutorial-card-num { font-family: var(--font-mono); font-size: 0.8125rem; color: var(--color-ash); flex-shrink: 0; padding-top: 0.125rem; }
.tutorial-card-title { font-size: 1.125rem; font-weight: 600; margin: 0 0 0.25rem; }
.tutorial-card-desc { font-size: 0.875rem; color: var(--color-ash); margin: 0; line-height: 1.5; }
</style>
</Base>
+10 -10
View File
@@ -1,14 +1,14 @@
import {
initGlassTerminal,
renderTerminalLayout,
} from "./js/components/glass-terminal.js";
import { initLensEffect } from "./js/components/lens.js";
import { initFrameworkViz } from "./js/components/framework-viz.js";
import { initScrollReveal } from "./js/utils/reveal.js";
import { initAnchorScroll, initHashTracking } from "./js/utils/scroll.js";
import { initSectionNav } from "./js/components/section-nav.js";
import { initFoundationGrid } from "./js/components/foundation-grid.js";
import { initLiveDemo } from "./js/components/live-demo.js";
} from "./components/glass-terminal.js";
import { initLensEffect } from "./components/lens.js";
import { initFrameworkViz } from "./components/framework-viz.js";
import { initScrollReveal } from "./utils/reveal.js";
import { initAnchorScroll, initHashTracking } from "./utils/scroll.js";
import { initSectionNav } from "./components/section-nav.js";
import { initFoundationGrid } from "./components/foundation-grid.js";
import { initLiveDemo } from "./components/live-demo.js";
// ============================================
// STATE
@@ -33,8 +33,8 @@ function escapeHtml(value) {
async function loadContent() {
try {
const [commandsRes, patternsRes] = await Promise.all([
fetch("/api/commands"),
fetch("/api/patterns"),
fetch("/_data/api/commands.json"),
fetch("/_data/api/patterns.json"),
]);
// Check for HTTP errors

Some files were not shown because too many files have changed in this diff Show More