Claude Skill

ui-craft

Lifecycle for user-facing surfaces — revise a shipped surface in the running app, lock a greenfield visual spec, build to a lock, critique/audit/polish a UI, or re-settle a locked term. Use for any request to design, review, or verify rendered UI (screens, dashboards, flows, comp

LLM Mart · 0 points · 17 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download ConnorGriffin-skills-skills_drivers_ui-craft-872be56.zip · 317 KB
Part of connorgriffin/skills — 25 skills

Install

skills CLI npx skills add https://github.com/ConnorGriffin/skills/tree/main/skills/drivers/ui-craft
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install connorgriffin-skills@llmmart
Git git clone https://github.com/ConnorGriffin/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole connorgriffin/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

UI craft

One skill for the whole life of a user-facing surface: revise a shipped surface in place, lock a greenfield visual spec, build to it, critique it, audit it, polish it, re-settle it. Parts absorbed from impeccable (Apache-2.0, by Paul Bakaus — see the repo NOTICE).

Vocabulary guard: in the engineering charter, interface means a module's API. This skill owns surfaces — rendered UI. If the request is about a Python class, function signature, or module boundary, this is the wrong skill.

Setup (every invocation)

  1. Resolve this skill's installed directory as UI_CRAFT_SKILL_DIR (e.g. ~/.claude/skills/ui-craft).
  2. Run node $UI_CRAFT_SKILL_DIR/scripts/context.mjs once per session (--target <path> inside a monorepo). It prints PRODUCT.md / DESIGN.md or reports NO_PRODUCT_MD — in that case follow reference/init.md first. Ignore any UPDATE_AVAILABLE directive; this is a maintained fork. If node is unavailable or a script here errors, say so, read PRODUCT.md / DESIGN.md directly, and continue — the scripts are accelerators, not gates.

Before routing any design change:

  1. Resolve whether the app already embodies the surface (shipped or greenfield). For a shipped surface, classify local runnability as runnable or unavailable; inspect its CLAUDE.md / AGENTS.md dev-server declaration and classify it as absent, complete, incomplete or ambiguous; classify its named data source as manufactured, synthetic or unknown.
  2. Run node $UI_CRAFT_SKILL_DIR/scripts/route.mjs --embodiment <state> --runnability <state> --declaration <state> --data-source <kind> and obey its mode. A refuse result is blocking. lock-fallback is the recorded predecessor fallback in reference/revise.md, not ordinary lock permission.

Ticket triage may prepare a shipped surface's first-revision evidence in its admitted checkout only after this route verifies a safe start and manufactured data. It runs behavior-sweep, then freezes the ledger and replay through the existing sanction path before source admission. This authorizes evidence preparation, not implementation, a new mock, or a replacement checkout/base.

For revise, lock, build, and general design invocations only:

  1. Read the project's design system: tokens, theme, one representative component or page. Use what's there when it works. The shipped app wins over any mock scaffold: when a repo carries both an app stylesheet and a mockups/ theme, the app is chrome ground truth for every surface that has shipped. revise operates on that app directly; lock mode's step-0 pre-flight (reference/lock.md) refuses to replace it with a fresh mock.
  2. Read the matching register reference: reference/brand.md when design IS the product (marketing, landing, portfolio), reference/product.md when design SERVES the product (app UI, dashboards, tools).
  3. New project with no committed tokens: run node $UI_CRAFT_SKILL_DIR/scripts/palette.mjs for a brand seed.

The contract follows the surface

For a greenfield surface that has been through lock, the contract is:

  • a ★ LOCKED header in the mockup HTML, and
  • a lock manifest — mockups/<surface>.lock.md — the checkable inventory build reads. Format in reference/lock.md.

For a surface the app already ships, revise bans a from-scratch mock. Its contract is the frozen behavior ledger plus its replay script, exercised against the built app. The app branch is the visual artifact; screenshots record the review, but no lock manifest is pinned to an app template.

Rules that bind every mode:

  • No arbitration in private. If two locked artifacts disagree, a locked term collides with the app's shipped design system beyond what the manifest's precedence line settles, or a revision drops shipped behavior, stop and ask. Implementer judgment never silently overrides a contract.
  • Deviation is recorded. Any build or refactor that changes a locked term goes through re-settle; any revision that changes shipped behavior amends the frozen behavior ledger. Both paths are dated and sanctioned, never quiet.
  • Evidence over green gates. A locked surface is done when every manifest term has evidence; a revision is done when every behavior story replayed and every affected state has before/after evidence. A green suite alone is never the finish line.

Modes

Route on the first word (or obvious intent). Read the mode's reference before acting — it defines the flow.

Mode Job Reference
revise [surface] Inventory shipped behavior, then iterate the running app on a branch; never mock the shipped surface from scratch reference/revise.md
lock [surface] Explore grounded variants for a greenfield surface, converge, lock spec + manifest reference/lock.md
behavior-sweep [surface] Freeze interactive behavior into a ledger + replay script. revise runs it against the built app before design; the lock fallback also diffs a shipped predecessor before locking reference/behavior-sweep.md
build [surface] Implement a locked spec; ship the fidelity ledger reference/build.md
critique [target] Heuristic scoring, slop verdict, persona walkthroughs reference/critique.md
audit [target] Technical checks (a11y, contrast, responsive, detector) + lock-fidelity audit when a manifest exists reference/audit.md
polish [target] Pre-ship quality gate; includes the manifest walk reference/polish.md
resettle [term] Amend a locked term with record-keeping reference/resettle.md
consensus [question] Settle a contested design decision via a 3-persona vote-and-negotiate panel (advisory; requires repo personas) reference/consensus.md
init / document Project context setup / generate DESIGN.md reference/init.md, reference/document.md

No argument: recommend the 1–3 most useful modes from context (a shipped surface → revise; a greenfield surface or explicitly recorded safe-start fallback → lock; an unlocked fallback mock with no predecessor verdicts → behavior-sweep's predecessor pass before lock; frozen manifest for a greenfield surface with interactive behavior and no behavior ledger → behavior-sweep before build; open manifest without a fidelity ledger → build; never critiqued → critique), then list the table. Never auto-run a mode.

Three artifacts carry the word ledger; always qualify it. The fidelity ledger is one row per manifest term (build; revise has none). The behavior ledger is mockups/<surface>.behavior.md, one entry per story plus one permanent entry per sanctioned retirement (behavior-sweep). The surface ledger is mockups/INDEX.md, one row per surface.

General design invocations with no mode match (e.g. "make this less bland", "fix the spacing") route by embodiment: shipped surface → revise-then-fix; greenfield surface → critique-then-fix. Use reference/design-rules.md for the craft pass.

When a mode settles an answer or completes a helper, resume the caller's next authorized step. Stop only the dependent work for an unanswered decision, denied sanction, unavailable required evidence, or explicit handoff.

Web implementation. For build, revise, and general UI implementation that changes CSS or JavaScript/TypeScript, read reference/web-implementation.md before changing code. It governs web mechanics; UI Craft retains lifecycle, visual-contract, behavior-preservation, and rendered-evidence ownership.

Design rules (all modes)

reference/design-rules.md carries the shared craft discipline: the design-brief template, token-system-before-components, typography/color/layout/motion rules, the absolute bans (side-stripes, gradient text, default glassmorphism, hero-metric template, identical card grids, eyebrow-on-every-section…), the AI-slop and category-reflex tests, and the layered critique order. It is required reading for lock, build, and any general invocation; the other modes consult it as needed.

Personas

Persona walkthroughs live in critique (five built-in archetypes plus project-specific ones). Repo personas win: if the repo has its own persona definitions (canonically .claude/qa/personas/*.md — written by init's persona-panel step — or a location named in its CLAUDE.md/AGENTS.md), use those personas — a diabetic user, a concerned parent — instead of inventing equivalents, and follow the repo's sweep protocol when one exists. The consensus mode requires these repo personas and refuses to run on generic archetypes.

"The repo's sweep protocol" is not behavior-sweep. That phrase means the repo's persona-driven QA pass — exploratory, judgment-led, run against a live app to find bugs. behavior-sweep is a mode of this skill: mechanical, run against the built app before a revision, or against a locked mock plus its shipped predecessor on the explicit fallback path. Its output is a contract. Neither substitutes for the other.

Grounding rules (inherited from ui-mockups, apply everywhere)

  • Ground every artifact in the app's real tokens, shipping UI/chart library at its shipping version, and real data shape from a safe, manufactured fixture — never production, personal, health, credential, or customer data. This is the shared default and it does not bend on convenience.
  • Real-data inversion — repo-scoped, opt-in. Some repos invert the rule: their build contracts must be grounded in the owner's own real data, because a fixture cannot reveal what the surface does at real scale. That inversion applies only where the repo's own CLAUDE.md/AGENTS.md declares an operator-sanctioned real-data protocol (e.g. a read-only snapshot flow), and only within that protocol's bounds. Absent that declaration, the manufactured default above governs — no exceptions inferred from context or precedent. Where the inversion does apply: renders of real personal/health data never commit and never attach to a PR (a PR is a publish) without the operator's exact authorizing sentence quoted in the record; committed and PR-attached evidence uses labeled synthetic fixtures, and real-data renders stay local, handed to the verifier with the pinned data.
  • Vary the concept, not the decoration; three variants that differ only in color are one design.
  • Inspect rendered output — source review alone never validates a visual artifact. Use drive-local-webapp for rendering; ask to install it if missing.
  • Sibling exactness (reference/sibling-fidelity.md): any element with a sibling in a shipped surface uses the shipped values exactly — geometry, type, alignment spines, chart furniture, and interaction idioms alike — and fidelity is proven with a computed-style diff against the running app, never by eyeball. Token bridges are verified by computed value on the consuming element; mock-global base styles (body font/line-height) are banned because they shift extracted chrome off its shipped pixels.
  • Keep mockups/INDEX.md as the surface ledger (one row per surface: Surface / Concept / Status / Issue / File). locked rows are binding precedent; shipped rows defer to the app itself, and are also how the next lock finds its predecessor (behavior-sweep §2). Every mode that touches a lock updates the ledger in the same change — a stale ledger is a defect.
Files (skills)
  • agents
    • openai.yaml 341 B
      interface:
        display_name: "UI Craft"
        short_description: "Revise shipped UI; lock, build, and review greenfield surfaces."
        default_prompt: "Use $ui-craft for this surface: revise shipped UI in the running app, lock a greenfield visual spec, build to a lock with fidelity evidence, critique with personas, audit, or run the polish gate."
      
  • reference
    • audit.md 9.3 KB
      Run systematic **technical** quality checks and generate a comprehensive report. Don't fix issues; document them for other commands to address.
      
      This is a code-level audit, not a design critique. Check what's measurable and verifiable in the implementation.
      
      ## Diagnostic Scan
      
      Run comprehensive checks across 5 dimensions. Score each dimension 0-4 using the criteria below.
      
      ### 1. Accessibility (A11y)
      
      **Check for**:
      - **Contrast issues**: Text contrast ratios < 4.5:1 (or 7:1 for AAA)
      - **Missing ARIA**: Interactive elements without proper roles, labels, or states
      - **Keyboard navigation**: Missing focus indicators, illogical tab order, keyboard traps
      - **Semantic HTML**: Improper heading hierarchy, missing landmarks, divs instead of buttons
      - **Alt text**: Missing or poor image descriptions
      - **Form issues**: Inputs without labels, poor error messaging, missing required indicators
      
      **Score 0-4**: 0=Inaccessible (fails WCAG A), 1=Major gaps (few ARIA labels, no keyboard nav), 2=Partial (some a11y effort, significant gaps), 3=Good (WCAG AA mostly met, minor gaps), 4=Excellent (WCAG AA fully met, approaches AAA)
      
      ### 2. Performance
      
      **Check for**:
      - **Layout thrashing**: Reading/writing layout properties in loops
      - **Expensive animations**: Casual layout-property animation, unbounded blur/filter/shadow effects, or effects that visibly drop frames
      - **Missing optimization**: Images without lazy loading, unoptimized assets, missing will-change
      - **Bundle size**: Unnecessary imports, unused dependencies
      - **Render performance**: Unnecessary re-renders, missing memoization
      
      **Score 0-4**: 0=Severe issues (layout thrash, unoptimized everything), 1=Major problems (no lazy loading, expensive animations), 2=Partial (some optimization, gaps remain), 3=Good (mostly optimized, minor improvements possible), 4=Excellent (fast, lean, well-optimized)
      
      ### 3. Theming
      
      **Check for**:
      - **Hard-coded colors**: Colors not using design tokens
      - **Broken dark mode**: Missing dark mode variants, poor contrast in dark theme
      - **Inconsistent tokens**: Using wrong tokens, mixing token types
      - **Theme switching issues**: Values that don't update on theme change
      
      **Score 0-4**: 0=No theming (hard-coded everything), 1=Minimal tokens (mostly hard-coded), 2=Partial (tokens exist but inconsistently used), 3=Good (tokens used, minor hard-coded values), 4=Excellent (full token system, dark mode works perfectly)
      
      ### 4. Responsive Design
      
      **Check for**:
      - **Fixed widths**: Hard-coded widths that break on mobile
      - **Touch targets**: Interactive elements < 44x44px
      - **Horizontal scroll**: Content overflow on narrow viewports
      - **Text scaling**: Layouts that break when text size increases
      - **Missing breakpoints**: No mobile/tablet variants
      
      **Score 0-4**: 0=Desktop-only (breaks on mobile), 1=Major issues (some breakpoints, many failures), 2=Partial (works on mobile, rough edges), 3=Good (responsive, minor touch target or overflow issues), 4=Excellent (fluid, all viewports, proper touch targets)
      
      ### 5. Anti-Patterns (CRITICAL)
      
      Check against ALL the bans and **DON'T** guidelines in `design-rules.md` (read it if not already loaded). Look for AI slop tells (AI color palette, gradient text, glassmorphism, hero metrics, card grids, generic fonts) and general design anti-patterns (gray on color, nested cards, bounce easing, redundant copy).
      
      **Score 0-4**: 0=AI slop gallery (5+ tells), 1=Heavy AI aesthetic (3-4 tells), 2=Some tells (1-2 noticeable), 3=Mostly clean (subtle issues only), 4=No AI tells (distinctive, intentional design)
      
      ## Generate Report
      
      ### Audit Health Score
      
      | # | Dimension | Score | Key Finding |
      |---|-----------|-------|-------------|
      | 1 | Accessibility | ? | [most critical a11y issue or "--"] |
      | 2 | Performance | ? | |
      | 3 | Responsive Design | ? | |
      | 4 | Theming | ? | |
      | 5 | Anti-Patterns | ? | |
      | **Total** | | **??/20** | **[Rating band]** |
      
      **Rating bands**: 18-20 Excellent (minor polish), 14-17 Good (address weak dimensions), 10-13 Acceptable (significant work needed), 6-9 Poor (major overhaul), 0-5 Critical (fundamental issues)
      
      ### Anti-Patterns Verdict
      **Start here.** Pass/fail: Does this look AI-generated? List specific tells. Be brutally honest.
      
      ### Executive Summary
      - Audit Health Score: **??/20** ([rating band])
      - Total issues found (count by severity: P0/P1/P2/P3)
      - Top 3-5 critical issues
      - Recommended next steps
      
      ### Detailed Findings by Severity
      
      Tag every issue with **P0-P3 severity**:
      - **P0 Blocking**: Prevents task completion. Fix immediately
      - **P1 Major**: Significant difficulty or WCAG AA violation. Fix before release
      - **P2 Minor**: Annoyance, workaround exists. Fix in next pass
      - **P3 Polish**: Nice-to-fix, no real user impact. Fix if time permits
      
      For each issue, document:
      - **[P?] Issue name**
      - **Location**: Component, file, line
      - **Category**: Accessibility / Performance / Theming / Responsive / Anti-Pattern
      - **Impact**: How it affects users
      - **WCAG/Standard**: Which standard it violates (if applicable)
      - **Recommendation**: How to fix it
      - **Suggested command**: Which ui-craft mode addresses it (`revise` for shipped-surface changes, `critique` for greenfield design issues, `polish` for fix passes, `resettle` for locked-term changes, `document` for missing DESIGN.md)
      
      ### Patterns & Systemic Issues
      
      Identify recurring problems that indicate systemic gaps rather than one-off mistakes:
      - "Hard-coded colors appear in 15+ components, should use design tokens"
      - "Touch targets consistently too small (<44px) throughout mobile experience"
      
      ### Positive Findings
      
      Note what's working well: good practices to maintain and replicate.
      
      ## Recommended Actions
      
      List recommended commands in priority order (P0 first, then P1, then P2):
      
      1. **[P?] `/command-name`**: Brief description (specific context from audit findings)
      2. **[P?] `/command-name`**: Brief description (specific context)
      
      **Rules**: Only recommend ui-craft modes (`revise`, `critique`, `polish`, `resettle`, `document`, or a targeted fix under the general invocation). A shipped-surface change routes to `revise`; map the remaining findings to the most appropriate mode. End with `/ui-craft polish` as the final step if any fixes were recommended.
      
      After presenting the summary, tell the user:
      
      > You can ask me to run these one at a time, all at once, or in any order you prefer.
      >
      > Re-run `/ui-craft audit` after fixes to see your score improve.
      
      **IMPORTANT**: Be thorough but actionable. Too many P3 issues creates noise. Focus on what actually matters.
      
      **NEVER**:
      - Report issues without explaining impact (why does this matter?)
      - Provide generic recommendations (be specific and actionable)
      - Skip positive findings (celebrate what works)
      - Forget to prioritize (everything can't be P0)
      - Report false positives without verification
      
      ## Lock-fidelity audit (when the surface has a lock manifest)
      
      If `mockups/<surface>.lock.md` exists for the audited surface, the audit has
      a sixth, non-optional dimension: **does the shipped surface match the lock?**
      This is the check that catches drift no green gate notices.
      
      1. Render the ★ LOCKED mockup and the live surface **side by side** — same
         fixture (honoring the manifest's fixture obligations), same viewports and
         themes the manifest names.
         **Every view/mode/tab is its own render, including the ones that hand off to
         a sibling or shipped surface.** A named state (`dense`, `empty`, `selected`)
         is not a view; a matrix of five states inside one view leaves the other views
         unlooked-at. A real Diagnose audit rendered five states, all of them in the
         comparison view, and so never once put the Glucose view on screen — where the
         missing navigation would have been obvious at a glance. Landing on the
         surface with no parameters counts as a view and gets its own render, since it
         is what most readers actually see.
         **Chrome the views share is compared numerically against one reference view,
         never by eye.** Rendering each view catches what is *missing* from one of
         them; it does not catch a uniform offset, because nobody looks at two views
         at once and 6px looks like nothing on its own. Pick the shipped or
         longest-standing view as the reference, read the shared geometry off it —
         rail row and padding, pane grid, pane header heights, first content baseline
         — and diff every other view against those numbers. The same Diagnose surface
         that lost its navigation later sat 6px low in two of its three views, through
         an audit that had by then rendered all of them.
      2. Walk the manifest term by term:
         - `gate` terms: confirm a `LOCK:<surface>:<n>`-tagged assertion exists in
           the rendered gate AND holds. A gate term with no assertion is a finding
           (P1) even when the pixels currently match — it is drift waiting for the
           next refactor.
         - `eye` terms: judge from the paired renders. Name the term number in
           every finding.
      3. Check verbatim strings against the manifest exactly (legend chips, labels,
         button text).
      4. Report a fidelity table: term / met / diverged / untested, alongside the
         Audit Health Score. Divergence is P0/P1 — the lock is the spec; "the app
         looks fine" does not close a diverged term. Either the build moves to the
         lock or the term goes through `resettle`.
      
      Run this audit after any refactor that touches a locked surface's render
      path, not only at ship time — late refactors are how already-correct visuals
      regress.
      
    • behavior-sweep.md 30.6 KB
      # Mode: behavior-sweep
      
      Sweep a surface's **interactive behavior** into a contract before design changes
      it. It has two routes:
      
      - **`revise` (default for a shipped surface):** inventory and exercise the base
        app before the design conversation. The ledger plus its app-only replay script
        is the frozen contract; no lock manifest exists.
      - **`lock` fallback or legacy lock:** sweep the locked mock after `lock`, before
        `build`. Its one backward-looking pass, the **predecessor inventory** (§2),
        runs before the lock closes; `lock.md` §9 gates on it.
      
      Both routes cover handlers, gestures, hover states, keyboard paths and resize
      response. Unless a section says otherwise, “target” below means the base app for
      `revise` and the locked mock for the fallback route.
      
      A lock manifest describes what a mock *looks like*, but reliably fails to
      describe what it *does*. A shipping app has the opposite advantage: preserving
      it is the default, and a deletion appears in the branch diff. In both routes the
      behavior ledger makes the executable contract explicit; the replay script keeps
      it closed.
      
      Two root failures this mode exists to make impossible, one per direction:
      
      > Something approximated the locked artifact instead of using it, and a checker
      > accepted the approximation.
      
      > Something dropped the predecessor's behavior, and no checker was looking in
      > that direction.
      
      Input for `revise`: the base app's source and running build at the recorded SHA,
      plus any existing ledger and replay script. Input for fallback: the ★ LOCKED
      mockup(s), `mockups/<surface>.lock.md`, every imported module, and the
      predecessor's own source and running build (§2). On fallback, `<surface>` is the
      basename of the lock manifest. Under `revise`, it is the stable surface name
      already used by `mockups/INDEX.md` or the app route. No agent picks a nickname.
      
      **Proportionality.** Sweep depth scales with the handler inventory: a surface
      whose inventory fits on one screen may fold the passes below into a single
      sitting. Keep the **ledger**, **completeness check**, predecessor diff wherever
      a predecessor exists, and every genuinely unanswered QUESTION. Do not require a
      ceremonial question round or elapsed-time busywork when the admitted behavior
      leaves no decision open.
      
      ## 1. Inventory (static — this is not evidence)
      
      Enumerate every behavior the target registers:
      
      - `addEventListener` calls (click, mousedown/move/up, keydown, resize, …);
      - chart/graphics-library instance handlers (e.g. an ECharts `on()` /
        `updateAxisPointer` / `globalout` set, zr events);
      - `ResizeObserver` / `IntersectionObserver` / `MutationObserver` registrations;
      - inline `on*=` attributes;
      - CSS that *encodes behavior* — `:hover`, `:focus`, `:active`, transitions.
      
      **Including handlers registered inside imported modules**, which the host module
      never shows. A chart module that installs four instance handlers when given an
      `onHover` callback is four inventory rows; missing them is how a latched readout
      gets reinvented.
      
      On the fallback route, **exclude mock-harness chrome by name** — the theme
      toggle, mock bar and variant switcher are not surface behavior. Name the excluded
      files in the ledger header. Excluded from the inventory is not excluded from the
      page: the harness and every imported module are still served, because an
      unserved import throws before any listener registers. `revise` has no mock
      harness exclusion; chrome the app ships is behavior like anything else.
      
      **The exclusion is by purpose, not by position.** Chrome that exists to serve
      the mockup is out; chrome that ships to the reader is in, however the mock came
      by it. A mock that lifts the app's real topbar and footer — and a lock that pins
      them in a term — has put them on the surface, so their behavior is inventoried
      here and diffed in §2 like anything else. Read the exclusion positionally
      instead and that chrome is owned by nobody: the mock sweep waves it through as
      chrome, the predecessor sweep waves it through as not-this-surface, and a lifted
      navigation that quietly lost its behavior passes both. Where another surface's
      frozen ledger already covers the same chrome, cite its stories rather than
      re-ruling them.
      
      **Handlers are not the only source of stories.** The inventory finds everything
      the surface *does*; it cannot find what the surface must *keep true* while doing
      it. Those invariants own no handler, so a handler-complete ledger can still have
      none of them — and they fail silently, because each one is only visible by
      comparing two renders nobody compares. Add a story for each:
      
      - **Across views/modes**, chrome the reader keeps seeing does not move. Measure
        every other view against one designated reference view, not against an
        average, so the reference stays authoritative when they drift together.
      - **Across interaction**, nothing reflows that the interaction did not name.
        A header that grows when its hover readout fills moves the content under the
        pointer — reserve the space at rest and assert the resting and active
        geometry are equal.
      - **Across data shape**, containers sized for live values do not resize per
        value (counts, timestamps, currency).
      
      A real Diagnose surface shipped 6px low in two of its three views, and grew its
      chart header on first hover, with a handler-complete ledger where every story
      passed. Each story replayed one view, alone, where a uniform offset is invisible.
      
      Static reading produces the inventory. It never produces a story.
      
      ## 2. Predecessor inventory (fallback and legacy locks only)
      
      `revise` does not run this mock-to-predecessor diff: its target is the base app
      itself, so §1 and §3 inventory the shipped behavior directly. This section is
      the retained fallback when `revise` cannot safely start the app, and the repair
      path for locks that predate `revise`. Fallback never authorizes an app start. If
      no separately declared safe harness can supply the live predecessor, keep every
      unexercised row as QUESTION; do not convert static reading into evidence.
      
      §1 inventories the mock, and **a mock-side inventory cannot see an absence**. A
      behavior the mock never implemented registers no handler, so it produces no row;
      a computed-style harness and a chart-option diff have no opinion about an
      interaction that is not there either. Every other check in the lifecycle reads
      the mock, so all of them are blind in the same direction. This pass is the only
      one that reads the other artifact.
      
      A Diagnose workstation was explored as a grounded mock over ten rounds,
      critiqued by a persona panel, put through a 52-run technical audit, and locked
      as a 60-term contract. The app it replaced let the reader **drag in the plot
      body to draw a custom selection window**, resize it from two titled grab
      handles, and ran one grammar where an explicit choice — a preset press or a drag
      — outranked the default window. The mock drew five inert preset buttons and
      nothing else. Nobody noticed at any round, so the lock froze the retirement of a
      shipped interaction as contract, and a build agent reading that lock would have
      shipped the retirement faithfully. It was caught by eye, on a screenshot, after
      the lock merged. Nothing was skipped — the sequence had no step that looked
      backwards.
      
      **When it applies.** Whenever the surface has a shipped ancestor: a `shipped`
      row in `mockups/INDEX.md` for this surface or the one it replaces, a lock
      manifest whose `Supersedes:` line names a prior lock, or a surface being
      replaced in the running app. **A greenfield surface with no predecessor skips
      this pass**, and says so in one line in the ledger header naming what was looked
      for and not found. The skip is recorded so it stays a fact about the surface,
      rather than becoming a step everyone learns to wave through.
      
      **Start from the predecessor's own frozen ledger, when it has one.** A surface
      swept under this lifecycle left `mockups/<predecessor>.behavior.md` and
      `frontend/<predecessor>-behavior.replay.mjs` behind: replaying that script
      against the running predecessor is one command, and it returns a validated
      backbone of rows plus live evidence for every story it carries — cheaper than
      hand-driving the same ground, and better evidence than reading it. Look for both
      before opening the source. **Never stop there.** A prior ledger is only as
      complete as the sweep that froze it, and says nothing about what the surface
      grew afterwards: the run that found the drawn window inherited 28 executable
      stories, and still had to find a keyboard cursor, a histogram, a crumb ladder
      and lane dimming by hand. The ledger seeds the inventory; it never bounds it.
      
      **What it inventories.** The PREDECESSOR's behavior, by §1's rules exactly —
      `addEventListener`, chart/graphics-library instance handlers, observers, inline
      `on*=`, and CSS that encodes behavior — read from the shipped code, never from
      memory of using the app. **Including handlers registered inside imported modules
      the host HTML never shows.** That clause is load-bearing: it is exactly where
      the drawn window's grab handles were registered, and a host-HTML-only sweep
      misses them a second time.
      
      Then **exercise both sides live** (§3's rules, same viewport). The predecessor,
      because a gesture assembled across three handlers is legible as one behavior
      only from the running surface, and suppression conditions — a handle that does
      not appear in one state — exist nowhere in the source at all. And the mock,
      because §3 has not run yet when this pass does, so the mock side of the diff
      rests on nothing unless this pass drives it — and **"the mock" stops being one
      artifact the moment it reuses a shipped painter**. A Diagnose mock's evidence
      table was extracted whole from production and plainly emits row titles and
      chevrons; two lock terms assert the surface shows neither. Both were true — the
      surface strips them after paint. Either source read alone gives the wrong
      verdict, and only the running mock settles it.
      
      **Row grain.** A row is the unit the operator rules on, so split by decision,
      not by listener: **two behaviors are one row when neither can be ruled on
      alone, and separate rows when either could survive without the other.**
      Listeners have no fixed ratio to rows in either direction — a drawn selection
      window registered across five listeners carries several separately retirable
      behaviors (draw by dragging, resize from a handle, which way the window grows,
      its precedence over the presets), while one keydown listener running a keyboard
      cursor carries one row per key whose fate is separable. This is where §1's grain
      and this one legitimately differ: that inventory is keyed to handlers because
      its output is coverage — every handler maps to ≥1 story — and this one is keyed
      to decisions because its output is a ruling. Where a split is genuinely
      arguable, split. An over-split row costs the operator one sentence ruling both
      together; an under-split row hides a behavior nobody ever chose to drop, which
      is the entire defect. The row count is this pass's headline number, and two
      agents sweeping the same surface are meant to arrive at the same one.
      
      **The diff.** Every predecessor row gets exactly one verdict against the mock:
      
      - **kept** — the mock implements it. It is already a §1 story or it becomes one;
        the two inventories meet in that single ledger entry.
      - **deferred** — the mock does not implement it, and a lock term contracts the
        build to. Neither `kept` (there is nothing to observe on the mock) nor
        `missed` (nothing was dropped): a term saying a control is real but not
        exercisable by this fixture is *implemented* by a build that wires it, not
        violated. The test is whether an app shipping **without** the behavior would
        violate the named term. If it would not, that term does not contract the
        behavior and the row is `missed`. A `deferred` row's evidence is app-side
        only — the port PR that turns the app-opener leg green is where it is proved,
        and no mock evidence can close it.
      - **retired** — the mock deliberately does not implement it, and a human ruled
        that it goes. Requires the **sanction line** below.
      - **missed** — the mock does not implement it and nobody decided that. **A
        `missed` row fails the sweep.**
      
      `missed` is the default verdict, and a row leaves it only by being built, by
      being contracted in a named term, or by being sanctioned. "It was probably
      intentional" is `missed`. A retirement argued by the agent that wrote the mock
      is `missed`. An inventory that ends with
      no `missed` rows because the sweep could not find the predecessor's source is
      `missed` for every row it could not read.
      
      **The sanction line.** A `retired` verdict carries, in its ledger entry:
      
      ```
      sanction: <who ruled it> · <date> · "<why, in their own words>"
      ```
      
      `<who>` is a person, named. `<date>` is the day they ruled. The reason is
      **quoted, not paraphrased** — the same evidence standard the two drop paths in
      §5 already hold. **An unsanctioned retirement fails the sweep, and there is no
      waiver for it**: not by proportionality, not by an agent's judgment that the
      behavior was vestigial, not by the operator's silence when asked. A behavior
      that reaches `retired` without a human's name and date on it is the exact defect
      this pass exists to prevent, and in the ledger it is indistinguishable from a
      `missed` one — which is why it fails as one.
      
      **A ruling recorded somewhere else is not a sanction — and is not nothing,
      either.** Run this pass against a lock that has already merged and some rows
      will turn out to have been genuinely ruled on, in a dated lock term or an issue
      resolution, but written up by whoever drafted that artifact: the words are not
      the operator's, so the sanction line cannot be filled from them. Annotate the
      row and leave the verdict where it is:
      
      ```
      ruled-elsewhere: <artifact> · <locator> · "<the ruling as written there>"
      ```
      
      The row stays `missed`. It still blocks the lock, still blocks the freeze, and
      still goes to the QUESTION round; the annotation decides nothing except which
      pile it lands in when it gets there — confirm and quote, rather than rule from
      scratch. Keep that distinction: it is the difference between six rows the
      operator answers in a sentence each and thirty-seven he has to think about, and
      collapsing the six into the thirty-seven is how a ruling session gets long
      enough to defer.
      
      **A lock term is never promoted to a sanction, however verbatim the quote.**
      That is the obvious shortcut and it is the one loophole that would empty this
      pass, because the lock is precisely the artifact that encodes a retirement by
      omission: let it sanction its own omissions and the check goes circular, its
      evidence being the thing under check. The Diagnose lock ran to sixty terms
      describing five inert preset buttons, and every one of those terms was true.
      None of them was a decision to drop the drawn window, because nobody had yet
      noticed there was one to drop. A term records what the surface has; a sanction
      records what the operator chose to lose, and only he can say that.
      
      **Ordering.** This pass runs **before the lock closes**, alone among the mode's
      passes; `lock.md` §9 makes it a lock precondition and argues why. It needs the
      finalist only as a diff target, never a frozen mock. Its verdict table is
      carried into the ledger when the sweep proper runs after the lock. A `missed`
      verdict discovered *after* a lock has merged is a `resettle` trigger, not a
      build decision and not a bug fix — see `resettle.md`.
      
      ## 3. Exercise (live — this is the evidence)
      
      Drive the target in a **real browser engine at an explicitly recorded viewport**
      — the lock's viewport on fallback, never a driver default. Perform each behavior
      for real: hover every hoverable, drag from inside an element *and* from its
      edges, click every control, run every keyboard path (Esc, arrows, Tab), resize.
      Verify in a second engine when the behavior is rendering-sensitive.
      
      Two further passes, folded in or run separately per proportionality:
      
      - **Data pass.** Load the target on *each* authorized fixture, including the
        largest realistic shape the surface will actually see. Record what every
        data-dependent visual *encodes* — clustering, scoping on selection, level
        content, count shapes. A visual whose meaning disappears at real scale
        (uniform glyphs, empty scoping) is a QUESTION, not a build call: a design rule
        is missing and must be settled in the design conversation.
      - **Content pass.** For every information surface (detail panels, headers, meta
        rows, tags) capture the target's **actual rendered markup and text structure**.
        The story links to markup, never to the lock term's prose — this is what makes
        "built from the description" detectable at review time.
      
      **Completeness check (mechanical, not judgment):** every inventoried handler maps
      to ≥1 story, every applicable §2 predecessor row carries a verdict, and every
      story was observed live. A handler found in code but not reproducible in-browser
      becomes a QUESTION entry — never a silent skip. Handler coverage is not ledger
      completeness: a surface with more than one view, or with any interaction that
      swaps content in place, also owes its invariant stories above. Record them with
      no handler named, since none exists.
      
      ## 4. The replay script (committed)
      
      Alongside the ledger, commit a replay script — `frontend/<surface>-behavior.replay.mjs`
      or the repo's equivalent path — that re-runs the sweep mechanically. Hand-driving
      is fine for discovery; a story enters the ledger only once its replay function
      passes against the target.
      
      Spec:
      
      - **One exported async function per story**, `export const S12 = async (page) => { … }`.
      - On fallback, each function carries a `// LOCK:<surface>:<n>` tag for every lock
        term it exercises, so manifest coverage stays greppable. Under `revise`, each
        carries `// STORY:<surface>:<id>` for its behavior-ledger entry; there is no
        lock term to cite.
      - **Openers follow the route.** `revise` has one app opener parameterized by the
        server's base URL, so the same stories run against base and revision worktrees.
        Fallback has two openers in the script: a mock opener (mock root, theme CSS,
        vendored libraries, fixture stub) and an app opener (route stubs, auth,
        navigation). The story functions are shared between them.
      - Every opener is **loud on unstubbed or missing requests**. A catch-all
        `200 {}` renders a build that is missing an asset and still passes.
      - Every opener **asserts the rendered state equals the requested one**, so state
        addressability drift (a `?state=` param the mock silently ignores) is loud
        rather than a quietly identical render.
      - **On fallback, selector parity is a port obligation, not a script concern.**
        Replay-against-both only holds if the ported markup keeps the mock's selectors;
        a rename that breaks a replay selector is a port defect. Under `revise`, update
        a selector only with the behavior change it represents and prove the old replay
        failed first.
      - **Drive every story through the affordance a reader would use.** A story about
        reaching, leaving, or returning to a state is only evidence if the replay gets
        there the way a person does — clicking the control, typing in the field. Browser
        history (`goBack`), a direct URL, or calling the surface's own API bypasses the
        affordance, so the story passes on a surface that offers *no route at all*. A
        shipped Diagnose view once lost its entire view switcher while its "returning to
        an event view restores it" story stayed green, because the replay returned with
        `goBack()`. When a story's verb is navigational, assert the control exists and is
        visible, then use it.
      - **Prove every replay can fail once.** Under `revise`, knock the feature out on
        the revision branch, observe the right failure, then restore it. On fallback,
        use the built app or a scratch copy of the mock, never the ★ LOCKED mock in
        place. A transient edit to a contract artifact risks an unrestored diff.
      - **A retired behavior gets a replay function too, and it is never silent.** The
        function asserts the absence — no grab handle in the DOM, the gesture does
        nothing — and **prints its sanction line on every run**, from a
        `// RETIRED:<who>:<date>` tag beside the `LOCK:` tags. An absence asserted
        without the sanction on screen quietly makes the retirement permanent, and
        reads exactly like a feature nobody ever had; the point of running it at all is
        that a reader of the output sees a shipped behavior was deliberately dropped
        and by whom. The script **fails closed on a retired function whose sanction tag
        is missing**, exactly as it does on a missing driver. Under `revise`, the
        ledger amendment that moves the entry back to STORY also deletes the absence
        assertion. On fallback, the `resettle` change set makes both changes. The
        assertion is never deleted on its own.
      - **A retired function asserts the premise the retirement reasoned from, not
        only the absence.** Every ruling has a because — "the renderer is not visible
        or focusable, so it cannot honestly claim a reader keyboard path" — and a
        function that asserts the conclusion alone goes on passing after that because
        stops being true. Record the premise on the ledger entry and assert it beside
        the absence. When the premise fails while the behavior is still absent, the
        story goes red as a **QUESTION-round trigger asking for a fresh ruling**; it is
        not a `replayed-fail`, which stays reserved for the retired behavior being
        present. A surface made visible again by a later change is exactly this case,
        and it is silent without the premise: the absence assertion is still true, and
        the reason it was allowed to be absent is gone.
      - **The script fails closed.** Missing browser dependencies (driver module,
        vendored assets, executable) exit nonzero — never `skip`. A skipped run exits 0,
        and a green step that executed zero stories is precisely the silent skip this
        mode exists to prevent.
      - Wire the script into CI **explicitly**. Test globs discover `*.test.js`, not a
        `.replay.mjs`; a browser job that hand-lists its files gets a hand-added step in
        the same change.
      - **On fallback, the mock's CI leg is temporary: it runs from lock until the surface ships, and
        then it is deleted.** While the port is being built the mock is the contract
        artifact and the `TARGET=mock` leg is what guards it; once the app-opener leg is
        green the app is the contract artifact and the mock leg guards nothing the app
        leg does not. **Retirement is atomic with the port**: the same PR that turns the
        app leg green deletes that surface's mock leg, flips its row in `mockups/INDEX.md`
        to `shipped`, and archives the mockup (`lock.md`'s archive-after-ship rule). A
        follow-up issue for any of the three is forbidden — that is exactly how surfaces
        end up merged-but-still-`locked`. The anti-drift guarantee is not lost with the
        leg, because it never lived there: it lives in the ledger's stories replayed
        against the built app, which run forever. A permanent mock leg instead means the
        surface's logic exists twice and is hand-synced, and that drifts — one port branch
        carried three divergent wordings of a single copy-pasted note builder.
      
      ## 5. The behavior ledger
      
      One entry per story — and one per sanctioned retirement — in
      `mockups/<surface>.behavior.md`:
      
      Under `revise`, `source:` points to the shipping module and `evidence:` points to
      the app-only replay. Its first frozen version is a full inventory of the base
      app. Before each later revision, replay and re-inventory the base: new observed
      behavior becomes a STORY; a prior story missing from the base becomes a QUESTION
      until a named person supplies a dated, quoted sanction. Re-freeze that diff
      before design begins. This is the next-revision stale-ledger check, not automatic
      recovery.
      
      ```
      S12 · Dragging a window's edge resizes it; edges are full-height ±5px grab
            zones; Esc clears the window.
        element:  .brace .edge / #grip-a / #grip-b
        source:   <mock file>:2446-2600 (installDrag)
        lock:     terms 6, 7, 21
        data:     any
        evidence: replay fn S12 + screenshot ref
        status:   (revision/build phase fills: ported | replayed-pass | replayed-fail |
                   re-settle requested)
      ```
      
      A retirement from §2 takes the same shape, keyed `R`:
      
      ```
      R3 · Dragging in the plot body drew a custom selection window; two handles
           titled "Drag to resize" resized it, growing away from the edge not being
           dragged.
        predecessor: frontend/day-chart.js:812-980 (installBrush), shipped app
        verdict:  retired
        sanction: Connor · 2026-08-18 · "the presets cover every window I use; the
                  drawn window goes."
        premise:  the preset window controls are present and cover the plot's range
        replay:   fn R3 asserts absence and the premise, and prints this sanction line
        status:   retired (permanent)
      ```
      
      On fallback, a `deferred` row is not a third block. It is a STORY like any other,
      naming the term it defers to on its `lock:` line and `app opener only` on its
      `evidence:` line, so the build's fidelity ledger can see that mock evidence was
      never owed. `revise` has no `deferred` verdict because there is no manifest to
      defer to.
      
      Sweep screenshots and clips live in `mockups/sweep/<surface>/`. Register both the
      ledger and that directory in `mockups/INDEX.md`, per resettle's
      INDEX-moves-with-the-lock rule.
      
      Entry types are exactly three: **STORY** (observed behavior), **QUESTION** (a
      behavior or meaning gap the operator must rule on), and **RETIRED** (a
      predecessor behavior deliberately dropped, with its sanction). **RETIRED is not
      a waiver** — it is the record a waiver would have replaced. A §2 row still at
      `missed`, including one carrying `ruled-elsewhere`, is a QUESTION entry until
      the operator rules it.
      
      **What a retirement owes, in one place.** Every path that retires a behavior —
      `revise`'s ledger amendment, `resettle`'s post-lock ruling, a §2 `missed` row the
      operator rules — owes the same four things, and this page is the only one that
      enumerates them:
      
      1. the **sanction line** above, naming a person, dated, their reason quoted;
      2. a permanent **RETIRED entry** in this ledger;
      3. an **absence assertion** in the replay script that prints that sanction on
         every run (§4); and
      4. the **premise** the ruling reasoned from, recorded on the entry and asserted
         beside the absence (§4), so a dead premise asks for a fresh ruling.
      
      A page that routes a retirement here names the case and points at this list. It
      does not restate it: a list kept in three places is how a fourth obligation
      reaches only one of them.
      
      **Retired entries are permanent.** The ledger stops being a record of only what
      the surface does and becomes a record of what it does **and what it deliberately
      stopped doing**; the retired entries with their sanctions are the audit trail,
      and they stay through every later sweep, revision, port and lock of that surface. Deleting
      one leaves either an unrecorded reinstatement or a second undocumented
      retirement, and no way to tell which — the same illegible state the pass exists
      to prevent, arrived at from the other side.
      
      **There is no waiver entry type.** A drop is always a dated, operator-sanctioned
      record, by one of exactly two paths:
      
      - a story that **cites a lock term** → `resettle`; the manifest row is the record
        and the operator's exact authorizing sentence is that resettle's evidence;
      - a story with **no lock term** (the ledger's whole reason to exist — resettle
        cannot run where there is no manifest row) → dropped only by an operator ruling
        recorded **inline in the ledger** under a QUESTION entry, his exact sentence
        quoted.
      
      Both paths carry the date and the sanction. A drop path that skips the record is
      exactly how a dropped term recurs. A §2 retirement lands as a RETIRED entry
      either way; where the retired behavior also touches a manifest row, the
      `resettle` is what carries the manifest side, and its date and sanction are the
      same ones.
      
      ## 6. QUESTION round (one numbered round)
      
      Batch every QUESTION to the operator in **one numbered round** at the end of the
      sweep — behavior gaps, meaning-lost-at-scale findings, irreproducible handlers,
      and any fixture-set authorization the gate will need (shapes and in-band labeling
      authorized together; see the data defaults in SKILL.md's grounding rules).
      Answers are recorded inline under their QUESTION entries.
      
      **On fallback, §2's verdicts get their own round, and it happens before the lock**, since
      that pass does. Every row still at `missed` or at an unsanctioned `retired` goes
      in it, each stating what the predecessor did, where it was registered, and what
      the mock offers instead — a retirement is ruled there or it is not ruled at all.
      Rows carrying `ruled-elsewhere` are listed first and apart, each quoting the
      ruling it cites, because on those the operator is confirming a decision he
      already made and dictating a sentence for it, not weighing a new one; mixed into
      the pile they flatten the one distinction that makes the round short. Answers
      become `kept` (build it, or `resettle` the lock if it has already closed),
      `deferred` against a named term, or `retired` with the sanction line written
      down as given.
      
      ## 7. Freeze
      
      Operator approval stamps a header line on the ledger:
      
      ```
      ★ FROZEN <date> · base <sha> · generator <sha or n/a> · window <start>..<end or n/a>
        · fixtures <name: sha256-prefix, …>   (tripwire, not contract)
        · predecessor <ref, or "none — greenfield">   · retired <n>
      ```
      
      - The pinned **inputs** (generator commit, data window) are a **provenance
        record** — where the bytes came from. In a live-data repo they are not
        themselves reproducible.
      - The **transported bytes are the contract**: the builder hands the verifier the
        exact fixture files by a retained path recorded in the ledger header. "Same
        data" means the same bytes, not a re-run of the generator.
      - Fixture **hashes are a tripwire**, never the contract — they detect unintended
        regeneration. A hash over data that grows daily is unreproducible tomorrow by
        construction.
      
      A `revise` header names the base app SHA and the exact safe data source even when
      no generator or time window applies. A fallback header names the predecessor ref
      as before.
      
      A `behavior.md` without the `★ FROZEN` header is a sweep in progress, not a
      contract; revising or building against it is a blocking finding. Regenerating any pinned
      fixture after freeze requires a recorded reason under the header. **A ledger
      carrying a `missed` row cannot be frozen** — the freeze is what makes a verdict
      table binding, and freezing an undecided drop is how the retirement becomes
      contract without anyone choosing it. A row annotated `ruled-elsewhere` is a
      `missed` row for that purpose: the annotation routes it, it does not decide it.
      A `deferred` row freezes like any story.
      
      On fallback, **ledger + lock manifest together are the build contract**. The
      manifest alone is insufficient: the ledger's stories cover behavior and meaning,
      behavior replay in `build` exercises it, and §2's verdicts preserve predecessor
      behavior. Under `revise`, **the ledger plus its replay script is the contract**;
      the built branch is the visual artifact and no lock manifest is created.
      
    • brand.md 10.2 KB
      # Brand register
      
      When design IS the product: brand sites, landing pages, marketing surfaces, campaign pages, portfolios, long-form content, about pages. The deliverable is the design itself; a visitor's impression is the thing being made.
      
      The register spans every genre. A tech brand (Stripe, Linear, Vercel). A luxury brand (a hotel, a fashion house). A consumer product (a restaurant, a travel site, a CPG packaging page). A creative studio, an agency portfolio, a band's album page. They all share the stance (*communicate, not transact*) and diverge wildly in aesthetic. Don't collapse them into a single look.
      
      ## The brand slop test
      
      If someone could look at this and say "AI made that" without hesitation, it's failed. The bar is distinctiveness; a visitor should ask "how was this made?", not "which AI made this?"
      
      Brand isn't a neutral register. AI-generated landing pages have flooded the internet, and average is no longer findable. Restraint without intent now reads as mediocre, not refined. Brand surfaces need a POV, a specific audience, a willingness to risk strangeness. Go big or go home.
      
      **The second slop test: aesthetic lane.** Before committing to moves, name the reference. A Klim-style specimen page is one lane; Stripe-minimal is another; Liquid-Death-acid-maximalism is another. Don't drift into editorial-magazine aesthetics on a brief that isn't editorial. A hiking brand with Cormorant italic drop caps has the wrong register within the register.
      
      Then the inverse test: in one sentence, describe what you're about to build the way a competitor would describe theirs. If that sentence fits the modal landing page in the category, restart.
      
      ## Typography
      
      ### Font selection procedure
      
      Every project. Never skip.
      
      1. Read the brief. Write three concrete brand-voice words. Not "modern" or "elegant," but "warm and mechanical and opinionated" or "calm and clinical and careful." Physical-object words.
      2. List the three fonts you'd reach for by reflex. If any appear in the reflex-reject list below, reject them; they are training-data defaults and they create monoculture.
      3. Browse a real catalog (Google Fonts, Pangram Pangram, Future Fonts, Adobe Fonts, ABC Dinamo, Klim, Velvetyne) with the three words in mind. Find the font for the brand as a *physical object*: a museum caption, a 1970s terminal manual, a fabric label, a cheap-newsprint children's book, a concert poster, a receipt from a mid-century diner. Reject the first thing that "looks designy."
      4. Cross-check. "Elegant" is not necessarily serif. "Technical" is not necessarily sans. "Warm" is not Fraunces. If the final pick lines up with the original reflex, start over.
      
      ### Reflex-reject list
      
      Training-data defaults. Ban list. Look further:
      
      Fraunces · Newsreader · Lora · Crimson · Crimson Pro · Crimson Text · Playfair Display · Cormorant · Cormorant Garamond · Syne · IBM Plex Mono · IBM Plex Sans · IBM Plex Serif · Space Mono · Space Grotesk · Inter · DM Sans · DM Serif Display · DM Serif Text · Outfit · Plus Jakarta Sans · Instrument Sans · Instrument Serif
      
      ### Reflex-reject aesthetic lanes
      
      Parallel to the font list. Currently saturated aesthetic families that have flooded brand surfaces. If a brief lands in one of these lanes without a register reason that *requires* it (a literal magazine, a literal terminal, a literal industrial signage system), it's the second-order training reflex: the trap one tier deeper than picking a Fraunces font. Look further.
      
      - **Editorial-typographic.** Display serif (often italic) + small mono labels + ruled separators + monochromatic restraint. Klim-influenced, magazine-cover affectation. By 2026, every Stripe-adjacent and Notion-adjacent brand has landed here. The fingerprint: three rule-separated columns, an italic Fraunces / Recoleta / Newsreader headline, lowercase track-spaced metadata, no imagery.
      
      (More entries land here on the same cadence the font list updates. Brutalist-utility and acid-maximalism may join when they saturate. Removing entries when they fall back below saturation is also fine.)
      
      The reflex-reject lists apply to **new design choices**. When the existing brand has already committed to a font or a lane as part of its identity, identity-preservation wins; variants on an existing surface don't second-guess what's already shipping. The reflex-reject lists are for greenfield decisions and for departure-mode variants.
      
      ### Pairing and voice
      
      Distinctive + refined is the goal. The specific shape depends on the brand, not on the brand's category. A category ("restaurant", "dev tool", "magazine", "fintech") is not a recipe; treating it as one is the first-order reflex SKILL.md warns against.
      
      Two families minimum is the rule *only* when the voice needs it. A single well-chosen family with committed weight/size contrast is stronger than a timid display+body pair.
      
      ### Scale
      
      Modular scale, fluid `clamp()` for headings, ≥1.25 ratio between steps. Flat scales (1.1× apart) read as uncommitted.
      
      Light text on dark backgrounds: add 0.05–0.1 to line-height. Light type reads as lighter weight and needs more breathing room.
      
      ## Color
      
      Brand surfaces have permission for Committed, Full palette, and Drenched strategies. Use them. A single saturated color spread across a hero is not excess; it's voice. A beige-and-muted-slate landing page ignores the register.
      
      - Name a real reference before picking a strategy. "Klim Type Foundry #ff4500 orange drench", "Stripe purple-on-white restraint", "Liquid Death acid-green full palette", "Mailchimp yellow full palette", "Condé Nast Traveler muted navy restraint", "Vercel pure black monochrome". Unnamed ambition becomes beige.
      - Palette IS voice. A calm brand and a restless brand should not share palette mechanics.
      - When the strategy is Committed or Drenched, color carries the brand. Don't hedge with neutrals around the edges. Commit.
      - Don't converge across projects. Each brand surface differentiates from the last.
      - When a cultural-symbol palette is the obvious pull, reach past it. Let the cultural reading come from typography, imagery, and copy, not the palette.
      
      ## Layout
      
      - Asymmetric compositions are one option. Break the grid intentionally for emphasis.
      - Fluid spacing with `clamp()` that breathes on larger viewports. Vary for rhythm: generous separations, tight groupings.
      - For image-led briefs (hotels, restaurants, magazines, photography), full-bleed hero imagery with overlaid menu and centered headline is a canonical move; let the photograph be the design.
      - When cards ARE the right affordance, use `grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))` for breakpoint-free responsiveness.
      
      ## Imagery
      
      Brand surfaces lean on imagery. A restaurant, hotel, magazine, or product landing page without any imagery reads as incomplete, not as restrained. A solid-color rectangle where a hero image should go is worse than a representative stock photo.
      
      **When the brief implies imagery, you must ship imagery.** Zero images is a bug, not a design choice. "Restraint" is not an excuse. If the approved comp or brief is image-led, ship real project assets, generated raster assets, or a credible canvas/SVG/WebGL scene. Do not replace photographic, architectural, product, or place imagery with generic CSS panels, decorative diagrams, cards, bullets, or copy.
      
      - **For greenfield work without local assets, use stock imagery.** Unsplash is the default. The URL shape is `https://images.unsplash.com/photo-{id}?auto=format&fit=crop&w=1600&q=80`. **Verify the URLs before referencing them.** If you have an image-search MCP, web-fetch tool, or browser access, use it to find real photo IDs and confirm they resolve. Guessed IDs (even ones that look real) often 404 and ship as broken-image placeholders. Without a verification path, pick fewer photos you're confident exist over more that you guessed; never substitute colored `<div>` placeholders.
      - **Search for the brand's physical object**, not the generic category: "handmade pasta on a scratched wooden table" beats "Italian food"; "cypress trees above a limestone hotel facade at dusk" beats "luxury hotel".
      - **One decisive photo beats five mediocre ones.** Hero imagery should commit to a mood; padding with more stock doesn't rescue an indecisive one.
      - **Alt text is part of the voice.** "Coastal fettuccine, hand-cut, served on the terrace" beats "pasta dish".
      
      "Imagery" here is broader than stock photography: product screenshots, custom data visualizations, generated SVG, and canvas/WebGL scenes are all imagery. Text-only pages where typography alone carries the entire visual weight are the failure mode.
      
      ## Motion
      
      - One well-orchestrated page-load beats scattered micro-interactions, when the brand invites it. Some brands skip entrance motion entirely; the restraint is the voice.
      
      ## Brand bans (on top of the shared absolute bans)
      
      - Monospace as lazy shorthand for "technical / developer." If the brand isn't technical, mono reads as costume.
      - Large rounded-corner icons above every heading. Screams template.
      - Single-family pages that picked the family by reflex, not voice. (A single family chosen deliberately is fine.)
      - All-caps body copy. Reserve caps for short labels and headings.
      - Timid palettes and average layouts. Safe = invisible.
      - Zero imagery on a brief that implies imagery (restaurant, hotel, food, travel, fashion, photography, hobbyist). Colored blocks where a hero photo belongs.
      - Defaulting to editorial-magazine aesthetics (display serif + italic + drop caps + broadsheet grid) on briefs that aren't magazine-shaped. Editorial is ONE aesthetic lane, not the default brand aesthetic.
      - Repeated tiny uppercase tracked labels above every section heading. A single strong kicker can be voice; repeating it as section grammar is AI scaffolding unless it's a deliberate, named brand system.
      
      ## Brand permissions
      
      Brand can afford things product can't. Take them.
      
      - Ambitious first-load motion. Reveals and typographic choreography that earn their place; not fade-on-scroll for every section.
      - Single-purpose viewports. One dominant idea per fold, long scroll, deliberate pacing.
      - Unexpected color strategies. Palette IS voice; a calm brand and a restless brand should not share palette mechanics.
      - Art direction per section. Different sections can have different visual worlds if the narrative demands it. Consistency of voice beats consistency of treatment.
      
    • build.md 14.5 KB
      # Mode: build
      
      Implement a locked visual spec. The finish line is **every manifest term has
      evidence** — not "the gates are green". A build that passes every test while
      diverging from the lock is a failed build.
      
      This mode ports a greenfield lock or the explicitly recorded safe-start
      fallback. It does **not** revise a surface the app already ships. That work stays
      in `revise`, where the running branch is the visual artifact and the frozen
      behavior ledger plus app-only replay script is the contract. Do not manufacture a
      lock manifest or fidelity ledger for an app template in order to enter `build`.
      
      Input: the ★ LOCKED mockup(s) and `mockups/<surface>.lock.md`. If the
      manifest is missing, stop and create it first (run the manifest-extraction
      part of `lock` mode against the existing header — do not build from prose).
      
      Where the surface has interactive behavior, the input is **two** contracts: the
      manifest and the `★ FROZEN` behavior ledger from `behavior-sweep`. Building
      against an unfrozen (or absent) ledger is a blocking finding — check the header
      first. The manifest alone never encodes what the surface *does*.
      
      ## Before writing code
      
      Read [web-implementation.md](web-implementation.md) before changing CSS or
      JavaScript/TypeScript; it governs web mechanics while this mode's lock and evidence
      contract remains binding.
      
      1. Read the manifest, the mock headers, and the mock's **component CSS** —
         not just its layout. Diff the mock's component styling (buttons, chips,
         rows) against the app's shipped equivalents; where they differ, the
         manifest's precedence line decides. List the differences you will honor.
      2. **If an earlier surface was already ported into this shell, inventory what it
         declared before writing a line.** The Nth port inherits nothing and collides
         on everything: the mock is a whole page, so its rules sit at global scope,
         and the first port's rules are already there. Grep the shipped surface for
         what it owns **globally vs. host-scoped** — class names, element ids, and
         custom properties — then scope yours under a host class the way it did.
         Three collision classes, all seen in one port: duplicated class names
         (`.pane`, `.panes`) where the later stylesheet silently restyles the earlier
         surface; a duplicated element id (`#rd-time`) where a document-wide lookup
         paints the *other* surface's readout; and **design tokens declared on the
         first port's host element rather than `:root`**, which is the quiet one — a
         `var()` that resolves to nothing invalidates the whole declaration, so
         `padding: var(--ck-gap) 12px` computes to `0` and the grid loses its inset,
         its panel ground and its radius at once, with no error anywhere.
      3. If any two locked artifacts contradict each other, or a locked term
         collides with the design system beyond what precedence settles: **stop and
         surface it.** Implementer arbitration is how locks die.
      4. Read the fixture obligations. Build or extend fixtures until every locked
         visual feature actually renders under them. A tame fixture that leaves a
         ribbon invisible or a threshold untriggered cannot prove anything.
      5. **Pin the provenance.** Branch off a **freshly fetched** default branch and
         record the base SHA in the ledger header, along with the paths + SHAs of
         every source artifact (mock HTML, its module files, manifest, behavior
         ledger, fixtures, comparator). A branch cut from a pre-squash tip produces a
         conflicting PR that silently attaches no CI.
      
      ## Port, don't reimplement
      
      The mock is code, not a picture. **Its own JS and CSS are ported** — imported
      outright, or adapted line-by-line with a **diff-to-mock** attached. Reimplementing
      a feature from a lock term's *description* is a blocking finding, however faithful
      the result looks: the description was never the artifact.
      
      - **The diff unit is `mock line-range → app file`, verbatim-first.** "Per ported
        file" is undefined when the mock is a thousand inline lines plus a sibling
        module. Every non-verbatim line is listed with why it changed.
      - **New code is limited to adapters**: real data → the shape the mock's module
        already consumes, plus mounting and wiring into the app shell. If the mock's
        code cannot run against real app data without redesign, that is a QUESTION back
        to the operator — never a license to reimplement.
      - **Keep the mock's selectors.** The behavior replay script runs against mock and
        app alike; a rename that breaks a replay selector is a port defect, not a script
        defect.
      - **The one exception to porting the mock's code: where the mock re-derived a
        fact the backend owns, port it as a backend read, not as the mock's line.** A
        mock computes against a captured fixture, so it re-implements whatever the
        payload did not yet carry — a support floor, a direction, a "what counts as a
        meal" threshold. Transferring those faithfully re-introduces exactly the
        duplicated-predicate bugs the backend already fixed, through the one route
        nobody audits: the contract artifact itself. Each instance is listed in the
        diff-to-mock with the backend field it now reads, because a reviewer meeting it
        cold reads a deliberate correction as an infidelity. Expect the ported value to
        differ from the frozen render on the same data; say by how much.
      - Suspected data bugs surfaced by the sweep are verified against the real payload
        and fixed in the adapter or backend — never papered over in the port.
      - If the app has no static asset mount, every new frontend file gets its serving
        route **in the same commit**, and at least one verification pass runs against
        the real app server rather than a disk-serving harness, which is structurally
        blind to a missing route.
      - **The port needs guards the mock never needed.** A mock is fed static, always
        well-formed fixtures; the build is fed a response across a process boundary.
        Wherever the ported code indexes or dereferences the payload unconditionally,
        that is a trust boundary in the app and not in the mock — guard it, route the
        failure into the surface's own error path, and mark it as a deviation. Do not
        invent empty-state UI the manifest does not describe; the plain failure message
        is the whole fix. **Judge reachability from the caller's failure path, not its
        success path**: "the endpoint never returns that shape" is not the same claim as
        "no caller can produce it", and an error handler that mounts the surface before
        reporting the error produces it every time.
      
      ## While building
      
      - `design-rules.md` governs craft; the manifest governs content. Where they
        disagree, the manifest wins — improvements to a locked surface go through
        `resettle`, even mid-build, even when the improvement is real.
      - Every `gate` term gets an assertion in the rendered browser gate, tagged
        with its manifest number in a comment (`// LOCK:<surface>:<n>`), so
        coverage is greppable.
      - **Prove each lock assertion can fail.** Once per assertion: knock the
        feature out, watch the assertion go red for the right reason, restore.
        This is the charter's "failed first" applied to visual gates; it is what
        catches operator-precedence truthiness, wrong-state fixtures, and
        screenshots of nothing.
      - **Rewriting a test file transfers its invariants.** Before replacing any
        rendered gate, list the assertions the old file made; every one either
        reappears in the new file or is named as dropped (with why) in the PR.
        Silently dropped assertions are how locked terms become untested.
      - **"Its selectors are dead" is not "its coverage is duplicated."** A gate whose
        selectors no longer match the surface is asserting nothing and looks safe to
        delete — but that says only that the coverage is *already* gone, not that it
        lives somewhere else. Before dropping it, take the **union of lock-term tags**
        the other suite actually carries and diff it against the tags the deleted file
        claimed. Terms in the difference are asserted by nothing, and the PR says so by
        number. "Nearly every term" is how six of them go missing.
      
      ## The fidelity ledger
      
      The PR ships a ledger — in the PR body or `docs/` — one row per manifest
      term:
      
      ```markdown
      | # | Term | Status | Evidence |
      |---|------|--------|----------|
      | 1 | No page scroll at 1280x800 | met | LOCK:settings-audit:1 assertion |
      | 3 | Excursion aligns with block | met | paired render R3 |
      | 7 | Meal blocks colour-washed | re-settle requested | see PR comment |
      ```
      
      Statuses: `met`, `re-settle requested` (with the resettle recorded), or
      `blocked` (with why). There is no "partially" and no silent omission — a term
      absent from the ledger is a blocking gap.
      
      **There is no waiver.** A term that will not be built is dropped by one of the
      two dated paths in `behavior-sweep.md`: cites a lock term → `resettle` (the
      manifest row is the record); no lock term → an operator ruling recorded inline
      under a QUESTION entry, his exact sentence quoted. An unverified waiver in a PR
      body is how a dropped term recurs.
      
      ## Behavior replay
      
      Where a behavior ledger exists, its committed replay script re-exercises **every
      STORY against the built app** — not against the mock; a run wired through the
      mock opener exercises the mock and proves nothing about the port.
      
      - Each story's ledger `status` moves to `ported` → `replayed-pass` /
        `replayed-fail` / `re-settle requested`. **`replayed-fail` blocks.**
      - **RETIRED entries replay too, and their sanction prints.** Their absence
        assertions run against the built app like any story, each printing the who and
        the date that sanctioned the drop, so a retirement never becomes a silent green
        line. A RETIRED entry whose behavior is *present* in the build is a
        `replayed-fail`: the port restored something a human ruled gone, which is a
        deviation in the same way dropping a locked term is. A RETIRED entry whose
        *premise* has failed while its behavior stays absent is `re-settle requested`,
        never `replayed-fail` — the drop still holds but the reason for it does not, so
        the operator rules again.
      - The script's fail-closed rule holds here: a green step that executed zero
        stories is a failure, so confirm the run **reported its applicable story
        count**, not merely that the step ran.
      - Where PHI or other sensitive data splits the runs, the CI gate replays a
        committed labeled-synthetic fixture set and the real-data run stays local; the
        two subsets must **union to the full ledger**, and the ledger records which run
        covers each story.
      - **A build that adds a data feed must teach the real-data generator about it.**
        The committed script that rebuilds the local real-data payload is part of the
        contract, not a convenience: when the port introduces an endpoint, that script
        calls it **exactly as the app's own route calls it**, same arguments and all.
        Otherwise every real-data artifact — the local replay, the real-data pairs, the
        bytes handed to the verifier — is generated against a payload the app would
        never serve, and passes while proving nothing about the feed.
      
      ## Paired renders
      
      For every `eye` term and every state the manifest names: render the **locked
      mock** and the **built surface** side by side and attach the pairs to the PR as
      the charter's proof-of-match.
      
      - **Render the matrix early, not as the last step before the PR.** The pairs are
        the only check that sees what the surface actually *looks* like; a behavior gate
        can pass every story while the build draws in the wrong ink, because assertions
        read structure and text, not colour and geometry. A token that resolves to the
        empty string, a fallback palette, a rule scoped to a subtree the code reads from
        the document root — all of these are invisible to a green replay and obvious in
        the first pair. Render one pair as soon as the surface boots.
      - **A PR is a publish: real-data pairs never attach to one.** Where the data is
        sensitive, PR-attached pairs render the labeled synthetic set only. Real-data
        pairs are still made — they are the fidelity check that matters — but they stay
        on the machine that made them and reach the verifier out of band with the pinned
        payload. The commit rule alone does not cover this: an attachment is published
        without ever being committed.
      - **Same data, not merely the same kind of data.** Both sides render identical
        bytes. Comparing a synthetic build render against a real-capture mock render is
        not a pair; it is two pictures.
      - **The pairs cover every state and viewport the lock's own terms enumerate** —
        all of them, in both themes — not one convenient state. This is the only
        accepted fidelity evidence.
      - **Any harness, comparator, or screenshot rig loads the app's real CSS.**
        Re-declaring theme tokens from memory is a blocking finding: it puts wrong
        colors into the "proof" and can mask a real token bug in the shipped build.
      - Verify each pair actually exercises its term before attaching (fixture
        obligations again).
      - Drive the build into each enumerated state deliberately. If the mock is
        state-addressable by URL and the app is not, the port carries an equivalent
        hook (or a `goto<State>(page)` export per state in the replay script) — without
        it the comparator renders two of the matrix's pairs and reports a full sweep.
      
      ## Review handoff
      
      - **The verifier is a freshly spawned session** that did not author the port and
        has **no access to the builder's transcript or working notes**. Hand it only
        the contract artifacts — manifest, frozen behavior ledger, replay script, the
        pinned data files, the renders, the diff-to-mock set — and instruct it to
        distrust the port. A verifier sharing the builder's context is a self-graded
        table with extra steps, whatever its model tier.
      - **The builder's session may not mark any status beyond `ported`.** Statuses are
        recorded by the verifier from raw script output. A self-graded fidelity table
        is an index into evidence, never evidence.
      - **Craft and eye judgment route to the top model tier**, or the coordinator's own
        eyes **when the coordinator did not drive the port** (otherwise the verifier's
        or the operator's). Mid-tier polish grades are not accepted for craft.
      - **Every `eye` term gets its own named judgment entry** in the PR evidence,
        written by the eye reviewer. An eye term is *never* satisfied by gate
        assertions passing.
      - An eye term defined over a **gesture in progress** is viewed **live in-browser**;
        static pairs cannot evidence it.
      - Their checklist is the manifest and the ledger, not the diff. "Does render N
        match mock N on terms X, Y, Z" is an answerable question.
      - **Surface the evidence unprompted** — pairs, replay output, judgment entries,
        diffs-to-mock go in the PR. Evidence withheld until demanded is a process
        violation.
      
    • consensus.md 3.7 KB
      # Mode: consensus
      
      Settle a contested visual-design decision with a fixed panel of 3 product-derived AI reviewer personas. The panel votes independently, negotiates vetoes and trade-offs, and surfaces the recommendation to you. **The panel is advisory — you always have final say.**
      
      Use this when a design question has real trade-offs between user situations (accessibility vs. density, discoverability vs. minimalism, mobile vs. desktop) and different user situations genuinely want different answers. Do not use for trivial tweaks or unanimous positions.
      
      ## Preconditions
      
      - Personas must be repo-specific at `.claude/qa/personas/<name>.md` — one file per persona with lens, job, and rules, plus the declared tie-break ranking in `.claude/qa/personas/RANKING.md`. **If personas are missing or generic archetypes only, STOP and run persona-init (reference/init.md) first.** Never run the panel on untuned personas.
      - Panel size is exactly 3.
      - Personas are spawned as cheap sub-agents (Haiku); reuse and resume (SendMessage) when history matters. They judge **rendered screenshots only** — never source code.
      
      ## Step 1: Fixture coverage
      
      Enumerate every content dimension the question touches (fractions, long names, empty states, worst-case string lengths). **If the current fixture doesn't exercise one, patch it or build a test copy with edge content injected before round 1.** A fixture gap found late forces rework; catch it now.
      
      ## Step 2: Neutral framing
      
      Pose the question with all options argued evenly. **Never reveal your lean, the orchestrator's lean, or the originating comment.** Force commitment: "no fence-sitting; verdict line at top." Provide rendered screenshots of both candidates.
      
      ## Step 3: Independent votes
      
      Spawn all 3 personas in parallel; they must not see each other's output. Tally votes. **Unanimity ends the panel — go to Step 6.** The orchestrator (you) gives its take only *after* votes, from its own screenshots — never before (no anchoring).
      
      ## Step 4: Confirmation pass (split vote only)
      
      When building a hybrid, send each persona a head-to-head: hybrid vs. their own previous winner. They must **ACCEPT** or name a **CONCRETE LOSS** — vague preference restatements don't count.
      
      ## Step 5: Negotiation rounds (hard cap: 3)
      
      - **Round 1:** show each persona the other two's positions; each proposes one concrete spec delta per element/text role.
      - **Round 2:** enumerate remaining open axes as explicit packages (P1…Pn); each picks one ACCEPT plus any vetoes.
      - **Round 3:** audit vetoes for stale premises — personas sometimes defend positions another has abandoned. Correct the record (quote the persona's own prior words) and re-ask only the holdout. A late-round holdout may escalate to a stronger model.
      
      **Two guards:** (a) "correct facts, don't argue taste" — factual corrections must cite rendered evidence (screenshot or measurable property), never bare assertion; (b) ties resolve by declared persona ranking. If ranking is genuinely uncertain for this question, escalate to the user and write their answer into persona files. **Still split after round 3 → the panel is HUNG:** bring the split to the user with each side's best case. Never loop past the cap.
      
      ## Step 6: Build and verify rendered
      
      Implement the agreed spec as a new mockup variant, screenshot it, show the user. If Step 1 missed a content dimension, run the edge-content check now (throwaway copy with edge cases, one panel look).
      
      ## Step 7: Record
      
      Write a snapshot to `.impeccable/consensus/<timestamp>__<slug>.md`: the question, votes per round, packages/vetoes, final spec, and hung/settled status. Surface the verdict via the critique widget where applicable, marked "(recommended, panel N-0)" — the user always picks.
      
    • critique.md 38 KB
      ### Purpose
      
      Resolve one stable target, run two independent assessments, synthesize a design critique, persist a snapshot, and ask the user what to improve next. The chat response is the primary deliverable; the snapshot is an archive/backlog for future commands.
      
      ### Hard Invariants
      
      - Assessment A (design review) and Assessment B (detector/browser evidence) are both required.
      - Assessment A and B MUST run as two isolated sub-agents whenever a sub-agent/Task tool is exposed. Running them inline in this context is "possible" but is NOT permitted; it is a degraded run. Inline is allowed ONLY when no sub-agent tool exists (or the user declined, on harnesses that ask).
      - If you degrade for any reason, the report's first line MUST be a banner: `⚠️ DEGRADED: single-context (<reason>)`. A silent degraded critique is a failed critique.
      - Assessment A must finish before detector findings enter the parent synthesis context. Detector output is deterministic, but it still anchors judgment.
      - A skipped detector is a failed critique run unless `detect.mjs` is missing or crashes after a real attempt.
      - Viewable targets require browser inspection when available.
      - Any local server started only for critique visualization must run in the background, have a recorded stop method, and be stopped before final reporting unless the user asks to keep it.
      - Do not claim a user-visible overlay exists unless script injection succeeded and the detector ran in the page.
      
      ### Setup
      
      1. **Resolve the target** to a concrete file path or URL. Prefer a source path over a dev-server URL when both identify the same surface; ports drift, paths do not.
         - "the homepage" -> `site/pages/index.astro` or `index.html`
         - "the settings modal" -> the primary component file
         - "this page" -> the current URL or source file
      2. **Compute the slug**:
         ```bash
         node $UI_CRAFT_SKILL_DIR/scripts/critique-storage.mjs slug "<resolved-path-or-url>"
         ```
         Keep it. If the command exits non-zero, skip persistence and trend for this run, but continue the critique.
      3. **Read `.impeccable/critique/ignore.md`** if it exists. Drop matching findings silently; it is the only prior-run input critique consumes.
      
      ### Assessment Orchestration
      
      Delegate Assessment A and Assessment B to separate sub-agents. They must not see each other's output. Do not show findings to the user until synthesis.
      
      Sub-agent gate (all harnesses):
      - Unless a harness-specific gate below overrides this, spawn A and B as two isolated, parallel sub-agents whenever a sub-agent/Task tool is exposed. This is the default and is mandatory; do not run them inline because it is faster.
      - "Unavailable" means exactly one thing: no sub-agent/Task tool is exposed in this session (or, on harnesses that ask, the user declined). It does not mean inconvenient.
      - If and only if sub-agents are unavailable, fall back sequentially: finish and record Assessment A, then run Assessment B, then synthesize, and emit the degraded banner.
      - Whichever path you take, declare it in the report header (see Report header provenance). Skipping sub-agents without the banner is the most common failure of this command.
      
      If browser automation is available, each assessment creates its own new tab. Never reuse an existing tab, even if it is already at the right URL.
      
      ### Assessment A: Design Review
      
      Read relevant source files and visually inspect the live page when browser automation is available. Think like a design director.
      
      Evaluate:
      - **AI slop**: Would someone believe "AI made this" immediately? Check all DON'T guidance from the parent Impeccable skill.
      - **Holistic design**: hierarchy, IA, emotional fit, discoverability, composition, typography, color, accessibility, states, copy, and edge cases.
      - **Cognitive load**: consult the [Cognitive Load Assessment](#cognitive-load-assessment) section below; report checklist failures and decision points with >4 visible options.
      - **Emotional journey**: peak-end rule, emotional valleys, reassurance at high-stakes moments.
      - **Nielsen heuristics**: consult the [Heuristics Scoring Guide](#heuristics-scoring-guide) section below; score all 10 heuristics 0-4.
      
      Return: AI slop verdict, heuristic scores, cognitive load, emotional journey, 2-3 strengths, 3-5 priority issues, persona red flags, minor observations, and provocative questions.
      
      ### Assessment B: Detector + Browser Evidence
      
      Run the bundled detector and browser visualization evidence. Assessment B is mandatory and must remain isolated from Assessment A until both are complete.
      
      CLI scan:
      ```bash
      node $UI_CRAFT_SKILL_DIR/scripts/detect.mjs --json [target]
      ```
      
      - Pass markup files/directories as `[target]`; do not pass CSS-only files.
      - For URLs, skip CLI scan and use browser visualization.
      - For very large trees (500+ scannable files), narrow scope or ask.
      - Exit code 0 = clean; 2 = findings.
      - If the detector entrypoint is missing or fails to load, report deterministic scan unavailable and continue with browser/manual review.
      
      Browser visualization is required for a viewable target when browser automation is available. Use a localhost dev/static URL for local files; avoid `file://` unless the available browser explicitly supports this workflow. Overlay flow:
      
      1. Create a fresh tab and navigate. Prefer the harness's native/browser-canvas screenshot path before hand-rolling a Playwright/Puppeteer script; only fall back to a custom script when no native browser tool is exposed.
      2. Preflight mutable injection by setting `document.title` and appending a `<script>` tag. Read-only evaluate APIs do not count.
      3. This fork does not bundle the live overlay server: skip overlay injection entirely. Rely on the CLI detector findings plus rendered screenshots of 3-5 representative pages/states, and report that the overlay step was skipped.
      
      Return: CLI findings JSON/counts, browser console findings if applicable, false positives, and skipped/failed browser steps with concrete reasons.
      
      After Assessment B returns usable CLI findings, reuse them. Do not rerun `detect.mjs` in the parent unless Assessment B failed, was truncated, or omitted count, rule names, or file locations.
      
      ### Generate Combined Critique Report
      
      Synthesize both assessments into a single report. Do NOT simply concatenate. Weave the findings together, noting where the LLM review and detector agree, where the detector caught issues the LLM missed, and where detector findings are false positives.
      
      The chat response is the primary user-facing deliverable. Present the full structured critique below in chat; do not replace it with a summary and a link. The persisted snapshot is only an archive/backlog for later commands.
      
      Structure your feedback as a design director would:
      
      #### Report header provenance
      
      The report's first line MUST declare how the assessments were run, so a degraded run is never silent:
      - Dual-agent: `Method: dual-agent (A: <agent-id> · B: <agent-id>)`
      - Degraded: `⚠️ DEGRADED: single-context (<reason, e.g. no sub-agent tool exposed>)`
      
      #### Design Health Score
      > *Consult the [Heuristics Scoring Guide](#heuristics-scoring-guide) section below.*
      
      Present the Nielsen's 10 heuristics scores as a table:
      
      | # | Heuristic | Score | Key Issue |
      |---|-----------|-------|-----------|
      | 1 | Visibility of System Status | ? | [specific finding or "n/a" if solid] |
      | 2 | Match System / Real World | ? | |
      | 3 | User Control and Freedom | ? | |
      | 4 | Consistency and Standards | ? | |
      | 5 | Error Prevention | ? | |
      | 6 | Recognition Rather Than Recall | ? | |
      | 7 | Flexibility and Efficiency | ? | |
      | 8 | Aesthetic and Minimalist Design | ? | |
      | 9 | Error Recovery | ? | |
      | 10 | Help and Documentation | ? | |
      | **Total** | | **??/40** | **[Rating band]** |
      
      Be honest with scores. A 4 means genuinely excellent. Most real interfaces score 20-32.
      
      #### Anti-Patterns Verdict
      
      **Start here.** Does this look AI-generated?
      
      **LLM assessment**: Your own evaluation of AI slop tells. Cover overall aesthetic feel, layout sameness, generic composition, missed opportunities for personality.
      
      **Deterministic scan**: Summarize what the automated detector found, with counts and file locations. Note any additional issues the detector caught that you missed, and flag any false positives.
      
      **Visual overlays** (if injection succeeded): Tell the user that overlays are now visible in the **[Human]** tab in their browser, highlighting the detected issues. Summarize what the console output reported. If browser visualization was attempted but injection failed, say that no reliable user-visible overlay is available and report the fallback signal instead.
      
      #### Overall Impression
      A brief gut reaction: what works, what doesn't, and the single biggest opportunity.
      
      #### What's Working
      Highlight 2-3 things done well. Be specific about why they work.
      
      #### Priority Issues
      The 3-5 most impactful design problems, ordered by importance.
      
      For each issue, tag with **P0-P3 severity** (see [Issue Severity below](#issue-severity-p0p3) for definitions):
      - **[P?] What**: Name the problem clearly
      - **Why it matters**: How this hurts users or undermines goals
      - **Fix**: What to do about it (be concrete)
      - **Suggested command**: Which command could address this (ui-craft modes only: `revise` for shipped-surface changes, `polish` for greenfield fix passes, `audit` for measurable checks, `resettle` for locked-term changes, `document` for a missing DESIGN.md, or a targeted greenfield fix under the general invocation)
      
      #### Persona Red Flags
      > *Consult the [Personas reference](#persona-based-design-testing) below.*
      
      Auto-select 2-3 personas most relevant to this interface type (use the selection table in the reference). If `CLAUDE.md` contains a `## Design Context` section from `ui-craft init`, also generate 1-2 project-specific personas from the audience/brand info.
      
      **Repo personas win.** If the repo defines its own personas — canonically `.claude/qa/personas/*.md` (written by `init`'s persona-panel step), or a location named in its CLAUDE.md/AGENTS.md — use those instead of inventing equivalents, and follow the repo's sweep protocol when one exists. The built-in archetypes only fill roles the repo's set doesn't cover.
      
      For each selected persona, walk through the primary user action and list specific red flags found:
      
      **Alex (Power User)**: No keyboard shortcuts detected. Form requires 8 clicks for primary action. Forced modal onboarding. High abandonment risk.
      
      **Jordan (First-Timer)**: Icon-only nav in sidebar. Technical jargon in error messages ("404 Not Found"). No visible help. Will abandon at step 2.
      
      Be specific. Name the exact elements and interactions that fail each persona. Don't write generic persona descriptions; write what broke for them.
      
      #### Minor Observations
      Quick notes on smaller issues worth addressing.
      
      #### Questions to Consider
      Provocative questions that might unlock better solutions:
      - "What if the primary action were more prominent?"
      - "Does this need to feel this complex?"
      - "What would a confident version of this look like?"
      
      **Remember**:
      - Be direct. Vague feedback wastes everyone's time.
      - Be specific. "The submit button," not "some elements."
      - Say what's wrong AND why it matters to users.
      - Give concrete suggestions. Cut "consider exploring..." entirely.
      - Prioritize ruthlessly. If everything is important, nothing is.
      - Don't soften criticism. Developers need honest feedback to ship great design.
      
      ### Persist the Snapshot
      
      Once the report above is finalized, write it to `.impeccable/critique/` so the user can refer back, and so `/ui-craft polish` can pick up the priority issues without a copy-paste.
      
      Skip this step if the Setup slug was null (vague or root-level target).
      
      1. **Write the body to a temp file** so you can pipe it to the helper. Use the full critique report (heuristic table, anti-patterns verdict, priority issues, persona red flags, minor observations, and questions), but stop before the "Ask the User" / "Recommended Actions" sections that come later.
      
      2. **Pass the structured metadata** through `IMPECCABLE_CRITIQUE_META` (JSON), then run the write command:
         ```bash
         IMPECCABLE_CRITIQUE_META='{"target":"<user phrasing>","total_score":<n>,"p0_count":<n>,"p1_count":<n>}' \
           node $UI_CRAFT_SKILL_DIR/scripts/critique-storage.mjs write <slug> <body-file>
         ```
         The helper prints the absolute path it wrote.
      
      3. **Delete the temp body file** after the write attempt completes, whether the write succeeded or failed. If deletion fails, mention `temp-file cleanup failed: <reason>` briefly in the final output, but do not block the critique.
      
      4. **Read the trend** for context:
         ```bash
         node $UI_CRAFT_SKILL_DIR/scripts/critique-storage.mjs trend <slug> 5
         ```
         This returns a JSON array of the last 5 frontmatter entries (including the one you just wrote).
      
      5. **Append a single line to the user-visible output**, after the report and before the questions:
      
         > **Trend for `<slug>` (last 5 runs): 24 → 28 → 32 → 29 → 32**
         > Wrote `.impeccable/critique/<filename>`.
      
         If this is the first run for the slug, the trend is just one score; say so: "First run for this target, no trend yet."
      
      This is fire-and-forget. Do not show the user the helper's JSON output; only the human-readable trend line and the written path. Failures here should not block the rest of the flow; print the error and move on.
      
      ### Ask the User
      
      **After presenting findings**, convert actionable decisions into visual either/or choices rather than prose questions—this is the default, not optional. These choices shape the action plan.
      
      **Visual decision widgets (primary approach)**: When a widget tool is available (`mcp__visualize__show_widget` or equivalent), render one widget containing 3-5 numbered questions. Each question describes a single tension found in plain English (no jargon, no CSS specifics), followed by 2-3 side-by-side option cards. Each card shows a small faithful visual preview of that option—rendered with actual fonts and colors where practical—plus a label (mark the recommended one "(recommended)") and a "Pick this" button wired via `sendPrompt()` to send a message identifying the question and chosen option. Purely mechanical fixes—unambiguous P0/P1 issues with one right answer—are not questions; list them in a single sentence as "I'll just fix these" and include them in the plan. Before rendering any widget, call `mcp__visualize__read_me` silently. After rendering, STOP and wait for picks via sendPrompt messages; accept prose answers as well.
      
      **Panel verdicts annotate, never decide.** If a `/ui-craft consensus` panel (see reference/consensus.md) has already settled one of these questions, still ask it — mark the winning option's label "(recommended, panel 3-0)" or note the split. The panel is advisory; the user picks.
      
      **Offer the panel once.** If several findings pulled the personas in genuinely different directions, add one line after the widget: "These trade-offs split the personas — I can have them hash it out themselves (`/ui-craft consensus`) and bring back a negotiated recommendation." Offer it at most once per session, only when repo personas exist (`.claude/qa/personas/`), and never auto-run it.
      
      **Fallback to prose questions (CLI-only sessions)**: If no widget tool is available, ask targeted prose questions based on specific findings. Adapt these to what you actually found; never ask generic questions:
      
      1. **Priority direction**: "I found problems with [X], [Y], and [Z]. Which area should we tackle first?" Offer the top 2-3 categories.
      2. **Design intent**: For tonal mismatches, ask if they were intentional and offer 2-3 directions that would fix the issues.
      3. **Scope**: "I found N issues. Want to address everything, or focus on the top 3?" Offer options like "Top 3 only", "All issues", "Critical only".
      4. **Constraints** (optional): If findings touch many areas, ask if anything should stay as-is—prevents the plan from touching areas the user considers done.
      
      **Rules for both approaches**:
      - Every question references specific findings. Never ask generic "who is your audience?" questions.
      - Keep to 2-4 questions in prose, up to 5 in a widget. Respect the user's time.
      - Offer concrete options, not open-ended prompts.
      - If findings are straightforward (only 1-2 clear issues), skip questions and go directly to Recommended Actions.
      
      ### Recommended Actions
      
      **After receiving the user's answers**, present a prioritized action summary reflecting the user's priorities and scope from Ask the User.
      
      #### Action Summary
      
      List recommended commands in priority order, based on the user's answers:
      
      1. **`/command-name`**: Brief description of what to fix (specific context from critique findings)
      2. **`/command-name`**: Brief description (specific context)
      ...
      
      **Rules for recommendations**:
      - Only recommend ui-craft modes: `revise`, `polish`, `audit`, `resettle`, `document`, or a targeted greenfield fix under the general invocation
      - Route every shipped-surface change through `revise`; `polish` and targeted fixes apply directly only to greenfield work
      - Order by the user's stated priorities first, then by impact
      - Each item's description should carry enough context that the command knows what to focus on
      - Map each Priority Issue to the appropriate command
      - Skip commands that would address zero issues
      - If the user chose a limited scope, only include items within that scope
      - If the user marked areas as off-limits, exclude commands that would touch those areas
      - End greenfield recommendations with `/ui-craft polish`; a shipped-surface set
        stays under `/ui-craft revise` through its own review-and-land gate
      
      After presenting the summary, tell the user:
      
      > You can ask me to run these one at a time, all at once, or in any order you prefer.
      >
      > Re-run `/ui-craft critique` after fixes to see your score improve.
      
      ---
      
      ## Reference Material
      
      The sections below were previously separate reference files (`cognitive-load.md`, `heuristics-scoring.md`, `personas.md`). They live inline now so the critique flow has all its deep context in one place.
      
      ### Cognitive Load Assessment
      
      Cognitive load is the total mental effort required to use an interface. Overloaded users make mistakes, get frustrated, and leave. This reference helps identify and fix cognitive overload.
      
      ---
      
      #### Three Types of Cognitive Load
      
      ##### Intrinsic Load: The Task Itself
      Complexity inherent to what the user is trying to do. You can't eliminate this, but you can structure it.
      
      **Manage it by**:
      - Breaking complex tasks into discrete steps
      - Providing scaffolding (templates, defaults, examples)
      - Progressive disclosure: show what's needed now, hide the rest
      - Grouping related decisions together
      
      ##### Extraneous Load: Bad Design
      Mental effort caused by poor design choices. **Eliminate this ruthlessly.** It's pure waste.
      
      **Common sources**:
      - Confusing navigation that requires mental mapping
      - Unclear labels that force users to guess meaning
      - Visual clutter competing for attention
      - Inconsistent patterns that prevent learning
      - Unnecessary steps between user intent and result
      
      ##### Germane Load: Learning Effort
      Mental effort spent building understanding. This is *good* cognitive load; it leads to mastery.
      
      **Support it by**:
      - Progressive disclosure that reveals complexity gradually
      - Consistent patterns that reward learning
      - Feedback that confirms correct understanding
      - Onboarding that teaches through action, not walls of text
      
      ---
      
      #### Cognitive Load Checklist
      
      Evaluate the interface against these 8 items:
      
      - [ ] **Single focus**: Can the user complete their primary task without distraction from competing elements?
      - [ ] **Chunking**: Is information presented in digestible groups (≤4 items per group)?
      - [ ] **Grouping**: Are related items visually grouped together (proximity, borders, shared background)?
      - [ ] **Visual hierarchy**: Is it immediately clear what's most important on the screen?
      - [ ] **One thing at a time**: Can the user focus on a single decision before moving to the next?
      - [ ] **Minimal choices**: Are decisions simplified (≤4 visible options at any decision point)?
      - [ ] **Working memory**: Does the user need to remember information from a previous screen to act on the current one?
      - [ ] **Progressive disclosure**: Is complexity revealed only when the user needs it?
      
      **Scoring**: Count the failed items. 0–1 failures = low cognitive load (good). 2–3 = moderate (address soon). 4+ = high cognitive load (critical fix needed).
      
      ---
      
      #### The Working Memory Rule
      
      **Humans can hold ≤4 items in working memory at once** (Miller's Law revised by Cowan, 2001).
      
      At any decision point, count the number of distinct options, actions, or pieces of information a user must simultaneously consider:
      - **≤4 items**: Within working memory limits, manageable
      - **5–7 items**: Pushing the boundary; consider grouping or progressive disclosure
      - **8+ items**: Overloaded; users will skip, misclick, or abandon
      
      **Practical applications**:
      - Navigation menus: ≤5 top-level items (group the rest under clear categories)
      - Form sections: ≤4 fields visible per group before a visual break
      - Action buttons: 1 primary, 1–2 secondary, group the rest in a menu
      - Dashboard widgets: ≤4 key metrics visible without scrolling
      - Pricing tiers: ≤3 options (more causes analysis paralysis)
      
      ---
      
      #### Common Cognitive Load Violations
      
      ##### 1. The Wall of Options
      **Problem**: Presenting 10+ choices at once with no hierarchy.
      **Fix**: Group into categories, highlight recommended, use progressive disclosure.
      
      ##### 2. The Memory Bridge
      **Problem**: User must remember info from step 1 to complete step 3.
      **Fix**: Keep relevant context visible, or repeat it where it's needed.
      
      ##### 3. The Hidden Navigation
      **Problem**: User must build a mental map of where things are.
      **Fix**: Always show current location (breadcrumbs, active states, progress indicators).
      
      ##### 4. The Jargon Barrier
      **Problem**: Technical or domain language forces translation effort.
      **Fix**: Use plain language. If domain terms are unavoidable, define them inline.
      
      ##### 5. The Visual Noise Floor
      **Problem**: Every element has the same visual weight; nothing stands out.
      **Fix**: Establish clear hierarchy: one primary element, 2–3 secondary, everything else muted.
      
      ##### 6. The Inconsistent Pattern
      **Problem**: Similar actions work differently in different places.
      **Fix**: Standardize interaction patterns. Same type of action = same type of UI.
      
      ##### 7. The Multi-Task Demand
      **Problem**: Interface requires processing multiple simultaneous inputs (reading + deciding + navigating).
      **Fix**: Sequence the steps. Let the user do one thing at a time.
      
      ##### 8. The Context Switch
      **Problem**: User must jump between screens/tabs/modals to gather info for a single decision.
      **Fix**: Co-locate the information needed for each decision. Reduce back-and-forth.
      
      ---
      
      ### Heuristics Scoring Guide
      
      Score each of Nielsen's 10 Usability Heuristics on a 0–4 scale. Be honest: a 4 means genuinely excellent, not "good enough."
      
      #### Nielsen's 10 Heuristics
      
      ##### 1. Visibility of System Status
      
      Keep users informed about what's happening through timely, appropriate feedback.
      
      **Check for**:
      - Loading indicators during async operations
      - Confirmation of user actions (save, submit, delete)
      - Progress indicators for multi-step processes
      - Current location in navigation (breadcrumbs, active states)
      - Form validation feedback (inline, not just on submit)
      
      **Scoring**:
      | Score | Criteria |
      |-------|----------|
      | 0 | No feedback; user is guessing what happened |
      | 1 | Rare feedback; most actions produce no visible response |
      | 2 | Partial; some states communicated, major gaps remain |
      | 3 | Good; most operations give clear feedback, minor gaps |
      | 4 | Excellent; every action confirms, progress is always visible |
      
      ##### 2. Match Between System and Real World
      
      Speak the user's language. Follow real-world conventions. Information appears in natural, logical order.
      
      **Check for**:
      - Familiar terminology (no unexplained jargon)
      - Logical information order matching user expectations
      - Recognizable icons and metaphors
      - Domain-appropriate language for the target audience
      - Natural reading flow (left-to-right, top-to-bottom priority)
      
      **Scoring**:
      | Score | Criteria |
      |-------|----------|
      | 0 | Pure tech jargon, alien to users |
      | 1 | Mostly confusing; requires domain expertise to navigate |
      | 2 | Mixed; some plain language, some jargon leaks through |
      | 3 | Mostly natural; occasional term needs context |
      | 4 | Speaks the user's language fluently throughout |
      
      ##### 3. User Control and Freedom
      
      Users need a clear "emergency exit" from unwanted states without extended dialogue.
      
      **Check for**:
      - Undo/redo functionality
      - Cancel buttons on forms and modals
      - Clear navigation back to safety (home, previous)
      - Easy way to clear filters, search, selections
      - Escape from long or multi-step processes
      
      **Scoring**:
      | Score | Criteria |
      |-------|----------|
      | 0 | Users get trapped; no way out without refreshing |
      | 1 | Difficult exits; must find obscure paths to escape |
      | 2 | Some exits; main flows have escape, edge cases don't |
      | 3 | Good control; users can exit and undo most actions |
      | 4 | Full control; undo, cancel, back, and escape everywhere |
      
      ##### 4. Consistency and Standards
      
      Users shouldn't wonder whether different words, situations, or actions mean the same thing.
      
      **Check for**:
      - Consistent terminology throughout the interface
      - Same actions produce same results everywhere
      - Platform conventions followed (standard UI patterns)
      - Visual consistency (colors, typography, spacing, components)
      - Consistent interaction patterns (same gesture = same behavior)
      
      **Scoring**:
      | Score | Criteria |
      |-------|----------|
      | 0 | Inconsistent everywhere; feels like different products stitched together |
      | 1 | Many inconsistencies; similar things look/behave differently |
      | 2 | Partially consistent; main flows match, details diverge |
      | 3 | Mostly consistent; occasional deviation, nothing confusing |
      | 4 | Fully consistent; cohesive system, predictable behavior |
      
      ##### 5. Error Prevention
      
      Better than good error messages is a design that prevents problems in the first place.
      
      **Check for**:
      - Confirmation before destructive actions (delete, overwrite)
      - Constraints preventing invalid input (date pickers, dropdowns)
      - Smart defaults that reduce errors
      - Clear labels that prevent misunderstanding
      - Autosave and draft recovery
      
      **Scoring**:
      | Score | Criteria |
      |-------|----------|
      | 0 | Errors easy to make; no guardrails anywhere |
      | 1 | Few safeguards; some inputs validated, most aren't |
      | 2 | Partial prevention; common errors caught, edge cases slip |
      | 3 | Good prevention; most error paths blocked proactively |
      | 4 | Excellent; errors nearly impossible through smart constraints |
      
      ##### 6. Recognition Rather Than Recall
      
      Minimize memory load. Make objects, actions, and options visible or easily retrievable.
      
      **Check for**:
      - Visible options (not buried in hidden menus)
      - Contextual help when needed (tooltips, inline hints)
      - Recent items and history
      - Autocomplete and suggestions
      - Labels on icons (not icon-only navigation)
      
      **Scoring**:
      | Score | Criteria |
      |-------|----------|
      | 0 | Heavy memorization; users must remember paths and commands |
      | 1 | Mostly recall; many hidden features, few visible cues |
      | 2 | Some aids; main actions visible, secondary features hidden |
      | 3 | Good recognition; most things discoverable, few memory demands |
      | 4 | Everything discoverable; users never need to memorize |
      
      ##### 7. Flexibility and Efficiency of Use
      
      Accelerators, invisible to novices, speed up expert interaction.
      
      **Check for**:
      - Keyboard shortcuts for common actions
      - Customizable interface elements
      - Recent items and favorites
      - Bulk/batch actions
      - Power user features that don't complicate the basics
      
      **Scoring**:
      | Score | Criteria |
      |-------|----------|
      | 0 | One rigid path; no shortcuts or alternatives |
      | 1 | Limited flexibility; few alternatives to the main path |
      | 2 | Some shortcuts; basic keyboard support, limited bulk actions |
      | 3 | Good accelerators; keyboard nav, some customization |
      | 4 | Highly flexible; multiple paths, power features, customizable |
      
      ##### 8. Aesthetic and Minimalist Design
      
      Interfaces should not contain irrelevant or rarely needed information. Every element should serve a purpose.
      
      **Check for**:
      - Only necessary information visible at each step
      - Clear visual hierarchy directing attention
      - Purposeful use of color and emphasis
      - No decorative clutter competing for attention
      - Focused, uncluttered layouts
      
      **Scoring**:
      | Score | Criteria |
      |-------|----------|
      | 0 | Overwhelming; everything competes for attention equally |
      | 1 | Cluttered; too much noise, hard to find what matters |
      | 2 | Some clutter; main content clear, periphery noisy |
      | 3 | Mostly clean; focused design, minor visual noise |
      | 4 | Perfectly minimal; every element earns its pixel |
      
      ##### 9. Help Users Recognize, Diagnose, and Recover from Errors
      
      Error messages should use plain language, precisely indicate the problem, and constructively suggest a solution.
      
      **Check for**:
      - Plain language error messages (no error codes for users)
      - Specific problem identification ("Email is missing @" not "Invalid input")
      - Actionable recovery suggestions
      - Errors displayed near the source of the problem
      - Non-blocking error handling (don't wipe the form)
      
      **Scoring**:
      | Score | Criteria |
      |-------|----------|
      | 0 | Cryptic errors; codes, jargon, or no message at all |
      | 1 | Vague errors; "Something went wrong" with no guidance |
      | 2 | Clear but unhelpful; names the problem but not the fix |
      | 3 | Clear with suggestions; identifies problem and offers next steps |
      | 4 | Perfect recovery; pinpoints issue, suggests fix, preserves user work |
      
      ##### 10. Help and Documentation
      
      Even if the system is usable without docs, help should be easy to find, task-focused, and concise.
      
      **Check for**:
      - Searchable help or documentation
      - Contextual help (tooltips, inline hints, guided tours)
      - Task-focused organization (not feature-organized)
      - Concise, scannable content
      - Easy access without leaving current context
      
      **Scoring**:
      | Score | Criteria |
      |-------|----------|
      | 0 | No help available anywhere |
      | 1 | Help exists but hard to find or irrelevant |
      | 2 | Basic help; FAQ or docs exist, not contextual |
      | 3 | Good documentation; searchable, mostly task-focused |
      | 4 | Excellent contextual help; right info at the right moment |
      
      ---
      
      #### Score Summary
      
      **Total possible**: 40 points (10 heuristics × 4 max)
      
      | Score Range | Rating | What It Means |
      |-------------|--------|---------------|
      | 36–40 | Excellent | Minor polish only; ship it |
      | 28–35 | Good | Address weak areas, solid foundation |
      | 20–27 | Acceptable | Significant improvements needed before users are happy |
      | 12–19 | Poor | Major UX overhaul required; core experience broken |
      | 0–11 | Critical | Redesign needed; unusable in current state |
      
      ---
      
      #### Issue Severity (P0–P3)
      
      Tag each individual issue found during scoring with a priority level:
      
      | Priority | Name | Description | Action |
      |----------|------|-------------|--------|
      | **P0** | Blocking | Prevents task completion entirely | Fix immediately; this is a showstopper |
      | **P1** | Major | Causes significant difficulty or confusion | Fix before release |
      | **P2** | Minor | Annoyance, but workaround exists | Fix in next pass |
      | **P3** | Polish | Nice-to-fix, no real user impact | Fix if time permits |
      
      **Tip**: If you're unsure between two levels, ask: "Would a user contact support about this?" If yes, it's at least P1.
      
      ---
      
      ### Persona-Based Design Testing
      
      Test the interface through the eyes of 5 distinct user archetypes. Each persona exposes different failure modes that a single "design director" perspective would miss.
      
      **How to use**: Select 2–3 personas most relevant to the interface being critiqued. Walk through the primary user action as each persona. Report specific red flags, not generic concerns.
      
      ---
      
      #### 1. Impatient Power User: "Alex"
      
      **Profile**: Expert with similar products. Expects efficiency, hates hand-holding. Will find shortcuts or leave.
      
      **Behaviors**:
      - Skips all onboarding and instructions
      - Looks for keyboard shortcuts immediately
      - Tries to bulk-select, batch-edit, and automate
      - Gets frustrated by required steps that feel unnecessary
      - Abandons if anything feels slow or patronizing
      
      **Test Questions**:
      - Can Alex complete the core task in under 60 seconds?
      - Are there keyboard shortcuts for common actions?
      - Can onboarding be skipped entirely?
      - Do modals have keyboard dismiss (Esc)?
      - Is there a "power user" path (shortcuts, bulk actions)?
      
      **Red Flags** (report these specifically):
      - Forced tutorials or unskippable onboarding
      - No keyboard navigation for primary actions
      - Slow animations that can't be skipped
      - One-item-at-a-time workflows where batch would be natural
      - Redundant confirmation steps for low-risk actions
      
      ---
      
      #### 2. Confused First-Timer: "Jordan"
      
      **Profile**: Never used this type of product. Needs guidance at every step. Will abandon rather than figure it out.
      
      **Behaviors**:
      - Reads all instructions carefully
      - Hesitates before clicking anything unfamiliar
      - Looks for help or support constantly
      - Misunderstands jargon and abbreviations
      - Takes the most literal interpretation of any label
      
      **Test Questions**:
      - Is the first action obviously clear within 5 seconds?
      - Are all icons labeled with text?
      - Is there contextual help at decision points?
      - Does terminology assume prior knowledge?
      - Is there a clear "back" or "undo" at every step?
      
      **Red Flags** (report these specifically):
      - Icon-only navigation with no labels
      - Technical jargon without explanation
      - No visible help option or guidance
      - Ambiguous next steps after completing an action
      - No confirmation that an action succeeded
      
      ---
      
      #### 3. Accessibility-Dependent User: "Sam"
      
      **Profile**: Uses screen reader (VoiceOver/NVDA), keyboard-only navigation. May have low vision, motor impairment, or cognitive differences.
      
      **Behaviors**:
      - Tabs through the interface linearly
      - Relies on ARIA labels and heading structure
      - Cannot see hover states or visual-only indicators
      - Needs adequate color contrast (4.5:1 minimum)
      - May use browser zoom up to 200%
      
      **Test Questions**:
      - Can the entire primary flow be completed keyboard-only?
      - Are all interactive elements focusable with visible focus indicators?
      - Do images have meaningful alt text?
      - Is color contrast WCAG AA compliant (4.5:1 for text)?
      - Does the screen reader announce state changes (loading, success, errors)?
      
      **Red Flags** (report these specifically):
      - Click-only interactions with no keyboard alternative
      - Missing or invisible focus indicators
      - Meaning conveyed by color alone (red = error, green = success)
      - Unlabeled form fields or buttons
      - Time-limited actions without extension option
      - Custom components that break screen reader flow
      
      ---
      
      #### 4. Deliberate Stress Tester: "Riley"
      
      **Profile**: Methodical user who pushes interfaces beyond the happy path. Tests edge cases, tries unexpected inputs, and probes for gaps in the experience.
      
      **Behaviors**:
      - Tests edge cases intentionally (empty states, long strings, special characters)
      - Submits forms with unexpected data (emoji, RTL text, very long values)
      - Tries to break workflows by navigating backwards, refreshing mid-flow, or opening in multiple tabs
      - Looks for inconsistencies between what the UI promises and what actually happens
      - Documents problems methodically
      
      **Test Questions**:
      - What happens at the edges (0 items, 1000 items, very long text)?
      - Do error states recover gracefully or leave the UI in a broken state?
      - What happens on refresh mid-workflow? Is state preserved?
      - Are there features that appear to work but produce broken results?
      - How does the UI handle unexpected input (emoji, special chars, paste from Excel)?
      
      **Red Flags** (report these specifically):
      - Features that appear to work but silently fail or produce wrong results
      - Error handling that exposes technical details or leaves UI in a broken state
      - Empty states that show nothing useful ("No results" with no guidance)
      - Workflows that lose user data on refresh or navigation
      - Inconsistent behavior between similar interactions in different parts of the UI
      
      ---
      
      #### 5. Distracted Mobile User: "Casey"
      
      **Profile**: Using phone one-handed on the go. Frequently interrupted. Possibly on a slow connection.
      
      **Behaviors**:
      - Uses thumb only; prefers bottom-of-screen actions
      - Gets interrupted mid-flow and returns later
      - Switches between apps frequently
      - Has limited attention span and low patience
      - Types as little as possible, prefers taps and selections
      
      **Test Questions**:
      - Are primary actions in the thumb zone (bottom half of screen)?
      - Is state preserved if the user leaves and returns?
      - Does it work on slow connections (3G)?
      - Can forms use autocomplete and smart defaults?
      - Are touch targets at least 44×44pt?
      
      **Red Flags** (report these specifically):
      - Important actions positioned at the top of the screen (unreachable by thumb)
      - No state persistence; progress lost on tab switch or interruption
      - Large text inputs required where selection would work
      - Heavy assets loading on every page (no lazy loading)
      - Tiny tap targets or targets too close together
      
      ---
      
      #### Selecting Personas
      
      Choose personas based on the interface type:
      
      | Interface Type | Primary Personas | Why |
      |---------------|-----------------|-----|
      | Landing page / marketing | Jordan, Riley, Casey | First impressions, trust, mobile |
      | Dashboard / admin | Alex, Sam | Power users, accessibility |
      | E-commerce / checkout | Casey, Riley, Jordan | Mobile, edge cases, clarity |
      | Onboarding flow | Jordan, Casey | Confusion, interruption |
      | Data-heavy / analytics | Alex, Sam | Efficiency, keyboard nav |
      | Form-heavy / wizard | Jordan, Sam, Casey | Clarity, accessibility, mobile |
      
      ---
      
      #### Project-Specific Personas
      
      If `CLAUDE.md` contains a `## Design Context` section (generated by `ui-craft init`), derive 1–2 additional personas from the audience and brand information:
      
      1. Read the target audience description
      2. Identify the primary user archetype not covered by the 5 predefined personas
      3. Create a persona following this template:
      
      ```
      ##### [Role]: "[Name]"
      
      **Profile**: [2-3 key characteristics derived from Design Context]
      
      **Behaviors**: [3-4 specific behaviors based on the described audience]
      
      **Red Flags**: [3-4 things that would alienate this specific user type]
      ```
      
      Only generate project-specific personas when real Design Context data is available. Don't invent audience details; use the 5 predefined personas when no context exists.
      
    • design-rules.md 7.8 KB
      # Design rules
      
      The shared craft discipline for every mode. Merged from `interface-craft` and
      `impeccable`'s general rules.
      
      ## Operating contract
      
      - Preserve product truth, real content, existing behavior, and accessibility.
      - Use the project's established design system when it is sound. Extend it with
        explicit tokens; never introduce a competing visual language accidentally.
      - Make one strong, defensible visual direction; don't blend fashionable
        aesthetics until they become anonymous.
      - References are evidence, not collage: borrow a principle, never a brand's
        surface treatment wholesale.
      - Match density to the job — marketing can breathe; operational tools earn
        every pixel with faster comprehension or action.
      - Don't add a dependency just to make a UI feel designed.
      
      ## The design brief (before code)
      
      ```text
      Job: what the person must understand or do here.
      Audience and setting: who uses it, when, under what pressure or attention.
      Direction: a precise visual world in one sentence.
      Signature move: one visible typographic, structural, material, or interaction choice.
      Density: sparse / balanced / dense, and why.
      Constraints: existing tokens, required content, a11y, responsiveness, performance.
      Anti-references: motifs this surface must avoid.
      ```
      
      If the brief is weak, make a provisional choice and label it. Never hide a
      generic default behind "clean", "modern", or "premium".
      
      ## System before components
      
      Every repeated visual value traces to a limited token system.
      
      - **Type:** roles for display, body, labels, data; deliberate scale; optical
        line-height; tabular figures for aligned numerics; prose at 65–75ch;
        sentence case unless uppercase carries meaning. Don't pair similar-but-not-
        identical fonts — pair on a contrast axis or use one family in weights.
        Display ceiling: clamp() max ≤ 6rem; letter-spacing floor ≥ -0.04em.
        `text-wrap: balance` on h1–h3, `pretty` on long prose.
      - **Color:** pick a strategy first — restrained / committed / full palette /
        drenched. One dominant accent role unless the data model needs more. Use
        OKLCH where the stack permits. Verify contrast: body ≥4.5:1 (placeholders
        too), large text ≥3:1. Gray text on a colored background looks washed out —
        use a darker shade of the background's hue or a transparency of the text
        color. Dark vs light is never a default: write one sentence of physical
        scene (who, where, ambient light, mood) and let it force the answer.
        Beware the warm-neutral cream/sand/beige body default — "warmth" belongs in
        accent, typography, and imagery, not reflexively in the body background.
      - **Space and geometry:** spacing rhythm, container behavior, corner logic,
        elevation, and a semantic z-index scale (dropdown → sticky → modal-backdrop
        → modal → toast → tooltip; never 999). Vary rhythm intentionally.
      - **Material:** surfaces, borders, shadows, texture from the direction — a
        blur or gradient must communicate hierarchy, atmosphere, or interaction.
      - **Motion:** standard durations, easing, transform origins, reduced-motion
        fallback — decided before effects are added.
      
      ## Explore before committing
      
      For a whole page or major surface: three conceptually different directions
      (information hierarchy, layout metaphor, interaction model, density — not
      colors). Name each for its idea; assess information shape, spatial model,
      signature move, and risk. Ground prototypes in the shipping theme and real
      data; compare rendered screenshots; select one and record it — don't blend.
      
      For an existing surface, don't redesign blindly: capture what users rely on,
      identify visual debt, and fix the highest-leverage constraints first —
      hierarchy/typography, then color/contrast, then layout/rhythm, then
      interaction feedback and missing states, then clichés. Don't migrate
      framework or CSS strategy unless that is the task.
      
      ## Build
      
      - Semantic HTML and native controls where they fit; DOM and CSS simple enough
        to survive responsive and state changes.
      - Layout expresses relationships — not a bordered, shadowed card around every
        grouping. Grid for 2D, flex for 1D (`flex-wrap` before Grid;
        `repeat(auto-fit, minmax(280px, 1fr))` for breakpointless grids). Test
        optical alignment.
      - Real product language and credible content — no lorem ipsum, fabricated
        metrics, or generic AI copy.
      - Loading, empty, error, offline, success, disabled, overflow, and
        permission states are first-class screens with a next action and a way back.
      - Keyboard focus, target size, logical tab order, announcements for state
        changes; skip link where structure warrants.
      - Design the narrow layout; don't merely stack the desktop one. Test long
        labels, large text, both themes. No horizontal scroll; layers that must
        escape a scrolling container use popover/`<dialog>`/portal/fixed —
        `position: absolute` inside `overflow: hidden|auto` gets clipped.
      
      ## Motion
      
      Four questions before any animation: How often is it seen (high-frequency and
      keyboard-driven actions get little or none)? What is its job (continuity,
      feedback, state, explanation, rare delight — never "feels expensive")? What
      movement fits (enter fast-then-settle, exit accelerates away, on-screen uses
      a balanced curve; ease-out-quart/quint/expo, no bounce)? Smallest effective
      duration (press 100–160ms, tooltip 125–200ms, dropdown 150–250ms,
      modal/drawer 200–500ms)?
      
      Animate transforms and opacity over layout properties. Reduced motion is not
      optional — every animation needs a `prefers-reduced-motion` alternative that
      keeps content and state reachable. Reveal animations enhance an
      already-visible default; never gate visibility on a class-triggered
      transition (hidden tabs and headless renderers ship the section blank).
      Stagger within one list is legitimate; one identical entrance on every
      section is the tell.
      
      ## Absolute bans
      
      Match-and-refuse — about to write one, rewrite the element structurally:
      
      - **Side-stripe borders** (`border-left/right` >1px as a colored accent).
        Full borders, background tints, leading numbers/icons, or nothing.
      - **Gradient text** (`background-clip: text` + gradient). Solid color;
        emphasis via weight or size.
      - **Glassmorphism as default.** Rare and purposeful, or nothing.
      - **The hero-metric template** (big number, small label, gradient accent).
      - **Identical card grids** (icon + heading + text, repeated).
      - **Nested cards** and repeated faint borders.
      - **Tiny uppercase tracked eyebrow above every section**, and its deeper
        variant, **numbered section markers (01/02/03) as default scaffolding** —
        numbers earn their place only when the order carries real information.
      - **A left sidebar because the surface is called a dashboard**; default icon
        metaphors, pill badges, avatar circles, sun/moon toggles by reflex.
      - **Text that overflows its container** — test heading copy at every
        breakpoint; the viewport is part of the design.
      
      Don't replace a cliché with arbitrary novelty; the alternative must make the
      hierarchy or task better.
      
      ## The slop test
      
      If someone could look at the surface and say "AI made that" without doubt, it
      failed. Category-reflex check at two altitudes: (1) if the theme + palette
      are guessable from the category alone, rework the scene sentence and color
      strategy; (2) if the aesthetic family is guessable from
      category-plus-anti-references ("fintech that's not navy-and-gold →
      terminal-native dark"), that's the trap one tier deeper — rework until
      neither answer is obvious.
      
      ## Critique in layers
      
      Review the rendered interface, not only source. In order: comprehension →
      product fit → composition → craft → interaction → resilience. Findings as
      exact before/after with a reason. Prioritize by user impact — no cosmetic
      punch list while an empty state, unclear action, or inaccessible contrast
      remains.
      
      ## Report style
      
      Report only: chosen direction, material changes, evidence checked, known
      limitations. No congratulating the work, no generic design theory.
      
    • document.md 27.6 KB
      Generate a `DESIGN.md` file at the project root that captures the current visual design system, so AI agents generating new screens stay on-brand.
      
      DESIGN.md follows the [official DESIGN.md format spec](https://raw.githubusercontent.com/google-labs-code/design.md/main/docs/spec.md): YAML frontmatter carrying machine-readable design tokens, followed by a markdown body with exactly six sections in a fixed order. **Tokens are normative; prose provides context for how to apply them.** Sections may be omitted when not relevant, but **do not reorder them and do not rename them**. Section headers must match the spec character-for-character so the file stays parseable by other DESIGN.md-aware tools (Stitch itself, awesome-design-md, skill-rest, etc.).
      
      ## The frontmatter: token schema
      
      The YAML frontmatter is the machine-readable layer. It's what Stitch's linter validates and what the live panel renders tiles from. Keep it tight; every entry should correspond to a token the project actually uses.
      
      ```yaml
      ---
      name: <project title>
      description: <one-line tagline>
      colors:
        primary: "#b8422e"
        neutral-bg: "#faf7f2"
        # ...one entry per extracted color; key = descriptive slug
      typography:
        display:
          fontFamily: "Cormorant Garamond, Georgia, serif"
          fontSize: "clamp(2.5rem, 7vw, 4.5rem)"
          fontWeight: 300
          lineHeight: 1
          letterSpacing: "normal"
        body:
          # ...
      rounded:
        sm: "4px"
        md: "8px"
      spacing:
        sm: "8px"
        md: "16px"
      components:
        button-primary:
          backgroundColor: "{colors.primary}"
          textColor: "{colors.neutral-bg}"
          rounded: "{rounded.sm}"
          padding: "16px 48px"
        button-primary-hover:
          backgroundColor: "{colors.primary-deep}"
      ---
      ```
      
      Rules that matter:
      
      - **Token refs** use `{path.to.token}` (e.g. `{colors.primary}`, `{rounded.md}`). Components may reference primitives; primitives may not reference each other.
      - **Stitch validates colors as hex sRGB only** (`#RGB` / `#RGBA` / `#RRGGBB` / `#RRGGBBAA`); OKLCH/HSL/P3 trigger a linter warning, not a hard error. YAML accepts the string either way and our own parser is format-agnostic. Choose based on project posture: (a) if the project has an "OKLCH-only" doctrine or uses Display-P3 values that don't round-trip through sRGB, put OKLCH directly in the frontmatter and accept the Stitch linter warning; (b) if the project wants strict Stitch compliance or plans to use their Tailwind/DTCG export pipeline, put hex in the frontmatter and keep OKLCH in prose as the canonical reference. Never split the source of truth without explicit reason.
      - **Component sub-tokens** are limited to 8 props: `backgroundColor`, `textColor`, `typography`, `rounded`, `padding`, `size`, `height`, `width`. Shadows, motion, focus rings, backdrop-filter: none of those fit. Carry them in the sidecar (Step 4b).
      - **Scale keys are open-ended.** Use whatever names the project already uses (`oxblood-deep`, `surface-container-low`). Don't rename to Material defaults.
      - **Variants are naming convention, not schema.** `button-primary` / `button-primary-hover` / `button-primary-active` as sibling keys.
      
      ## The markdown body: six sections (exact order)
      
      1. `## Overview`
      2. `## Colors`
      3. `## Typography`
      4. `## Elevation`
      5. `## Components`
      6. `## Do's and Don'ts`
      
      Optional evocative subtitles are allowed in the form `## 2. Colors: The [Name] Palette` (Stitch's own outputs do this), but the literal word in each header (Overview, Colors, Typography, Elevation, Components, Do's and Don'ts) must be present. Do NOT add extra top-level sections (Layout Principles, Responsive Behavior, Motion, Agent Prompt Guide). Fold that content into the six spec sections where it naturally belongs.
      
      ## When to run
      
      - The user just ran `/ui-craft init` and needs the visual side documented.
      - The skill noticed no `DESIGN.md` exists and nudged the user to create one.
      - An existing `DESIGN.md` is stale (the design has drifted).
      - Before a large redesign, to capture the current state as a reference.
      
      If a `DESIGN.md` already exists, **do not silently overwrite it**. Show the user
      the existing file and ask whether to refresh, overwrite, or merge. Use the host's
      supported choice interface where permitted; otherwise disclose the alternatives
      and costs in prose with a stable question identifier, then wait.
      
      ## Two paths
      
      - **Scan mode** (default): the project has design tokens, components, or rendered output. Extract, then confirm descriptive language. Use when there's code to analyze.
      - **Seed mode**: the project is pre-implementation (fresh init, nothing built yet). Interview for five high-level answers, write a minimal DESIGN.md marked `<!-- SEED -->`. Re-run in scan mode once there's code.
      
      Decide by scanning first (Scan mode Step 1). If the scan finds no tokens, no component files, and no rendered site, offer seed mode; don't silently switch. `/ui-craft document --seed` forces seed mode regardless of code presence.
      
      ## Scan mode (approach C: auto-extract, then confirm descriptive language)
      
      ### Step 1: Find the design assets
      
      Search the codebase in priority order:
      
      1. **CSS custom properties**: grep for `--color-`, `--font-`, `--spacing-`, `--radius-`, `--shadow-`, `--ease-`, `--duration-` declarations in CSS files (usually `src/styles/`, `public/css/`, `app/globals.css`, etc.). Record name, value, and the file it's defined in.
      2. **Tailwind config**: if `tailwind.config.{js,ts,mjs}` exists, read the `theme.extend` block for colors, fontFamily, spacing, borderRadius, boxShadow.
      3. **CSS-in-JS theme files**: styled-components, emotion, vanilla-extract, stitches; look for `theme.ts`, `tokens.ts`, or equivalent.
      4. **Design token files**: `tokens.json`, `design-tokens.json`, Style Dictionary output, W3C token community group format.
      5. **Component library**: scan the main button, card, input, navigation, dialog components. Note their variant APIs and default styles.
      6. **Global stylesheet**: the root CSS file usually has the base typography and color assignments.
      7. **Visible rendered output**: if browser automation tools are available, load the live site and sample computed styles from key elements (body, h1, a, button, .card). This catches values that tokens miss.
      
      ### Step 2: Auto-extract what can be auto-extracted
      
      Build a structured draft from the discovered tokens. For each token class:
      
      - **Colors**: Group into Primary / Secondary / Tertiary / Neutral (the Material-derived roles Stitch uses). If the project only has one accent, express it as Primary + Neutral; omit Secondary and Tertiary rather than inventing them.
      - **Typography**: Map observed sizes and weights to the Material hierarchy (display / headline / title / body / label). Note font-family stacks and the scale ratio.
      - **Elevation**: Catalogue the shadow vocabulary. If the project is flat and uses tonal layering instead, that's a valid answer; state it explicitly.
      - **Components**: For each common component (button, card, input, chip, list item, tooltip, nav), extract shape (radius), color assignment, hover/focus treatment, internal padding.
      - **Spacing + layout**: Fold into Overview or relevant Components. The spec does NOT have a Layout section.
      
      ### Step 2b: Stage the frontmatter
      
      From the auto-extracted tokens, draft the YAML frontmatter now (you'll write it at the top of DESIGN.md in Step 4). This is the machine-readable layer: what the live panel and Stitch's linter consume.
      
      - **Colors**: one entry per extracted color. Key = descriptive slug (`oxblood-deep`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex; see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value.
      - **Typography**: one entry per role (`display`, `headline`, `title`, `body`, `label`). Typography is an object; include only the props that are real for the project (`fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`, `fontFeature`, `fontVariation`).
      - **Rounded / Spacing**: whatever scale steps the project actually uses, keyed by whatever scale name the project uses (`sm` / `md` / `lg`, or `surface-sm`, or numeric steps).
      - **Components**: one entry per variant (`button-primary`, `button-primary-hover`, `button-ghost`). Reference primitives via `{colors.X}`, `{rounded.Y}`. If a variant needs a property Stitch's 8-prop set doesn't cover (shadow, focus ring, backdrop-filter), carry the full snippet in the sidecar instead.
      
      Skip anything the project doesn't have. Empty scale keys or fabricated tokens pollute the spec.
      
      ### Step 3: Ask the user for qualitative language
      
      The following require creative input that cannot be auto-extracted. Group them
      into one supported interview round. Where the host forbids a choice interface,
      state the considered alternatives and costs in prose with stable identifiers,
      then wait rather than treating the recommendation as accepted:
      
      - **Creative North Star**: a single named metaphor for the whole system ("The Editorial Sanctuary", "The Golden State Curator", "The Lab Notebook"). Offer 2-3 options that honor PRODUCT.md's brand personality.
      - **Overview voice**: mood adjectives, aesthetic philosophy in 2-3 sentences, anti-references (what the system should not feel like).
      - **Color character** (for auto-extracted colors): descriptive names ("Deep Muted Teal-Navy", not "blue-800"). Suggest 2-3 options per key color based on hue/saturation.
      - **Elevation philosophy**: flat/layered/lifted. If shadows exist, is their role ambient or structural?
      - **Component philosophy**: the feel of buttons, cards, inputs in one phrase ("tactile and confident" vs. "refined and restrained").
      
      Quote a line from PRODUCT.md when possible so the user sees their own strategic language carry forward.
      
      ### Step 4: Write DESIGN.md
      
      The file opens with the YAML frontmatter staged in Step 2b (schema documented at the top of this reference), then the markdown body using the structure below. Headers must match character-for-character. Optional evocative subtitles (e.g. `## 2. Colors: The Coastal Palette`) are allowed.
      
      ```markdown
      ---
      name: [Project Title]
      description: [one-line tagline]
      colors:
        # ... staged frontmatter from Step 2b
      ---
      
      # Design System: [Project Title]
      
      ## 1. Overview
      
      **Creative North Star: "[Named metaphor in quotes]"**
      
      [2-3 paragraph holistic description: personality, density, aesthetic philosophy. Start from the North Star and work outward. State what this system explicitly rejects (pulled from PRODUCT.md's anti-references). End with a short **Key Characteristics:** bullet list.]
      
      ## 2. Colors
      
      [Describe the palette character in one sentence.]
      
      ### Primary
      - **[Descriptive Name]** (#HEX / oklch(...)): [Where and why this color is used. Be specific about context, not just role.]
      
      ### Secondary (optional; omit if the project has only one accent)
      - **[Descriptive Name]** (#HEX): [Role.]
      
      ### Tertiary (optional)
      - **[Descriptive Name]** (#HEX): [Role.]
      
      ### Neutral
      - **[Descriptive Name]** (#HEX): [Text / background / border / divider role.]
      - [...]
      
      ### Named Rules (optional, powerful)
      **The [Rule Name] Rule.** [Short, forceful prohibition or doctrine, e.g. "The One Voice Rule. The primary accent is used on ≤10% of any given screen. Its rarity is the point."]
      
      ## 3. Typography
      
      **Display Font:** [Family] (with [fallback])
      **Body Font:** [Family] (with [fallback])
      **Label/Mono Font:** [Family, if distinct]
      
      **Character:** [1-2 sentence personality description of the pairing.]
      
      ### Hierarchy
      - **Display** ([weight], [size/clamp], [line-height]): [Purpose; where it appears.]
      - **Headline** ([weight], [size], [line-height]): [Purpose.]
      - **Title** ([weight], [size], [line-height]): [Purpose.]
      - **Body** ([weight], [size], [line-height]): [Purpose. Include max line length like 65–75ch if relevant.]
      - **Label** ([weight], [size], [letter-spacing], [case if uppercase]): [Purpose.]
      
      ### Named Rules (optional)
      **The [Rule Name] Rule.** [Short doctrine about type use.]
      
      ## 4. Elevation
      
      [One paragraph: does this system use shadows, tonal layering, or a hybrid? If "no shadows", say so explicitly and describe how depth is conveyed instead.]
      
      ### Shadow Vocabulary (if applicable)
      - **[Role name]** (`box-shadow: [exact value]`): [When to use it.]
      - [...]
      
      ### Named Rules (optional)
      **The [Rule Name] Rule.** [e.g. "The Flat-By-Default Rule. Surfaces are flat at rest. Shadows appear only as a response to state (hover, elevation, focus)."]
      
      ## 5. Components
      
      For each component, lead with a short character line, then specify shape, color assignment, states, and any distinctive behavior.
      
      ### Buttons
      - **Shape:** [radius described, exact value in parens]
      - **Primary:** [color assignment + padding, in semantic + exact terms]
      - **Hover / Focus:** [transitions, treatments]
      - **Secondary / Ghost / Tertiary (if applicable):** [brief description]
      
      ### Chips (if used)
      - **Style:** [background, text color, border treatment]
      - **State:** [selected / unselected, filter / action variants]
      
      ### Cards / Containers
      - **Corner Style:** [radius]
      - **Background:** [colors used]
      - **Shadow Strategy:** [reference Elevation section]
      - **Border:** [if any]
      - **Internal Padding:** [scale]
      
      ### Inputs / Fields
      - **Style:** [stroke, background, radius]
      - **Focus:** [treatment, e.g. glow, border shift, etc.]
      - **Error / Disabled:** [if applicable]
      
      ### Navigation
      - **Style, typography, default/hover/active states, mobile treatment.**
      
      ### [Signature Component] (optional; if the project has a distinctive custom component worth documenting)
      [Description.]
      
      ## 6. Do's and Don'ts
      
      Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific: include exact colors, pixel values, and named anti-patterns the user mentioned in PRODUCT.md. **Every anti-reference in PRODUCT.md should show up here as a "Don't" with the same language**, so the visual spec carries the strategic line through. Quote PRODUCT.md directly where possible: if PRODUCT.md says *"avoid dark mode with purple gradients, neon accents, glassmorphism"*, the Don'ts here should repeat that by name.
      
      ### Do:
      - **Do** [specific prescription with exact values / named rule].
      - **Do** [...]
      
      ### Don't:
      - **Don't** [specific prohibition, e.g. "use border-left greater than 1px as a colored stripe"].
      - **Don't** [...]
      - **Don't** [...]
      ```
      
      ### Step 4b: Write .impeccable/design.json sidecar (extensions only)
      
      The frontmatter owns token primitives (colors, typography, rounded, spacing, components). The sidecar at `.impeccable/design.json` carries **what Stitch's schema can't hold**: tonal ramps per color, shadow/elevation tokens, motion tokens, breakpoints, full component HTML/CSS snippets (the panel renders these into a shadow DOM), and narrative (north star, rules, do's/don'ts). It extends the frontmatter, it doesn't duplicate it.
      
      Regenerate the sidecar whenever you regenerate root `DESIGN.md`. If the user only asks to refresh the sidecar (e.g., from the live panel's stale-hint), preserve `DESIGN.md` and write only `.impeccable/design.json`.
      
      #### Schema
      
      ```json
      {
        "schemaVersion": 2,
        "generatedAt": "ISO-8601 string",
        "title": "Design System: [Project Title]",
        "extensions": {
          "colorMeta": {
            "primary":        { "role": "primary",  "displayName": "Editorial Magenta", "canonical": "oklch(60% 0.25 350)", "tonalRamp": ["...", "...", "..."] },
            "cool-paper": { "role": "neutral",  "displayName": "Cool Paper",    "canonical": "oklch(96% 0.005 230)", "tonalRamp": ["...", "...", "..."] }
          },
          "typographyMeta": {
            "display": { "displayName": "Display", "purpose": "Hero headlines only." }
          },
          "shadows": [
            { "name": "ambient-low", "value": "0 4px 24px rgba(0,0,0,0.12)", "purpose": "Diffuse hover glow under accent elements." }
          ],
          "motion": [
            { "name": "ease-standard", "value": "cubic-bezier(0.4, 0, 0.2, 1)", "purpose": "Default easing for state transitions." }
          ],
          "breakpoints": [
            { "name": "sm", "value": "640px" }
          ]
        },
        "components": [
          {
            "name": "Primary Button",
            "kind": "button | input | nav | chip | card | custom",
            "refersTo": "button-primary",
            "description": "One-line what and when.",
            "html": "<button class=\"ds-btn-primary\">GET STARTED</button>",
            "css": ".ds-btn-primary { background: #191c1d; color: #fff; padding: 16px 48px; letter-spacing: 0.05em; text-transform: uppercase; font-weight: 500; border: none; border-radius: 0; transition: background 0.2s, transform 0.2s; } .ds-btn-primary:hover { background: oklch(60% 0.25 350); transform: translateY(-2px); }"
          }
        ],
        "narrative": {
          "northStar": "The Editorial Sanctuary",
          "overview": "2-3 paragraphs of the philosophy, pulled from DESIGN.md Overview section.",
          "keyCharacteristics": ["...", "..."],
          "rules": [{ "name": "The One Voice Rule", "body": "...", "section": "colors|typography|elevation" }],
          "dos":   ["Do use ..."],
          "donts": ["Don't use ..."]
        }
      }
      ```
      
      **What changed from schemaVersion 1.** The old sidecar carried token primitive arrays (`tokens.colors[]`, `tokens.typography[]`, etc.). Those values now live in the frontmatter. The sidecar only carries metadata that can't live in the frontmatter (tonal ramps, canonical OKLCH when the hex is an approximation, display names, role hints), keyed by the frontmatter token name (`colorMeta.<token-name>`, `typographyMeta.<token-name>`). Components still carry full HTML/CSS because Stitch's 8-prop set can't hold them.
      
      #### Component translation rules
      
      The `html` and `css` fields must be **self-contained, drop-in snippets** that render correctly when injected into a shadow DOM. The panel applies them directly: no post-processing, no framework runtime.
      
      1. **Tailwind expansion.** If the source uses Tailwind (className="bg-primary text-white rounded-lg px-6 py-3"), expand every utility to literal CSS properties in the `css` string. Do **not** reference Tailwind classes; do **not** assume a Tailwind CSS bundle is loaded. Each component is self-contained.
      2. **Token resolution.** If the project exposes tokens as CSS custom properties on `:root` (e.g. `--color-primary`, `--radius-md`), reference them via `var(--color-primary)`; they inherit through the shadow DOM and stay live-bound. If tokens live only in JS theme objects (styled-components, CSS-in-JS), resolve to literal values at generation time.
      3. **Icons.** Inline as SVG. Do not reference Lucide/Heroicons packages, icon fonts, or `<img src="...">`. A typical icon is 16-24px; copy the SVG path data directly.
      4. **States.** Include `:hover`, `:focus-visible`, and (if meaningful) `:active` rules inline. A static default-only snapshot makes the panel feel dead. Hover + focus rules in the CSS make it feel alive.
      5. **Reset bloat.** Extract only the component's *distinctive* CSS (background, color, padding, border-radius, typography, transition). Skip universal resets (`box-sizing: border-box`, `line-height: inherit`, `-webkit-font-smoothing`). The panel already has a neutral canvas; don't re-ship resets.
      6. **Scoped class names.** Prefix every class with `ds-` (e.g. `ds-btn-primary`, `ds-input-search`) so component CSS doesn't collide with other components' CSS in the same shadow DOM.
      
      #### What to include
      
      Aim for a tight set of **5-10 components** that best represent the visual system:
      
      - **Canonical primitives (always include if the project has them):** button (each variant as a separate component entry), input/text field, navigation, chip/tag, card.
      - **Signature components (include if distinctive):** hero CTA, featured card, filter pill, any custom pattern the user mentioned as important in PRODUCT.md.
      - **Skip the rest.** Utility components, form building blocks, wrapper layouts: not worth documenting unless visually distinctive.
      
      If the project has **no component library yet** (bare landing page, new project), synthesize canonical primitives from the tokens using best-practice defaults consistent with the DESIGN.md's rules. Every `.impeccable/design.json` has *something* to render, even on day zero.
      
      #### Tonal ramps
      
      For each color token, generate an 8-step `tonalRamp` array: dark to light, same hue and chroma, stepped lightness from ~15% to ~95%. The panel renders this as a strip under the swatch. If the project already defines a tonal scale (Material `surface-container-low` family, Tailwind-style `blue-50..blue-900`), use those values. Otherwise synthesize in OKLCH.
      
      #### Narrative mapping
      
      Pull directly from the DESIGN.md you just wrote:
      
      - `narrative.northStar` → the `**Creative North Star: "..."**` line from Overview
      - `narrative.overview` → the philosophy paragraphs from Overview
      - `narrative.keyCharacteristics` → the bulleted `**Key Characteristics:**` list
      - `narrative.rules` → every `**The [Name] Rule.** [body]` across all sections, tagged with `section`
      - `narrative.dos` / `narrative.donts` → the bullet lists from Do's and Don'ts verbatim
      
      Do not reword. The panel shows these as secondary collapsible context; the same voice that's in the Markdown carries through.
      
      ### Step 5: Confirm and refine
      
      1. Show the user the full DESIGN.md you wrote. Briefly highlight the non-obvious creative choices (descriptive color names, atmosphere language, named rules).
      2. Mention that `.impeccable/design.json` was also written alongside; the live panel will now render this project's actual button/input/nav primitives instead of generic approximations.
      3. Offer to refine any section: "Want me to revise a section, add component patterns I missed, or adjust the atmosphere language?"
      
      Your own write is the freshest source; subsequent commands in this session don't need a reload.
      
      ## Seed mode
      
      For projects with no visual system to extract yet. Produces a minimal scaffold, not a full spec.
      
      ### Step 1: Confirm seed mode
      
      Before interviewing: "There's no existing visual system to scan. I'll ask five quick questions to seed a starter DESIGN.md. You can re-run `/ui-craft document` once there's code, to capture the real tokens and components. OK?"
      
      If the user prefers to skip, stop. No file.
      
      ### Step 2: Five questions
      
      Group into one supported interview round. Options must be concrete; when the
      host forbids a choice interface, state the alternatives and costs in prose with
      stable identifiers and wait for the answer.
      
      1. **Color strategy.** Pick one:
         - Restrained: tinted neutrals + one accent ≤10%
         - Committed: one saturated color carries 30–60% of the surface
         - Full palette: 3–4 named color roles, each deliberate
         - Drenched: the surface IS the color
         
         Then: one hue family or anchor reference ("deep teal", "mustard", "Klim #ff4500 orange").
      
      2. **Typography direction.** Pick one (specific fonts come later):
         - Serif display + sans body
         - Single sans (warm / technical / geometric / humanist; pick a feel)
         - Display + mono
         - Mono-forward
         - Editorial script + sans
      
      3. **Motion energy.** Pick one:
         - Restrained: state changes only
         - Responsive: feedback + transitions, no choreography
         - Choreographed: orchestrated entrances, scroll-driven sequences
      
      4. **Three named references.** Brands, products, printed objects. Not adjectives.
      
      5. **One anti-reference.** What it should NOT feel like. Also named.
      
      ### Step 3: Write seed DESIGN.md
      
      Use the six-section spec from Scan mode. Populate what the interview answers; leave the rest as honest placeholders. The seed is a scaffold, not a fabricated spec.
      
      Lead the file with:
      
      ```markdown
      <!-- SEED: re-run /ui-craft document once there's code to capture the actual tokens and components. -->
      ```
      
      Per-section guidance in seed mode:
      
      - **Overview**: Creative North Star and philosophy phrased from the answers (color strategy + motion energy + references). Reference the user's anti-reference directly.
      - **Colors**: Color strategy as a Named Rule (e.g. *"The Drenched Rule. The surface IS the color."*). Hue family or anchor reference. No hex values; mark as `[to be resolved during implementation]`.
      - **Typography**: the direction the user picked (e.g. "Serif display + sans body"). No font names yet: `[font pairing to be chosen at implementation]`.
      - **Elevation**: inferred from motion energy. Restrained/Responsive → flat by default; Choreographed → layered. One sentence.
      - **Components**: omit entirely; no components exist yet.
      - **Do's and Don'ts**: carry PRODUCT.md's anti-references directly plus the anti-reference named in Q5.
      
      Seed mode writes a minimal frontmatter with `name` and `description` only; no colors, typography, rounded, spacing, or components yet. Real tokens land on the next Scan-mode run. Skip the `.impeccable/design.json` sidecar in seed mode for the same reason: nothing to render.
      
      ### Step 4: Confirm
      
      1. Show the seed DESIGN.md. Call out that it is a seed (the marker is the literal commitment).
      2. Tell the user: "Re-run `/ui-craft document` once you have some code. That pass will extract real tokens and generate the sidecar."
      
      Your own write is the freshest source; no reload needed.
      
      ## Style guidelines
      
      - **Frontmatter first, prose second.** Tokens go in the YAML frontmatter; prose contextualizes them. Don't redefine a token value in two places; the frontmatter is normative.
      - **Cite PRODUCT.md anti-references by name** in the Do's and Don'ts section. If PRODUCT.md lists "SaaS landing-page clichés" or "generic AI tool marketing" as anti-references, the DESIGN.md Don'ts should repeat those phrases verbatim so the visual spec enforces the strategic line.
      - **Match the spec, don't invent new sections.** The six section names are fixed. If you have Layout/Motion/Responsive content to document, fold it into Overview (philosophy-level rules) or Components (per-component behavior).
      - **Descriptive > technical**: "Gently curved edges (8px radius)" > "rounded-lg". Include the technical value in parens, lead with the description.
      - **Functional > decorative**: for each token, explain WHERE and WHY it's used, not just WHAT it is.
      - **Exact values in parens**: hex codes, px/rem values, font weights; always the number in parens alongside the description.
      - **Use Named Rules**: `**The [Name] Rule.** [short doctrine]`. These are memorable, citable, and much stickier for AI consumers than bullet lists. Stitch's own outputs use them heavily ("The No-Line Rule", "The Ghost Border Fallback"). Aim for 1-3 per section.
      - **Be forceful**. The voice of a design director. "Prohibited", "forbidden", "never", "always", not "consider", "might", "prefer". Match PRODUCT.md's tone.
      - **Concrete anti-pattern tests**. Stitch writes things like *"If it looks like a 2014 app, the shadow is too dark and the blur is too small."* A one-sentence audit test beats a paragraph of principle.
      - **Reference PRODUCT.md**. The anti-references section of PRODUCT.md should directly inform the Do's and Don'ts section here. Quote or paraphrase.
      - **Group colors by role**, not by hex-order or hue-order. Primary / Secondary / Tertiary / Neutral is the spec ordering.
      
      ## Pitfalls
      
      - Don't paste raw CSS class names. Translate to descriptive language.
      - Don't extract every token. Stop at what's actually reused; one-offs pollute the system.
      - Don't invent components that don't exist. If the project only has buttons and cards, only document those.
      - Don't overwrite an existing DESIGN.md without asking.
      - Don't duplicate content from PRODUCT.md. DESIGN.md is strictly visual.
      - Don't add a "Layout Principles" or "Motion" or "Responsive Behavior" top-level section. The spec has six, not nine. Fold that content where it belongs.
      - Don't rename sections even slightly. "Colors" not "Color Palette & Roles". "Typography" not "Typography Rules". Tooling parsing depends on exact headers.
      - Don't duplicate token values between frontmatter and prose. If a color is in `colors.primary` as hex, the prose can name it and describe its role but should not reassert a different hex. The frontmatter is normative.
      - Don't invent frontmatter token groups outside Stitch's schema (no `motion:`, `breakpoints:`, `shadows:` at the top level). Stitch's Zod schema only accepts `colors`, `typography`, `rounded`, `spacing`, `components`. Anything else belongs in the sidecar's `extensions`.
      
    • init.md 12.7 KB
      # Init Flow
      
      The setup command for a project. One codebase crawl feeds everything it writes:
      
      - **PRODUCT.md** (strategic): root project file for register, target users, product purpose, brand personality, anti-references, strategic design principles. Answers "who/what/why".
      - **DESIGN.md** (visual): root project file for visual theme, color palette, typography, components, layout. Follows the [DESIGN.md format spec](https://raw.githubusercontent.com/google-labs-code/design.md/main/docs/spec.md). Answers "how it looks".
      - **`.claude/qa/personas/`** (reviewer personas): 3 distinct user-situation personas with lens, job, and concrete rules, plus a RANKING.md for tie-breaks. Fed to `/ui-craft critique` and required by `/ui-craft consensus`.
      - **`.impeccable/live/config.json`** (live mode): pre-configured so `/ui-craft live` boots straight into variant mode with no first-time detour.
      
      It closes by pointing the user at the best command to run next. Every other impeccable command reads PRODUCT.md and DESIGN.md before doing any work.
      
      ## Step 1: Load current state
      
      Check what already exists. PRODUCT.md and DESIGN.md live at the project root, or under `.agents/context/` or `docs/` (case-insensitive). Read whichever are present with your native file tool. Also note whether `.impeccable/live/config.json` already exists (Step 7 leaves it untouched if so).
      
      Decision tree:
      - **Neither file exists (empty project or no context yet)**: do Steps 2-4 (write PRODUCT.md), then decide on DESIGN.md based on whether there's code to analyze.
      - **PRODUCT.md exists, DESIGN.md missing**: skip to Step 6 and offer to run `/ui-craft document` for DESIGN.md.
      - **PRODUCT.md exists but has no `## Register` section (legacy)**: add it. Infer a hypothesis from the codebase (see Step 2), confirm with the user, write the field.
      - **Both exist**: ask which file to refresh and skip the one the user does not want changed. Use the host's supported choice interface when permitted; otherwise present the substantive alternatives and costs in prose with a stable question identifier, then wait for the answer.
      - **Just DESIGN.md exists (unusual)**: do Steps 2-4 to produce PRODUCT.md.
      
      Never silently overwrite an existing file. Always confirm first.
      
      If init was invoked as a setup blocker by another command, such as `/ui-craft craft landing page`, pause that command here. Complete init, then resume the original command. Your own writes are the freshest source; no reload needed. For craft, resume into shape next; init creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief.
      
      ## Step 2: Explore the codebase
      
      Before asking questions, thoroughly scan the project to discover what you can. This single crawl feeds PRODUCT.md, DESIGN.md, **and** the live-mode framework detection in Step 7, so be thorough once rather than re-scanning later:
      
      - **README and docs**: Project purpose, target audience, any stated goals
      - **Package.json / config files**: Tech stack, dependencies, existing design libraries, **and the framework** (Vite/SPA, Next.js, Nuxt, SvelteKit, Astro, multi-page static) plus the HTML entry the browser actually loads
      - **Existing components**: Current design patterns, spacing, typography in use
      - **Brand assets**: Logos, favicons, color values already defined
      - **Design tokens / CSS variables**: Existing color palettes, font stacks, spacing scales
      - **Any style guides or brand documentation**
      
      Also form a **register hypothesis** from what you find:
      
      - Brand signals: `/`, `/about`, `/pricing`, `/blog/*`, `/docs/*`, hero sections, big typography, scroll-driven sections, landing-page-shaped content.
      - Product signals: `/app/*`, `/dashboard`, `/settings`, `/(auth)`, forms, data tables, side/top nav, app-shell components.
      
      Register is a hypothesis at this point, not a decision; Step 3 confirms it.
      
      Note what you've learned and what remains unclear. Also note any rough edges worth a follow-up command (thin hierarchy, flat or gray palette, missing error/empty states, dull copy); Step 8 turns these into concrete recommendations without re-analyzing.
      
      ## Step 3: Ask strategic questions (for PRODUCT.md)
      
      Ask only about what you could not infer from the codebase. Use the host's supported
      choice interface when permitted; otherwise present substantive alternatives and
      costs in prose with a stable question identifier, then wait for the answer.
      
      ### Interview mode, not confirmation mode
      
      If the repo is empty or the user's brief is sparse, run a short interview before proposing PRODUCT.md. Do **not** turn a one-sentence request into a complete inferred PRODUCT.md and ask for blanket confirmation.
      
      - Use the harness's structured question tool when one exists. Otherwise, ask directly in chat and stop.
      - Ask **2-3 questions per round**, then wait for answers.
      - Use inferred answers as hypotheses or options, not as finished facts.
      - Complete at least one real user-answer round before drafting PRODUCT.md, unless every required answer is directly discoverable from repo docs.
      - Round 1 should establish register, users/purpose, and desired outcome.
      - Round 2 should establish brand personality or references, anti-references, and accessibility needs.
      
      ### Minimum viable interview
      
      Ask enough to complete PRODUCT.md. At minimum, cover register confirmation, users and purpose, brand personality, anti-references, and accessibility needs unless each answer is directly discoverable from repo context. After at least one interview round, you may propose inferred answers, but the user must confirm them before you write PRODUCT.md. Never synthesize PRODUCT.md from the original task prompt alone.
      
      ### Register (ask first; it shapes everything below)
      
      Every design task is either **brand** (marketing, landing, campaign, long-form content, portfolio: design IS the product) or **product** (app UI, admin, dashboards, tools: design SERVES the product).
      
      If Step 2 produced a clear hypothesis, lead with it: *"From the codebase, this looks like a [brand / product] surface. Does that match your intent, or should we treat it differently?"*
      
      If the signal is genuinely split (e.g. a product with a big marketing landing), ask which register describes the **primary** surface. The register can be overridden per task later, but PRODUCT.md carries one default.
      
      ### Users & Purpose
      - Who uses this? What's their context when using it?
      - What job are they trying to get done?
      - For brand: what emotions should the interface evoke? (confidence, delight, calm, urgency)
      - For product: what workflow are they in? What's the primary task on any given screen?
      
      ### Brand & Personality
      - How would you describe the brand personality in 3 words?
      - Reference sites or apps that capture the right feel? What specifically about them?
        - Push for specific named references with the *specific* thing about them that fits this brand, not generic "modern" adjectives or category-bucket lanes.
      - What should this explicitly NOT look like? Any anti-references?
      
      ### Accessibility & Inclusion
      - Specific accessibility requirements? (WCAG level, known user needs)
      - Considerations for reduced motion, color blindness, or other accommodations?
      
      Skip questions where the answer is already clear. **Do NOT ask about colors, fonts, radii, or visual styling here.** Those belong in DESIGN.md, not PRODUCT.md.
      
      ## Step 4: Write PRODUCT.md
      
      Write PRODUCT.md only after the user has confirmed the strategic answers from Step 3. If an inferred answer is uncertain or unconfirmed, ask before writing.
      
      Synthesize into a strategic document:
      
      ```markdown
      # Product
      
      ## Register
      
      product
      
      ## Users
      [Who they are, their context, the job to be done]
      
      ## Product Purpose
      [What this product does, why it exists, what success looks like]
      
      ## Brand Personality
      [Voice, tone, 3-word personality, emotional goals]
      
      ## Anti-references
      [What this should NOT look like. Specific bad-example sites or patterns to avoid.]
      
      ## Design Principles
      [3-5 strategic principles derived from the conversation. Principles like "practice what you preach", "show, don't tell", "expert confidence". NOT visual rules like "use OKLCH" or "magenta accent".]
      
      ## Accessibility & Inclusion
      [WCAG level, known user needs, considerations]
      ```
      
      Register is either `brand` or `product` as a bare value. No prose, no commentary.
      
      Write to `PROJECT_ROOT/PRODUCT.md`. If `.impeccable.md` existed, the loader already renamed it; merge into that content rather than starting from scratch.
      
      ## Step 5: Derive the persona panel
      
      Personas feed directly into `/ui-craft critique` (walkthrough scoring) and are **required** by `/ui-craft consensus` (group review). Build them now.
      
      From the freshly written PRODUCT.md, extract exactly 3 reviewer personas — distinct real user **situations**, not demographic sketches. A situation is the concrete context when someone uses this product: *e.g. printed recipe cards → mid-cook glancer, first-time cook, shopper*. If the product has only two genuine situations, split the heavier one into two lenses (first-time vs repeat, or different stages of the job).
      
      **Draft** each persona as `.claude/qa/personas/<name>.md` with:
      - **Lens**: the single question they judge everything by (*e.g. "does this stay readable mid-hands-full?"*)
      - **Job**: what they concretely do when reviewing a rendered surface (*e.g. "glance mid-cooking, spot what changed, move on"*)
      - **Rules/red flags**: what they must flag, in specifics—not vague ("is clear") but concrete (*e.g. "text smaller than 14px", "requires two-step navigation", "lacks progress indicator"*)
      
      Model the format on a stress-tester: lens + job + concrete rules, no fluff.
      
      Also draft `.claude/qa/personas/RANKING.md` with: the tie-break order derived from the product's PRIMARY use per PRODUCT.md (e.g. *"cards are used while cooking → glancer outranks shopper on ties"*), one sentence of justification. If primacy is genuinely unclear, ask the user one question and record the answer.
      
      **Confirm the drafted personas with the user** before writing files (same etiquette as Step 4). If `.claude/qa/personas/` already exists, offer a refresh per file rather than silently overwriting.
      
      ## Step 6: Decide on DESIGN.md
      
      Offer `/ui-craft document` either way. Two paths:
      
      - **Code exists** (CSS tokens, components, a running site): "I can generate a DESIGN.md that captures your visual system (colors, typography, components) so variants stay on-brand. Want to do that now?"
      - **Pre-implementation** (empty project): "I can seed a starter DESIGN.md from five quick questions about color strategy, type direction, motion energy, and references. You can re-run once there's code, to capture the real tokens. Want to do that now?"
      
      If the user agrees, delegate to `/ui-craft document` (it auto-detects scan vs seed). Load its reference and follow that flow.
      
      If the user prefers to skip, mention they can run `/ui-craft document` any time later.
      
      ## Step 7: Configure live mode (when code exists)
      
      Live mode is not part of this fork (it stayed with the upstream impeccable skill); skip live-mode pre-configuration entirely.
      
      ## Step 8: Recommend starting points, then wrap up
      
      Summarize tersely:
      - Register captured (brand / product)
      - What was written (PRODUCT.md, DESIGN.md, live config, or a subset)
      - The 3-5 strategic principles from PRODUCT.md that will guide future work
      - If DESIGN.md or live config is pending, one line on how to set it up later
      
      Then recommend the **best commands to run next**, drawn from what your Step 2 crawl already surfaced. Do not run a fresh analysis here; surface observations you already have. Tailor to register and to what you saw, offer the 2-4 most relevant (not a menu dump), and give the exact command to type. Group by intent:
      
      - **Build something new**: `/ui-craft craft <feature>` (shape, then build end-to-end) or `/ui-craft shape <feature>` (plan first). Lead with this for empty or early-stage projects.
      - **Improve what's there**: name the specific surface. `/ui-craft critique <page>` for a scored UX review; `/ui-craft audit <area>` for a11y / perf / responsive checks; `/ui-craft polish <component>` for a pre-ship pass. When the crawl flagged a specific weakness, point the matching command at it: thin hierarchy or spacing → `layout`, flat or gray palette → `colorize`, missing error / empty states → `harden` or `onboard`, dull or unclear copy → `clarify`.
      - **Iterate visually**: `/ui-craft live` (configured in Step 6) to pick elements in the browser and generate variants in place.
      
      The full command menu is one bare `/impeccable` away; keep this list short and pointed.
      
      If init was invoked as a blocker by another impeccable command (e.g. the user ran `/ui-craft polish` with no PRODUCT.md), resume that original task now. Your own writes are the freshest source; no reload needed.
      
      Optionally ask whether they want a brief PRODUCT.md summary appended to CLAUDE.md. Do not write it unless they explicitly agree; then append a short **Design Context** pointer section.
      
    • lock.md 14.7 KB
      # Mode: lock
      
      Explore a greenfield surface as several genuinely different,
      repository-grounded HTML mockups, converge with the reviewer, and end with a
      **lock** that a build agent cannot silently drift from: a ★ LOCKED mockup plus a
      lock manifest. A surface the app already ships uses `revise`, except for the
      explicit safe-start fallback below.
      
      ## Review mode
      
      - **Interactive:** keep the server running, give the user live URLs, wait for
        their choice before locking.
      - **Headless (AgentFlow):** never wait for browser review. Render required
        states, inspect screenshots and console output, choose the direction that
        best satisfies the brief, and lock it. Save evidence to the orchestrator's
        artifact directory.
      
      ## Workflow
      
      ### 0. Pre-flight — chrome ground truth (MANDATORY, before any concept work)
      
      A from-scratch mock of a shipped surface is banned. Before any concept work,
      check `mockups/INDEX.md`, the app routes and the shipping render modules. If the
      app already embodies this surface or the shell containing it, **refuse `lock`
      and name `revise`**. This is a blocking verdict, the same class as a `missed`
      predecessor behavior.
      
      There is one exception: `revise` pre-flight may have recorded that the repo has
      no dev-server declaration and routed the change here as the accepted fallback.
      That record must name the absent declaration and state that mockup plus
      predecessor inventory is weaker than app-branch iteration. A declared entrypoint
      that omits its data source is unsupported, not fallback. Without the fallback
      record, `lock` never proceeds on a shipped embodiment.
      
      The fallback does not authorize starting the app. Any live predecessor or chrome
      evidence must come through a separately declared safe harness; otherwise record
      the missing evidence and let the predecessor QUESTION gate block rather than
      guessing.
      
      On a greenfield surface, or on that recorded fallback, a lock round dies at
      review when its chrome doesn't match the app the reviewer uses every day. Run
      these checks before the ledger, and do not fan out a single variant until the
      fidelity gate passes:
      
      1. **Resolve the material ground truth for chrome.** If any locked surface has
         shipped — check `mockups/INDEX.md` for `shipped` rows and the log for port
         commits — the RUNNING APP's own stylesheet is the material truth for
         tokens, chrome, and component styling. The mock scaffold that *authored*
         the design is a stale ancestor the moment the port lands: same design,
         superseded values. Never source chrome from mockup lineage when a shipped
         embodiment exists. (Lesson: ciq-autotune #660 burned three rounds on a
         shell forked from `_theme.css` after #655 had re-materialized the same
         lock onto the app's `--wk-*` layer — wrong ground, wrong radii, wrong
         fonts, wrong button chrome, every round.)
      2. **Extract, never transcribe.** Materialize the app's token layer into
         `mockups/_theme-app.css` by script/verbatim copy from the app's stylesheet
         (both themes, plus the app's exact `body` ground and type rules), with a
         header naming the source file and how to refresh it. Post-ship mocks link
         this sheet; the legacy scaffold `_theme.css` serves only surfaces that
         predate the port. When two token layers coexist, SCAFFOLD.md must name
         which is ground truth — silence here is the defect that lets the next
         round anchor wrong.
      3. **Round-zero fidelity gate.** Render the empty shell/chrome (no concept
         content) and put it beside a screenshot of the safely running app, or an
         operator-provided base capture on fallback. Chrome —
         ground color, fonts, control sizes, radii, bar heights — must be
         indistinguishable BEFORE variants are briefed; save the pair as evidence.
         A reviewer should never have to argue the background color mid-round.
      4. **Chart medium check.** If the surface charts, the mock renders with the
         app's shipping chart library at its shipping version from step one — a
         hand-rolled SVG facsimile invalidates every judgment made on it.
      
      ### 1. Ledger
      
      Read `mockups/INDEX.md` (create if absent; one row per surface, columns
      Surface / Concept / Status / Issue / File). `locked` entries are binding
      precedent for adjacent surfaces; for `shipped` entries the app is ground
      truth, not old mockup markup. On the recorded fallback path, a `shipped` row
      covering this surface or the one it replaces also **triggers the predecessor
      pass** (§9.3) — note it here, where it is cheap, rather than discovering it at
      lock time. Outside that fallback, the row triggers the refusal above.
      
      ### 2. Grounding kit
      
      Collect: the screen and its primary user question; visual tokens and their
      source files; UI/chart library and version; the shipping render module; real
      data shape from a safe fixture; required states (empty, typical, dense,
      error, mobile, light, dark). Save runtime captures as
      `mockups/<surface>.capture.json`, gitignored unless deliberately manufactured
      and free of sensitive data.
      
      ### 3. Brief
      
      Write the design brief from `design-rules.md` (job, audience and setting,
      direction, signature move, density, constraints, anti-references) plus: one
      surface, one decision, hard constraints, states to render, and three or four
      named concept directions that differ in layout metaphor, information
      hierarchy, or interaction model — not decoration. Use `scope`'s interview mode to
      sharpen the brief when installed.
      
      ### 4. Prep the shared scaffold
      
      The scaffold is **repo-global, prepped once and extended per surface**: the
      first lock round in a repo creates it (extracted from an approved existing
      mockup if one exists, else built fresh, by the orchestrator or one cheap prep
      agent); every later surface's prep step *extends* the existing files — new
      mockbar states, new capture names — and never rewrites or forks them. The
      fixed filenames are the point: one theme, one shell, shared by every surface's
      variants.
      
      - `mockups/_theme.css` — theme tokens (light+dark), base page/card styles,
        the mock-shell layout (a plain width-constrained column).
      - `mockups/_shell.js` — ES module: `loadCapture(name)` fetch glue for the
        capture fixtures, `renderMockBar()` (state + theme toggle groups,
        `?theme=dark` deep-link), `resolveColors()` for theme-aware chart options.
      - `mockups/SCAFFOLD.md` — ~30 lines telling variant agents what the scaffold
        provides, how to link it, and what remains theirs (concept-specific markup
        and chart render logic).
      
      `resolveColors()` and the chart-library CDN line exist only when the surface
      family actually charts — a chartless repo's scaffold omits them. The CDN
      `<script>` tag cannot live in the ES module (load order); it stays a
      documented copy-paste line in SCAFFOLD.md.
      
      **Mock chrome is recessive:** width constraint, state/theme toggles, one-line
      concept note — no device bezels, frames, or decoration. Chrome that exists to
      serve the mockup gets out of the way.
      
      ### 5. Fan out
      
      One fresh subagent per concept, using
      [variant-agent-prompt.md](variant-agent-prompt.md) filled with the grounding
      kit, shared brief, and SCAFFOLD.md. Each writes `mockups/<surface>-<concept>.html` (+
      `-chart.js` when render logic is non-trivial). Sequential with isolated
      briefs if parallel agents are unavailable.
      
      ### 6. Render and inspect
      
      Serve `mockups/` over HTTP; render every required state with
      `drive-local-webapp`; inspect the actual images and console errors.
      Screenshots go outside the repo.
      
      ### 7. Review tersely
      
      One line of design bet + one line of judgment per variant; recommend one.
      Incorporate feedback by re-rendering, not arguing from source.
      
      ### 8. Persona round and craft gate
      
      Walk the primary task as 2–3 relevant personas (repo personas first — see
      SKILL.md); name the first concrete element that stalls each walkthrough and
      fix it. Then run the `audit` technical checks (contrast, keyboard focus,
      overflow, target sizes) on the finalist.
      
      **On the recorded fallback path, run the predecessor inventory on the finalist
      here** ([behavior-sweep.md](behavior-sweep.md) §2) and take its
      QUESTION round to the operator with the rest of this gate's findings. This is
      the last point at which a dropped interaction is still a design conversation
      rather than an amendment to a merged contract. The personas walk the mock and the
      audit measures the mock; neither can catch what the mock does not contain,
      because neither is looking at anything else.
      
      ### 9. Lock
      
      Locking is complete only when ALL of these exist:
      
      1. **★ LOCKED header** in the winning HTML (+ companion JS), dated and
         attributed, carrying the narrative spec as today.
      2. **Consistency check across locked artifacts.** Read every artifact the
         lock touches — sibling mockups (desktop/mobile), copy specs, glossaries,
         prior locks being superseded. Any contradiction is resolved *now*, by the
         user (interactive) or explicitly in the header (headless) — never left
         for the implementer to arbitrate.
      3. **The predecessor diff on the recorded fallback or a legacy lock.** If a
         `shipped` row in `mockups/INDEX.md` covers this surface or the one it
         replaces, or a prior lock is being superseded, or the running app already
         does this job, then `behavior-sweep`'s **predecessor inventory**
         ([behavior-sweep.md](behavior-sweep.md) §2) has run and every predecessor
         behavior carries a verdict: `kept`, `deferred` to a term this manifest states
         and an omitting build would violate, or `retired` with its sanction line. **A
         `missed` row blocks the lock.** So does an unsanctioned `retired` row, and so
         does one whose only sanction is a term of this very manifest — a lock cannot
         sanction the omissions it is itself freezing. A greenfield surface records the
         one-line skip and moves on.
      
         **This one pass precedes the lock; the rest of the sweep follows it — and the
         ordering is the whole point.** The rest needs a frozen mock, so it cannot run
         earlier; this pass needs the finalist only as a diff target, so it can. Lock
         first and the record inverts: **the lock is what turns a retirement into
         contract**, so a pass that runs afterwards is not deciding anything, it is
         transcribing a decision the project already made by omission — and every
         later mode reads the lock, so the omission is now the spec. A real Diagnose
         lock froze the retirement of a shipped drag-to-draw selection window its mock
         had simply never implemented. Ten exploration rounds, a persona panel and a
         52-run audit all read the mock, and a mock-side reading cannot see an absence;
         the operator caught it by eye on a screenshot after the lock merged. Nothing
         in that sequence was skipped. The sequence had no step that looked backwards,
         and this is that step.
      4. **The lock manifest** — `mockups/<surface>.lock.md` (format below).
      5. Losing variants and their screenshots deleted — but never the scaffold
         files (`_theme.css`, `_shell.js`, `SCAFFOLD.md`), which the locked mock
         links and other surfaces share; `mockups/INDEX.md` set to `locked`,
         pointing at mock + manifest; the implementation issue references both.
      
      After implementation ships, set the surface-ledger row to `shipped`, archive the
      mockup, and delete that surface's `TARGET=mock` CI leg; the app becomes the
      source of truth. **All three land in the port PR itself** — the PR that turns the
      app-opener replay leg green — never in a follow-up issue, which is how a surface
      stays `locked` long after it merged. The mock, its screenshots, the manifest and
      the behavior ledger stay as the design record, and the ledger's stories keep
      running against the built app forever; post-ship hotfixes owe no mock pass and no
      mock sync, and later design work starts with `revise` from the shipping app
      rather than a runnable mock. `resettle` remains only for terms in the archived
      lock.
      
      ## Lock manifest format
      
      The manifest is the machine-walkable extraction of the header prose. The
      header stays the narrative; the manifest is the contract.
      
      ```markdown
      # Lock manifest — <surface>
      Locked: <date> by <who>   Mocks: <files, incl. the scaffold files the mock links (_theme.css, _shell.js)>   Supersedes: <prior lock, the shipped surface it replaces + its source path, or —>
      
      ## Precedence
      <One sentence: which artifact wins for component-level styling when the mock
      and the app's shipped design system disagree. Default: the mock wins for
      anything it states explicitly; the app system wins for anything it doesn't.>
      
      ## Terms
      | # | Term | Kind | Evidence expected |
      |---|------|------|-------------------|
      | 1 | No page scroll at 1280x800 or 1440x900 | gate | browser-gate assertion |
      | 2 | Primary button tinted, never solid; min-height 36px; radius 8px | gate | assertion |
      | 3 | Outcome excursion aligns vertically with its setting block | eye | paired render |
      
      ## Fixture obligations
      <What the fixture must exercise for the locked visuals to be provable —
      e.g. "glucose spread wide enough that the p10–90 envelope is visible and the
      low-tail fill fires". A fixture that cannot show a term cannot prove it.>
      
      ## Verbatim strings
      <Legend chips, labels, button text — copied exactly from the mock, so text
      drift is a diff, not a judgment call.>
      ```
      
      **`Supersedes:` is how the predecessor stays findable.** The surface ledger
      identifies it by its `shipped` row — and that row flips to `locked` in this same
      change, so from then on this line is the only record of what this surface
      replaced. Name the shipped surface and where its behavior is registered, not just
      a prior lock; a bare `—` on a replacement surface reads as greenfield to the next
      round, and a greenfield surface skips the predecessor pass.
      
      Rules for writing terms:
      
      - Every load-bearing sentence in the header becomes a numbered term. If it's
        precise enough to violate, it's precise enough to list.
      - `gate` = mechanically assertable (geometry, overflow, colors, counts,
        text). `eye` = needs rendered human/vision judgment. When in doubt, `gate`
        — an assertable term that goes unasserted is how drift ships.
      - Terms carried forward from a superseded lock are restated here, not
        referenced — the manifest must stand alone.
      - **A surface with more than one view/mode/tab owes two extra terms: which one
        is the default, and what chrome persists across all of them.** Name the
        switcher itself as persistent chrome. Both are the kind of thing a lock is
        silent on because it felt obvious while the mock was open in front of you,
        and silence is what a builder resolves in private.
      - **"Mounts <existing surface> unchanged" is not a term — it is a hole.** When
        one view hands off to a sibling or shipped surface, say what the host keeps
        on screen around it. Read literally, "unchanged" licenses dropping the
        navigation that got the reader there, which is exactly how a real Diagnose
        lock shipped a view you could enter and never leave. If a rail's contents
        differ per view, spell out each view's contents rather than describing the
        rail once from whichever view you happened to be designing.
      
    • polish.md 13.1 KB
      > **Additional context needed**: quality bar (MVP vs flagship).
      
      Perform a meticulous final pass to catch all the small details that separate good work from great work. The difference between shipped and polished.
      
      Detector and automated QA output are defect evidence only. A clean script result is never proof that the design is strong; gather browser evidence and inspect the real interaction path.
      
      ## Design System Discovery
      
      Aligning the feature to the design system is **not optional**. Polish without alignment is decoration on top of drift, and it makes the next person's job harder. Discovery comes before any other polish work.
      
      1. **Find the design system**: Search for design system documentation, component libraries, style guides, or token definitions. Study the core patterns: design principles, target audience, color tokens, spacing scale, typography styles, component API, motion conventions.
      2. **Note the conventions**: How are shared components imported? What spacing scale is used? Which colors come from tokens vs hard-coded values? What motion and interaction patterns are established? What flow shapes are used for comparable actions (modal vs full-page, inline vs route, save-on-blur vs explicit submit)?
      3. **Identify drift, then name the root cause**: For every deviation, classify it as a **missing token** (the value should exist in the system but doesn't), a **one-off implementation** (a shared component already exists but wasn't used), or a **conceptual misalignment** (the feature's flow, IA, or hierarchy doesn't match neighboring features). The fix differs by category: patch the value, swap to the shared component, or rework the flow. Fixing the symptom without naming the cause is how drift compounds.
      
      If a design system exists, polish **must** align the feature with it. If none exists, polish against the conventions visible in the codebase. **If anything about the system is ambiguous, ask. Never guess at design system principles.**
      
      ## Pre-Polish Assessment
      
      Understand the current state and goals before touching anything:
      
      1. **Review completeness**:
         - Is it functionally complete?
         - Are there known issues to preserve (mark with TODOs)?
         - What's the quality bar? (MVP vs flagship feature?)
         - When does it ship? (How much time for polish?)
      
      2. **Think experience-first**: Who actually uses this, and what's the best possible experience for them? Effective design beats decorative polish; a feature that looks beautiful but fights the user's flow is not polished. Walk the path from their perspective before opening DevTools.
      
      3. **Identify polish areas**:
         - Visual inconsistencies
         - Spacing and alignment issues
         - Interaction state gaps
         - Copy inconsistencies
         - Edge cases and error states
         - Loading and transition smoothness
         - Information architecture and flow drift (does this feature reveal complexity the way neighboring features do?)
      
      4. **Pull in any prior critique** (optional signal): If `/ui-craft critique` has been run on the same target, its priority issues are a useful prior for what to address first. Resolve the target to a file path or URL, then:
         ```bash
         slug=$(node $UI_CRAFT_SKILL_DIR/scripts/critique-storage.mjs slug "<resolved>")
         node $UI_CRAFT_SKILL_DIR/scripts/critique-storage.mjs latest "$slug"
         ```
         Exit 0 with body = found; fold the P0/P1 items into your polish list and mention the snapshot path so the user sees what you read. Exit 2 = no snapshot, continue without it. The critique is one input among many. Do your own pass either way.
      
      5. **Triage cosmetic vs functional**: Classify each issue as **cosmetic** (looks off, doesn't impede the user) or **functional** (breaks, blocks, or confuses the experience). When polish time is tight, functional issues ship first; cosmetic ones can land in a follow-up. Quality should be consistent; never perfect one corner while leaving another rough.
      
      **CRITICAL**: Polish is the last step, not the first. Don't polish work that's not functionally complete.
      
      ## Polish Systematically
      
      Work through these dimensions methodically:
      
      ### Visual Alignment & Spacing
      
      - **Pixel-perfect alignment**: Everything lines up to grid
      - **Consistent spacing**: All gaps use spacing scale (no random 13px gaps)
      - **Optical alignment**: Adjust for visual weight (icons may need offset for optical centering)
      - **Responsive consistency**: Spacing and alignment work at all breakpoints
      - **Grid adherence**: Elements snap to baseline grid
      
      **Check**:
      - Enable grid overlay and verify alignment
      - Check spacing with browser inspector
      - Test at multiple viewport sizes
      - Look for elements that "feel" off
      
      ### Information Architecture & Flow
      
      Visual polish on a misshapen flow is wasted work. Match the *shape* of the experience to the system, not just the surface.
      
      - **Progressive disclosure**: Match how much is revealed when, compared to neighboring features. A settings page exposing 40 fields when the rest of the app reveals 5 at a time is drift, even if every field is perfectly styled.
      - **Established user flows**: Multi-step actions follow the same shape as comparable flows elsewhere: modal vs full-page, inline edit vs separate route, save-on-blur vs explicit submit, optimistic vs pessimistic updates.
      - **Hierarchy & complexity**: The same conceptual weight gets the same visual weight throughout. Primary actions don't become tertiary in one corner of the product, and tertiary actions don't shout.
      - **Empty, loading, and arrival transitions**: How content arrives, updates, and leaves matches how it does in adjacent features.
      - **Naming and mental model**: The feature uses the same nouns and verbs as the rest of the system. A "Workspace" here shouldn't be a "Project" three screens away.
      
      ### Typography Refinement
      
      - **Hierarchy consistency**: Same elements use same sizes/weights throughout
      - **Line length**: 45-75 characters for body text
      - **Line height**: Appropriate for font size and context
      - **Widows & orphans**: No single words on last line
      - **Hyphenation**: Appropriate for language and column width
      - **Kerning**: Adjust letter spacing where needed (especially headlines)
      - **Font loading**: No FOUT/FOIT flashes
      
      ### Color & Contrast
      
      - **Contrast ratios**: All text meets WCAG standards
      - **Consistent token usage**: No hard-coded colors, all use design tokens
      - **Theme consistency**: Works in all theme variants
      - **Color meaning**: Same colors mean same things throughout
      - **Accessible focus**: Focus indicators visible with sufficient contrast
      - **Gray on color**: Never put gray text on colored backgrounds; use a shade of that color or transparency
      
      ### Interaction States
      
      Every interactive element needs all states:
      
      - **Default**: Resting state
      - **Hover**: Subtle feedback (color, scale, shadow)
      - **Focus**: Keyboard focus indicator (never remove without replacement)
      - **Active**: Click/tap feedback
      - **Disabled**: Clearly non-interactive
      - **Loading**: Async action feedback
      - **Error**: Validation or error state
      - **Success**: Successful completion
      
      **Missing states create confusion and broken experiences**.
      
      ### Micro-interactions & Transitions
      
      - **Smooth transitions**: All state changes animated appropriately (150-300ms)
      - **Consistent easing**: Use ease-out-quart/quint/expo for natural deceleration. Never bounce or elastic; they feel dated.
      - **No jank**: Smooth animations; use atmospheric blur/filter/mask/shadow effects when they add polish, but bound expensive paint areas and avoid casual layout-property animation
      - **Appropriate motion**: Motion serves purpose, not decoration
      - **Reduced motion**: Respects `prefers-reduced-motion`
      
      ### Content & Copy
      
      - **Consistent terminology**: Same things called same names throughout
      - **Consistent capitalization**: Title Case vs Sentence case applied consistently
      - **Grammar & spelling**: No typos
      - **Appropriate length**: Not too wordy, not too terse
      - **Punctuation consistency**: Periods on sentences, not on labels (unless all labels have them)
      
      ### Icons & Images
      
      - **Consistent style**: All icons from same family or matching style
      - **Appropriate sizing**: Icons sized consistently for context
      - **Proper alignment**: Icons align with adjacent text optically
      - **Alt text**: All images have descriptive alt text
      - **Loading states**: Images don't cause layout shift, proper aspect ratios
      - **Retina support**: 2x assets for high-DPI screens
      
      ### Forms & Inputs
      
      - **Label consistency**: All inputs properly labeled
      - **Required indicators**: Clear and consistent
      - **Error messages**: Helpful and consistent
      - **Tab order**: Logical keyboard navigation
      - **Auto-focus**: Appropriate (don't overuse)
      - **Validation timing**: Consistent (on blur vs on submit)
      
      ### Edge Cases & Error States
      
      - **Loading states**: All async actions have loading feedback
      - **Empty states**: Helpful empty states, not just blank space
      - **Error states**: Clear error messages with recovery paths
      - **Success states**: Confirmation of successful actions
      - **Long content**: Handles very long names, descriptions, etc.
      - **No content**: Handles missing data gracefully
      - **Offline**: Appropriate offline handling (if applicable)
      
      ### Responsiveness
      
      - **All breakpoints**: Test mobile, tablet, desktop
      - **Touch targets**: 44x44px minimum on touch devices
      - **Readable text**: No text smaller than 14px on mobile
      - **No horizontal scroll**: Content fits viewport
      - **Appropriate reflow**: Content adapts logically
      
      ### Performance
      
      - **Fast initial load**: Optimize critical path
      - **No layout shift**: Elements don't jump after load (CLS)
      - **Smooth interactions**: No lag or jank
      - **Optimized images**: Appropriate formats and sizes
      - **Lazy loading**: Off-screen content loads lazily
      
      ### Code Quality
      
      - **Remove console logs**: No debug logging in production
      - **Remove commented code**: Clean up dead code
      - **Remove unused imports**: Clean up unused dependencies
      - **Consistent naming**: Variables and functions follow conventions
      - **Type safety**: No TypeScript `any` or ignored errors
      - **Accessibility**: Proper ARIA labels and semantic HTML
      
      ## Polish Checklist
      
      Go through systematically:
      
      - [ ] Aligned to the design system (drift named and resolved by root cause)
      - [ ] Information architecture and flow shape match neighboring features
      - [ ] Visual alignment perfect at all breakpoints
      - [ ] Spacing uses design tokens consistently
      - [ ] Typography hierarchy consistent
      - [ ] All interactive states implemented
      - [ ] All transitions smooth (60fps)
      - [ ] Copy is consistent and polished
      - [ ] Icons are consistent and properly sized
      - [ ] All forms properly labeled and validated
      - [ ] Error states are helpful
      - [ ] Loading states are clear
      - [ ] Empty states are welcoming
      - [ ] Touch targets are 44x44px minimum
      - [ ] Contrast ratios meet WCAG AA
      - [ ] Keyboard navigation works
      - [ ] Focus indicators visible
      - [ ] No console errors or warnings
      - [ ] No layout shift on load
      - [ ] Works in all supported browsers
      - [ ] Respects reduced motion preference
      - [ ] Code is clean (no TODOs, console.logs, commented code)
      
      **IMPORTANT**: Polish is about details. Zoom in. Squint at it. Use it yourself. The little things add up.
      
      Sweat the details. Zoom in until the alignment is right and the spacing reads as deliberate. Then ship.
      
      **NEVER**:
      - Polish before it's functionally complete
      - Polish without aligning to the design system; that's decoration on drift
      - Guess at design system principles instead of asking when something is ambiguous
      - Spend hours on polish if it ships in 30 minutes (triage)
      - Introduce bugs while polishing (test thoroughly)
      - Ignore systematic issues (if spacing is off everywhere, fix the system, not just one screen)
      - Perfect one thing while leaving others rough (consistent quality level)
      - Create new one-off components when design system equivalents exist
      - Hard-code values that should use design tokens
      - Introduce new patterns or flows that diverge from established ones
      
      ## Final Verification
      
      Before marking as done:
      
      - **Use it yourself**: Actually interact with the feature.
      - **Test on real devices**: Not just browser DevTools.
      - **Get fresh evidence when proportionate**: use another reviewer when the
        admitted contract requires it; do not make an external participant mandatory
        for a small, otherwise evidenced change.
      - **Compare to design**: Match intended design.
      - **Walk the lock manifest**: If the surface has `mockups/<surface>.lock.md`, run the lock-fidelity audit from `audit.md` — paired mock/build renders, every term met or in `resettle`. A locked surface cannot pass polish on the generic checklist alone; drift found here blocks completion.
      - **Check all states**: Don't just test happy path.
      - **Treat automation carefully**: Run detector or QA commands when they are available and relevant, fix their defects, but never cite a clean result as proof that the work is polished.
      
      ## Clean Up
      
      After polishing, ensure code quality:
      
      - **Replace custom implementations**: If the design system provides a component you reimplemented, switch to the shared version.
      - **Remove orphaned code**: Delete unused styles, components, or files made obsolete by polish.
      - **Consolidate tokens**: If you introduced new values, check whether they should be tokens.
      - **Verify DRYness**: Look for duplication introduced during polishing and consolidate.
      
    • product.md 3.7 KB
      # Product register
      
      When design SERVES the product: app UIs, admin dashboards, settings panels, data tables, tools, authenticated surfaces, anything where the user is in a task.
      
      ## The product slop test
      
      Not "would someone say AI made this." Familiarity is often a feature here. The test is: would a user fluent in the category's best tools (Linear, Figma, Notion, Raycast, Stripe come to mind) sit down and trust this interface, or pause at every subtly-off component?
      
      Product UI's failure mode isn't flatness, it's strangeness without purpose: over-decorated buttons, mismatched form controls, gratuitous motion, display fonts where labels should be, invented affordances for standard tasks. The bar is earned familiarity. The tool should disappear into the task.
      
      ## Typography
      
      - **One family is often right.** Product UIs don't need display/body pairing. A well-tuned sans carries headings, buttons, labels, body, data.
      - **Fixed rem scale, not fluid.** Clamp-sized headings don't serve product UI. Users view at consistent DPI, and a fluid h1 that shrinks in a sidebar looks worse, not better.
      - **Tighter scale ratio.** 1.125–1.2 between steps is typical. More type elements here than on brand surfaces; exaggerated contrast creates noise.
      - **Line length still applies for prose** (65–75ch). Data and compact UI can run denser; tables at 120ch+ are fine.
      
      ## Color
      
      Product defaults to Restrained. A single surface can earn Committed (a dashboard where one category color carries a report, an onboarding flow with a drenched welcome screen), but Restrained is the floor.
      
      - State-rich semantic vocabulary: hover, focus, active, disabled, selected, loading, error, warning, success, info. Standardize these.
      - Accent color used for primary actions, current selection, and state indicators only, not decoration.
      - A second neutral layer for sidebars, toolbars, and panels (slightly cooler or warmer than the content surface).
      
      ## Layout
      
      - Responsive behavior is structural (collapse sidebar, responsive table, breakpoint-driven columns), not fluid typography.
      
      ## Components
      
      Every interactive component has: default, hover, focus, active, disabled, loading, error. Don't ship with half of these.
      
      - Skeleton states for loading, not spinners in the middle of content.
      - Empty states that teach the interface, not "nothing here."
      - Consistent affordances across the surface. Same button shape. Same form-control vocabulary. Same icon style.
      
      ## Motion
      
      - 150–250 ms on most transitions. Users are in flow; don't make them wait for choreography.
      - Motion conveys state, not decoration. State change, feedback, loading, reveal: nothing else.
      - No orchestrated page-load sequences. Product loads into a task; users don't want to watch it load.
      
      ## Product bans (on top of the shared absolute bans)
      
      - Decorative motion that doesn't convey state.
      - Inconsistent component vocabulary across screens. If the "save" button looks different in two places, one is wrong.
      - Display fonts in UI labels, buttons, data.
      - Reinventing standard affordances for flavor (custom scrollbars, weird form controls, non-standard modals).
      - Heavy color or full-saturation accents on inactive states.
      - Modal as first thought. Modals are usually laziness. Exhaust inline / progressive alternatives first.
      
      ## Product permissions
      
      Product can afford things brand surfaces can't.
      
      - System fonts and familiar sans defaults (Inter, SF Pro, system-ui stacks).
      - Standard navigation patterns: top bar + side nav, breadcrumbs, tabs, command palettes.
      - Density. Tables with many rows, panels with many labels, dense information when users need it.
      - Consistency over surprise. The same visual vocabulary screen to screen is a virtue; delight is saved for moments, not pages.
      
    • resettle.md 4.4 KB
      # Mode: resettle
      
      Amend a locked term. This is the only legitimate path for changing anything a
      lock manifest lists — including genuine improvements discovered mid-build.
      
      `revise` has no lock manifest, so behavior changes on a shipped app branch do
      not come here unless they also amend a still-binding legacy lock term. `revise`
      records added, changed and retired behavior directly in the frozen behavior
      ledger, with the same dated sanction rules. Never invent a term merely to route
      an app revision through `resettle`.
      
      A re-settle is sanctioned by the user (interactive) or, in headless runs, by
      an explicit instruction in the work order. An agent may *propose* one at any
      time; it may never *apply* one on its own judgment.
      
      ## A post-lock `missed` verdict lands here
      
      When `behavior-sweep`'s predecessor inventory ([behavior-sweep.md](behavior-sweep.md)
      §2) returns a `missed` verdict on a lock that has already merged — a shipped
      behavior the lock neither kept nor sanctioned as retired — **this is the path**,
      whether it surfaced during the build, in an audit, or by eye on a screenshot.
      It is not a build call and not a bug fix: the merged lock currently says the
      behavior is gone, so restoring it quietly is the private arbitration the lock
      exists to forbid, and dropping it quietly is the original defect a second time.
      
      This is also where the pass lands when it runs late — against a lock that closed
      before anyone was looking backwards. Expect a pile rather than a row: the first
      predecessor inventory run against a merged 60-term lock returned 43 unruled
      retirements, one drawn selection window accounting for seventeen of them.
      
      The operator rules it, and the ruling lands as one of two change sets:
      
      - **kept** — re-settle the terms that have to describe the behavior. Usually
        there is no row to amend, because the lock was silent about it; then the
        re-settle **adds** the term and says in the mock header's `RE-SETTLED TERM`
        block that it restores a predecessor behavior the lock omitted. The behavior
        ledger gains its STORY and the replay script its function, in the same set.
      - **retired** — no term moves. The behavior ledger gains its RETIRED entry and
        the replay script its absence assertion, to `behavior-sweep`'s enumeration of
        what a retirement owes; this page adds nothing to that list.
        Where the merged lock already spoke to the retirement, that citation rides
        along as the row's `ruled-elsewhere` annotation and nothing more: it is what
        turns the ruling into a one-sentence confirmation instead of a fresh decision,
        and it is never the sanction itself.
      
      Either way it is dated and recorded, and the ledger keeps the trail.
      
      ## Steps
      
      1. Name the term (manifest number), the change, and who sanctioned it.
      2. Update, in one change set:
         - the mock header — a dated `RE-SETTLED TERM` block quoting the old term,
           the new term, and the sanction ("supersedes …", the existing repo
           convention);
         - the **manifest** — rewrite the term row, and fixture obligations or
           verbatim strings if they moved;
         - sibling locked artifacts the term appears in (the other form factor's
           mock, a copy spec) so the locked set stays self-consistent;
         - the surface's **behavior ledger** (`mockups/<surface>.behavior.md`) if any
           story cites the term — rewrite the entry and record the reason under its
           `★ FROZEN` header, since the freeze pins a contract that would otherwise go
           silently stale. A mid-build resettle that leaves the frozen ledger untouched
           is incomplete. **RETIRED entries are amended in place, never deleted**: a
           reinstated behavior moves back to STORY in this change set and its
           retirement stays visible above it as the record of what was reversed;
         - `mockups/INDEX.md` if the surface's status or files changed (columns
           Surface / Concept / Status / Issue / File) — it registers the behavior
           ledger and the sweep-evidence directory `mockups/sweep/<surface>/` too.
      3. If the term had a `LOCK:` assertion, update the assertion in the same
         change and prove the new one can fail.
      4. If a build is in flight, update its fidelity ledger row from
         `re-settle requested` to `met` with the new evidence.
      
      A re-settle that touches only the code, or only the mock, is incomplete —
      the header, manifest, assertions, and siblings move together or the lock is
      in an inconsistent state that some future build will arbitrate in private.
      
    • revise.md 13.1 KB
      # Mode: revise
      
      Revise a surface the app already ships by changing the running app on a branch.
      **Never rebuild that surface as a from-scratch mock.** In the app, a behavior
      survives unless the diff deletes it; in a mock, absence has no representation.
      
      The contract is the surface's frozen **behavior ledger** plus its replay script,
      run against the built app. The branch is the visual artifact. Review screenshots
      record what changed, but no lock manifest or fidelity ledger is pinned to an app
      template.
      
      Before changing CSS or JavaScript/TypeScript, read
      [web-implementation.md](web-implementation.md); it governs web mechanics while this
      mode's behavior ledger and rendered evidence remain binding.
      
      This mode is for a shipped embodiment of the surface, including a new view inside
      a shell that already ships. A wholly greenfield surface uses `lock`.
      
      ## Route map
      
      - [Review mode](#review-mode)
      - [0. Prove the app is safe to start](#0-prove-the-app-is-safe-to-start)
      - [1. Establish the base](#1-establish-the-base)
      - [2. Freeze shipped behavior before design](#2-freeze-shipped-behavior-before-design)
      - [3. Choose convergent or divergent work](#3-choose-convergent-or-divergent-work)
      - [4. Iterate the running app](#4-iterate-the-running-app)
      - [5. Review and land](#5-review-and-land)
      
      ## Review mode
      
      - **Interactive:** the human and agent may explore divergent wireframes together,
        then iterate the app branch live.
      - **Headless (AgentFlow):** go straight to an app branch. Never start a divergent
        wireframe phase for a surface whose shell ships. If the work cannot be settled
        without that conversation, stop and return the decision rather than inventing
        it.
      
      ## 0. Prove the app is safe to start
      
      Read the repo's `CLAUDE.md` / `AGENTS.md` before running any server. App-branch
      iteration is allowed only when that file declares:
      
      1. the exact safe dev-server entrypoint; and
      2. the data source that entrypoint reads.
      
      Resolve the named source before starting the process. For `revise`, it must be a
      manufactured fixture or synthetic database. A command that may read real,
      production or authoritative data, fetch from a vendor, select a database
      implicitly, or leave the source ambiguous is unsafe. **Do not run it to discover
      what it does.** This mode's stricter risk contract overrides any repo-scoped
      real-data inversion used for other evidence.
      
      Record the declaration path, quoted command, named data source, and source
      provenance in the change's decision record.
      
      - **No dev-server declaration:** record the accepted fallback and run
        `lock`'s mockup flow plus `behavior-sweep`'s predecessor inventory. This is the
        only path by which `lock` may accept a shipped surface. The record says that
        the result is weaker because absence is visible only through the predecessor
        pass. Fallback does not authorize starting the app; any live predecessor
        evidence must already satisfy a separately declared safe harness.
      - **The app cannot run locally at all, or a declared entrypoint names no data
        source:** `revise` is unsupported. Do not start the app and do not improvise a
        new entrypoint. An incomplete declaration is not the same as no declaration.
      - **The declaration is ambiguous:** stop. Ambiguity is not permission and is
        never resolved by a trial run.
      
      ## 1. Establish the base
      
      Fetch the default branch, branch or create a worktree from its exact tip, and
      record the base SHA. The revision changes the real app in place; never build a
      duplicate route that carries a second implementation of the surface.
      
      When live before/after comparison matters, use `spin-worktree` to serve the base
      branch from a second worktree. Give the two servers distinct ports and the same
      safe data bytes. A second route inside the revision branch is not a comparison:
      it duplicates the surface and can drift with it.
      
      Capture the base surface at every state, viewport and theme the change may touch.
      Screenshots stay in the change's decision record or its external review evidence;
      sensitive renders obey SKILL.md's publish rules.
      
      ## 2. Freeze shipped behavior before design
      
      Run `behavior-sweep` against the **base app**, before discussing a replacement
      layout or writing a wireframe. On the surface's first revision, make one full
      source-and-live inventory and freeze it as:
      
      - `mockups/<surface>.behavior.md`; and
      - `frontend/<surface>-behavior.replay.mjs`, or the repo's equivalent test path.
      
      The directory name does not turn the ledger into a mock: no mock artifact exists
      on this path. The replay script has an app opener only and runs every story
      against the built app. Create `mockups/INDEX.md` if absent and register the
      shipped surface, ledger and replay path there.
      
      On every later revision:
      
      1. replay the frozen ledger against the base app;
      2. inventory the base source and live surface again;
      3. diff the observed inventory against the ledger; and
      4. amend and re-freeze the ledger before design.
      
      New shipped behavior becomes a STORY with a replay function. A ledger story no
      longer present on the base is an unsanctioned retirement until a named person
      rules it with the date and their quoted reason. This is how a stale inventory is
      caught at the next revision rather than silently trusted.
      
      No design conversation starts while a story is unreplayable, an observed
      behavior has no story, or a retirement lacks its sanction. The QUESTION round in
      `behavior-sweep` owns those decisions.
      
      ## 3. Choose convergent or divergent work
      
      **Convergent work** changes the app branch directly: refinements within the
      shipping surface's current information and interaction model, or a direction the
      work order has already settled.
      
      **Divergent work** may use throwaway wireframes, including for a new view inside
      the shipping shell, under all of these constraints:
      
      - human and agent work the wireframe together; it is never an unattended phase;
      - it lives in the change's own decision record — its OpenSpec change folder when
        the repo uses OpenSpec, otherwise the repository's resolved ADR home;
      - it never lives in `mockups/`;
      - it says `WIREFRAME — NO FIDELITY CLAIM — NOT LOCKABLE` at its top;
      - it may explore structure and interaction, but makes no claim to match the app;
        and
      - the runnable wireframe is deleted when the change lands. Only screenshots and
        the decisions they evidence survive.
      
      Do not run `lock` on the wireframe, produce a lock manifest for it, or cite it as
      fidelity evidence. A wireframe is a conversation aid, not a second surface.
      
      ### Running the rounds
      
      Interactive divergent work only. The headless rule above stands: a headless
      session never opens this phase, and a direction it cannot settle is returned as
      a decision rather than invented.
      
      The constraints above say what a wireframe is. These say how the conversation
      converges, so that iterating in the running app is never how direction gets
      settled:
      
      - **One question per round.** Each round narrows what the next may ask, and
        nothing ruled reopens. A round that asks two questions gets an answer to
        neither.
      - **Options differ in concept, not decoration** — five ways to site a control,
        not five colors of one control. Render every option live under the coverage
        §1 already requires, plus the element sizes the option itself varies, and give
        each state its own specimen: a small tile at rest is not evidence about a
        large one being held.
      - **Every option carries a stated cost, the recommended one included.** An
        option offered as free is one whose cost has not been found yet, and it wins
        the round on an omission.
      - **Measure the claim instead of asserting it, and own a wrong measurement out
        loud.** A retracted number costs one round; an unretracted one is believed for
        the rest of the change.
      - **The operator's eye outranks the measurement when the metric measures the
        wrong thing.** An area-blind contrast ratio is a correct number answering a
        question nobody asked. Say which of those is happening rather than arguing the
        number.
      - **Build only after the direction is ruled.** That is what moves review from
        "is this right", which the rounds settle, to "is this broken", which the app
        branch and the frozen stories settle.
      
      When the decisions settle, lift them into the app branch or worktree and continue
      there. Re-run the behavior sweep against the base app immediately before the
      lift, so the branch starts from a fresh frozen contract.
      
      ## 4. Iterate the running app
      
      Serve the revision branch through the declared safe entrypoint and inspect it in
      a real browser. Use the app's shipping tokens, components, chart library and data
      shape; do not translate the wireframe's temporary styling into a new design
      system.
      
      Work in short, reviewable rounds:
      
      1. change the app branch;
      2. replay every frozen story against the built app;
      3. render the affected states, viewports and themes;
      4. compare them with the base-worktree renders; and
      5. take the running branch to the human for the next decision in interactive
         mode.
      
      The replay must report its applicable story count. Zero stories, a missing
      driver, an unstubbed request, or a story that does not reach the requested state
      is failure, never skip.
      
      ### Behavior changes
      
      - **Preserved:** the existing story and replay function stay green.
      - **Added:** add a STORY and replay function in the same change.
      - **Changed:** amend the STORY, record the dated decision under the frozen
        header, and prove the old replay fails before the new one passes.
      - **Retired:** the behavior owes what `behavior-sweep` requires of a retirement,
        which that page enumerates in one place. Amend the ledger there, and carry the
        entry back into this change with its replay function.
      - **Moved:** a behavior that changed which surface owns it owes the same
        ceremony as a removal, and lands as an amended STORY rather than a retirement
        — the behavior still ships, so it keeps its entry and its replay, and no
        sanction is owed. Name the fact, the surface it left, the surface it landed
        on, and update every story that reads it in the same change, so the next
        revision's inventory finds it where it now lives instead of reading its
        absence from the old surface as an unsanctioned retirement. A move is the case
        that looks like nothing happened: the app is right, the behavior is intact,
        and the stories still reading the old surface were not wrong when they were
        written and nobody was told they had become wrong.
      
      A dropped, changed or moved behavior without that record blocks the revision.
      There is no lock term to `resettle`; the ledger amendment is the decision
      record.
      
      A move, a duplication or a rename also widens what the round owes outside the
      ledger, because each of them changes what an existing assertion means without
      changing its text:
      
      - **A second instance of an existing element under-specifies every selector that
        names it.** Once a fact renders twice, a locator meaning "the element for this
        item" matches both, and the locators do not all mean the same thing — some
        meant the first instance, some the second, some "wherever this lives now",
        readings that were indistinguishable while there was one copy. Re-read each
        one's intent; find-and-replace picks a reading for you, silently, and breaks a
        different set.
      - **A surface that can be conditionally absent invalidates every assertion using
        it as a presence proxy.** Re-read them including the fallback paths that have
        never executed: a dead branch in a replay driver is invisible until the day the
        proxy starts failing, and it then runs against selectors the same change
        deleted.
      - **A rename's blast radius is the repository, not the directory.** A name
        outlives the fact it names, and a consumer outside the swept directory surfaces
        as a type error inside a gate rather than anywhere near the edit.
      
      ## 5. Review and land
      
      The PR evidence includes:
      
      - the frozen behavior ledger, with the base SHA and data provenance;
      - raw replay output against the built revision, including the story count and
        every retirement sanction;
      - before/after renders from the base and revision worktrees for every affected
        state, viewport and theme; and
      - the decision-record screenshots for any divergent wireframe, with the
        wireframe itself deleted.
      
      Review the running app, not source alone. Run the repo persona sweep when one
      exists, then `audit` and `polish` against the revision branch. A visual change is
      judged against the stated decision and the before/after renders; it does not
      invent a lock manifest after the fact.
      
      Before landing, confirm:
      
      - every frozen story replayed against the built app;
      - for any round that moved a surface, the gates that replay those stories have
        actually run, launched by whoever can launch them. An agent whose sandbox
        cannot start a browser has no signal about the surface it just moved, and the
        suites that do run there staying green is not one;
      - every added, changed or moved behavior is represented in the ledger;
      - every retirement carries everything `behavior-sweep` requires of one;
      - no wireframe or duplicated comparison route survives; and
      - the safe-start declaration and exact data source still match what produced the
        evidence.
      
      The updated ledger and replay script run against the app from then on. A later
      change made outside `revise` is not repaired automatically; the next revision's
      base diff exposes it.
      
    • sibling-fidelity.md 4.8 KB
      # Sibling fidelity — one panel, one app
      
      When a mock or new surface lives beside shipped surfaces, the bar is: moving
      between the shipped surface and the new one must feel like switching panes on a
      single dashboard. **Any element with a sibling in a shipped surface uses the
      shipped values exactly — steal, never approximate.** A 3px chip-height drift, a
      square corner where the app rounds, a mono axis label where the app uses the
      default face: each one breaks the one-app illusion, and the operator sees it
      even when the builder doesn't. Distilled from the #660 Verify round
      (ciq-autotune, 2026-08-12), where every rule below was learned as a correction.
      
      ## Steal list (what "sibling" covers)
      
      Not just tokens and colors. Every one of these bit in practice:
      
      - **Geometry**: pane radius, paddings, gaps, row heights, full-bleed vs inset
        hairlines, chrome icon pixel sizes.
      - **Alignment spines**: the shipped app aligns pane title, axis caption
        (`mg/dL`) and axis labels on the plot's own left edge (a `--*-grid-left`
        token shared by CSS and chart config). New panes join the spine, and the
        chart grid uses the same number.
      - **Type**: rank (a pane title is caps-rank like every sibling pane, even when
        it "feels like" a heading), family (axis labels in the app's default face,
        not mono, when that is what the shipped chart does), line-height.
      - **Chart furniture**: axis scale rules (the shipped fixed clinical axis —
        e.g. 40–300/60 — beats data-hugging bounds; comparable zoom across views is
        the point), threshold-band treatment (fill + labeled knock-out rules, ported
        mixes), tick visibility, legend position/scale/case.
      - **Interaction idioms**: hover is chrome too. If the app has a docked-readout
        hover (crosshair only, numbers land in the pane header's swap line, no
        floating tooltip), the mock ports that behavior — `showContent:false`,
        `updateAxisPointer` → header paint, `globalout` clears — not a generic
        tooltip.
      
      ## Audit empirically, not by eyeball
      
      Run the shipped app and the mock in the same headless browser and **diff
      computed styles + bounding rects on shared selectors** (radius, padding, gap,
      font-size/family/weight, letter-spacing, line-height, colors, rect). Eyeballs
      miss 1–3px; the diff doesn't. Re-run the diff after edits — it is the fidelity
      gate, not a one-off. Two reporting artifacts to ignore: `width`/`height`
      computed-property mismatches caused by box-sizing differences when the rects
      match, and 0×0 rects for app elements that exist but aren't laid out in the
      probed tab (fall back to the app's source values for those).
      
      **The diff's blind spot is canvas.** Chart text (axis labels, ticks, legend,
      in-chart labels) is painted, not DOM — a computed-style diff cannot see it at
      all. Audit chart typography and furniture at the **option level**: diff the
      mock's chart config (family, sizes, tick visibility, positions) against the
      shipped chart source or a live `getOption()` dump, as its own audit step.
      
      ## Token bridges lie — verify resolution, not names
      
      A mock that bridges onto extracted app CSS via alias tokens
      (`--ck-r: var(--wk-radius)`) can silently resolve to nothing or to a wrong
      literal: the name may come from a retired lineage, or the real value may live
      in a **scoped** block (the app's workstation sets `--ck-r: 8px` inside `.dw`;
      the global `--wk-radius` is `0px`). Grep proving a name exists proves nothing.
      Verify every bridge token by **computed value on the element that uses it**,
      and port scoped token blocks as literals with a comment naming their source.
      
      ## No mock-global base styles
      
      A `body { line-height / font-size }` in the mock leaks into chrome extracted
      from the app and shifts its pixel metrics (chip heights, footer text). Chrome
      inherits the app's own base; scope any interior typography to the surface's
      own containers.
      
      ## Findings close by edit, not by note
      
      In a persona/critique sweep against a mock, a confirmed finding is closed by
      an edited file and a fresh render — finding → edit → screenshot, one at a
      time. Noting findings for later is the failure mode the sweep exists to kill.
      
      ## Concrete traps (chart library)
      
      - ECharts 5.5 legend: custom `path://` icons collapse the legend layout (all
        items stack at 0,0, clipped). Replicate line marks via inheritance instead:
        plain string `data`, `itemStyle: { opacity: 0 }` to hide the symbol dot,
        `lineStyle: { width: 'auto', type: 'inherit' }`.
      - ECharts value-axis charts whose x range spans 0: default `onZero` pins the
        y-axis (and its name) to x=0, not the plot edge — set
        `axisLine: { onZero: false }`.
      - A threshold band (`markArea`) covering the whole visible axis is a uniform
        wash that reads as nothing; a fixed axis (above) keeps its edges in frame.
      - Aggregate bins keyed by bin start: extend the final segment to the bin's
        end, or the series dies short of the axis edge with a cliff.
      
    • variant-agent-prompt.md 1.8 KB
      # Per-variant prompt
      
      Fill every placeholder. Give one fresh subagent one concept and no sibling
      implementation details.
      
      ```markdown
      Design one UI mockup variant. Commit to the assigned concept; do not blend it
      with the sibling directions.
      
      ## Assigned concept
      
      <concept name> — <layout, visualization, and interaction bet; explain how it
      differs from the named sibling concepts>
      
      ## Surface and decision
      
      <screen or component, the one question it must answer, and hard constraints>
      
      ## Shared scaffold (mandatory)
      
      `mockups/_theme.css` and `mockups/_shell.js` exist for this surface — read
      `mockups/SCAFFOLD.md` and follow it. Link the stylesheet, import the module,
      use `loadCapture()`, `renderMockBar()`, and `resolveColors()` from it. Do NOT
      re-implement theme tokens, base card styles, mockbar chrome, or fetch glue —
      build only the concept-specific markup and chart render logic.
      
      ## Grounding kit
      
      Theme tokens and fetch glue live in the scaffold above — do not restate them
      here.
      
      - UI/chart library: <library, exact version, and existing loading pattern>
      - Data shape: `loadCapture('<surface>')` returns <real field names and shape>
      - Render source: <shipping module or component to fork>
      
      Use only the safe development fixture or captured demo payload supplied here.
      Do not access production, customer, credential, health, or personal data.
      
      ## Required states
      
      <empty, typical, dense, error, mobile, light, dark, or named fixture states>
      
      ## Deliverables
      
      - `mockups/<surface>-<concept>.html`
      - `mockups/<surface>-<concept>-chart.js` when rendering is non-trivial
      
      Lift the shipping theme and library. Bind the real fields. Mark mock-only
      controls with a `not part of the real app` comment. Do not start a server or
      take screenshots; the orchestrator handles rendering.
      
      Report the files written and one sentence describing the design bet.
      ```
      
    • web-implementation.md 2.6 KB
      # Web implementation
      
      Use this reference for CSS or JavaScript/TypeScript changes made through UI Craft's
      `build`, `revise`, or general UI implementation route. It guides mechanics, not
      lifecycle: the lock manifest, where present; the frozen behavior ledger, where
      applicable; and rendered evidence remain the UI Craft contract.
      
      ## Establish the baseline
      
      Before selecting syntax, APIs, or CSS features, inspect the project's browser and
      support policy, client and server runtime, TypeScript target and libraries,
      bundler/transforms, and intentional polyfills. Record the implementation decision
      against that baseline.
      
      Keep three questions separate:
      
      1. **ECMAScript standardization:** is the language feature in the standard?
      2. **Runtime or host support:** do the target browsers and runtimes provide it?
      3. **Transform or polyfill support:** does the toolchain transform it, and is any
         required runtime behavior supplied intentionally?
      
      ## Preserve behavior and choose CSS deliberately
      
      Keep async sequencing, concurrency, and cancellation ownership explicit. Preserve
      the difference between mutation and change-by-copy, between nullish and other
      property states, and between ECMAScript features and browser or host APIs.
      
      For CSS, decide cascade/source order and specificity before adding overrides. Use
      Grid for two-dimensional layout and Flexbox for one-dimensional layout; choose
      viewport queries for viewport-wide adaptation and container queries for component
      adaptation. Prefer logical properties where direction can vary, honor reduced motion
      and accessibility needs, and use `@supports` with an explicit fallback or acceptable
      degradation when support varies.
      
      Every choice must preserve the locked visual contract or behavior ledger and be
      proven in the rendered evidence required by the active UI Craft mode. Do not replace
      that contract with a compatibility claim.
      
      ## Boundaries and provenance
      
      Do not maintain dated browser or Node matrices, edition snapshots, proposal tables,
      or exhaustive feature catalogs here. Check the project baseline and stable primary
      standards instead: [ECMA-262](https://tc39.es/ecma262/),
      [HTML](https://html.spec.whatwg.org/),
      [CSS Cascade](https://www.w3.org/TR/css-cascade-5/), and
      [CSS Containment](https://www.w3.org/TR/css-contain-3/).
      
      This is a pack-native synthesis informed by robust-skills contributors (c) 2026:
      [modern JavaScript](https://github.com/ccheney/robust-skills/tree/0ace9a7f5c20d19cad678b894a717945da2ea8ed/skills/modern-javascript)
      and [modern CSS](https://github.com/ccheney/robust-skills/tree/0ace9a7f5c20d19cad678b894a717945da2ea8ed/skills/modern-css).
      
  • scripts
    • detector
      • browser
        • injected
          • index.mjs 75 KB · in bundle
      • cli
        • main.mjs 12.1 KB · in bundle
      • engines
        • browser
          • detect-url.mjs 9.7 KB · in bundle
        • regex
          • detect-text.mjs 24.2 KB · in bundle
        • static-html
          • css-cascade.mjs 33.6 KB · in bundle
          • detect-html.mjs 10.1 KB · in bundle
        • visual
          • screenshot-contrast.mjs 6.4 KB · in bundle
      • node
        • file-system.mjs 6.7 KB · in bundle
      • profile
        • profiler.mjs 4.4 KB · in bundle
      • registry
        • antipatterns.mjs 18.7 KB · in bundle
      • rules
        • checks.mjs 114.8 KB · in bundle
      • shared
        • color.mjs 4.4 KB · in bundle
        • constants.mjs 4.1 KB · in bundle
        • inline-ignores.mjs 6 KB · in bundle
        • page.mjs 252 B · in bundle
      • design-system.mjs 24.1 KB · in bundle
      • detect-antipatterns-browser.js 214 KB
        /**
         * Anti-Pattern Browser Detector for Impeccable
         * Copyright (c) 2026 Paul Bakaus
         * SPDX-License-Identifier: Apache-2.0
         *
         * GENERATED -- do not edit. Source: cli/engine/browser/injected/index.mjs
         * Rebuild: node scripts/build-browser-detector.js
         *
         * Usage: <script src="detect-antipatterns-browser.js"></script>
         * Re-scan: window.impeccableScan()
         */
        (function () {
        if (typeof window === 'undefined') return;
        // --- cli/engine/shared/constants.mjs ---
        // ─── Section 1: Constants ───────────────────────────────────────────────────
        
        const SAFE_TAGS = new Set([
          'blockquote', 'nav', 'a', 'input', 'textarea', 'select',
          'pre', 'code', 'span', 'th', 'td', 'tr', 'li', 'label',
          'button', 'hr', 'html', 'head', 'body', 'script', 'style',
          'link', 'meta', 'title', 'br', 'img', 'svg', 'path', 'circle',
          'rect', 'line', 'polyline', 'polygon', 'g', 'defs', 'use',
        ]);
        
        // Per-check safe-tags override for the border (side-tab / border-accent)
        // rule. We intentionally re-allow <label> here because card-shaped clickable
        // labels (e.g. .checklist-item wrapping a checkbox + content) are one of the
        // canonical side-tab anti-pattern shapes and must be detected. The rule's
        // other preconditions (non-neutral color, width >= 2px on a single side,
        // radius > 0 or width >= 3, element size >= 20x20 in the browser path)
        // already filter out plain inline form labels so this does not introduce
        // false positives. See modern-color-borders.html for the test matrix.
        const BORDER_SAFE_TAGS = new Set(
          [...SAFE_TAGS].filter(t => t !== 'label')
        );
        
        const OVERUSED_FONTS = new Set([
          // Older monoculture (still ubiquitous):
          'inter', 'roboto', 'open sans', 'lato', 'montserrat', 'arial', 'helvetica',
          // Newer monoculture (the Anthropic-skill / Vercel / GitHub default wave):
          'fraunces', 'instrument sans', 'instrument serif',
          'geist', 'geist sans', 'geist mono',
          'mona sans',
          'plus jakarta sans', 'space grotesk', 'recoleta',
        ]);
        
        // Brand-associated fonts: don't flag these as "overused" on the brand's own domains.
        // Keys are font names, values are arrays of hostname suffixes where the font is allowed.
        const GOOGLE_DOMAINS = [
          'google.com', 'youtube.com', 'android.com', 'chromium.org',
          'chrome.com', 'web.dev', 'gstatic.com', 'firebase.google.com',
        ];
        const VERCEL_DOMAINS = ['vercel.com', 'nextjs.org', 'v0.app'];
        const GITHUB_DOMAINS = ['github.com', 'githubnext.com'];
        const BRAND_FONT_DOMAINS = {
          'roboto': GOOGLE_DOMAINS,
          'google sans': GOOGLE_DOMAINS,
          'product sans': GOOGLE_DOMAINS,
          'geist': VERCEL_DOMAINS,
          'geist sans': VERCEL_DOMAINS,
          'geist mono': VERCEL_DOMAINS,
          'mona sans': GITHUB_DOMAINS,
        };
        
        function isBrandFontOnOwnDomain(font) {
          if (typeof location === 'undefined') return false;
          const allowed = BRAND_FONT_DOMAINS[font];
          if (!allowed) return false;
          const host = location.hostname.toLowerCase();
          return allowed.some(suffix => host === suffix || host.endsWith('.' + suffix));
        }
        
        const GENERIC_FONTS = new Set([
          'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
          'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded',
          '-apple-system', 'blinkmacsystemfont', 'segoe ui',
          'inherit', 'initial', 'unset', 'revert',
        ]);
        
        // WCAG large text thresholds are defined in points: 18pt normal text and
        // 14pt bold text. Browsers expose font-size in CSS pixels at 96px per inch.
        const WCAG_LARGE_TEXT_PX = 18 * (96 / 72);
        const WCAG_LARGE_BOLD_TEXT_PX = 14 * (96 / 72);
        
        // Serif faces that show up in italic-display heroes. The rule also fires when
        // the primary face is unknown but the stack ends in the generic `serif` token,
        // which catches custom/private faces with a serif fallback.
        const KNOWN_SERIF_FONTS = new Set([
          'fraunces', 'recoleta', 'newsreader', 'playfair display', 'playfair',
          'cormorant', 'cormorant garamond', 'garamond', 'eb garamond',
          'tiempos', 'tiempos headline', 'tiempos text',
          'lora', 'vollkorn', 'spectral',
          'source serif pro', 'source serif 4', 'source serif',
          'ibm plex serif', 'merriweather',
          'libre caslon', 'libre baskerville', 'baskerville',
          'georgia', 'times new roman', 'times',
          'dm serif display', 'dm serif text',
          'instrument serif', 'gt sectra', 'ogg', 'canela',
          'freight display', 'freight text',
        ]);
        
        // --- cli/engine/registry/antipatterns.mjs ---
        const ANTIPATTERNS = [
          // ── AI slop: tells that something was AI-generated ──
          {
            id: 'side-tab',
            category: 'slop',
            name: 'Side-tab accent border',
            description:
              'Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.',
            skillSection: 'Visual Details',
            skillGuideline: 'colored accent stripe',
          },
          {
            id: 'border-accent-on-rounded',
            category: 'slop',
            name: 'Border accent on rounded element',
            description:
              'Thick accent border on a rounded card — the border clashes with the rounded corners. Remove the border or the border-radius.',
            skillSection: 'Visual Details',
            skillGuideline: 'colored accent stripe',
          },
          {
            id: 'overused-font',
            category: 'slop',
            name: 'Overused font',
            description:
              'Inter, Roboto, Fraunces, Geist, Plus Jakarta Sans, and Space Grotesk are used on so many sites they no longer feel distinctive. Each new wave of AI-generated UIs converges on the same handful of faces. Choose a face that gives your interface personality.',
            skillSection: 'Typography',
            skillGuideline: 'overused fonts like Inter',
          },
          {
            id: 'single-font',
            category: 'slop',
            name: 'Single font for everything',
            description:
              'Only one font family is used for the entire page. Pair a distinctive display font with a refined body font to create typographic hierarchy.',
            skillSection: 'Typography',
            skillGuideline: 'only one font family for the entire page',
          },
          {
            id: 'flat-type-hierarchy',
            category: 'slop',
            name: 'Flat type hierarchy',
            description:
              'Font sizes are too close together — no clear visual hierarchy. Use fewer sizes with more contrast (aim for at least a 1.25 ratio between steps).',
            skillSection: 'Typography',
            skillGuideline: 'flat type hierarchy',
          },
          {
            id: 'gradient-text',
            category: 'slop',
            name: 'Gradient text',
            description:
              'Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.',
            skillSection: 'Color & Contrast',
            skillGuideline: 'gradient text for',
          },
          {
            id: 'ai-color-palette',
            category: 'slop',
            name: 'AI color palette',
            description:
              'Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. Choose a distinctive, intentional palette.',
            skillSection: 'Color & Contrast',
            skillGuideline: 'AI color palette',
          },
          {
            id: 'cream-palette',
            category: 'slop',
            name: 'Cream / beige palette',
            description:
              'A warm cream or beige page background has become the default "tasteful" AI surface, reached for by reflex. Choose a background that comes from a deliberate palette, not the safe warm off-white.',
            skillSection: 'Color & Contrast',
            skillGuideline: 'cream and beige as the default surface',
          },
          {
            id: 'nested-cards',
            category: 'slop',
            name: 'Nested cards',
            description:
              'Cards inside cards create visual noise and excessive depth. Flatten the hierarchy — use spacing, typography, and dividers instead of nesting containers.',
            skillSection: 'Layout & Space',
            skillGuideline: 'Nest cards inside cards',
          },
          {
            id: 'monotonous-spacing',
            category: 'slop',
            name: 'Monotonous spacing',
            description:
              'The same spacing value used everywhere — no rhythm, no variation. Use tight groupings for related items and generous separations between sections.',
            skillSection: 'Layout & Space',
            skillGuideline: 'same spacing everywhere',
          },
          {
            id: 'bounce-easing',
            category: 'slop',
            name: 'Bounce or elastic easing',
            description:
              'Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.',
            skillSection: 'Motion',
            skillGuideline: 'bounce or elastic easing',
          },
          {
            id: 'dark-glow',
            category: 'slop',
            name: 'Dark mode with glowing accents',
            description:
              'Dark backgrounds with colored box-shadow glows are the default "cool" look of AI-generated UIs. Use subtle, purposeful lighting instead — or skip the dark theme entirely.',
            skillSection: 'Color & Contrast',
            skillGuideline: 'dark mode with glowing accents',
          },
          {
            id: 'icon-tile-stack',
            category: 'slop',
            name: 'Icon tile stacked above heading',
            description:
              'A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.',
            skillSection: 'Typography',
            skillGuideline: 'large icons with rounded corners above every heading',
          },
          {
            id: 'italic-serif-display',
            category: 'slop',
            name: 'Italic serif display headline',
            description:
              'Oversized italic serif (Fraunces, Recoleta, Playfair, Newsreader-italic) as the primary hero headline reads as taste in isolation but has become the universal AI-startup landing page hero. Set roman, or move to a non-serif display face. Editorial / magazine register may legitimately want this — judge by context.',
            skillSection: 'Typography',
            skillGuideline: 'oversized italic serif as the hero headline',
          },
          {
            id: 'hero-eyebrow-chip',
            category: 'slop',
            name: 'Hero eyebrow / pill chip',
            description:
              'A tiny uppercase letter-spaced label sitting immediately above an oversized hero headline — or the same shape rendered as a pill chip — is now the default AI SaaS hero. Drop the eyebrow, integrate the kicker into the headline, or run it as a navigation breadcrumb instead.',
            skillSection: 'Typography',
            skillGuideline: 'tiny uppercase tracked label above the hero headline',
          },
          {
            id: 'repeated-section-kickers',
            category: 'slop',
            severity: 'advisory',
            name: 'Repeated section kicker labels',
            description:
              'Repeating tiny uppercase tracked labels above section headings turns a brand page into AI editorial scaffolding. Replace them with stronger structure, artifacts, imagery, or a deliberate brand system.',
            skillSection: 'Typography',
            skillGuideline: 'repeated eyebrow or kicker labels as section scaffolding',
          },
          {
            id: 'numbered-section-markers',
            category: 'slop',
            severity: 'advisory',
            name: 'Numbered section markers (01 / 02 / 03)',
            description:
              'Numbered display markers as section labels (01, 02, 03) are the AI editorial scaffold one tier deeper than tracked eyebrow chips. If you find yourself reaching for them, choose a different section cadence.',
            skillSection: 'Layout & Space',
            skillGuideline: 'numbered section markers',
          },
          {
            id: 'em-dash-overuse',
            category: 'slop',
            name: 'Em-dash overuse',
            description:
              'More than two em-dashes (— or --) in body copy is an AI cadence tell. Use commas, colons, periods, or parentheses instead.',
            skillSection: 'Copy',
            skillGuideline: 'no em dashes',
          },
          {
            id: 'marketing-buzzword',
            category: 'slop',
            name: 'Marketing buzzword',
            description:
              'Generic SaaS phrases (streamline / empower / supercharge / world-class / enterprise-grade / next-generation / cutting-edge / etc) are instant AI tells. Pick a specific verb and noun that says what the product literally does.',
            skillSection: 'Copy',
            skillGuideline: 'marketing buzzwords',
          },
          {
            id: 'aphoristic-cadence',
            category: 'slop',
            name: 'Aphoristic-cadence copy',
            description:
              'Three or more sections landing on a short rebuttal sentence ("X. No Y." / "X. Just Y.") or a manufactured-contrast aphorism ("Not a feature. A platform.") reads as AI cadence, not voice. Once is fine; the pattern is the tell.',
            skillSection: 'Copy',
            skillGuideline: 'aphoristic cadence',
          },
          {
            id: 'oversized-h1',
            category: 'slop',
            name: 'Oversized hero headline',
            description:
              'A full-sentence headline set at display size ends up dominating the viewport, leaving no room for anything else above the fold. A punchy one- or two-word headline at that size is fine — the problem is a long headline blown up too large. Set long headlines smaller, or tighten the copy.',
            skillSection: 'Typography',
            skillGuideline: 'long headline set at display size',
          },
          {
            id: 'extreme-negative-tracking',
            category: 'slop',
            name: 'Crushed letter spacing',
            description:
              'Letter-spacing pulled tighter than the point where characters keep their own shapes costs legibility. Tighten display type optically, not destructively.',
            skillSection: 'Typography',
            skillGuideline: 'letter spacing crushed past legibility',
          },
          {
            id: 'broken-image',
            category: 'quality',
            name: 'Broken or placeholder image',
            description:
              '<img> tags with empty src, missing src, or placeholder values ship as broken-image boxes. Use real images, generated assets, or remove the tag.',
            skillSection: 'Imagery',
            skillGuideline: 'broken image references',
          },
        
          // ── Quality: general design and accessibility issues ──
          {
            id: 'gray-on-color',
            category: 'quality',
            name: 'Gray text on colored background',
            description:
              'Gray text looks washed out on colored backgrounds. Use a darker shade of the background color instead, or white/near-white for contrast.',
            skillSection: 'Color & Contrast',
            skillGuideline: 'gray text on colored backgrounds',
          },
          {
            id: 'low-contrast',
            category: 'quality',
            name: 'Low contrast text',
            description:
              'Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.',
          },
          {
            id: 'layout-transition',
            category: 'quality',
            name: 'Layout property animation',
            description:
              'Animating width, height, padding, or margin causes layout thrash and janky performance. Use transform and opacity instead, or grid-template-rows for height animations.',
            skillSection: 'Motion',
            skillGuideline: 'Animate layout properties',
          },
          {
            id: 'line-length',
            category: 'quality',
            name: 'Line length too long',
            description:
              'Text lines wider than ~80 characters are hard to read. The eye loses its place tracking back to the start of the next line. Add a max-width (65ch to 75ch) to text containers.',
            skillSection: 'Layout & Space',
            skillGuideline: 'wrap beyond ~80 characters',
          },
          {
            id: 'cramped-padding',
            category: 'quality',
            name: 'Cramped padding',
            description:
              'Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.',
            skillSection: 'Layout & Space',
            skillGuideline: 'inside bordered or colored containers',
          },
          {
            id: 'body-text-viewport-edge',
            category: 'quality',
            name: 'Body text touching viewport edge',
            description:
              'Body paragraphs render flush against the left or right viewport edge with no container providing horizontal padding. Wrap content in a container with at least 16px (ideally 24-32px) of horizontal padding, or apply max-width with mx-auto.',
          },
          {
            id: 'tight-leading',
            category: 'quality',
            name: 'Tight line height',
            description:
              'Line height below 1.3x the font size makes multi-line text hard to read. Use 1.5 to 1.7 for body text so lines have room to breathe.',
          },
          {
            id: 'skipped-heading',
            category: 'quality',
            name: 'Skipped heading level',
            description:
              'Heading levels should not skip (e.g. h1 then h3 with no h2). Screen readers use heading hierarchy for navigation. Skipping levels breaks the document outline.',
          },
          {
            id: 'justified-text',
            category: 'quality',
            name: 'Justified text',
            description:
              'Justified text without hyphenation creates uneven word spacing ("rivers of white"). Use text-align: left for body text, or enable hyphens: auto if you must justify.',
          },
          {
            id: 'tiny-text',
            category: 'quality',
            name: 'Tiny body text',
            description:
              'Body text below 12px is hard to read, especially on high-DPI screens. Use at least 14px for body content, 16px is ideal.',
          },
          {
            id: 'all-caps-body',
            category: 'quality',
            name: 'All-caps body text',
            description:
              'Long passages in uppercase are hard to read. We recognize words by shape (ascenders and descenders), which all-caps removes. Reserve uppercase for short labels and headings.',
            skillSection: 'Typography',
            skillGuideline: 'long body passages in uppercase',
          },
          {
            id: 'wide-tracking',
            category: 'quality',
            name: 'Wide letter spacing on body text',
            description:
              'Letter spacing above 0.05em on body text disrupts natural character groupings and slows reading. Reserve wide tracking for short uppercase labels only.',
          },
          {
            id: 'text-overflow',
            category: 'quality',
            name: 'Content overflowing its container',
            description:
              'Content renders wider than its container, spilling out or forcing a horizontal scrollbar. Let text wrap, constrain widths, or give the region a deliberate scroll affordance.',
            skillSection: 'Layout & Space',
            skillGuideline: 'content wider than its container',
          },
          {
            id: 'clipped-overflow-container',
            category: 'quality',
            name: 'Positioned child clipped by overflow container',
            description:
              'A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.',
            skillSection: 'Layout & Space',
            skillGuideline: 'overflow container clipping positioned children',
          },
          {
            id: 'design-system-font',
            category: 'quality',
            name: 'Font outside DESIGN.md',
            description:
              'A font is used that is not declared in DESIGN.md typography. Use the documented type system or update DESIGN.md if this is an intentional brand addition.',
            skillSection: 'Typography',
            skillGuideline: 'font family outside the project design system',
          },
          {
            id: 'design-system-color',
            category: 'quality',
            severity: 'advisory',
            name: 'Color outside DESIGN.md',
            description:
              'A literal color is outside the DESIGN.md palette and sidecar tonal ramps. This may be legitimate, but it should be an intentional design-system addition rather than drift.',
            skillSection: 'Color & Contrast',
            skillGuideline: 'literal color outside the project design system',
          },
          {
            id: 'design-system-radius',
            category: 'quality',
            severity: 'advisory',
            name: 'Radius outside DESIGN.md',
            description:
              'A border-radius value is outside the DESIGN.md rounded scale. Use a documented radius token or update the design system if the new shape is intentional.',
            skillSection: 'Visual Details',
            skillGuideline: 'border radius outside the project design system',
          },
        
          // ── Provider tells: opt-in via --gpt / --gemini (gated off by default) ──
          {
            id: 'gpt-thin-border-wide-shadow',
            category: 'slop',
            severity: 'advisory',
            gated: 'gpt',
            name: 'Hairline border with wide shadow',
            description:
              'A hairline border paired with a wide, diffuse shadow is a recurring generated-UI signature. Commit to one — a defined edge or a soft elevation — rather than both at once.',
            skillSection: 'Visual Details',
            skillGuideline: 'hairline border plus wide diffuse shadow',
          },
          {
            id: 'repeating-stripes-gradient',
            category: 'slop',
            severity: 'advisory',
            gated: 'gpt',
            name: 'Repeating-gradient stripes',
            description:
              'Repeating-gradient stripes used as surface decoration are a recurring generated-UI signature. Reach for a deliberate texture or leave the surface plain.',
            skillSection: 'Visual Details',
            skillGuideline: 'repeating-gradient decorative stripes',
          },
          {
            id: 'codex-grid-background',
            category: 'slop',
            severity: 'advisory',
            gated: 'gpt',
            name: 'Decorative grid-line background',
            description:
              'A two-axis grid drawn with hairline linear-gradient layers ("1px, transparent 1px" on both axes) is a recurring generated-UI signature. Reserve grid overlays for actual canvas, map, blueprint, or measurement surfaces; elsewhere use product structure or a plain surface.',
            skillSection: 'Visual Details',
            skillGuideline: 'two-axis grid-line gradient background',
          },
          {
            id: 'theater-slop-phrase',
            category: 'slop',
            severity: 'advisory',
            gated: 'gpt',
            name: 'Theater framing copy',
            description:
              'Dismissing something as "theater" is a recurring generated-copy tic. Say plainly what the thing does or does not do.',
            skillSection: 'Copy',
            skillGuideline: 'theater framing copy',
          },
          {
            id: 'image-hover-transform',
            category: 'slop',
            severity: 'advisory',
            gated: 'gemini',
            name: 'Image hover transform',
            description:
              'Scaling or rotating an image on hover is a recurring generated-UI signature. Let imagery sit still, or use a subtler, purposeful interaction.',
            skillSection: 'Motion',
            skillGuideline: 'image scale or rotate on hover',
          },
        ];
        
        // --- cli/engine/shared/color.mjs ---
        // ─── Section 2: Color Utilities ─────────────────────────────────────────────
        
        function isNeutralColor(color) {
          if (!color || color === 'transparent') return true;
        
          // rgb/rgba — use channel spread. Threshold 30 ≈ 11.7% of the 0–255 range.
          const rgb = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
          if (rgb) {
            return (Math.max(+rgb[1], +rgb[2], +rgb[3]) - Math.min(+rgb[1], +rgb[2], +rgb[3])) < 30;
          }
        
          // oklch()/lch() — chroma is the second numeric component.
          // oklch chroma is ~0–0.4 in sRGB gamut; >= 0.02 reads as tinted, not gray.
          // lch chroma is ~0–150; >= 3 reads as tinted. jsdom emits both formats
          // literally (it does NOT convert them to rgb).
          const oklch = color.match(/oklch\(\s*[\d.]+%?\s*([\d.-]+)/i);
          if (oklch) return parseFloat(oklch[1]) < 0.02;
          const lch = color.match(/lch\(\s*[\d.]+%?\s*([\d.-]+)/i);
          if (lch) return parseFloat(lch[1]) < 3;
        
          // oklab()/lab() — a and b are signed axes; chroma = sqrt(a² + b²).
          // oklab a/b are ~-0.4..0.4, threshold 0.02. lab a/b are ~-128..127, threshold 3.
          const oklab = color.match(/oklab\(\s*[\d.]+%?\s*([\d.-]+)\s+([\d.-]+)/i);
          if (oklab) {
            const a = parseFloat(oklab[1]), b = parseFloat(oklab[2]);
            return Math.hypot(a, b) < 0.02;
          }
          const lab = color.match(/lab\(\s*[\d.]+%?\s*([\d.-]+)\s+([\d.-]+)/i);
          if (lab) {
            const a = parseFloat(lab[1]), b = parseFloat(lab[2]);
            return Math.hypot(a, b) < 3;
          }
        
          // hsl/hsla — saturation is the second numeric component (percent).
          // Modern jsdom usually converts hsl() to rgb, but handle it directly for
          // safety across versions and for any engine that preserves the format.
          const hsl = color.match(/hsla?\(\s*[\d.-]+\s*,?\s*([\d.]+)%/i);
          if (hsl) return parseFloat(hsl[1]) < 10;
        
          // hwb(hue whiteness% blackness%) — a pixel is fully gray when
          // whiteness + blackness >= 100; chroma-like saturation = 1 - (w+b)/100.
          const hwb = color.match(/hwb\(\s*[\d.-]+\s+([\d.]+)%\s+([\d.]+)%/i);
          if (hwb) {
            const w = parseFloat(hwb[1]), b = parseFloat(hwb[2]);
            return (1 - Math.min(100, w + b) / 100) < 0.1;
          }
        
          // Unknown / unrecognized format — err on the side of DETECTING rather
          // than silently skipping. This is the opposite of the previous default,
          // which was the root cause of the oklch bug.
          return false;
        }
        
        function parseRgb(color) {
          if (!color || color === 'transparent') return null;
          const m = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
          if (!m) return null;
          return { r: +m[1], g: +m[2], b: +m[3], a: m[4] !== undefined ? +m[4] : 1 };
        }
        
        function relativeLuminance({ r, g, b }) {
          const [rs, gs, bs] = [r / 255, g / 255, b / 255].map(c =>
            c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4
          );
          return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
        }
        
        function contrastRatio(c1, c2) {
          const l1 = relativeLuminance(c1);
          const l2 = relativeLuminance(c2);
          return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
        }
        
        function parseGradientColors(bgImage) {
          if (!bgImage || !bgImage.includes('gradient')) return [];
          const colors = [];
          for (const m of bgImage.matchAll(/rgba?\([^)]+\)/g)) {
            const c = parseRgb(m[0]);
            if (c) colors.push(c);
          }
          for (const m of bgImage.matchAll(/#([0-9a-f]{6}|[0-9a-f]{3})\b/gi)) {
            const h = m[1];
            if (h.length === 6) {
              colors.push({ r: parseInt(h.slice(0,2),16), g: parseInt(h.slice(2,4),16), b: parseInt(h.slice(4,6),16), a: 1 });
            } else {
              colors.push({ r: parseInt(h[0]+h[0],16), g: parseInt(h[1]+h[1],16), b: parseInt(h[2]+h[2],16), a: 1 });
            }
          }
          return colors;
        }
        
        function hasChroma(c, threshold = 30) {
          if (!c) return false;
          return (Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b)) >= threshold;
        }
        
        function getHue(c) {
          if (!c) return 0;
          const r = c.r / 255, g = c.g / 255, b = c.b / 255;
          const max = Math.max(r, g, b), min = Math.min(r, g, b);
          if (max === min) return 0;
          const d = max - min;
          let h;
          if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
          else if (max === g) h = ((b - r) / d + 2) / 6;
          else h = ((r - g) / d + 4) / 6;
          return Math.round(h * 360);
        }
        
        function colorToHex(c) {
          if (!c) return '?';
          return '#' + [c.r, c.g, c.b].map(v => v.toString(16).padStart(2, '0')).join('');
        }
        
        // --- cli/engine/rules/checks.mjs ---
        const DETECTOR_IS_BROWSER = typeof window !== 'undefined';
        
        // ─── Section 3: Pure Detection ──────────────────────────────────────────────
        
        function checkBorders(tag, widths, colors, radius) {
          if (BORDER_SAFE_TAGS.has(tag)) return [];
          const findings = [];
          const sides = ['Top', 'Right', 'Bottom', 'Left'];
        
          for (const side of sides) {
            const w = widths[side];
            if (w < 1 || isNeutralColor(colors[side])) continue;
        
            const otherSides = sides.filter(s => s !== side);
            const maxOther = Math.max(...otherSides.map(s => widths[s]));
            if (!(w >= 2 && (maxOther <= 1 || w >= maxOther * 2))) continue;
        
            const sn = side.toLowerCase();
            const isSide = side === 'Left' || side === 'Right';
        
            if (isSide) {
              if (radius > 0) findings.push({ id: 'side-tab', snippet: `border-${sn}: ${w}px + border-radius: ${radius}px` });
              else if (w >= 3) findings.push({ id: 'side-tab', snippet: `border-${sn}: ${w}px` });
            } else {
              if (radius > 0 && w >= 2) findings.push({ id: 'border-accent-on-rounded', snippet: `border-${sn}: ${w}px + border-radius: ${radius}px` });
            }
          }
        
          return findings;
        }
        
        // Returns true if the given text is composed entirely of emoji characters
        // (plus whitespace / variation selectors). Emojis render as multicolor glyphs
        // regardless of CSS `color`, so contrast checks against the element's text
        // color are meaningless for these nodes.
        const EMOJI_CHAR_RE = /[\u{1F1E6}-\u{1F1FF}\u{1F300}-\u{1F9FF}\u{1FA00}-\u{1FAFF}\u{2600}-\u{27BF}\u{2300}-\u{23FF}\u{FE0F}\u{200D}\u{1F3FB}-\u{1F3FF}]/u;
        const EMOJI_CHARS_GLOBAL = /[\u{1F1E6}-\u{1F1FF}\u{1F300}-\u{1F9FF}\u{1FA00}-\u{1FAFF}\u{2600}-\u{27BF}\u{2300}-\u{23FF}\u{FE0F}\u{200D}\u{1F3FB}-\u{1F3FF}]/gu;
        function isEmojiOnlyText(text) {
          if (!text) return false;
          if (!EMOJI_CHAR_RE.test(text)) return false;
          return text.replace(EMOJI_CHARS_GLOBAL, '').trim() === '';
        }
        
        function checkColors(opts) {
          const { tag, textColor, bgColor, effectiveBg, effectiveBgStops, fontSize, fontWeight, hasDirectText, isEmojiOnly, bgClip, bgImage, classList } = opts;
          if (SAFE_TAGS.has(tag)) {
            // Exception for <a> and <button> elements styled as buttons. SAFE_TAGS
            // exists to suppress contrast noise on inline links and unstyled controls,
            // where the element has no own background and the contrast against the
            // ancestor surface is already the intended visual. When the element has
            // its own opaque background and direct text, it is a styled button — and
            // contrast on its own surface is a real, frequent bug worth flagging.
            const isStyledButton = (tag === 'a' || tag === 'button')
              && hasDirectText
              && bgColor && bgColor.a > 0.5;
            if (!isStyledButton) return [];
          }
          const findings = [];
        
          if (hasDirectText && textColor && !isEmojiOnly) {
            // Run background-dependent checks against either a solid bg or, if the
            // ancestor is a gradient, against every gradient stop (use the worst case).
            const bgs = effectiveBg ? [effectiveBg] : (effectiveBgStops && effectiveBgStops.length ? effectiveBgStops : null);
            if (bgs) {
              // Gray on colored background — flag if every stop is chromatic
              const textLum = relativeLuminance(textColor);
              const isGray = !hasChroma(textColor, 20) && textLum > 0.05 && textLum < 0.85;
              if (isGray && bgs.every(b => hasChroma(b, 40))) {
                const bgLabel = effectiveBg ? colorToHex(effectiveBg) : `gradient(${bgs.map(colorToHex).join(', ')})`;
                findings.push({ id: 'gray-on-color', snippet: `text ${colorToHex(textColor)} on bg ${bgLabel}` });
              }
        
              // Low contrast (WCAG AA) — worst case across all bg stops
              const ratios = bgs.map(b => contrastRatio(textColor, b));
              let worstIdx = 0;
              for (let i = 1; i < ratios.length; i++) if (ratios[i] < ratios[worstIdx]) worstIdx = i;
              const ratio = ratios[worstIdx];
              const isLargeText = fontSize >= WCAG_LARGE_TEXT_PX || (fontSize >= WCAG_LARGE_BOLD_TEXT_PX && fontWeight >= 700);
              const threshold = isLargeText ? 3.0 : 4.5;
              if (ratio < threshold) {
                // Skip the false-positive class where text has alpha < 1 AND we
                // couldn't find an opaque ancestor (effectiveBg is null, we're
                // comparing against gradient-stop fallback). In jsdom mode the
                // detector can't resolve `var(--X)` color tokens, so a dark
                // section sitting between the text and the body's decorative
                // gradient is invisible to us — we end up measuring contrast
                // against the body's paper-grain noise instead of the real
                // local bg. Real low-contrast bugs use alpha=1 and have a
                // resolvable opaque ancestor; semi-transparent Tailwind tokens
                // like `text-paper/60` on `bg-ink` sections are the FP pattern.
                const isAlphaFallbackFP = !DETECTOR_IS_BROWSER && !effectiveBg && (textColor.a != null && textColor.a < 1);
                if (!isAlphaFallbackFP) {
                  findings.push({ id: 'low-contrast', snippet: `${ratio.toFixed(1)}:1 (need ${threshold}:1) — text ${colorToHex(textColor)} on ${colorToHex(bgs[worstIdx])}` });
                }
              }
            }
        
            // AI palette: purple/violet on headings
            if (hasChroma(textColor, 50)) {
              const hue = getHue(textColor);
              if (hue >= 260 && hue <= 310 && (['h1', 'h2', 'h3'].includes(tag) || fontSize >= 20)) {
                findings.push({ id: 'ai-color-palette', snippet: `Purple/violet text (${colorToHex(textColor)}) on heading` });
              }
            }
          }
        
          // Gradient text
          if (bgClip === 'text' && bgImage && bgImage.includes('gradient')) {
            findings.push({ id: 'gradient-text', snippet: 'background-clip: text + gradient' });
          }
        
          // Tailwind class checks
          if (classList) {
            const classStr = typeof classList === 'string' ? classList : Array.from(classList).join(' ');
        
            const grayMatch = classStr.match(/\btext-(?:gray|slate|zinc|neutral|stone)-\d+\b/);
            const colorBgMatch = classStr.match(/\bbg-(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d+\b/);
            if (grayMatch && colorBgMatch) {
              findings.push({ id: 'gray-on-color', snippet: `${grayMatch[0]} on ${colorBgMatch[0]}` });
            }
        
            if (/\bbg-clip-text\b/.test(classStr) && /\bbg-gradient-to-/.test(classStr)) {
              findings.push({ id: 'gradient-text', snippet: 'bg-clip-text + bg-gradient (Tailwind)' });
            }
        
            const purpleText = classStr.match(/\btext-(?:purple|violet|indigo)-\d+\b/);
            if (purpleText && (['h1', 'h2', 'h3'].includes(tag) || /\btext-(?:[2-9]xl)\b/.test(classStr))) {
              findings.push({ id: 'ai-color-palette', snippet: `${purpleText[0]} on heading` });
            }
        
            if (/\bfrom-(?:purple|violet|indigo)-\d+\b/.test(classStr) && /\bto-(?:purple|violet|indigo|blue|cyan|pink|fuchsia)-\d+\b/.test(classStr)) {
              findings.push({ id: 'ai-color-palette', snippet: 'Purple/violet gradient (Tailwind)' });
            }
          }
        
          return findings;
        }
        
        function isCardLikeFromProps(hasShadow, hasBorder, hasRadius, hasBg) {
          if (!hasShadow && !hasBorder) return false;
          return hasRadius || hasBg;
        }
        
        const HEADING_TAGS = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']);
        
        // Pure check: given a heading and metrics about its previousElementSibling,
        // decide if the sibling is the canonical "icon-tile-stacked-above-heading" shape.
        //
        // Triggers when ALL of the following hold for the sibling:
        //   • size 32–128px on both axes (not too small, not a hero image)
        //   • aspect ratio 0.7–1.4 (squarish — excludes wide thumbnails / pill badges)
        //   • has a non-transparent background-color, background-image, OR a visible border
        //     (covers solid colors, white-with-border, gradients — anything that visually
        //      defines a tile)
        //   • border-radius < width/2 (excludes round avatars; rounded squares pass)
        //   • contains an <svg> or icon-class <i> element that's smaller than the tile
        //   • the tile sits above the heading (its bottom is above the heading's top)
        function checkIconTile(opts) {
          const { headingTag, headingText, headingTop,
                  siblingTag, siblingWidth, siblingHeight, siblingBottom,
                  siblingBgColor, siblingBgImage, siblingBorderWidth, siblingBorderRadius,
                  hasIconChild, iconChildWidth } = opts;
          if (!HEADING_TAGS.has(headingTag)) return [];
          if (!siblingTag) return [];
          // Don't recurse into nested headings (e.g. h2 above h3 in a section header)
          if (HEADING_TAGS.has(siblingTag)) return [];
        
          // Size window: 32–128px on each axis
          if (!(siblingWidth >= 32 && siblingWidth <= 128)) return [];
          if (!(siblingHeight >= 32 && siblingHeight <= 128)) return [];
        
          // Squarish aspect ratio
          const ratio = siblingWidth / siblingHeight;
          if (ratio < 0.7 || ratio > 1.4) return [];
        
          // Must have something that visually defines the tile
          const bgVisible = (siblingBgColor && siblingBgColor.a > 0.1)
            || (siblingBgImage && siblingBgImage !== 'none' && siblingBgImage !== '');
          const borderVisible = siblingBorderWidth > 0;
          if (!bgVisible && !borderVisible) return [];
        
          // Exclude circles (avatars). Rounded squares pass.
          if (siblingBorderRadius >= siblingWidth / 2) return [];
        
          // Must contain an icon element smaller than the tile
          if (!hasIconChild) return [];
          if (iconChildWidth && iconChildWidth >= siblingWidth * 0.95) return [];
        
          // Vertical stacking: tile must end above where the heading starts.
          // (Allow the check to skip when both top/bottom are 0 — jsdom layout case.)
          if (headingTop && siblingBottom && siblingBottom > headingTop + 4) return [];
        
          const text = (headingText || '').trim().slice(0, 60);
          return [{
            id: 'icon-tile-stack',
            snippet: `${Math.round(siblingWidth)}x${Math.round(siblingHeight)}px icon tile above ${headingTag} "${text}"`,
          }];
        }
        
        // Resolve the primary (non-generic) face from a font-family string and return
        // whether the resolved primary is serif. Two paths:
        //   1. Primary face is in KNOWN_SERIF_FONTS → serif.
        //   2. Primary face is unknown but the stack ends in the generic `serif`
        //      token → treat as serif. Authors who declare `font-family: 'X', serif`
        //      almost always have a serif primary; a sans declared with a serif
        //      fallback is a code smell, not the common case.
        // Returns { primary, isSerif } so the snippet can name the face.
        function resolveSerif(fontFamily) {
          if (!fontFamily) return { primary: null, isSerif: false };
          const tokens = fontFamily.split(',').map(f => f.trim().replace(/^['"]|['"]$/g, '').toLowerCase());
          const primary = tokens.find(f => f && !GENERIC_FONTS.has(f)) || null;
          if (!primary) return { primary: null, isSerif: false };
          if (KNOWN_SERIF_FONTS.has(primary)) return { primary, isSerif: true };
          if (tokens.includes('serif')) return { primary, isSerif: true };
          return { primary, isSerif: false };
        }
        
        function checkItalicSerif(opts) {
          const { tag, fontStyle, fontFamily, fontSize, headingText } = opts;
          if (fontStyle !== 'italic') return [];
          // Anchor the rule on hero-scale text. h1 is the canonical hero element;
          // h2 ≥ 48px catches the cases where the design demotes the visual hero
          // to an h2 but keeps the size.
          if (tag !== 'h1' && !(tag === 'h2' && fontSize >= 48)) return [];
          if (fontSize < 48) return [];
          const { primary, isSerif } = resolveSerif(fontFamily);
          if (!isSerif) return [];
        
          const text = (headingText || '').trim().slice(0, 60);
          return [{
            id: 'italic-serif-display',
            snippet: `italic serif ${tag} (${primary || 'serif'}) at ${Math.round(fontSize)}px "${text}"`,
          }];
        }
        
        // Color saturation check. Returns true when the color has visible
        // chroma — i.e., it's an "accent color" rather than near-neutral.
        // Handles rgb()/rgba(), #hex, oklch(), and hsl(). var() refs are
        // expected to be pre-resolved by the caller.
        function isAccentColor(cssColor) {
          if (!cssColor) return false;
          const s = String(cssColor).trim();
          // rgb / rgba — direct channel-distance check.
          const rgbM = /rgba?\(\s*(\d+)\s*,?\s+|\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/.exec(s.replace(/rgba?\(\s*/, 'rgb(').replace(/,/g, ', '));
          const rgbStrict = /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/.exec(s);
          if (rgbStrict) {
            const r = +rgbStrict[1], g = +rgbStrict[2], b = +rgbStrict[3];
            return (Math.max(r, g, b) - Math.min(r, g, b)) >= 40;
          }
          // #hex — 3, 4, 6, or 8 digit.
          const hexM = /^#([0-9a-f]{3,8})\b/i.exec(s);
          if (hexM) {
            let h = hexM[1];
            if (h.length === 3 || h.length === 4) h = h.split('').map((c) => c + c).join('').slice(0, 6);
            else h = h.slice(0, 6);
            if (h.length === 6) {
              const r = parseInt(h.slice(0, 2), 16);
              const g = parseInt(h.slice(2, 4), 16);
              const b = parseInt(h.slice(4, 6), 16);
              return (Math.max(r, g, b) - Math.min(r, g, b)) >= 40;
            }
          }
          // oklch(L C H) — chroma C is what matters. Typical neutral grays
          // have C < 0.02; visible accents are 0.05+. CSS minification can
          // collapse spaces between L% and C ("oklch(43%.15 34)"), so we
          // extract all numbers and take the second rather than matching a
          // strict L-then-whitespace-then-C pattern.
          if (/^oklch\(/i.test(s)) {
            const nums = s.match(/\d*\.\d+|\d+/g);
            if (nums && nums.length >= 2) {
              const c = parseFloat(nums[1]);
              return !Number.isNaN(c) && c >= 0.05;
            }
          }
          // hsl(H, S%, L%) — saturation > 20% reads as accent.
          const hslM = /hsla?\(\s*[\d.]+\s*,\s*([\d.]+)%/i.exec(s);
          if (hslM) {
            const sat = parseFloat(hslM[1]);
            return !Number.isNaN(sat) && sat >= 20;
          }
          return false;
        }
        
        // Sibling-relationship rule. Anchor on a hero-scale h1, look at the
        // previousElementSibling, and gate on EITHER the classic tracked-
        // uppercase eyebrow OR the modern accent-colored bold eyebrow.
        function checkHeroEyebrow(opts) {
          const {
            headingTag, headingText, headingFontSize,
            siblingTag, siblingText, siblingTextTransform,
            siblingFontSize, siblingLetterSpacing,
            siblingFontWeight, siblingColor,
          } = opts;
          if (headingTag !== 'h1') return [];
          // We previously gated on headingFontSize >= 48 to anchor "hero scale".
          // But modern hero h1s use clamp() / vw / var(--text-*), none of which
          // jsdom can resolve — the computed value comes back as "2em" or
          // "var(--text-9xl)" and parseFloat returns 2 or NaN. The gate fails
          // on virtually every Tailwind v4 / framework build. The other gates
          // (sibling text 2-60 chars, font-size ≤ 14px, accent-bold OR
          // tracked-caps) are tight enough to avoid false positives on non-
          // hero h1s — a tiny tan label directly above any h1 is the
          // antipattern regardless of how big the h1 ends up.
          if (!siblingTag) return [];
          // An h2 above an h1 is a different anti-pattern (heading hierarchy / dual
          // headings) — never an eyebrow.
          if (HEADING_TAGS.has(siblingTag)) return [];
        
          const text = (siblingText || '').trim();
          if (text.length < 2 || text.length > 60) return [];
          if (!(siblingFontSize > 0 && siblingFontSize <= 14)) return [];
        
          // Branch A: classic tracked-uppercase eyebrow.
          const isUppercased = siblingTextTransform === 'uppercase'
            || (/[A-Z]/.test(text) && !/[a-z]/.test(text));
          const isClassicTracked = isUppercased && siblingLetterSpacing >= 1.6;
        
          // Branch B: modern accent-bold eyebrow — sentence case, low
          // tracking, but bold + accent-colored. The style choices changed;
          // the pattern is the same kicker-above-headline anti-pattern.
          const weight = Number(siblingFontWeight) || 400;
          const isAccentBold = weight >= 700 && isAccentColor(siblingColor || '');
        
          if (!isClassicTracked && !isAccentBold) return [];
        
          const headingTextSnippet = (headingText || '').trim().slice(0, 60);
          const eyebrowSnippet = text.slice(0, 40);
          const style = isClassicTracked ? 'tracked-caps' : 'accent-bold';
          return [{
            id: 'hero-eyebrow-chip',
            snippet: `eyebrow chip (${style}) "${eyebrowSnippet}" above ${headingTag} "${headingTextSnippet}"`,
          }];
        }
        
        function checkRepeatedSectionKickers(opts) {
          const { candidates, minCount = 3 } = opts;
          if (!Array.isArray(candidates) || candidates.length < minCount) return [];
          return candidates.map(candidate => ({
            id: 'repeated-section-kickers',
            snippet: `repeated section kicker "${candidate.kickerText}" before ${candidate.headingTag} "${candidate.headingText}" (${candidates.length} on page)`,
          }));
        }
        
        const LAYOUT_TRANSITION_PROPS = new Set([
          'width', 'height', 'padding', 'margin',
          'max-height', 'max-width', 'min-height', 'min-width',
          'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
          'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
        ]);
        
        function checkMotion(opts) {
          const { tag, transitionProperty, animationName, timingFunctions, classList } = opts;
          if (SAFE_TAGS.has(tag)) return [];
          const findings = [];
        
          // --- Bounce/elastic easing ---
          if (animationName && animationName !== 'none' && /bounce|elastic|wobble|jiggle|spring/i.test(animationName)) {
            findings.push({ id: 'bounce-easing', snippet: `animation: ${animationName}` });
          }
          if (classList && /\banimate-bounce\b/.test(classList)) {
            findings.push({ id: 'bounce-easing', snippet: 'animate-bounce (Tailwind)' });
          }
        
          // Check timing functions for overshoot cubic-bezier (y values outside [0, 1])
          if (timingFunctions) {
            const bezierRe = /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g;
            let m;
            while ((m = bezierRe.exec(timingFunctions)) !== null) {
              const y1 = parseFloat(m[2]), y2 = parseFloat(m[4]);
              if (y1 < -0.1 || y1 > 1.1 || y2 < -0.1 || y2 > 1.1) {
                findings.push({ id: 'bounce-easing', snippet: `cubic-bezier(${m[1]}, ${m[2]}, ${m[3]}, ${m[4]})` });
                break;
              }
            }
          }
        
          // --- Layout property transition ---
          if (transitionProperty && transitionProperty !== 'all' && transitionProperty !== 'none') {
            const props = transitionProperty.split(',').map(p => p.trim().toLowerCase());
            const layoutFound = props.filter(p => LAYOUT_TRANSITION_PROPS.has(p));
            if (layoutFound.length > 0) {
              findings.push({ id: 'layout-transition', snippet: `transition: ${layoutFound.join(', ')}` });
            }
          }
        
          return findings;
        }
        
        function checkGlow(opts) {
          const { boxShadow, effectiveBg } = opts;
          if (!boxShadow || boxShadow === 'none') return [];
          if (!effectiveBg) return [];
        
          // Only flag on dark backgrounds (luminance < 0.1)
          const bgLum = relativeLuminance(effectiveBg);
          if (bgLum >= 0.1) return [];
        
          // Split multiple shadows (commas not inside parentheses)
          const parts = boxShadow.split(/,(?![^(]*\))/);
          for (const shadow of parts) {
            const colorMatch = shadow.match(/rgba?\([^)]+\)/);
            if (!colorMatch) continue;
            const color = parseRgb(colorMatch[0]);
            if (!color || !hasChroma(color, 30)) continue;
        
            // Extract px values — in computed style: "color Xpx Ypx BLURpx [SPREADpx]"
            const afterColor = shadow.substring(shadow.indexOf(colorMatch[0]) + colorMatch[0].length);
            const beforeColor = shadow.substring(0, shadow.indexOf(colorMatch[0]));
            const pxVals = [...beforeColor.matchAll(/([\d.]+)px/g), ...afterColor.matchAll(/([\d.]+)px/g)]
              .map(m => parseFloat(m[1]));
        
            // Third value is blur (offset-x, offset-y, blur, [spread])
            if (pxVals.length >= 3 && pxVals[2] > 4) {
              return [{ id: 'dark-glow', snippet: `Colored glow (${colorToHex(color)}) on dark background` }];
            }
          }
        
          return [];
        }
        
        /**
         * Regex-on-HTML checks shared between browser and Node page-level detection.
         * These don't need DOM access, just the raw HTML string.
         */
        function checkHtmlPatterns(html) {
          const findings = [];
        
          // --- Color ---
        
          // AI color palette: purple/violet
          const purpleHexRe = /#(?:7c3aed|8b5cf6|a855f7|9333ea|7e22ce|6d28d9|6366f1|764ba2|667eea)\b/gi;
          if (purpleHexRe.test(html)) {
            const purpleTextRe = /(?:(?:^|;)\s*color\s*:\s*(?:.*?)(?:#(?:7c3aed|8b5cf6|a855f7|9333ea|7e22ce|6d28d9))|gradient.*?#(?:7c3aed|8b5cf6|a855f7|764ba2|667eea))/gi;
            if (purpleTextRe.test(html)) {
              findings.push({ id: 'ai-color-palette', snippet: 'Purple/violet accent colors detected' });
            }
          }
        
          // Gradient text (background-clip: text + gradient)
          const gradientRe = /(?:-webkit-)?background-clip\s*:\s*text/gi;
          let gm;
          while ((gm = gradientRe.exec(html)) !== null) {
            const start = Math.max(0, gm.index - 200);
            const context = html.substring(start, gm.index + gm[0].length + 200);
            if (/gradient/i.test(context)) {
              findings.push({ id: 'gradient-text', snippet: 'background-clip: text + gradient' });
              break;
            }
          }
          if (/\bbg-clip-text\b/.test(html) && /\bbg-gradient-to-/.test(html)) {
            findings.push({ id: 'gradient-text', snippet: 'bg-clip-text + bg-gradient (Tailwind)' });
          }
        
          // --- Layout ---
        
          // Monotonous spacing
          const spacingValues = [];
          const spacingRe = /(?:padding|margin)(?:-(?:top|right|bottom|left))?\s*:\s*(\d+)px/gi;
          let sm;
          while ((sm = spacingRe.exec(html)) !== null) {
            const v = parseInt(sm[1], 10);
            if (v > 0 && v < 200) spacingValues.push(v);
          }
          const gapRe = /gap\s*:\s*(\d+)px/gi;
          while ((sm = gapRe.exec(html)) !== null) {
            spacingValues.push(parseInt(sm[1], 10));
          }
          const twSpaceRe = /\b(?:p|px|py|pt|pb|pl|pr|m|mx|my|mt|mb|ml|mr|gap)-(\d+)\b/g;
          while ((sm = twSpaceRe.exec(html)) !== null) {
            spacingValues.push(parseInt(sm[1], 10) * 4);
          }
          const remSpacingRe = /(?:padding|margin)(?:-(?:top|right|bottom|left))?\s*:\s*([\d.]+)rem/gi;
          while ((sm = remSpacingRe.exec(html)) !== null) {
            const v = Math.round(parseFloat(sm[1]) * 16);
            if (v > 0 && v < 200) spacingValues.push(v);
          }
          const roundedSpacing = spacingValues.map(v => Math.round(v / 4) * 4);
          if (roundedSpacing.length >= 10) {
            const counts = {};
            for (const v of roundedSpacing) counts[v] = (counts[v] || 0) + 1;
            const maxCount = Math.max(...Object.values(counts));
            const dominantPct = maxCount / roundedSpacing.length;
            const unique = [...new Set(roundedSpacing)].filter(v => v > 0);
            if (dominantPct > 0.6 && unique.length <= 3) {
              const dominant = Object.entries(counts).sort((a, b) => b[1] - a[1])[0][0];
              findings.push({
                id: 'monotonous-spacing',
                snippet: `~${dominant}px used ${maxCount}/${roundedSpacing.length} times (${Math.round(dominantPct * 100)}%)`,
              });
            }
          }
        
          // --- Motion ---
        
          // Bounce/elastic animation names
          const bounceRe = /animation(?:-name)?\s*:\s*([^;{}]*(?:bounce|elastic|wobble|jiggle|spring)[^;{}]*)/gi;
          const bounceMatch = bounceRe.exec(html);
          if (bounceMatch) {
            const animationToken = bounceMatch[1]
              .split(/[,\s]+/)
              .find((part) => /bounce|elastic|wobble|jiggle|spring/i.test(part));
            findings.push({ id: 'bounce-easing', snippet: `animation: ${animationToken || bounceMatch[1].trim()}` });
          }
        
          // Overshoot cubic-bezier
          const bezierRe = /cubic-bezier\(\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*,\s*([\d.-]+)\s*\)/g;
          let bm;
          while ((bm = bezierRe.exec(html)) !== null) {
            const y1 = parseFloat(bm[2]), y2 = parseFloat(bm[4]);
            if (y1 < -0.1 || y1 > 1.1 || y2 < -0.1 || y2 > 1.1) {
              findings.push({ id: 'bounce-easing', snippet: `cubic-bezier(${bm[1]}, ${bm[2]}, ${bm[3]}, ${bm[4]})` });
              break;
            }
          }
        
          // Layout property transitions
          const transRe = /transition(?:-property)?\s*:\s*([^;{}]+)/gi;
          let tm;
          while ((tm = transRe.exec(html)) !== null) {
            const val = tm[1].toLowerCase();
            if (/\ball\b/.test(val)) continue;
            const found = val.match(/\b(?:(?:max|min)-)?(?:width|height)\b|\bpadding(?:-(?:top|right|bottom|left))?\b|\bmargin(?:-(?:top|right|bottom|left))?\b/gi);
            if (found) {
              findings.push({ id: 'layout-transition', snippet: `transition: ${found.join(', ')}` });
              break;
            }
          }
        
          // --- Dark glow ---
        
          const darkBgRe = /background(?:-color)?\s*:\s*(?:#(?:0[0-9a-f]|1[0-9a-f]|2[0-3])[0-9a-f]{4}\b|#(?:0|1)[0-9a-f]{2}\b|rgb\(\s*(\d{1,2})\s*,\s*(\d{1,2})\s*,\s*(\d{1,2})\s*\))/gi;
          const twDarkBg = /\bbg-(?:gray|slate|zinc|neutral|stone)-(?:9\d{2}|800)\b/;
          if (darkBgRe.test(html) || twDarkBg.test(html)) {
            const shadowRe = /box-shadow\s*:\s*([^;{}]+)/gi;
            let shm;
            while ((shm = shadowRe.exec(html)) !== null) {
              const val = shm[1];
              const colorMatch = val.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
              if (!colorMatch) continue;
              const [r, g, b] = [+colorMatch[1], +colorMatch[2], +colorMatch[3]];
              if ((Math.max(r, g, b) - Math.min(r, g, b)) < 30) continue;
              const pxVals = [...val.matchAll(/(\d+)px|(?<![.\d])\b(0)\b(?![.\d])/g)].map(p => +(p[1] || p[2]));
              if (pxVals.length >= 3 && pxVals[2] > 4) {
                findings.push({ id: 'dark-glow', snippet: `Colored glow (rgb(${r},${g},${b})) on dark page` });
                break;
              }
            }
          }
        
          // --- Provider tells (gated): repeating-gradient stripes (GPT) ---
          if (/repeating-(?:linear|radial|conic)-gradient\s*\(/i.test(html)) {
            findings.push({ id: 'repeating-stripes-gradient', snippet: 'repeating-gradient decorative stripes' });
          }
        
          // --- Provider tells (gated): two-axis grid-line background (Codex/GPT) ---
          // The Codex grid tell is two hairline `linear-gradient(... <color> 1px,
          // transparent 1px)` layers (one per axis) tiled by a repeating
          // `background-size` cell. Both signals must co-occur in the SAME style block
          // (a CSS rule body or one inline `style="..."`): two hairline stops WITHOUT a
          // tiling background-size is a fixed crosshair, not a grid, and a single
          // hairline is a legitimate ruled line. Scoping to one block also stops
          // unrelated single-axis rules on separate elements from adding up across the
          // page. Count hairlines only inside `background`/`background-image` values so
          // a hairline in an unrelated property (mask-image, border-image) can't stand
          // in for the second axis. Colors like `oklch(96% 0.012 82 / 0.055)` carry
          // nested parens, so match the hairline stop directly rather than parsing
          // whole gradient layers.
          {
            const hairlineRe = /\b\d{1,3}px\s*,\s*transparent\s+\d{1,3}px/gi;
            const gridSizeRe = /background-size\s*:[^;{}"']*\b\d{1,3}px\b/i;
            const bgDeclRe = /\bbackground(?:-image)?\s*:\s*([^;{}"']*)/gi;
            const blockRe = /\{([^{}]*)\}|style\s*=\s*"([^"]*)"|style\s*=\s*'([^']*)'/gi;
            let blk;
            while ((blk = blockRe.exec(html)) !== null) {
              const block = blk[1] || blk[2] || blk[3] || '';
              if (!gridSizeRe.test(block)) continue;
              let hairlineCount = 0;
              let bm;
              bgDeclRe.lastIndex = 0;
              while ((bm = bgDeclRe.exec(block)) !== null) {
                const stops = bm[1].match(hairlineRe);
                if (stops) hairlineCount += stops.length;
              }
              if (hairlineCount >= 2) {
                findings.push({ id: 'codex-grid-background', snippet: 'two-axis grid-line gradient background' });
                break;
              }
            }
          }
        
          // --- Provider tells (gated): "X theater" framing copy (GPT) ---
          // Lives here (regex-on-HTML) rather than in the text-content analyzers so it
          // runs in the bundled browser path too, not just the CLI/static path.
          {
            const bodyText = html
              .replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, ' ')
              .replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, ' ')
              .replace(/<[^>]+>/g, ' ');
            const tm = /\b(\w+)\s+theater\b/i.exec(bodyText);
            if (tm) findings.push({ id: 'theater-slop-phrase', snippet: `"${tm[0].trim()}"` });
          }
        
          // --- Provider tells (gated): image hover transform (Gemini) ---
          // A CSS `img...:hover { transform: ... }` rule, or a Tailwind hover:scale /
          // hover:rotate / hover:translate utility on an <img>. Each distinct
          // mechanism is its own finding.
          const imgHoverCss = /\bimg\b[^,{}]*:hover\b[^{}]*\{[^}]*\btransform\s*:\s*(?:scale|rotate|translate|matrix|skew)/i;
          if (imgHoverCss.test(html)) {
            findings.push({ id: 'image-hover-transform', snippet: 'img:hover { transform } rule' });
          }
          const imgTagRe = /<img\b[^>]*\bclass\s*=\s*"([^"]*)"/gi;
          let im;
          while ((im = imgTagRe.exec(html)) !== null) {
            if (/\bhover:(?:scale|rotate|translate|skew)-/.test(im[1])) {
              findings.push({ id: 'image-hover-transform', snippet: 'Tailwind hover transform on <img>' });
            }
          }
        
          return findings;
        }
        
        // ─── Section 4: resolveBackground (unified) ─────────────────────────────────
        
        // Read the element's own background color, computed-style first, with a
        // jsdom-friendly fallback that parses the inline `background:` shorthand
        // from the raw style attribute. jsdom (~v29) does not decompose the
        // shorthand into `backgroundColor`, so without this fallback the CLI silently
        // returns null for any element styled via `background: rgb(...)` or
        // `background: #abc`. Real browsers always decompose, so the fallback is
        // a no-op there.
        function readOwnBackgroundColor(el, computedStyle) {
          const bg = parseRgb(computedStyle.backgroundColor);
          if (DETECTOR_IS_BROWSER || (bg && bg.a >= 0.1)) return bg;
          const rawStyle = el.getAttribute?.('style') || '';
          const bgMatch = rawStyle.match(/background(?:-color)?\s*:\s*([^;]+)/i);
          const inlineBg = bgMatch ? bgMatch[1].trim() : '';
          if (!inlineBg) return bg;
          if (/gradient/i.test(inlineBg) || /url\s*\(/i.test(inlineBg)) return bg;
          const fromRgb = parseRgb(inlineBg);
          if (fromRgb) return fromRgb;
          const hexMatch = inlineBg.match(/#([0-9a-f]{6}|[0-9a-f]{3})\b/i);
          if (hexMatch) {
            const h = hexMatch[1];
            if (h.length === 6) {
              return { r: parseInt(h.slice(0, 2), 16), g: parseInt(h.slice(2, 4), 16), b: parseInt(h.slice(4, 6), 16), a: 1 };
            }
            return { r: parseInt(h[0] + h[0], 16), g: parseInt(h[1] + h[1], 16), b: parseInt(h[2] + h[2], 16), a: 1 };
          }
          return bg;
        }
        
        function resolveBackground(el, win, customPropMap) {
          let current = el;
          while (current && current.nodeType === 1) {
            const style = DETECTOR_IS_BROWSER ? getComputedStyle(current) : win.getComputedStyle(current);
            const bgImage = style.backgroundImage || '';
            const hasGradientOrUrl = bgImage && bgImage !== 'none' && (/gradient/i.test(bgImage) || /url\s*\(/i.test(bgImage));
        
            // Try the solid bg-color FIRST. If the element has both a solid color
            // and a gradient/url overlay (a common pattern: `background: var(--paper)
            // radial-gradient(...)` for paper-grain texture), the solid color is the
            // dominant visible surface for contrast purposes; the overlay is
            // decorative. The old behavior bailed on any gradient ancestor, which
            // caused massive false-positive contrast findings on grain-textured
            // body backgrounds.
            let bg = parseRgb(style.backgroundColor);
            if (!DETECTOR_IS_BROWSER && (!bg || bg.a < 0.1)) {
              // jsdom returns literal "var(--X)" / "oklch(...)" strings. Resolve
              // through customPropMap so Tailwind v4 color tokens become RGB.
              if (customPropMap) {
                bg = parseColorResolved(style.backgroundColor, customPropMap);
              }
              if (!bg || bg.a < 0.1) {
                // Inline-style fallback. jsdom doesn't decompose background
                // shorthand, so colors set via inline style are otherwise invisible.
                const rawStyle = current.getAttribute?.('style') || '';
                const bgMatch = rawStyle.match(/background(?:-color)?\s*:\s*([^;]+)/i);
                const inlineBg = bgMatch ? bgMatch[1].trim() : '';
                if (inlineBg && !/gradient/i.test(inlineBg) && !/url\s*\(/i.test(inlineBg)) {
                  bg = parseColorResolved(inlineBg, customPropMap) || parseAnyColor(inlineBg);
                }
              }
            }
        
            if (bg && bg.a > 0.1) {
              if (DETECTOR_IS_BROWSER || bg.a >= 0.5) return bg;
            }
            // No solid bg-color at this level. If THIS level has a gradient/url
            // with no underlying solid color we can read:
            //   • on body/html: assume white. Body-level gradients are almost
            //     always decorative texture (paper grain, noise) on top of a
            //     solid bg-color the page set via `background: var(--paper)`
            //     shorthand — which jsdom can't decompose into bg-color. The
            //     downstream gradient-stops fallback path produces catastrophic
            //     false positives in this case (gradient noise stops have
            //     accidental browns/blacks that look like card backgrounds).
            //   • on other elements: bail to null and let the caller fall back
            //     to gradient stops (gradient buttons / hero sections are real
            //     bgs worth checking against).
            if (hasGradientOrUrl) {
              if (current.tagName === 'BODY' || current.tagName === 'HTML') {
                return { r: 255, g: 255, b: 255, a: 1 };
              }
              return null;
            }
            current = current.parentElement;
          }
          return { r: 255, g: 255, b: 255 };
        }
        
        // Walk parents looking for a gradient background and return its color stops.
        // Used as a fallback when resolveBackground() returns null because the
        // effective background is a gradient (no single solid color to compare against).
        function resolveGradientStops(el, win) {
          let current = el;
          while (current && current.nodeType === 1) {
            const style = DETECTOR_IS_BROWSER ? getComputedStyle(current) : win.getComputedStyle(current);
            const bgImage = style.backgroundImage || '';
            if (bgImage && bgImage !== 'none' && /gradient/i.test(bgImage)) {
              const stops = parseGradientColors(bgImage);
              if
      • detect-antipatterns.mjs 1.7 KB · in bundle
      • findings.mjs 363 B · in bundle
    • lib
      • design-parser.mjs 25.2 KB · in bundle
      • impeccable-config.mjs 21 KB · in bundle
      • impeccable-paths.mjs 4.4 KB · in bundle
      • is-generated.mjs 2.2 KB · in bundle
      • main-guard.mjs 1.2 KB · in bundle
      • target-args.mjs 1.1 KB · in bundle
    • context-signals.mjs 7.7 KB · in bundle
    • context.mjs 33.3 KB · in bundle
    • critique-storage.mjs 8.1 KB · in bundle
    • detect-csp.mjs 6.6 KB · in bundle
    • detect.mjs 620 B · in bundle
    • palette.mjs 57 KB · in bundle
    • route.mjs 2.5 KB · in bundle
  • SKILL.md 12.2 KB
    ---
    name: ui-craft
    description: Lifecycle for user-facing surfaces — revise a shipped surface in the running app, lock a greenfield visual spec, build to a lock, critique/audit/polish a UI, or re-settle a locked term. Use for any request to design, review, or verify rendered UI (screens, dashboards, flows, components). Not for backend-only work or module/API design ("interface" in the code sense — use codebase-design for that).
    ---
    
    # UI craft
    
    One skill for the whole life of a user-facing surface: **revise** a shipped
    surface in place, **lock** a greenfield visual spec, **build** to it,
    **critique** it, **audit** it, **polish** it, **re-settle** it.
    Parts absorbed from `impeccable` (Apache-2.0, by Paul Bakaus — see the repo
    NOTICE).
    
    Vocabulary guard: in the engineering charter, *interface* means a module's API.
    This skill owns **surfaces** — rendered UI. If the request is about a Python
    class, function signature, or module boundary, this is the wrong skill.
    
    ## Setup (every invocation)
    
    1. Resolve this skill's installed directory as `UI_CRAFT_SKILL_DIR` (e.g.
       `~/.claude/skills/ui-craft`).
    2. Run `node $UI_CRAFT_SKILL_DIR/scripts/context.mjs` once per session
       (`--target <path>` inside a monorepo). It prints PRODUCT.md / DESIGN.md or
       reports `NO_PRODUCT_MD` — in that case follow `reference/init.md` first.
       Ignore any `UPDATE_AVAILABLE` directive; this is a maintained fork.
       If `node` is unavailable or a script here errors, say so, read
       PRODUCT.md / DESIGN.md directly, and continue — the scripts are
       accelerators, not gates.
    
    Before routing any design change:
    
    3. Resolve whether the app already embodies the surface (`shipped` or
       `greenfield`). For a shipped surface, classify local runnability as `runnable`
       or `unavailable`; inspect its `CLAUDE.md` / `AGENTS.md` dev-server declaration
       and classify it as `absent`, `complete`, `incomplete` or `ambiguous`; classify
       its named data source as `manufactured`, `synthetic` or `unknown`.
    4. Run `node $UI_CRAFT_SKILL_DIR/scripts/route.mjs --embodiment <state>
       --runnability <state> --declaration <state> --data-source <kind>` and obey its
       mode. A `refuse` result is blocking. `lock-fallback` is the recorded
       predecessor fallback in `reference/revise.md`, not ordinary `lock`
       permission.
    
    Ticket triage may prepare a shipped surface's first-revision evidence in its
    admitted checkout only after this route verifies a safe start and manufactured
    data. It runs `behavior-sweep`, then freezes the ledger and replay through the
    existing sanction path before source admission. This authorizes evidence
    preparation, not implementation, a new mock, or a replacement checkout/base.
    
    For `revise`, `lock`, `build`, and general design invocations only:
    
    5. Read the project's design system: tokens, theme, one representative
       component or page. Use what's there when it works. **The shipped app wins
       over any mock scaffold**: when a repo carries both an app stylesheet and a
       `mockups/` theme, the app is chrome ground truth for every surface that
       has shipped. `revise` operates on that app directly; `lock` mode's step-0
       pre-flight (reference/lock.md) refuses to replace it with a fresh mock.
    6. Read the matching register reference: `reference/brand.md` when design IS
       the product (marketing, landing, portfolio), `reference/product.md` when
       design SERVES the product (app UI, dashboards, tools).
    7. New project with no committed tokens: run
       `node $UI_CRAFT_SKILL_DIR/scripts/palette.mjs` for a brand seed.
    
    ## The contract follows the surface
    
    For a greenfield surface that has been through `lock`, the contract is:
    
    - a `★ LOCKED` header in the mockup HTML, and
    - a **lock manifest** — `mockups/<surface>.lock.md` — the checkable inventory
      `build` reads. Format in `reference/lock.md`.
    
    For a surface the app already ships, `revise` bans a from-scratch mock. Its
    contract is the frozen **behavior ledger** plus its replay script, exercised
    against the built app. The app branch is the visual artifact; screenshots record
    the review, but no lock manifest is pinned to an app template.
    
    Rules that bind every mode:
    
    - **No arbitration in private.** If two locked artifacts disagree, a locked
      term collides with the app's shipped design system beyond what the manifest's
      precedence line settles, or a revision drops shipped behavior, stop and ask.
      Implementer judgment never silently overrides a contract.
    - **Deviation is recorded.** Any build or refactor that changes a locked term
      goes through `re-settle`; any revision that changes shipped behavior amends
      the frozen behavior ledger. Both paths are dated and sanctioned, never quiet.
    - **Evidence over green gates.** A locked surface is done when every manifest
      term has evidence; a revision is done when every behavior story replayed and
      every affected state has before/after evidence. A green suite alone is never
      the finish line.
    
    ## Modes
    
    Route on the first word (or obvious intent). Read the mode's reference before
    acting — it defines the flow.
    
    | Mode | Job | Reference |
    | --- | --- | --- |
    | `revise [surface]` | Inventory shipped behavior, then iterate the running app on a branch; never mock the shipped surface from scratch | [reference/revise.md](reference/revise.md) |
    | `lock [surface]` | Explore grounded variants for a greenfield surface, converge, lock spec + manifest | [reference/lock.md](reference/lock.md) |
    | `behavior-sweep [surface]` | Freeze interactive behavior into a ledger + replay script. `revise` runs it against the built app before design; the lock fallback also diffs a shipped predecessor before locking | [reference/behavior-sweep.md](reference/behavior-sweep.md) |
    | `build [surface]` | Implement a locked spec; ship the fidelity ledger | [reference/build.md](reference/build.md) |
    | `critique [target]` | Heuristic scoring, slop verdict, persona walkthroughs | [reference/critique.md](reference/critique.md) |
    | `audit [target]` | Technical checks (a11y, contrast, responsive, detector) + lock-fidelity audit when a manifest exists | [reference/audit.md](reference/audit.md) |
    | `polish [target]` | Pre-ship quality gate; includes the manifest walk | [reference/polish.md](reference/polish.md) |
    | `resettle [term]` | Amend a locked term with record-keeping | [reference/resettle.md](reference/resettle.md) |
    | `consensus [question]` | Settle a contested design decision via a 3-persona vote-and-negotiate panel (advisory; requires repo personas) | [reference/consensus.md](reference/consensus.md) |
    | `init` / `document` | Project context setup / generate DESIGN.md | [reference/init.md](reference/init.md), [reference/document.md](reference/document.md) |
    
    No argument: recommend the 1–3 most useful modes from context (a shipped
    surface → `revise`; a greenfield surface or explicitly recorded safe-start
    fallback → `lock`; an unlocked fallback mock with no predecessor verdicts →
    `behavior-sweep`'s predecessor pass **before** `lock`; frozen manifest for a
    greenfield surface with interactive behavior and no behavior ledger →
    `behavior-sweep` **before** `build`; open manifest without a fidelity ledger →
    `build`; never critiqued → `critique`), then list the table. Never auto-run a
    mode.
    
    Three artifacts carry the word *ledger*; always qualify it. The **fidelity
    ledger** is one row per manifest term (`build`; `revise` has none). The
    **behavior ledger** is
    `mockups/<surface>.behavior.md`, one entry per story plus one permanent entry
    per sanctioned retirement (`behavior-sweep`). The **surface ledger** is
    `mockups/INDEX.md`, one row per surface.
    
    General design invocations with no mode match (e.g. "make this less bland",
    "fix the spacing") route by embodiment: shipped surface → `revise`-then-fix;
    greenfield surface → `critique`-then-fix. Use
    [reference/design-rules.md](reference/design-rules.md) for the craft pass.
    
    When a mode settles an answer or completes a helper, resume the caller's next
    authorized step. Stop only the dependent work for an unanswered decision, denied
    sanction, unavailable required evidence, or explicit handoff.
    
    **Web implementation.** For `build`, `revise`, and general UI implementation that
    changes CSS or JavaScript/TypeScript, read
    [reference/web-implementation.md](reference/web-implementation.md) before changing
    code. It governs web mechanics; UI Craft retains lifecycle, visual-contract,
    behavior-preservation, and rendered-evidence ownership.
    
    ## Design rules (all modes)
    
    [reference/design-rules.md](reference/design-rules.md) carries the shared
    craft discipline: the design-brief template, token-system-before-components,
    typography/color/layout/motion rules, the absolute bans (side-stripes,
    gradient text, default glassmorphism, hero-metric template, identical card
    grids, eyebrow-on-every-section…), the AI-slop and category-reflex tests, and
    the layered critique order. It is required reading for `lock`, `build`, and
    any general invocation; the other modes consult it as needed.
    
    ## Personas
    
    Persona walkthroughs live in `critique` (five built-in archetypes plus
    project-specific ones). **Repo personas win:** if the repo has its own persona
    definitions (canonically `.claude/qa/personas/*.md` — written by `init`'s
    persona-panel step — or a location named in its CLAUDE.md/AGENTS.md), use
    those personas — a diabetic user, a concerned parent — instead of inventing
    equivalents, and follow the repo's sweep protocol when one exists. The
    `consensus` mode requires these repo personas and refuses to run on generic
    archetypes.
    
    **"The repo's sweep protocol" is not `behavior-sweep`.** That phrase means the
    repo's persona-driven QA pass — exploratory, judgment-led, run against a live
    app to find bugs. `behavior-sweep` is a mode of this skill: mechanical, run
    against the **built app** before a revision, or against a locked mock plus its
    shipped predecessor on the explicit fallback path. Its output is a contract.
    Neither substitutes for the other.
    
    ## Grounding rules (inherited from ui-mockups, apply everywhere)
    
    - Ground every artifact in the app's real tokens, shipping UI/chart library
      at its shipping version, and real data shape from a **safe, manufactured
      fixture** — never production, personal, health, credential, or customer
      data. **This is the shared default and it does not bend on convenience.**
    - **Real-data inversion — repo-scoped, opt-in.** Some repos invert the rule:
      their build contracts must be grounded in the owner's *own real* data, because
      a fixture cannot reveal what the surface does at real scale. That inversion
      applies **only** where the repo's own `CLAUDE.md`/`AGENTS.md` declares an
      operator-sanctioned real-data protocol (e.g. a read-only snapshot flow), and
      only within that protocol's bounds. Absent that declaration, the manufactured
      default above governs — no exceptions inferred from context or precedent.
      Where the inversion does apply: renders of real personal/health data **never
      commit and never attach to a PR** (a PR is a publish) without the operator's
      exact authorizing sentence quoted in the record; committed and PR-attached
      evidence uses labeled synthetic fixtures, and real-data renders stay local,
      handed to the verifier with the pinned data.
    - Vary the concept, not the decoration; three variants that differ only in
      color are one design.
    - Inspect rendered output — source review alone never validates a visual
      artifact. Use `drive-local-webapp` for rendering; ask to install it if
      missing.
    - **Sibling exactness** ([reference/sibling-fidelity.md](reference/sibling-fidelity.md)):
      any element with a sibling in a shipped surface uses the shipped values
      exactly — geometry, type, alignment spines, chart furniture, and interaction
      idioms alike — and fidelity is proven with a computed-style diff against the
      running app, never by eyeball. Token bridges are verified by computed value
      on the consuming element; mock-global base styles (body font/line-height)
      are banned because they shift extracted chrome off its shipped pixels.
    - Keep `mockups/INDEX.md` as the surface ledger (one row per surface:
      Surface / Concept / Status / Issue / File). `locked` rows are binding
      precedent; `shipped` rows defer to the app itself, and are also how the next
      lock finds its predecessor (`behavior-sweep` §2). Every mode that touches
      a lock updates the ledger in the same change — a stale ledger is a defect.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related