Compare commits

..
Author SHA1 Message Date
Paul BakausandClaude Fable 5 68b1129634 Release bumps: skill 4.0.3, CLI 3.4.0, extension 1.3.0, with synced output
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 19:17:09 -07:00
Paul BakausandClaude Fable 5 ce4dcf9a93 Split breadth from rating in the challenger and staging pools
Rating grades quality, breadth says whether a world can serve an
arbitrary build at all; while they shared one field, the only way to
hold a narrow world back was calling it marginal, which made excellent
but narrow unrecordable and corrupted the ratings as a calibration
signal for the next authoring round. Both axes now exclude
independently, either kind of hold keeps its approval for direct
briefs, an all-niche tier falls back rather than starving, and
stagings honour the same gate with the same fallback. Tests cover the
niche exclusion at strength, the fallback parity with marginal-only
tiers, and the staging gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 19:17:09 -07:00
github-actions[bot] d3c7b05a3e Sync generated provider output 2026-07-28 01:56:22 +00:00
Paul BakausandClaude Fable 5 690e24129a CLAUDE.md: the rule engine is a facade now; drop the dead line numbers
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 18:55:52 -07:00
Paul BakausandClaude Fable 5 33a1c5fcae Ban kickers outright: one eyebrow above a heading is one too many
The detector's repeated-section-kickers rule waited for three tracked
labels before calling the pattern; generated pages earn the finding on
the first one. Retire that id and replace it with kicker-above-heading,
which flags any tracked-caps or small-caps label block sitting directly
above an h1-h4 or heading-role element, at full warning severity.

The candidate gate absorbs the false-positive shapes the repetition
count used to paper over: editorial category-and-date meta lines,
breadcrumbs with separators, legal and chapter numbering, application
panel context labels, nav landmarks before page titles, and stat
callouts with the label below the number. Hero-scale h1 eyebrows stay
with hero-eyebrow-chip so one element gets one finding, and the static
cascade now carries font-variant so small-caps kickers register.

The craft floor entry moves from caution to ban in the same breath.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 18:55:52 -07:00
github-actions[bot] 806a48aef2 Sync generated provider output 2026-07-28 01:52:13 +00:00
Paul BakausandClaude Fable 5 6a7d75b6fe Bound the finish by verdict, not by count, and teach the matrix medium and type
The hard stop landed one step early: one review, one batched fix, one
recapture, then done, with nobody ever judging whether the fixes reached
the quality the findings named. A recapture measures positions; the
model then presented mechanical confirmation as artistic success over a
page whose display face, material, and hero legibility had all drifted
from the approved comp. The finish now ends on a verdict: the recaptured
screenshots go back to the same reviewer, which scores every material
fix resolved, partial, or unresolved and names at most three regressions
the batch introduced, no new hunt. Partial and unresolved fixes earn
exactly one more round; two rounds is the ceiling, the second verdict
ends the work whatever it says, and the final verdict table goes to the
user as it stands, open items included.

Three blindnesses from the same run close alongside. The matrix gains
two mandatory rows: TYPE, where a display face of a different character
is contradicted however the layout matches, and MATERIAL, where flat CSS
standing in for painted, textured, or dimensional artwork is contradicted
regardless of placement. And the Truth check now requires every produced
asset visibly present in the screenshots, because a paper texture at
0.16 opacity is a compliance token, not a shipped material.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 18:51:32 -07:00
github-actions[bot] 270f177d1d Sync generated provider output 2026-07-28 01:19:54 +00:00
Paul BakausandClaude Fable 5 09a33bc58b One sketch, one agent: retire the batch producer and its supervision
The batch producer was the clumsy piece: one subagent owning eight
jobs needed heartbeat rules, reclaim windows, and a page full of
fallbacks to survive its own opacity. The unit of work is now a single
card. With parallel subagents, the set fans out one agent per card, up
to four in flight, landing everything in roughly the time of one; a
single-sketch agent has no planning phase and no batch to stall, so a
failure costs one slot and its remedies fit one sentence: regenerate an
empty slot when its agent returns, drop it when the user answers first.
Without parallel subagents, the main thread generates in reading order
after serving, and the harness's own generation display carries the
progress. The page-side streaming is unchanged; it never cared who
writes the files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 18:19:25 -07:00
github-actions[bot] f59c5223a4 Sync generated provider output 2026-07-28 01:13:37 +00:00
Paul BakausandClaude Fable 5 4329f757f5 Only the visible card face is interactive
A hidden backface still hit-tests in Chrome, so after flipping a card
the front's picture-in-picture sat invisibly over the back's chips,
showing its zoom cursor and eating the flip-back click. Pointer events
now follow visibility: the back is inert until the card flips, and the
front goes inert while it is flipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 18:13:04 -07:00
github-actions[bot] 69bf1e9523 Sync generated provider output 2026-07-28 01:11:27 +00:00
Paul BakausandClaude Fable 5 ca88ea008b Patience while sketches land, honesty when standing in
Field data: the first image of a real batch took ninety seconds and the
page's 150-second fallback then silently promoted catalog art to full
bleed, unlabeled, which is exactly the this-is-your-design misread the
picture-in-picture treatment exists to prevent. The policy is now
patience while there is progress: a slot shows its inspiration only
after waiting four minutes with nothing landing anywhere on the page
for four minutes, the stand-in is dimmed and labeled 'inspiration ·
sketch pending', and polling continues so the real sketch still swaps
in whenever it arrives. Slots with no inspiration keep the honest
elapsed shimmer instead of folding. The parent's reclaim rule matches:
files landing steadily is health at any pace, and only total silence,
no first file in three minutes, takes the batch back inline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 18:10:56 -07:00
github-actions[bot] c3fe6d8064 Sync generated provider output 2026-07-28 01:06:14 +00:00
Paul BakausandClaude Fable 5 17bc2701f3 Put the full read on the card's back; the front is for choosing
Field feedback: with every fact stacked under the media the cards ran
past a screen tall. The front now carries only what the choice needs,
sketch, lineage, title, thesis, identity, and the honest risk clamped
to two lines, while first viewport and the case read on the back behind
a Details chip, sharing the face with the board when the world has one.
Risk stays on the front because the counterweights are pointless if the
downside hides behind a flip, and once the sketch lands the first
viewport is a picture anyway. The schema notes now ask for one-sentence
facts, since a long fact should cost the reader a flip, not the page
its scanability.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 18:05:41 -07:00
github-actions[bot] aef8cbac34 Sync generated provider output 2026-07-28 00:49:56 +00:00
Paul BakausandClaude Fable 5 0eb443d29b Bound the hand, greek the copy, and treat waiting as supervision
A codex field run dealt six challengers into an eight-sketch batch
behind an opaque subagent, and the user stared at a page of shimmer
asking whether anything was happening at all. Four fixes from that run.
A hand now holds at most three challengers, the rest banked for
re-rolls, so fairness within the hand stops multiplying into a queue.
Sketches greek everything but the product's real name and one real
headline, because an invented spec, price, or ship date in a sketch is
a claim PRODUCT.md never made, and comps have solved this for a century.
Sketch production follows the user's reading order with the first file
doubling as the producer's heartbeat, and the parent's --wait loop
checks the sketch directory each pass, reclaiming the batch inline when
two minutes pass with nothing landed. And a failed --start now captures
the daemon's stderr to a per-key log and names the sandbox as the usual
suspect, instead of reporting only that failure occurred. The shimmer
counts its elapsed seconds, and gives up at 150 instead of 300.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 17:49:21 -07:00
github-actions[bot] 149d71a772 Sync generated provider output 2026-07-28 00:15:34 +00:00
Paul BakausandClaude Fable 5 58a2d3dccd Bleed the deck to the viewport, fade the fuller side, let the glance take over
Three field notes from a live review. The deck now escapes the content
column and runs edge to edge, so a cut-off card sits at the screen edge
where it reads as more cards instead of at an invisible container edge
where it reads as a bug; the first card still aligns with the column
via scroll padding. Whichever side hides more content wears a fade, and
a hard edge means the end. The vertical pager grows from a bare chevron
into labeled Back and More pills, because in a column deck it is the
primary way forward. And hovering the inspiration thumb now takes over
the whole media region instead of a timid zoom; the sketch is the
promise, the inspiration is a glance, and the glance must cost nothing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 17:14:59 -07:00
github-actions[bot] f5827256d0 Sync generated provider output 2026-07-28 00:11:12 +00:00
Paul BakausandClaude Fable 5 e6612ea8ef Page the deck on its long axis, and never let decoration hide the cards
Field-checked in a real browser, which surfaced three defects the DOM
tests could not: the generic .media img display rule defeated [hidden]
and floated an empty block over the shimmer and its sketching note; the
deal animation left every card at opacity zero in an unfocused tab,
because rAF throttling is real and decoration must never gate content;
and the sketch poll's cache-busting query missed the anchored /img
route, so a landed sketch kept shimmering forever.

The grid is now a snap-scrolling deck: one row in a wide viewport, one
column in a tall one, with edge arrows that appear only on overflow and
page one card at a time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 17:10:39 -07:00
github-actions[bot] a07e4ed787 Sync generated provider output 2026-07-28 00:03:34 +00:00
Paul BakausandClaude Fable 5 d89ee5f87c Deal every card the same hand: anatomy, sketches, and the standing door
The decision page compared unlike things: the grounded direction was a
wall of text beside curated catalog art, the catalog art read as a
promise of the build, the weighing silently shrank the challenger set,
and the standing exit hid in the footer under the cards it must not
soften. Every card now shares one anatomy (thesis, palette chips,
material tags, first viewport, case, risk), every dealt challenger is
presented with the weighing written on it rather than applied to it,
the catalog image rides picture-in-picture as labeled inspiration with
the lightbox a click away, and canonCard renders the category standard
as one honest, subordinate card.

When image generation exists, each card declares a sketch slot the page
polls: serve first, generate after, through one shared deliberately
unfinished frame, so the comparison stays about direction instead of
rendering luck. The asset producer takes the batch when subagents
exist; the chosen sketch returns in ANSWER to seed at most one comp
probe, and the comp round still renders its full set.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 17:03:05 -07:00
github-actions[bot] 5bec5408e5 Sync generated provider output 2026-07-27 22:52:08 +00:00
Paul BakausandClaude Fable 5 f482d9405e Teach the reading-heavy subagents to write before the ceiling lands
Raising the reviewer's turn budget did not change its fate, only its
reading: 43 tool uses instead of 22, still reaped mid-read with nothing
written, because the SDK ends a run at max-turns without warning and the
model never feels the deadline. The definitions now carry the deadline
themselves: reading is an allowance, batch Reads per turn, take the
decisive inputs first, sample instead of walking the tree, and write by
mid-budget, naming what went unread. A review built from what you saw
beats a perfect review that never arrives.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 15:51:36 -07:00
github-actions[bot] d7d07cb0d6 Sync generated provider output 2026-07-27 22:14:03 +00:00
Paul BakausandClaude Fable 5 c9213835e7 Review fidelity against the comp itself, not the builder's summary of it
A codex run turned an approved comp into a related second art direction
and the finish reviewer passed it: the review anchored on the direction
contract, a lossy abstraction the builder wrote, and every element that
abstraction dropped passed silently. Four changes close that chain. The
reviewer inventories the comp's salient elements before reading the
contract and classifies each one (match, adaptation, missing,
contradicted, added without approval), with adaptations citing the
answer, brief, accessibility need, or product truth that forced them,
and fidelity failures outranking craft in material_fixes. The visualize
inventory gate records compositional commitments alongside asset media,
since the 150-word contract cannot carry them. The north-star allowance
now says what it permits: translation, never recomposition. And the
finish sequence recaptures the same viewports once after the fix batch,
so what the documenter records is what actually shipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 15:13:27 -07:00
github-actions[bot] d52077414c Sync generated provider output 2026-07-27 21:50:35 +00:00
Paul BakausandClaude Fable 5 9e4990765f Give the reading-heavy subagents turn budgets that survive their inputs
A finish review reads the artifact, two full-page screenshots, the
approved comp, the quality-bar cards, and the contract before it may
write a word; at max-turns 12 the SDK reaps it mid-read and the parent
receives the opening sentence as the whole review. Observed twice in a
row (spawn and respawn) on the first real subagent run. The documenter
reads at least as much, and the asset producer pays per asset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 14:50:03 -07:00
dependabot[bot]andGitHub e0144ed585 Bump the bun-minor-and-patch group with 9 updates (#429)
Prepared with AI assistance from OpenAI Codex under maintainer automation instructions.
2026-07-27 10:24:55 -07:00
265 changed files with 9150 additions and 2652 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 4.0.2
version: 4.0.3
---
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as a award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
@@ -13,6 +13,10 @@ Your job is production cleanup, not new art direction. Work only from the approv
Do not redesign. Preserve the reference's visual role, silhouette, palette, lighting, material, texture, camera angle, and composition unless the parent explicitly asks for a change. Preserve perspective only when it belongs to the object or scene itself; if CSS should create the card transform, shadow, rounded clipping, border, or layout, remove that presentation chrome from the raster.
## Decision Sketches
When the parent hands you a decision card packet instead of an approved mock, the job is one sketch: one card, one file, written to the card's declared `sketch` path the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a sketch is reported back, not padded from imagination. Render through the parent's shared frame: the requested surface's first viewport as a flat, matte design sketch in the card's own palette and type character, deliberately unfinished, no photorealism, no gloss. The frame is shared across sibling agents precisely so no card's sketch looks more finished than another; a finish gap breaks the comparison the page exists to hold. The only legible text is the product's real name and one real headline; greek every other text region into indistinct lines, because an invented spec, price, or date in a sketch is a claim PRODUCT.md never made. Return one line naming the path and any deviation, nothing more.
## Input Contract
Expect:
@@ -7,6 +7,8 @@ developer_instructions = '''
You record a project's design system after the build is done. Ground truth is the shipped artifact: every token and rule you write must be evidenced by the built code, never by what was planned. Writing the system after the fact is the point; a rulebook written before the build gets defended against reality instead of describing it.
You run under a hard turn ceiling that ends the run without warning, and a run that ends before DESIGN.md is written has recorded nothing. Batch several Reads into each turn, take `reference/document.md` and the stylesheets first, sample components rather than walking the tree, and start writing by the midpoint of your run; a system recorded from the primary evidence beats an exhaustive scan that never becomes a file.
## Input Contract
Expect: the project root; the artifact path(s); the direction contract text (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; the path to the skill's `reference/document.md`; and the boundary to write at (project or app root). An existing DESIGN.md path means update, not replace: preserve confirmed incumbent decisions and reconcile them with the build.
@@ -9,21 +9,27 @@ You are the finishing reviewer for an Impeccable build: fresh eyes on a done art
You have no browser. Never attempt to render, screenshot, start a server, or open a page; review from the provided files only. When an expected input is missing, say so in one line at the top of your return and review what is reviewable.
You run under a hard turn ceiling that ends the run without warning, and a run that ends before the five sections are written returns nothing; a review built from what you saw beats a perfect review that never arrives. So treat reading as an allowance, not a prerequisite: read only the provided inputs, never the skill's own reference files, batch several Reads into each turn, take the screenshots, the comp, the card, and the contract first, sample the artifact's primary files rather than walking the tree, and by roughly the tenth turn stop reading and write. Name whatever went unread in the line above the sections.
## Input Contract
Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path. When the harness can view images, open the screenshots, the comp, and the card before judging.
Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: the contract is the builder's abstraction of the comp, and a review anchored on it inherits whatever that abstraction dropped.
## Checks, in order
1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding.
2. **Fidelity.** Against the approved comp: topology, reading order, focal scale, overlaps and z-order, density, signature geometry. Name every drift and classify it intentional or defect. The comp governs composition; it is not a pixel spec for semantics, accessibility, or responsive behavior.
2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement.
3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. The card governs commitment and finish, never composition.
4. **Contract, promise by promise.** First verify FORM carries the seed key the concept roll printed; a contract with no seed key, or one the parent cannot corroborate, means the roll was skipped and that is a material fix ahead of any craft point. Then, for each of the five blocks, does the render keep the promise? Apply the memory test to the first viewport.
5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one.
5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one, and every produced asset visibly present in the screenshots; an asset applied at near-zero opacity or buried behind other paint is a compliance token, not a shipped material.
Do not run a second detector pass; mechanical findings belong to the parent's hooks.
## Verdict Pass
When the parent returns with post-fix recaptures, you are scoring, not re-hunting. For each material fix from your review, one line: resolved, partial, or unresolved, tied to what the new screenshots visibly show; a fix answered mechanically, positions moved but the quality the finding named still absent, is partial at best. Then name at most three regressions the fix batch itself introduced, judged by the same matrix rules, and nothing else; no new hunt, no new checks. Return exactly two sections: `verdict` (the scored list) and `remaining` (what stays open, or "clear").
## Output Contract
Return exactly five sections: `persistence` (pass/fail with specifics), `fidelity` (drift list with intentional/defect per item, or "faithful"), `ceiling` (unused native devices, or "reached"), `material_fixes` (ordered, most material first, each one line tied to a check or contract promise, at most eight), and `keep` (one line naming what must not be diluted while fixing). Missing inputs are named in one line above the sections. No praise, no summary prose.
Return exactly five sections: `persistence` (pass/fail with specifics), `fidelity` (the element matrix: match, adaptation, missing, contradicted, or added without approval per salient element, adaptations citing their evidence, or "faithful"), `ceiling` (unused native devices, or "reached"), `material_fixes` (ordered, most material first, fidelity failures ahead of craft, each one line tied to a check or contract promise, at most eight), and `keep` (one line naming what must not be diluted while fixing). Missing inputs are named in one line above the sections. No praise, no summary prose.
'''
@@ -23,7 +23,7 @@ Page scaffolds:
- Same-size cards of icon plus heading plus text as the page structure. Cards are the lazy container; nested cards are always wrong.
- The hero-metric template: big number, small label, supporting stats, accent.
- A tracked uppercase eyebrow over every section. One named kicker is a system; an eyebrow everywhere is grammar you did not choose.
- A kicker or eyebrow above a heading. This one is a ban, not a default: no brief earns it back. The heading carries its own weight; delete the label and let the heading speak.
- Section numbers (01 / 02 / 03) unless the sequence itself carries information the reader needs.
- A modal for a task that needs neither interruption nor protected focus.
@@ -11,6 +11,10 @@ Your job is production cleanup, not new art direction. Work only from the approv
Do not redesign. Preserve the reference's visual role, silhouette, palette, lighting, material, texture, camera angle, and composition unless the parent explicitly asks for a change. Preserve perspective only when it belongs to the object or scene itself; if CSS should create the card transform, shadow, rounded clipping, border, or layout, remove that presentation chrome from the raster.
## Decision Sketches
When the parent hands you a decision card packet instead of an approved mock, the job is one sketch: one card, one file, written to the card's declared `sketch` path the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a sketch is reported back, not padded from imagination. Render through the parent's shared frame: the requested surface's first viewport as a flat, matte design sketch in the card's own palette and type character, deliberately unfinished, no photorealism, no gloss. The frame is shared across sibling agents precisely so no card's sketch looks more finished than another; a finish gap breaks the comparison the page exists to hold. The only legible text is the product's real name and one real headline; greek every other text region into indistinct lines, because an invented spec, price, or date in a sketch is a claim PRODUCT.md never made. Return one line naming the path and any deviation, nothing more.
## Input Contract
Expect:
@@ -5,6 +5,8 @@ This harness has no subagent capability, so you are running this role inline. St
You record a project's design system after the build is done. Ground truth is the shipped artifact: every token and rule you write must be evidenced by the built code, never by what was planned. Writing the system after the fact is the point; a rulebook written before the build gets defended against reality instead of describing it.
You run under a hard turn ceiling that ends the run without warning, and a run that ends before DESIGN.md is written has recorded nothing. Batch several Reads into each turn, take `reference/document.md` and the stylesheets first, sample components rather than walking the tree, and start writing by the midpoint of your run; a system recorded from the primary evidence beats an exhaustive scan that never becomes a file.
## Input Contract
Expect: the project root; the artifact path(s); the direction contract text (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; the path to the skill's `reference/document.md`; and the boundary to write at (project or app root). An existing DESIGN.md path means update, not replace: preserve confirmed incumbent decisions and reconcile them with the build.
@@ -7,20 +7,26 @@ You are the finishing reviewer for an Impeccable build: fresh eyes on a done art
You have no browser. Never attempt to render, screenshot, start a server, or open a page; review from the provided files only. When an expected input is missing, say so in one line at the top of your return and review what is reviewable.
You run under a hard turn ceiling that ends the run without warning, and a run that ends before the five sections are written returns nothing; a review built from what you saw beats a perfect review that never arrives. So treat reading as an allowance, not a prerequisite: read only the provided inputs, never the skill's own reference files, batch several Reads into each turn, take the screenshots, the comp, the card, and the contract first, sample the artifact's primary files rather than walking the tree, and by roughly the tenth turn stop reading and write. Name whatever went unread in the line above the sections.
## Input Contract
Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path. When the harness can view images, open the screenshots, the comp, and the card before judging.
Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: the contract is the builder's abstraction of the comp, and a review anchored on it inherits whatever that abstraction dropped.
## Checks, in order
1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding.
2. **Fidelity.** Against the approved comp: topology, reading order, focal scale, overlaps and z-order, density, signature geometry. Name every drift and classify it intentional or defect. The comp governs composition; it is not a pixel spec for semantics, accessibility, or responsive behavior.
2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement.
3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. The card governs commitment and finish, never composition.
4. **Contract, promise by promise.** First verify FORM carries the seed key the concept roll printed; a contract with no seed key, or one the parent cannot corroborate, means the roll was skipped and that is a material fix ahead of any craft point. Then, for each of the five blocks, does the render keep the promise? Apply the memory test to the first viewport.
5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one.
5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one, and every produced asset visibly present in the screenshots; an asset applied at near-zero opacity or buried behind other paint is a compliance token, not a shipped material.
Do not run a second detector pass; mechanical findings belong to the parent's hooks.
## Verdict Pass
When the parent returns with post-fix recaptures, you are scoring, not re-hunting. For each material fix from your review, one line: resolved, partial, or unresolved, tied to what the new screenshots visibly show; a fix answered mechanically, positions moved but the quality the finding named still absent, is partial at best. Then name at most three regressions the fix batch itself introduced, judged by the same matrix rules, and nothing else; no new hunt, no new checks. Return exactly two sections: `verdict` (the scored list) and `remaining` (what stays open, or "clear").
## Output Contract
Return exactly five sections: `persistence` (pass/fail with specifics), `fidelity` (drift list with intentional/defect per item, or "faithful"), `ceiling` (unused native devices, or "reached"), `material_fixes` (ordered, most material first, each one line tied to a check or contract promise, at most eight), and `keep` (one line naming what must not be diluted while fixing). Missing inputs are named in one line above the sections. No praise, no summary prose.
Return exactly five sections: `persistence` (pass/fail with specifics), `fidelity` (the element matrix: match, adaptation, missing, contradicted, or added without approval per salient element, adaptations citing their evidence, or "faithful"), `ceiling` (unused native devices, or "reached"), `material_fixes` (ordered, most material first, fidelity failures ahead of craft, each one line tied to a check or contract promise, at most eight), and `keep` (one line naming what must not be diluted while fixing). Missing inputs are named in one line above the sections. No praise, no summary prose.
@@ -44,9 +44,11 @@ The script assigns which structure gets built: your top-ranked structure is what
2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, the notation, publications, identity programs, data graphics, and interfaces it reads daily, not only its physical objects; a nameable abstract system (a school of poster, a documentation standard, a data-graphic tradition) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; the audience's world is larger than that, so dig until the list spans at least three families.
3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience.
4. Run `node .agents/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode <mode>` and follow what it prints. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point.
5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the one or two fused challengers that survived the weighing as named alternates with a one-line case each, plus re-roll with an optional one-line steer. What you never present is a ranked menu of your own grounded candidates; a lineup of those invites the safest card, while dealt challengers carry no such rut. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option.
5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. What you never present is a ranked menu of your own grounded candidates; a lineup of those invites the safest card, while dealt challengers carry no such rut. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option.
The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Run `node .agents/skills/impeccable/scripts/serve-question.mjs --start --payload <file>` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key <key>`, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry.
The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, so a grounded direction is never a wall of text against rendered art, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you, so it is never yours to recommend. Run `node .agents/skills/impeccable/scripts/serve-question.mjs --start --payload <file>` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key <key>`, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry.
When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land, so generation never delays the question. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once, so the whole set lands in roughly the time of one. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version.
Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images for exactly this reason (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product.
@@ -98,6 +100,6 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi
Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary.
After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, and the QUALITY BAR card and approved comp paths. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. Apply the material fixes in one batch, rebuild once, and stop; the reviewer ran so that you do not re-open your own hunt. Do not run a second detector.
After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, and the QUALITY BAR card and approved comp paths. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. Apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get exactly one more batch, recapture, and verdict; two correction rounds is the ceiling, the second verdict ends the work whatever it says, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included: presenting mechanical confirmation as artistic success is how a failed build gets announced as a finished one. Do not run a second detector.
Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded.
@@ -6,7 +6,7 @@ The purpose of a probe is to test composition, narrative, hierarchy, density, fo
## Generate three compositional options
Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session; record the approved comp's path in the surface brief. Base them on the real content and the surface concepts already developed with the user. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building.
Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session; record the approved comp's path in the surface brief. Base them on the real content and the surface concepts already developed with the user. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. A decision-page sketch is not a probe: it chose the direction at deliberately unfinished fidelity, so the three comps render regardless, and the chosen card's sketch seeds at most one of them.
- When the user shortlisted multiple concepts, spread the three across them.
- When one direction is committed, vary the structural uncertainty an image can resolve: topology, sequence, density, hierarchy, focal composition, or interaction framing.
@@ -25,11 +25,11 @@ After approval, summarize the composition and the parts of the comp that must no
## Inventory implementation fidelity
Before building, inventory the approved comp's major visible ingredients in writing (a short table in the surface brief or working notes; the finish reviewer audits shipped assets against it) and choose an implementation medium for each: semantic HTML/CSS/SVG, existing project asset, generated raster, sourced raster, icon library, canvas/WebGL, or accepted omission. Textures, portraits, and scenes are raster-by-default; a CSS gradient is not a texture medium. Every `produce` entry is produced before the build ships, through the asset producer or in the current thread; an inventory with unproduced entries is an unfinished build, and this gate is where imagery-free pages come from when it is skipped.
Before building, inventory the approved comp's major visible ingredients in writing (a short table in the surface brief or working notes; the finish reviewer audits shipped assets against it) and choose an implementation medium for each: semantic HTML/CSS/SVG, existing project asset, generated raster, sourced raster, icon library, canvas/WebGL, or accepted omission. The same written inventory names the comp's compositional commitments: navigation items and icons, headline levels and their scale relationship, signature geometry such as seams, masks, and overlaps, and each section's arrangement and density. An element never written down is the element the build silently drops, and the direction contract's 150 words cannot carry this list, so this inventory is where it lives. Textures, portraits, and scenes are raster-by-default; a CSS gradient is not a texture medium. Every `produce` entry is produced before the build ships, through the asset producer or in the current thread; an inventory with unproduced entries is an unfinished build, and this gate is where imagery-free pages come from when it is skipped.
Pay special attention to the dominant composition, signature use, image-native content, second-fold system, and any interaction the still image only implies. If the concept depends on a photograph, architectural scene, product object, portrait, or other raster-native material, do not silently replace it with generic CSS scenery.
Treat the comp as a north star, not something to trace. Do not rasterize core UI text or controls. Do not substitute a different visual driver after approval without asking.
Treat the comp as a north star, not something to trace, and know what that allows: translation into semantic, responsive, accessible code, never recomposition. Keeping the palette and mood while redrawing the topology is a second art direction, not an adaptation. Do not rasterize core UI text or controls. Do not substitute a different visual driver after approval without asking.
## Produce only the assets the build needs
@@ -206,7 +206,13 @@ ${grammar}
// returns no staging. Re-rolls exclude every earlier set until the pool runs out.
export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) {
const pool = sourceCompositions ?? requireLocalConcepts().compositions;
// Stagings honour the same breadth gate as worlds: a staging too specific to
// serve an arbitrary build stays approved for direct briefs and leaves the
// challenger pool. Falls back to the full approved set rather than returning
// nothing if every approved staging is marked niche.
let approved = pool.filter(composition => composition.status === 'approved');
const broad = approved.filter(composition => composition.review?.breadth !== 'niche');
if (broad.length > 0) approved = broad;
if (approved.length === 0) return [];
if (mode) {
const matching = approved.filter(composition => composition.surface === mode);
@@ -271,12 +277,19 @@ export function selectApprovedChallengers({ scope, key, reroll = 0, sourceConcep
// graphic systems beside instrument languages and atmosphere worlds, with
// the second pick preferring a different family for diversity. Tier order
// in the rendered list is rolled too, to avoid positional bias.
// Approval ratings weight the draw: a 3-star world earns a second ticket
// (roughly double odds), a 1-star keeps its approval for direct briefs but
// leaves the challenger pool unless a tier has nothing else.
// Two separate axes, and both can exclude. Rating grades quality: a 3-star
// earns a second ticket, a 1-star marginal keep leaves the pool. Breadth says
// whether a world can serve an arbitrary build at all, so a niche world
// leaves the pool however good it is. Breadth was split out of rating because
// the only way to hold a narrow world back used to be calling it marginal,
// which made "excellent but narrow" unrecordable and corrupted the ratings as
// a calibration signal for the next authoring round.
const ticketsFor = pool => pool.flatMap(concept => {
const rating = concept.review?.rating;
if (rating === 1) return [];
// Two independent exclusions: a marginal world is too weak to challenge,
// a niche world too narrow. Either one keeps its approval for direct
// briefs and leaves the pool.
if (rating === 1 || concept.review?.breadth === 'niche') return [];
return rating === 3
? [{ concept, ticket: 0 }, { concept, ticket: 1 }]
: [{ concept, ticket: 0 }];
@@ -1529,7 +1529,7 @@ if (IS_BROWSER) {
addBrowserFindings(groupMap, document.body, typoFindings);
}
const sectionKickerFindings = checkRepeatedSectionKickersDOM()
const sectionKickerFindings = checkKickerAboveHeadingDOM()
.map(f => ({ type: f.id, detail: f.snippet }))
.filter(f => _ruleOk(f.type));
if (sectionKickerFindings.length > 0) {
@@ -309,15 +309,14 @@ const ANTIPATTERNS = [
skillGuideline: 'tiny uppercase tracked label above the hero headline',
},
{
id: 'repeated-section-kickers',
id: 'kicker-above-heading',
category: 'slop',
scopes: ['type'],
severity: 'advisory',
name: 'Repeated section kicker labels',
name: 'Kicker / eyebrow label above heading',
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.',
'A tiny tracked uppercase or small-caps label sitting as its own block directly above a heading is banned outright, repeated or not. Generated kickers never earn their place: the heading carries its own weight. Delete the label and let the heading speak; if the words matter, work them into the heading or the body.',
skillSection: 'Typography',
skillGuideline: 'repeated eyebrow or kicker labels as section scaffolding',
skillGuideline: 'kicker or eyebrow labels above headings',
},
{
id: 'numbered-section-labels',
@@ -1258,12 +1257,15 @@ function checkHeroEyebrow(opts) {
}];
}
function checkRepeatedSectionKickers(opts) {
const { candidates, minCount = 3 } = opts;
if (!Array.isArray(candidates) || candidates.length < minCount) return [];
// Outright ban: one kicker is one too many, so every collected candidate is
// a finding. The judgment lives in the candidate gate (isKickerCandidate) and
// the collector's context skips, not in a repetition count.
function checkKickerAboveHeading(opts) {
const { candidates } = opts;
if (!Array.isArray(candidates)) return [];
return candidates.map(candidate => ({
id: 'repeated-section-kickers',
snippet: `repeated section kicker "${candidate.kickerText}" before ${candidate.headingTag} "${candidate.headingText}" (${candidates.length} on page)`,
id: 'kicker-above-heading',
snippet: `kicker "${candidate.kickerText}" above ${candidate.headingTag} "${candidate.headingText}"`,
}));
}
@@ -3190,7 +3192,7 @@ function parseColorResolved(str, customPropMap) {
return parseAnyColor(resolved);
}
const REPEATED_KICKER_SKIP_SELECTOR = [
const KICKER_SKIP_SELECTOR = [
'nav',
'form',
'table',
@@ -3209,7 +3211,7 @@ const REPEATED_KICKER_SKIP_SELECTOR = [
'[data-impeccable-allow-kickers]',
].join(',');
const REPEATED_KICKER_CARD_CONTEXT_SELECTOR = [
const KICKER_CARD_CONTEXT_SELECTOR = [
'article',
'button',
'a',
@@ -3227,23 +3229,32 @@ function cleanInlineText(el) {
.trim();
}
function isRepeatedKickerCardContext(heading, kicker) {
const item = heading.closest?.(REPEATED_KICKER_CARD_CONTEXT_SELECTOR);
function isKickerCardContext(heading, kicker) {
const item = heading.closest?.(KICKER_CARD_CONTEXT_SELECTOR);
return Boolean(item && (!item.contains || item.contains(kicker)));
}
function isRepeatedKickerCandidate(opts) {
// Meta lines above headlines join category and date (or path crumbs) with
// separator glyphs, or carry a year. A kicker is one short phrase; metadata
// keeps its markers.
const KICKER_META_TEXT_RE = /[·•|]|\s[\/›»>]\s|\b(19|20)\d{2}\b/;
// Legal and document numbering: "Section 4.2", "Article IX", "§ 12.3",
// dotted decimal outlines. The label identifies the clause, so it stays.
const KICKER_DOC_NUMBERING_RE = /^(§|\d+(\.\d+)+\b|(section|article|clause|appendix|exhibit|schedule|chapter|part|rule|title)\s+([\divxlc]+\b|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)\b)/i;
function isKickerCandidate(opts) {
const {
headingTag,
headingLevel,
headingText,
headingFontSize,
kickerTag,
kickerText,
kickerTextTransform,
kickerFontVariant,
kickerFontSize,
kickerLetterSpacing,
} = opts;
if (!['h2', 'h3', 'h4'].includes(headingTag)) return false;
if (!headingLevel || headingLevel > 4) return false;
if (!headingText || headingText.length < 3) return false;
if (/^\/[\w-]+/i.test(headingText.replace(/^"|"$/g, '').trim())) return false;
if (!(headingFontSize >= 20)) return false;
@@ -3251,9 +3262,13 @@ function isRepeatedKickerCandidate(opts) {
if (!['p', 'span', 'div', 'small'].includes(kickerTag)) return false;
if (!kickerText || kickerText.length < 2 || kickerText.length > 34) return false;
if (/^step\s*\d+/i.test(kickerText) || /^\d{1,2}$/.test(kickerText)) return false;
if (KICKER_META_TEXT_RE.test(kickerText)) return false;
if (KICKER_DOC_NUMBERING_RE.test(kickerText)) return false;
const isSmallCaps = /small-caps/.test(kickerFontVariant || '');
const isUppercased = kickerTextTransform === 'uppercase'
|| (/[A-Z]/.test(kickerText) && !/[a-z]/.test(kickerText));
|| (/[A-Z]/.test(kickerText) && !/[a-z]/.test(kickerText))
|| isSmallCaps;
if (!isUppercased) return false;
if (!(kickerFontSize > 0 && kickerFontSize <= 14)) return false;
const minTrackedSpacing = Math.max(1, kickerFontSize * 0.08);
@@ -3261,37 +3276,64 @@ function isRepeatedKickerCandidate(opts) {
return true;
}
function collectRepeatedSectionKickerCandidates(doc, getStyle, resolveLetterSpacing) {
// Resolve a heading level for the anchor element: 1-4 for h1-h4, aria-level
// (default 2) for role="heading" elements, 0 otherwise.
function kickerHeadingLevel(heading) {
const tag = heading.tagName.toLowerCase();
const byTag = /^h([1-6])$/.exec(tag);
if (byTag) return parseInt(byTag[1], 10);
const role = heading.getAttribute?.('role') || '';
if (role.toLowerCase() !== 'heading') return 0;
const ariaLevel = parseInt(heading.getAttribute?.('aria-level') || '', 10);
return Number.isFinite(ariaLevel) && ariaLevel >= 1 ? ariaLevel : 2;
}
function collectKickerCandidates(doc, getStyle, resolveLetterSpacing) {
const candidates = [];
for (const heading of doc.querySelectorAll('h2, h3, h4')) {
if (heading.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
for (const heading of doc.querySelectorAll('h1, h2, h3, h4, [role="heading"]')) {
const headingLevel = kickerHeadingLevel(heading);
if (!headingLevel || headingLevel > 4) continue;
if (heading.closest?.(KICKER_SKIP_SELECTOR)) continue;
// Application contexts (tab panels, dialogs) use compact context labels
// above headings to describe state, not to decorate. Same carve-out the
// hero-eyebrow rule makes.
if (heading.closest?.('[role="tabpanel"], [role="dialog"], [role="application"], dialog')) continue;
const kicker = heading.previousElementSibling;
if (!kicker || kicker.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
if (isRepeatedKickerCardContext(heading, kicker)) continue;
if (!kicker || kicker.closest?.(KICKER_SKIP_SELECTOR)) continue;
if (isKickerCardContext(heading, kicker)) continue;
const headingStyle = getStyle(heading);
const kickerStyle = getStyle(kicker);
const headingTag = heading.tagName.toLowerCase();
const headingText = (heading.textContent || '').replace(/\s+/g, ' ').trim();
const kickerText = cleanInlineText(kicker) || (kicker.textContent || '').replace(/\s+/g, ' ').trim();
const headingFontSize = resolveLetterSpacing(headingStyle.fontSize || '', 16) || parseFloat(headingStyle.fontSize) || 0;
const kickerFontSize = resolveLetterSpacing(kickerStyle.fontSize || '', 16) || parseFloat(kickerStyle.fontSize) || 0;
const kickerLetterSpacing = resolveLetterSpacing(kickerStyle.letterSpacing || '', kickerFontSize);
if (!isRepeatedKickerCandidate({
headingTag: heading.tagName.toLowerCase(),
if (!isKickerCandidate({
headingLevel,
headingText,
headingFontSize,
kickerTag: kicker.tagName.toLowerCase(),
kickerText,
kickerTextTransform: kickerStyle.textTransform || '',
kickerFontVariant: `${kickerStyle.fontVariant || ''} ${kickerStyle.fontVariantCaps || ''}`,
kickerFontSize,
kickerLetterSpacing,
})) {
continue;
}
// A tracked-caps eyebrow above a hero-scale h1 belongs to
// hero-eyebrow-chip (which also covers the accent-bold and dash-prefix
// stylings there). Stand down so one element gets one finding.
if (headingTag === 'h1' && headingFontSize >= 48 && kickerLetterSpacing >= 1.6) {
continue;
}
candidates.push({
headingTag: heading.tagName.toLowerCase(),
headingTag,
headingText: headingText.replace(/^"|"$/g, '').slice(0, 60),
kickerText: kickerText.slice(0, 40),
});
@@ -3299,17 +3341,17 @@ function collectRepeatedSectionKickerCandidates(doc, getStyle, resolveLetterSpac
return candidates;
}
function checkRepeatedSectionKickersDOM() {
const candidates = collectRepeatedSectionKickerCandidates(
function checkKickerAboveHeadingDOM() {
const candidates = collectKickerCandidates(
document,
(el) => getComputedStyle(el),
(value, fontSize) => resolveLengthPx(value, fontSize) || 0,
);
return checkRepeatedSectionKickers({ candidates });
return checkKickerAboveHeading({ candidates });
}
// ── Numbered section labels ─────────────────────────────────────────────────
// Sibling of the repeated-kicker rule: instead of a tracked uppercase word,
// Sibling of the kicker-above-heading rule: instead of a tracked uppercase word,
// the section scaffold is a tiny numeric index riding beside each section
// heading — bare and zero-padded, or an index joined to a short micro-label
// by a separator glyph. The kicker rule deliberately excludes bare 1-2 digit
@@ -3363,7 +3405,7 @@ function collectNumberedSectionLabelCandidates(doc, getStyle, resolveLetterSpaci
const candidates = [];
const seenLabels = new Set();
for (const heading of doc.querySelectorAll('h2, h3, h4')) {
if (heading.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
if (heading.closest?.(KICKER_SKIP_SELECTOR)) continue;
// The index sits either directly before the heading, or before the
// wrapper the heading leads (label | <div><h2>…</h2>…</div>).
let label = heading.previousElementSibling;
@@ -3373,9 +3415,9 @@ function collectNumberedSectionLabelCandidates(doc, getStyle, resolveLetterSpaci
if (firstChild === heading) label = parent.previousElementSibling;
}
if (!label || seenLabels.has(label)) continue;
if (label.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
if (label.closest?.(KICKER_SKIP_SELECTOR)) continue;
if (HEADING_TAGS.has(label.tagName.toLowerCase())) continue;
if (isRepeatedKickerCardContext(heading, label)) continue;
if (isKickerCardContext(heading, label)) continue;
const labelText = cleanInlineText(label) || (label.textContent || '').replace(/\s+/g, ' ').trim();
const parsed = parseNumberedLabelText(labelText);
@@ -4548,13 +4590,13 @@ function checkElementHeroEyebrow(el, style, tag, window, customPropMap) {
});
}
function checkRepeatedSectionKickersFromDoc(doc, win) {
const candidates = collectRepeatedSectionKickerCandidates(
function checkKickerAboveHeadingFromDoc(doc, win) {
const candidates = collectKickerCandidates(
doc,
(el) => win.getComputedStyle(el),
(value, fontSize) => resolveLengthPx(value, fontSize) || 0,
);
return checkRepeatedSectionKickers({ candidates });
return checkKickerAboveHeading({ candidates });
}
function checkElementMotion(tag, style) {
@@ -7712,7 +7754,7 @@ if (IS_BROWSER) {
addBrowserFindings(groupMap, document.body, typoFindings);
}
const sectionKickerFindings = checkRepeatedSectionKickersDOM()
const sectionKickerFindings = checkKickerAboveHeadingDOM()
.map(f => ({ type: f.id, detail: f.snippet }))
.filter(f => _ruleOk(f.type));
if (sectionKickerFindings.length > 0) {
@@ -223,7 +223,7 @@ function unwrapCssAtLayer(source) {
// ---------------------------------------------------------------------------
const STATIC_INHERITED_PROPS = new Set([
'color', 'fontFamily', 'fontSize', 'fontStyle', 'fontWeight',
'color', 'fontFamily', 'fontSize', 'fontStyle', 'fontWeight', 'fontVariant',
'lineHeight', 'letterSpacing', 'textTransform', 'textAlign', 'hyphens',
'webkitHyphens',
]);
@@ -252,6 +252,7 @@ const STATIC_DEFAULT_STYLE = {
fontFamily: '',
fontSize: '16px',
fontStyle: 'normal',
fontVariant: 'normal',
fontWeight: '400',
lineHeight: 'normal',
letterSpacing: 'normal',
@@ -27,11 +27,11 @@ import {
checkElementRadialSpotlight,
checkCreamPalette,
checkHtmlPatterns,
checkKickerAboveHeadingFromDoc,
checkNumberedSectionLabelsFromDoc,
checkPageLayout,
checkPageQualityFromDoc,
checkRepeatedContainerTextFromDoc,
checkRepeatedSectionKickersFromDoc,
resolveBackground,
resolveBorderRadiusPx,
} from '../../rules/checks.mjs';
@@ -202,7 +202,7 @@ async function detectHtml(filePath, options = {}) {
for (const f of runPageCheck('typography-rules', () => checkStaticPageTypography(document, window))) {
findings.push(finding(f.id, filePath, f.snippet));
}
for (const f of runPageCheck('repeated-section-kickers', () => checkRepeatedSectionKickersFromDoc(document, window))) {
for (const f of runPageCheck('kicker-above-heading', () => checkKickerAboveHeadingFromDoc(document, window))) {
findings.push(finding(f.id, filePath, f.snippet));
}
for (const f of runPageCheck('numbered-section-labels', () => checkNumberedSectionLabelsFromDoc(document, window))) {
@@ -198,15 +198,14 @@ const ANTIPATTERNS = [
skillGuideline: 'tiny uppercase tracked label above the hero headline',
},
{
id: 'repeated-section-kickers',
id: 'kicker-above-heading',
category: 'slop',
scopes: ['type'],
severity: 'advisory',
name: 'Repeated section kicker labels',
name: 'Kicker / eyebrow label above heading',
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.',
'A tiny tracked uppercase or small-caps label sitting as its own block directly above a heading is banned outright, repeated or not. Generated kickers never earn their place: the heading carries its own weight. Delete the label and let the heading speak; if the words matter, work them into the heading or the body.',
skillSection: 'Typography',
skillGuideline: 'repeated eyebrow or kicker labels as section scaffolding',
skillGuideline: 'kicker or eyebrow labels above headings',
},
{
id: 'numbered-section-labels',
@@ -456,12 +456,15 @@ function checkHeroEyebrow(opts) {
}];
}
function checkRepeatedSectionKickers(opts) {
const { candidates, minCount = 3 } = opts;
if (!Array.isArray(candidates) || candidates.length < minCount) return [];
// Outright ban: one kicker is one too many, so every collected candidate is
// a finding. The judgment lives in the candidate gate (isKickerCandidate) and
// the collector's context skips, not in a repetition count.
function checkKickerAboveHeading(opts) {
const { candidates } = opts;
if (!Array.isArray(candidates)) return [];
return candidates.map(candidate => ({
id: 'repeated-section-kickers',
snippet: `repeated section kicker "${candidate.kickerText}" before ${candidate.headingTag} "${candidate.headingText}" (${candidates.length} on page)`,
id: 'kicker-above-heading',
snippet: `kicker "${candidate.kickerText}" above ${candidate.headingTag} "${candidate.headingText}"`,
}));
}
@@ -2388,7 +2391,7 @@ function parseColorResolved(str, customPropMap) {
return parseAnyColor(resolved);
}
const REPEATED_KICKER_SKIP_SELECTOR = [
const KICKER_SKIP_SELECTOR = [
'nav',
'form',
'table',
@@ -2407,7 +2410,7 @@ const REPEATED_KICKER_SKIP_SELECTOR = [
'[data-impeccable-allow-kickers]',
].join(',');
const REPEATED_KICKER_CARD_CONTEXT_SELECTOR = [
const KICKER_CARD_CONTEXT_SELECTOR = [
'article',
'button',
'a',
@@ -2425,23 +2428,32 @@ function cleanInlineText(el) {
.trim();
}
function isRepeatedKickerCardContext(heading, kicker) {
const item = heading.closest?.(REPEATED_KICKER_CARD_CONTEXT_SELECTOR);
function isKickerCardContext(heading, kicker) {
const item = heading.closest?.(KICKER_CARD_CONTEXT_SELECTOR);
return Boolean(item && (!item.contains || item.contains(kicker)));
}
function isRepeatedKickerCandidate(opts) {
// Meta lines above headlines join category and date (or path crumbs) with
// separator glyphs, or carry a year. A kicker is one short phrase; metadata
// keeps its markers.
const KICKER_META_TEXT_RE = /[·•|]|\s[\/›»>]\s|\b(19|20)\d{2}\b/;
// Legal and document numbering: "Section 4.2", "Article IX", "§ 12.3",
// dotted decimal outlines. The label identifies the clause, so it stays.
const KICKER_DOC_NUMBERING_RE = /^(§|\d+(\.\d+)+\b|(section|article|clause|appendix|exhibit|schedule|chapter|part|rule|title)\s+([\divxlc]+\b|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)\b)/i;
function isKickerCandidate(opts) {
const {
headingTag,
headingLevel,
headingText,
headingFontSize,
kickerTag,
kickerText,
kickerTextTransform,
kickerFontVariant,
kickerFontSize,
kickerLetterSpacing,
} = opts;
if (!['h2', 'h3', 'h4'].includes(headingTag)) return false;
if (!headingLevel || headingLevel > 4) return false;
if (!headingText || headingText.length < 3) return false;
if (/^\/[\w-]+/i.test(headingText.replace(/^"|"$/g, '').trim())) return false;
if (!(headingFontSize >= 20)) return false;
@@ -2449,9 +2461,13 @@ function isRepeatedKickerCandidate(opts) {
if (!['p', 'span', 'div', 'small'].includes(kickerTag)) return false;
if (!kickerText || kickerText.length < 2 || kickerText.length > 34) return false;
if (/^step\s*\d+/i.test(kickerText) || /^\d{1,2}$/.test(kickerText)) return false;
if (KICKER_META_TEXT_RE.test(kickerText)) return false;
if (KICKER_DOC_NUMBERING_RE.test(kickerText)) return false;
const isSmallCaps = /small-caps/.test(kickerFontVariant || '');
const isUppercased = kickerTextTransform === 'uppercase'
|| (/[A-Z]/.test(kickerText) && !/[a-z]/.test(kickerText));
|| (/[A-Z]/.test(kickerText) && !/[a-z]/.test(kickerText))
|| isSmallCaps;
if (!isUppercased) return false;
if (!(kickerFontSize > 0 && kickerFontSize <= 14)) return false;
const minTrackedSpacing = Math.max(1, kickerFontSize * 0.08);
@@ -2459,37 +2475,64 @@ function isRepeatedKickerCandidate(opts) {
return true;
}
function collectRepeatedSectionKickerCandidates(doc, getStyle, resolveLetterSpacing) {
// Resolve a heading level for the anchor element: 1-4 for h1-h4, aria-level
// (default 2) for role="heading" elements, 0 otherwise.
function kickerHeadingLevel(heading) {
const tag = heading.tagName.toLowerCase();
const byTag = /^h([1-6])$/.exec(tag);
if (byTag) return parseInt(byTag[1], 10);
const role = heading.getAttribute?.('role') || '';
if (role.toLowerCase() !== 'heading') return 0;
const ariaLevel = parseInt(heading.getAttribute?.('aria-level') || '', 10);
return Number.isFinite(ariaLevel) && ariaLevel >= 1 ? ariaLevel : 2;
}
function collectKickerCandidates(doc, getStyle, resolveLetterSpacing) {
const candidates = [];
for (const heading of doc.querySelectorAll('h2, h3, h4')) {
if (heading.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
for (const heading of doc.querySelectorAll('h1, h2, h3, h4, [role="heading"]')) {
const headingLevel = kickerHeadingLevel(heading);
if (!headingLevel || headingLevel > 4) continue;
if (heading.closest?.(KICKER_SKIP_SELECTOR)) continue;
// Application contexts (tab panels, dialogs) use compact context labels
// above headings to describe state, not to decorate. Same carve-out the
// hero-eyebrow rule makes.
if (heading.closest?.('[role="tabpanel"], [role="dialog"], [role="application"], dialog')) continue;
const kicker = heading.previousElementSibling;
if (!kicker || kicker.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
if (isRepeatedKickerCardContext(heading, kicker)) continue;
if (!kicker || kicker.closest?.(KICKER_SKIP_SELECTOR)) continue;
if (isKickerCardContext(heading, kicker)) continue;
const headingStyle = getStyle(heading);
const kickerStyle = getStyle(kicker);
const headingTag = heading.tagName.toLowerCase();
const headingText = (heading.textContent || '').replace(/\s+/g, ' ').trim();
const kickerText = cleanInlineText(kicker) || (kicker.textContent || '').replace(/\s+/g, ' ').trim();
const headingFontSize = resolveLetterSpacing(headingStyle.fontSize || '', 16) || parseFloat(headingStyle.fontSize) || 0;
const kickerFontSize = resolveLetterSpacing(kickerStyle.fontSize || '', 16) || parseFloat(kickerStyle.fontSize) || 0;
const kickerLetterSpacing = resolveLetterSpacing(kickerStyle.letterSpacing || '', kickerFontSize);
if (!isRepeatedKickerCandidate({
headingTag: heading.tagName.toLowerCase(),
if (!isKickerCandidate({
headingLevel,
headingText,
headingFontSize,
kickerTag: kicker.tagName.toLowerCase(),
kickerText,
kickerTextTransform: kickerStyle.textTransform || '',
kickerFontVariant: `${kickerStyle.fontVariant || ''} ${kickerStyle.fontVariantCaps || ''}`,
kickerFontSize,
kickerLetterSpacing,
})) {
continue;
}
// A tracked-caps eyebrow above a hero-scale h1 belongs to
// hero-eyebrow-chip (which also covers the accent-bold and dash-prefix
// stylings there). Stand down so one element gets one finding.
if (headingTag === 'h1' && headingFontSize >= 48 && kickerLetterSpacing >= 1.6) {
continue;
}
candidates.push({
headingTag: heading.tagName.toLowerCase(),
headingTag,
headingText: headingText.replace(/^"|"$/g, '').slice(0, 60),
kickerText: kickerText.slice(0, 40),
});
@@ -2497,17 +2540,17 @@ function collectRepeatedSectionKickerCandidates(doc, getStyle, resolveLetterSpac
return candidates;
}
function checkRepeatedSectionKickersDOM() {
const candidates = collectRepeatedSectionKickerCandidates(
function checkKickerAboveHeadingDOM() {
const candidates = collectKickerCandidates(
document,
(el) => getComputedStyle(el),
(value, fontSize) => resolveLengthPx(value, fontSize) || 0,
);
return checkRepeatedSectionKickers({ candidates });
return checkKickerAboveHeading({ candidates });
}
// ── Numbered section labels ─────────────────────────────────────────────────
// Sibling of the repeated-kicker rule: instead of a tracked uppercase word,
// Sibling of the kicker-above-heading rule: instead of a tracked uppercase word,
// the section scaffold is a tiny numeric index riding beside each section
// heading — bare and zero-padded, or an index joined to a short micro-label
// by a separator glyph. The kicker rule deliberately excludes bare 1-2 digit
@@ -2561,7 +2604,7 @@ function collectNumberedSectionLabelCandidates(doc, getStyle, resolveLetterSpaci
const candidates = [];
const seenLabels = new Set();
for (const heading of doc.querySelectorAll('h2, h3, h4')) {
if (heading.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
if (heading.closest?.(KICKER_SKIP_SELECTOR)) continue;
// The index sits either directly before the heading, or before the
// wrapper the heading leads (label | <div><h2>…</h2>…</div>).
let label = heading.previousElementSibling;
@@ -2571,9 +2614,9 @@ function collectNumberedSectionLabelCandidates(doc, getStyle, resolveLetterSpaci
if (firstChild === heading) label = parent.previousElementSibling;
}
if (!label || seenLabels.has(label)) continue;
if (label.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
if (label.closest?.(KICKER_SKIP_SELECTOR)) continue;
if (HEADING_TAGS.has(label.tagName.toLowerCase())) continue;
if (isRepeatedKickerCardContext(heading, label)) continue;
if (isKickerCardContext(heading, label)) continue;
const labelText = cleanInlineText(label) || (label.textContent || '').replace(/\s+/g, ' ').trim();
const parsed = parseNumberedLabelText(labelText);
@@ -3746,13 +3789,13 @@ function checkElementHeroEyebrow(el, style, tag, window, customPropMap) {
});
}
function checkRepeatedSectionKickersFromDoc(doc, win) {
const candidates = collectRepeatedSectionKickerCandidates(
function checkKickerAboveHeadingFromDoc(doc, win) {
const candidates = collectKickerCandidates(
doc,
(el) => win.getComputedStyle(el),
(value, fontSize) => resolveLengthPx(value, fontSize) || 0,
);
return checkRepeatedSectionKickers({ candidates });
return checkKickerAboveHeading({ candidates });
}
function checkElementMotion(tag, style) {
@@ -5393,7 +5436,7 @@ export {
checkItalicSerif,
isAccentColor,
checkHeroEyebrow,
checkRepeatedSectionKickers,
checkKickerAboveHeading,
checkMotion,
checkGlow,
scanCssTextForGlow,
@@ -5424,9 +5467,9 @@ export {
parseAnyColor,
parseColorResolved,
cleanInlineText,
isRepeatedKickerCandidate,
collectRepeatedSectionKickerCandidates,
checkRepeatedSectionKickersDOM,
isKickerCandidate,
collectKickerCandidates,
checkKickerAboveHeadingDOM,
parseNumberedLabelText,
isNumberedSectionLabelCandidate,
collectNumberedSectionLabelCandidates,
@@ -5458,7 +5501,7 @@ export {
checkElementIconTile,
checkElementItalicSerif,
checkElementHeroEyebrow,
checkRepeatedSectionKickersFromDoc,
checkKickerAboveHeadingFromDoc,
checkElementMotion,
checkElementGlow,
checkTypography,
@@ -24,21 +24,36 @@
* "label": "Fillmore Handbill",
* "kicker": "THE ROLL", // optional badge; the assigned option leads
* "lineage": "1966-71 Fillmore ...", // optional
* "body": "why it fits, first viewport, risk ...", // optional, plain text
* "hero": "https://... or /abs/path.webp", // optional image
* "thesis": "one line: the idea this direction owns", // optional
* "palette": ["#1a2f5e", "oklch(84% .19 80)", ...], // optional, rendered as chips
* "materials": ["letterpress", "newsprint"], // optional, rendered as tags
* "viewport": "one line: the first-viewport composition", // optional
* "case": "one line: the fusion verdict, honest", // optional
* "risk": "one line: the honest risk", // optional
* "body": "fallback prose when the structured fields are absent",
* "sketch": ".impeccable/sketches/assigned.webp", // optional; may not exist
* // yet: the page shimmer-waits and polls the
* // slot until the file lands, so serve first
* // and generate after
* "hero": "https://... or /abs/path.webp", // optional inspiration image;
* // rides picture-in-picture when a sketch exists
* "board": "https://... or /abs/path.webp" // optional secondary image
* }, ...
* ],
* "reroll": true, // adds a re-roll action (returns {"optionId":"reroll"})
* "canon": true, // adds the quiet "Play it straight" standing exit
* // (returns {"optionId":"canon"}); direction rounds only
* "canon": true, // adds the "Play it straight" standing exit;
* // direction rounds only (returns {"optionId":"canon"})
* "canonCard": { ... }, // optional: the standing exit as a full card with the
* // same anatomy (label, thesis, palette, sketch, ...);
* // rendered last and visually subordinate. Without it,
* // canon stays a quiet footer action.
* "steer": true // adds a free-text steer field returned with any answer
* }
*
* Options render as large cards: hero render first when present (the dealt
* catalog worlds already have cards; grounded directions may present text-only
* or a freshly generated mock). Local image paths are served by this server;
* nothing is uploaded anywhere.
* Options render as large cards: the sketch leads when present, with the
* inspiration image picture-in-picture; a hero alone renders full-bleed; a
* text-only direction gets its identity from the palette chips and tags.
* Local image paths are served by this server; nothing is uploaded anywhere.
*
* Modes:
* (default) block until answered; ANSWER on stdout; exit 0.
@@ -107,6 +122,9 @@ function printAnswer(raw) {
if (a.hero || a.board) {
console.log("CHOSEN CARD: open the chosen world's board and hero images now, before any code. When your harness only reads files, or runs sandboxed, download them INTO the workspace and open the relative path; a sandboxed viewer rejects absolute paths outside it. They set the craft bar the build must reach.");
}
if (a.sketch) {
console.log('CHOSEN SKETCH: the decision sketch at that path may seed one comp probe; the comp round still renders its full set, because a sketch chose the direction, not the composition.');
}
if (a.optionId === 'canon') {
console.log('CANON CHOSEN: the user picked the category standard on purpose. Ask once for two or three products this should sit alongside; their craft level becomes the quality bar. Execute the canon at full commitment, conventions embraced without irony or smuggled quirk.');
}
@@ -125,14 +143,15 @@ if (hasFlag('schema')) {
title: 'Choose the visual world',
question: 'The roll assigned Fillmore Handbill. Keep it, take an alternate, or re-roll.',
options: [
{ id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', body: 'Why it fits, the first viewport, the honest risk.', hero: 'https://impeccable.style/worlds/cards/fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/fillmore-handbill.webp' },
{ id: 'challenger-teletext', label: 'Teletext Service', lineage: 'broadcast teletext magazines', body: 'Fused alternate.', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' },
{ id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', sketch: '.impeccable/sketches/assigned.webp', hero: 'https://impeccable.style/worlds/cards/fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/fillmore-handbill.webp' },
{ id: 'challenger-teletext', label: 'Teletext Service', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', case: 'Fuses cleanly: releases map to numbered pages.', sketch: '.impeccable/sketches/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' },
],
reroll: true,
canon: true,
canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', viewport: 'The arrangement a visitor expects, at full craft.', sketch: '.impeccable/sketches/canon.webp' },
steer: true,
}, null, 2));
console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board accept URLs or local paths. canon adds a quiet standing "Play it straight" action for direction decisions: the user\'s explicit door to the category standard. Include it only for visual-direction rounds; never present canon as your own recommendation.');
console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/sketch accept URLs or local paths; sketch slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the sketch, never as the promise of the build. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability.');
process.exit(0);
}
@@ -203,14 +222,25 @@ if (hasFlag('start')) {
const key = arg('key') || Math.random().toString(16).slice(2, 10);
// In start mode the agent is alive and owns browser routing; the server
// only opens the system browser itself when --open forces it.
// The daemon's output lands in a per-key log so a startup failure can say
// what actually went wrong instead of only that it did.
const logFile = path.join(QUESTION_DIR, `${key}.log`);
const logFd = fs.openSync(logFile, 'a');
const child = spawn(process.execPath, [
fileURLToPath(import.meta.url), '--payload', payloadPath, '--detached-serve', '--key', key,
'--timeout', String(timeoutSec), ...(hasFlag('open') ? [] : ['--no-open']),
], { detached: true, stdio: 'ignore' });
], { detached: true, stdio: ['ignore', logFd, logFd] });
child.unref();
fs.closeSync(logFd);
const deadline = Date.now() + 8000;
while (Date.now() < deadline && !fs.existsSync(stateFile(key))) await new Promise((r) => setTimeout(r, 100));
if (!fs.existsSync(stateFile(key))) { console.error('serve-question: server failed to start'); process.exit(1); }
if (!fs.existsSync(stateFile(key))) {
let tail = '';
try { tail = fs.readFileSync(logFile, 'utf8').trim().split('\n').slice(-4).join('\n '); } catch { /* log never written */ }
console.error(`serve-question: server failed to start${tail ? `\n ${tail}` : ''}`);
console.error(`serve-question: log at ${path.relative(process.cwd(), logFile) || logFile}. A sandboxed exec that cannot listen on localhost causes exactly this; rerun this command once through the harness's network-enabled or unsandboxed command tool before falling back.`);
process.exit(1);
}
const state = JSON.parse(fs.readFileSync(stateFile(key), 'utf8'));
console.log(`QUESTION URL: ${state.url}`);
console.log(`QUESTION KEY: ${key}`);
@@ -244,12 +274,28 @@ function loadRound(json) {
localImages.push(abs);
return `/img/${localImages.length - 1}`;
};
// Sketches stream in after the page is served, so their slots register
// whether or not the file exists yet; /img answers 404 until it lands and
// the page polls the slot. Remote sketch URLs pass through untouched.
const sketchSrc = (value) => {
if (!value) return null;
if (/^https?:\/\//.test(value)) return value;
localImages.push(path.resolve(value));
return `/img/${localImages.length - 1}`;
};
payload = parsed;
options = parsed.options.map((option) => ({
const decorate = (option) => ({
...option,
heroSrc: imageSrc(option.hero),
boardSrc: imageSrc(option.board),
}));
sketchSrc: sketchSrc(option.sketch),
});
options = parsed.options.map(decorate);
// The standing exit as a full card: same anatomy, reserved id, rendered
// subordinate by the page. Without it, canon stays the quiet footer action.
if (parsed.canonCard && typeof parsed.canonCard === 'object') {
options = [...options, { ...decorate(parsed.canonCard), id: 'canon', isCanon: true }];
}
}
try { loadRound(raw); } catch (error) { console.error(`serve-question: ${error.message}`); process.exit(1); }
const detachedKey = hasFlag('detached-serve') ? arg('key') : null;
@@ -260,30 +306,81 @@ const esc = (s) => String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<
function page() {
const flipChip = (label) => `<button type="button" class="chip flip" aria-label="Flip the card"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4a8 8 0 1 1-8 8" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/><path d="M4 5.5V12h6.5" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg><span>${label}</span></button>`;
const expandChip = `<button type="button" class="chip expand" aria-label="Expand the image"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 9V4h5M20 15v5h-5M20 9V4h-5M4 15v5h5" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg></button>`;
const cards = options.map((option, index) => `
<article class="card" style="--fan:${index === 0 ? '0deg' : (index % 2 ? '1.4deg' : '-1.2deg')};--deal:${index * 90}ms" data-id="${esc(option.id)}">
<div class="card-inner">
<div class="face front${index === 0 ? ' lead' : ''}${option.heroSrc || option.boardSrc ? '' : ' text-only'}">
${option.kicker ? `<span class="kicker">${esc(option.kicker)}</span>` : ''}
${option.heroSrc || option.boardSrc ? `<div class="media">
// Structured anatomy: chips and one-line facts render when the payload
// carries them; a plain body falls back to the prose block. Palette chips
// and material tags give a text-only direction an immediate identity that
// no generation luck can distort.
const fact = (label, value, cls = '') => value ? `<p class="fact${cls ? ` ${cls}` : ''}"><span class="fact-label">${label}</span>${esc(value)}</p>` : '';
const hasBack = (option) => Boolean(option.viewport || option.case || (option.boardSrc && option.heroSrc));
const anatomy = (option) => {
const rows = [];
if (option.thesis) rows.push(`<p class="thesis">${esc(option.thesis)}</p>`);
const idBits = [];
if (Array.isArray(option.palette) && option.palette.length) {
idBits.push(`<span class="swatches">${option.palette.slice(0, 6).map((c) => `<i style="background:${esc(c)}" title="${esc(c)}"></i>`).join('')}</span>`);
}
if (Array.isArray(option.materials) && option.materials.length) {
idBits.push(option.materials.slice(0, 4).map((m) => `<span class="tag">${esc(m)}</span>`).join(''));
}
if (idBits.length) rows.push(`<div class="identity">${idBits.join('')}</div>`);
// The front carries only what the choice needs: thesis, identity, and the
// honest risk clamped to two lines. First viewport and the case read on
// the card's back; once the sketch lands, the first viewport is a picture.
rows.push(fact('Risk', option.risk, 'clamp'));
if (!option.thesis && option.body) rows.push(`<p class="detail">${esc(option.body)}</p>`);
else if (option.body && option.thesis && !hasBack(option)) rows.push(`<p class="detail more">${esc(option.body)}</p>`);
return rows.join('\n ');
};
const backFacts = (option) => [
fact('First viewport', option.viewport),
fact('The case', option.case),
fact('Risk', option.risk),
option.body && option.thesis ? `<p class="detail more">${esc(option.body)}</p>` : '',
].filter(Boolean).join('\n ');
const media = (option) => {
const inspiration = option.heroSrc ? `<figure class="pip" title="Inspiration: the world this direction draws from. Your page will not look like this image.">
<img src="${esc(option.heroSrc)}" alt="">
<figcaption>inspiration</figcaption>
</figure>` : '';
const details = hasBack(option) ? flipChip('Details') : '';
if (option.sketchSrc) {
return `<div class="media sketching" data-sketch="${esc(option.sketchSrc)}">
<div class="shimmer"><span class="sketch-note">sketching&hellip;</span></div>
<img class="sketch" alt="" hidden>
${inspiration}
<div class="chips">${expandChip}${details}</div>
</div>`;
}
if (option.heroSrc || option.boardSrc) {
return `<div class="media">
<img src="${esc(option.heroSrc || option.boardSrc)}" alt="">
<div class="chips">${expandChip}${option.boardSrc && option.heroSrc ? flipChip('Board') : ''}</div>
</div>` : ''}
<div class="chips">${expandChip}${details}</div>
</div>`;
}
return '';
};
const cards = options.map((option, index) => `
<article class="card${option.isCanon ? ' canon' : ''}" style="--fan:${index === 0 ? '0deg' : (index % 2 ? '1.4deg' : '-1.2deg')};--deal:${index * 90}ms" data-id="${esc(option.id)}">
<div class="card-inner">
<div class="face front${index === 0 ? ' lead' : ''}${media(option) ? '' : ' text-only'}">
${option.kicker ? `<span class="kicker">${esc(option.kicker)}</span>` : option.isCanon ? '<span class="kicker standing">The standing door</span>' : ''}
${media(option)}
<div class="body">
${option.lineage ? `<p class="tier">${esc(option.lineage)}</p>` : ''}
<h2>${esc(option.label)}</h2>
${option.body ? `<p class="detail">${esc(option.body)}</p>` : ''}
<button class="choose" data-id="${esc(option.id)}">Build this</button>
${anatomy(option)}
<button class="choose" data-id="${esc(option.id)}">${option.isCanon ? 'Play it straight' : 'Build this'}</button>
</div>
</div>
${option.boardSrc && option.heroSrc ? `<div class="face back${index === 0 ? ' lead' : ''}">
<div class="media back-media">
${hasBack(option) ? `<div class="face back${index === 0 ? ' lead' : ''}">
${option.boardSrc ? `<div class="media back-media">
<img src="${esc(option.boardSrc)}" alt="">
<div class="chips">${expandChip}${flipChip('Hero')}</div>
</div>
<div class="body back-bar">
<p class="tier">Design-system board &middot; ${esc(option.label)}</p>
<button class="choose" data-id="${esc(option.id)}">Build this</button>
<div class="chips">${expandChip}${flipChip('Front')}</div>
</div>` : `<div class="back-head"><p class="tier">The full read &middot; ${esc(option.label)}</p>${flipChip('Front')}</div>`}
<div class="body back-body">
${option.boardSrc ? `<p class="tier">The full read &middot; ${esc(option.label)}</p>` : ''}
${backFacts(option)}
<button class="choose" data-id="${esc(option.id)}">${option.isCanon ? 'Play it straight' : 'Build this'}</button>
</div>
</div>` : ''}
</div>
@@ -318,7 +415,7 @@ function page() {
--ks-mono: "SFMono-Regular", "Roboto Mono", "JetBrains Mono", Consolas, monospace;
}
* { box-sizing: border-box; margin: 0; }
body { background: var(--ks-lacquer); color: var(--ks-text); font: 15px/1.55 var(--ks-font); padding: 1.8rem clamp(1rem, 5vw, 4rem) 2rem; min-height: 100dvh; display: flex; flex-direction: column; }
body { background: var(--ks-lacquer); color: var(--ks-text); font: 15px/1.55 var(--ks-font); padding: 1.8rem clamp(1rem, 5vw, 4rem) 2rem; min-height: 100dvh; display: flex; flex-direction: column; overflow-x: clip; }
#ambient { position: fixed; inset: -40px; z-index: 0; background-size: cover; background-position: center; filter: blur(34px) saturate(1.05); opacity: 0; transition: opacity .55s ease, background-image .2s; pointer-events: none; }
#scrim { position: fixed; inset: 0; z-index: 0; background: linear-gradient(180deg, oklch(7% 0.006 95 / 0.62), oklch(7% 0.006 95 / 0.78)); pointer-events: none; }
header, main, footer { position: relative; z-index: 1; }
@@ -336,7 +433,42 @@ function page() {
.question { color: var(--ks-text-muted); margin-top: .7rem; max-width: 52rem; }
main { flex: 1; display: flex; align-items: center; width: 100%; max-width: 90rem; margin: 0 auto; }
.stage { width: 100%; display: flex; flex-direction: column; gap: 1.5rem; }
.grid { display: grid; gap: 1.6rem; grid-template-columns: repeat(auto-fit, minmax(min(23rem, 100%), 1fr)); width: 100%; }
/* The deck bleeds to the viewport edges while the first card aligns with the
content column; a carousel cut off at an invisible container edge reads as
a rendering bug, but one cut off at the screen edge reads as more cards. */
.deck-shell { position: relative; width: 100vw; margin-left: calc(50% - 50vw); }
/* One row in a wide viewport, one column in a tall one; the deck scrolls on
its axis with snap points and the arrows page it card by card. */
.grid { --deck-inset: max(clamp(1rem, 5vw, 4rem), calc((100vw - 90rem) / 2)); display: flex; gap: 1.6rem; width: 100%; overflow-x: auto; overflow-y: hidden; scroll-snap-type: x mandatory; scrollbar-width: none; padding: 6px var(--deck-inset); scroll-padding-inline: var(--deck-inset); align-items: stretch; }
.grid::-webkit-scrollbar { display: none; }
.grid > .card { flex: 0 0 clamp(20rem, 27vw, 27rem); scroll-snap-align: center; }
.nav { position: absolute; z-index: 6; width: 42px; height: 42px; display: flex; align-items: center; justify-content: center; border-radius: 50%; background: oklch(7% 0.006 95 / 0.78); border: 1px solid var(--ks-rule); color: var(--ks-kinpaku); cursor: pointer; backdrop-filter: blur(6px); transition: border-color .2s, color .2s, opacity .2s; }
.nav:hover { border-color: var(--ks-kinpaku-deep); color: var(--ks-kinpaku-pale); }
.nav[disabled] { opacity: .25; cursor: default; }
.nav[hidden] { display: none; }
.nav svg { width: 16px; height: 16px; }
.nav.prev { left: 14px; top: 50%; transform: translateY(-50%); }
.nav.next { right: 14px; top: 50%; transform: translateY(-50%); }
/* A side that hides more cards fades out; a hard edge means the end. */
.fade { position: absolute; z-index: 5; pointer-events: none; opacity: 0; transition: opacity .3s ease; }
.fade-prev { left: 0; top: 0; bottom: 0; width: 88px; background: linear-gradient(90deg, var(--ks-lacquer), transparent); }
.fade-next { right: 0; top: 0; bottom: 0; width: 88px; background: linear-gradient(270deg, var(--ks-lacquer), transparent); }
.deck-shell.can-prev .fade-prev { opacity: 1; }
.deck-shell.can-next .fade-next { opacity: 1; }
@media (max-aspect-ratio: 1/1) {
.grid { flex-direction: column; overflow-x: hidden; overflow-y: auto; scroll-snap-type: y mandatory; max-height: min(68dvh, 44rem); scroll-padding-block: 6px; }
.grid > .card { flex: 0 0 auto; }
/* In the vertical deck the pager is the primary way forward, so it grows
into a labeled pill instead of a bare chevron nobody notices. */
.nav { width: auto; height: 38px; border-radius: 19px; padding: 0 16px; gap: 8px; border-color: var(--ks-kinpaku-deep); background: oklch(7% 0.006 95 / 0.88); font-family: var(--ks-mono); font-size: .62rem; letter-spacing: .2em; text-transform: uppercase; }
.nav svg { transform: rotate(90deg); }
.nav.prev::after { content: "Back"; }
.nav.next::after { content: "More"; }
.nav.prev { left: 50%; top: 6px; transform: translate(-50%, 0); }
.nav.next { right: auto; left: 50%; top: auto; bottom: 6px; transform: translate(-50%, 0); }
.fade-prev { top: 0; left: 0; right: 0; bottom: auto; width: auto; height: 72px; background: linear-gradient(180deg, var(--ks-lacquer), transparent); }
.fade-next { top: auto; left: 0; right: 0; bottom: 0; width: auto; height: 72px; background: linear-gradient(0deg, var(--ks-lacquer), transparent); }
}
.card { position: relative; perspective: 1400px; transform: rotate(var(--fan, 0deg)); transition: transform .25s cubic-bezier(.16, 1, .3, 1); }
.card:hover { transform: rotate(0deg) translateY(-4px); }
.card-inner { position: relative; height: 100%; transform-style: preserve-3d; transition: transform .7s cubic-bezier(.16, 1, .3, 1); }
@@ -344,6 +476,12 @@ function page() {
.face { background: var(--ks-lacquer-raised); border: 1px solid var(--ks-rule); border-radius: 10px; box-shadow: 0 18px 40px oklch(0% 0 0 / 0.35); overflow: hidden; display: flex; flex-direction: column; backface-visibility: hidden; -webkit-backface-visibility: hidden; }
.face.front { position: relative; height: 100%; }
.face.back { position: absolute; inset: 0; transform: rotateY(180deg); }
/* Only the visible face is interactive: a hidden backface still hit-tests
in Chrome, so the front's pip would otherwise sit invisibly over the
back's chips, showing its zoom cursor and eating the flip-back click. */
.face.back { pointer-events: none; }
.card.flipped .face.back { pointer-events: auto; }
.card.flipped .face.front { pointer-events: none; }
.face.lead { border-color: var(--ks-kinpaku); box-shadow: 0 0 0 1px var(--ks-kinpaku), 0 18px 40px oklch(0% 0 0 / 0.45); }
.card:hover .face { border-color: var(--ks-kinpaku-deep); }
.card:hover .face.lead { border-color: var(--ks-kinpaku); }
@@ -355,7 +493,7 @@ function page() {
.face.text-only .body { padding-top: 12px; }
.media { position: relative; width: 100%; aspect-ratio: 16/9; flex: none; }
.media img { width: 100%; height: 100%; object-fit: cover; display: block; background: linear-gradient(100deg, var(--ks-graphite) 40%, var(--ks-graphite-2) 50%, var(--ks-graphite) 60%); }
.face.back { background: var(--ks-lacquer-deep); }
.face.back { background: var(--ks-lacquer-raised); }
.back-bar { margin-top: auto; background: var(--ks-lacquer-raised); }
.hero-blank { width: 100%; height: 100%; background: linear-gradient(100deg, var(--ks-graphite) 40%, var(--ks-graphite-2) 50%, var(--ks-graphite) 60%); }
.back-bar { flex: none; flex-direction: row; align-items: center; justify-content: space-between; gap: .8rem; }
@@ -367,6 +505,48 @@ function page() {
.tier { font-family: var(--ks-mono); font-size: .625rem; letter-spacing: .24em; text-transform: uppercase; color: var(--ks-text-faint); }
h2 { font-family: var(--ks-font); font-size: 1.125rem; font-weight: 500; line-height: 1.35; color: var(--ks-champagne); }
.detail { color: var(--ks-text-muted); font-size: .88rem; white-space: pre-wrap; }
.detail.more { font-size: .8rem; color: var(--ks-text-faint); }
.thesis { color: var(--ks-text); font-size: .95rem; line-height: 1.45; }
.identity { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin: 2px 0; }
.swatches { display: inline-flex; gap: 4px; margin-right: 4px; }
.swatches i { width: 18px; height: 18px; border-radius: 5px; border: 1px solid oklch(100% 0 0 / 0.18); box-shadow: inset 0 0 0 1px oklch(0% 0 0 / 0.25); }
.tag { font-family: var(--ks-mono); font-size: .6rem; letter-spacing: .14em; text-transform: uppercase; color: var(--ks-text-muted); border: 1px solid var(--ks-rule); border-radius: 4px; padding: 3px 7px; }
.fact { font-size: .8rem; color: var(--ks-text-muted); line-height: 1.45; }
.fact-label { display: inline-block; font-family: var(--ks-mono); font-size: .6rem; letter-spacing: .18em; text-transform: uppercase; color: var(--ks-text-faint); margin-right: .55em; transform: translateY(-1px); }
.fact.clamp { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
/* The back is the full read: first viewport, the case, the whole risk, and
the board when the world has one. */
.back-head { display: flex; align-items: center; justify-content: space-between; gap: .8rem; padding: 14px 14px 0; }
.media.back-media { aspect-ratio: 16/6; }
.media.back-media img { width: 100%; height: 100%; object-fit: cover; }
.body.back-body { overflow-y: auto; flex: 1; scrollbar-width: thin; }
/* Inspiration rides picture-in-picture: the catalog world explains where the
direction comes from without promising what the build will look like. */
/* Hovering the inspiration takes over the whole media region; the sketch is
the promise, the inspiration is a glance, so the glance must cost nothing. */
.pip { position: absolute; z-index: 2; left: 10px; bottom: 10px; margin: 0; width: 84px; height: 64px; border: 1px solid var(--ks-rule); border-radius: 6px; overflow: hidden; background: var(--ks-lacquer); cursor: zoom-in; transition: left .35s cubic-bezier(.16,1,.3,1), bottom .35s cubic-bezier(.16,1,.3,1), width .35s cubic-bezier(.16,1,.3,1), height .35s cubic-bezier(.16,1,.3,1), border-radius .35s ease; box-shadow: 0 6px 18px oklch(0% 0 0 / 0.45); }
.pip img { display: block; width: 100%; height: 100%; object-fit: cover; }
.pip figcaption { position: absolute; left: 0; right: 0; bottom: 0; font-family: var(--ks-mono); font-size: .5rem; letter-spacing: .2em; text-transform: uppercase; color: var(--ks-text); text-align: center; padding: 3px 0 4px; background: oklch(7% 0.006 95 / 0.72); backdrop-filter: blur(3px); }
.pip:hover { left: 0; bottom: 0; width: 100%; height: 100%; border-radius: 0; z-index: 3; }
.sketch-note { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-family: var(--ks-mono); font-size: .66rem; letter-spacing: .22em; text-transform: uppercase; color: var(--ks-text-faint); }
/* A stand-in is honest about being one: dimmed, labeled, and replaced by
the real sketch whenever it lands. */
.media.stand-in img.sketch { filter: brightness(.72) saturate(.85); }
.media.stand-in .pip { display: none; }
.stand-in-label { position: absolute; z-index: 2; left: 0; right: 0; bottom: 0; margin: 0; font-family: var(--ks-mono); font-size: .56rem; letter-spacing: .2em; text-transform: uppercase; color: var(--ks-text); text-align: center; padding: 4px 0 5px; background: oklch(7% 0.006 95 / 0.78); backdrop-filter: blur(3px); }
.media.sketching { position: relative; }
.media.sketching .shimmer { position: absolute; inset: 0; }
.media img.sketch { position: relative; z-index: 1; }
/* The generic .media img display:block would defeat [hidden] and float an
empty block over the shimmer; an unloaded sketch must truly not render. */
.media img[hidden] { display: none; }
/* The standing exit as a card: present with full anatomy, never dressed as a
contender. Graphite instead of kinpaku, and it never takes the lead ring. */
.card.canon .face { border-color: var(--ks-rule); background: var(--ks-graphite); }
.card.canon:hover .face { border-color: var(--ks-text-faint); }
.card.canon .kicker.standing { background: transparent; border: 1px solid var(--ks-rule); color: var(--ks-text-faint); }
.card.canon button.choose { background: transparent; color: var(--ks-text); border: 1px solid var(--ks-rule); }
.card.canon button.choose:hover { border-color: var(--ks-text-muted); background: var(--ks-graphite-2); }
button.choose { margin-top: auto; align-self: start; background: var(--ks-kinpaku); color: var(--ks-dark-ink); border: 0; font-family: var(--ks-font); font-size: 1rem; font-weight: 500; line-height: 1.35; padding: 10px 38px; border-radius: 6px; cursor: pointer; transition: background .15s; }
button.choose:hover { background: var(--ks-kinpaku-pale); }
footer { width: 100%; max-width: 90rem; margin: 1.6rem auto 0; display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; }
@@ -408,13 +588,19 @@ function page() {
<h1>${esc(payload.title || 'Choose a direction')}</h1>
</div>
${payload.question ? `<p class="question">${esc(payload.question)}</p>` : ''}
<div class="grid">${cards}</div>
<div class="deck-shell">
<div class="grid">${cards}</div>
<div class="fade fade-prev" aria-hidden="true"></div>
<div class="fade fade-next" aria-hidden="true"></div>
<button class="nav prev" hidden aria-label="Previous card"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14.5 5 8 12l6.5 7" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
<button class="nav next" hidden aria-label="Next card"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9.5 5 16 12l-6.5 7" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
</div>
</div>
</main>
<footer>
${payload.steer ? '<input id="steer" placeholder="Optional steer: what should be different or kept?">' : ''}
${payload.reroll ? '<button id="reroll"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="4" fill="none" stroke="currentColor" stroke-width="1.6"/><circle cx="8.4" cy="8.4" r="1.5" fill="currentColor"/><circle cx="15.6" cy="8.4" r="1.5" fill="currentColor"/><circle cx="8.4" cy="15.6" r="1.5" fill="currentColor"/><circle cx="15.6" cy="15.6" r="1.5" fill="currentColor"/><circle cx="12" cy="12" r="1.5" fill="currentColor"/></svg><span>Re-roll</span></button>' : ''}
${payload.canon ? '<button id="canon" title="Skip the roll: build the page this category ships, executed impeccably">Play it straight</button>' : ''}
${payload.canon && !payload.canonCard ? '<button id="canon" title="Skip the roll: build the page this category ships, executed impeccably">Play it straight</button>' : ''}
</footer>
<script>
const steer = () => document.getElementById('steer')?.value || '';
@@ -434,7 +620,11 @@ function page() {
// Deal from the stack: cards begin piled at the grid's center, blurred,
// then travel to their seats with a stagger.
const cards = [...document.querySelectorAll('.card')];
if (!matchMedia('(prefers-reduced-motion: reduce)').matches && cards.length) {
// The deal is decoration: a hidden tab throttles rAF, so never let the
// animation hold the cards at opacity 0. Skip it when hidden, and force
// the final state after a beat no matter what the animation did.
setTimeout(() => cards.forEach(c => { c.style.opacity = ''; c.style.transform = ''; c.style.filter = ''; c.style.transition = ''; c.style.zIndex = ''; }), 1600);
if (!matchMedia('(prefers-reduced-motion: reduce)').matches && cards.length && !document.hidden) {
const grid = document.querySelector('.grid').getBoundingClientRect();
const cx = grid.left + grid.width / 2, cy = grid.top + grid.height / 2;
cards.forEach((card, i) => {
@@ -460,12 +650,99 @@ function page() {
}));
}
// Ambient: the hovered card's hero bleeds into the page ground under a scrim.
// Sketches stream in after the deal: poll each slot until the file lands,
// then swap the shimmer for the image. Generation is genuinely slow and a
// sequential batch puts the last card many minutes out, so patience is the
// default: a slot only shows its inspiration as a stand-in when it has
// waited four minutes AND nothing has landed anywhere for four minutes, the
// stand-in is labeled as such, and polling continues so the real sketch
// still swaps in whenever it arrives. Progress anywhere resets patience.
const landTracker = { last: Date.now() };
document.querySelectorAll('.media.sketching').forEach(m => {
const url = m.dataset.sketch;
const img = m.querySelector('img.sketch');
const note = m.querySelector('.sketch-note');
const started = Date.now();
// A live elapsed count is the difference between "working" and "frozen".
const tick = setInterval(() => { if (note) note.textContent = 'sketching · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000);
const settle = () => { clearInterval(tick); m.classList.remove('sketching', 'stand-in'); m.querySelector('.shimmer')?.remove(); m.querySelector('.stand-in-label')?.remove(); };
const standIn = () => {
const pip = m.querySelector('.pip img');
if (!pip || m.classList.contains('stand-in')) return;
img.src = pip.getAttribute('src'); img.hidden = false;
m.classList.add('stand-in');
m.querySelector('.shimmer')?.remove();
clearInterval(tick);
const label = document.createElement('p');
label.className = 'stand-in-label';
label.textContent = 'inspiration · sketch pending';
m.appendChild(label);
};
const tryLoad = () => {
const probe = new Image();
probe.onload = () => { landTracker.last = Date.now(); img.src = probe.src; img.hidden = false; settle(); };
probe.onerror = () => {
const quiet = Date.now() - landTracker.last > 240000;
if (Date.now() - started > 240000 && quiet) standIn();
setTimeout(tryLoad, m.classList.contains('stand-in') ? 5000 : 2500);
};
probe.src = url + (url.includes('?') ? '&' : '?') + 't=' + Date.now();
};
tryLoad();
});
// Inspiration PIP opens the full catalog card in the lightbox.
document.querySelectorAll('.pip').forEach(p => p.addEventListener('click', (e) => {
e.stopPropagation();
const img = p.querySelector('img');
if (!img) return;
lightboxImg.src = img.getAttribute('src');
lightbox.hidden = false;
requestAnimationFrame(() => lightbox.classList.add('open'));
}));
// Deck paging: arrows appear only when the deck overflows its axis, page
// one card at a time, and follow the aspect-ratio flip between row and column.
const deck = document.querySelector('.grid');
const prevBtn = document.querySelector('.nav.prev');
const nextBtn = document.querySelector('.nav.next');
const vertical = () => matchMedia('(max-aspect-ratio: 1/1)').matches;
function updateNav() {
if (!deck || !prevBtn) return;
const shell = deck.closest('.deck-shell');
const v = vertical();
const overflow = v ? deck.scrollHeight > deck.clientHeight + 4 : deck.scrollWidth > deck.clientWidth + 4;
prevBtn.hidden = nextBtn.hidden = !overflow;
const pos = v ? deck.scrollTop : deck.scrollLeft;
const max = v ? deck.scrollHeight - deck.clientHeight : deck.scrollWidth - deck.clientWidth;
const canPrev = overflow && pos > 2;
const canNext = overflow && pos < max - 2;
prevBtn.toggleAttribute('disabled', !canPrev);
nextBtn.toggleAttribute('disabled', !canNext);
shell?.classList.toggle('can-prev', canPrev);
shell?.classList.toggle('can-next', canNext);
}
function pageDeck(dir) {
const card = deck.querySelector('.card');
if (!card) return;
const r = card.getBoundingClientRect();
const step = (vertical() ? r.height : r.width) + 26;
deck.scrollBy(vertical() ? { top: dir * step, behavior: 'smooth' } : { left: dir * step, behavior: 'smooth' });
}
prevBtn?.addEventListener('click', () => pageDeck(-1));
nextBtn?.addEventListener('click', () => pageDeck(1));
deck?.addEventListener('scroll', updateNav, { passive: true });
addEventListener('resize', updateNav);
updateNav();
// Ambient: the hovered card's visible art bleeds into the page ground.
const ambient = document.getElementById('ambient');
document.querySelectorAll('.card').forEach(card => {
const hero = card.querySelector('.face.front .media img');
if (!hero) return;
card.addEventListener('mouseenter', () => { ambient.style.backgroundImage = 'url("' + hero.getAttribute('src') + '")'; ambient.style.opacity = '1'; });
card.addEventListener('mouseenter', () => {
const art = card.querySelector('.face.front .media img:not([hidden])') || card.querySelector('.face.front .pip img');
if (!art || !art.getAttribute('src')) return;
ambient.style.backgroundImage = 'url("' + art.getAttribute('src') + '")'; ambient.style.opacity = '1';
});
card.addEventListener('mouseleave', () => { ambient.style.opacity = '0'; });
});
@@ -476,8 +753,8 @@ function page() {
e.stopPropagation();
const card = b.closest('.card');
const face = card.classList.contains('flipped') ? '.face.back' : '.face.front';
const img = card.querySelector(face + ' .media img');
if (!img) return;
const img = card.querySelector(face + ' .media img:not([hidden])');
if (!img || !img.getAttribute('src')) return;
lightboxImg.src = img.getAttribute('src');
lightbox.hidden = false;
requestAnimationFrame(() => lightbox.classList.add('open'));
@@ -546,10 +823,10 @@ const server = http.createServer((req, res) => {
res.end(JSON.stringify({ ready: Boolean(pending && fs.existsSync(pending)) }));
return;
}
const imageMatch = req.method === 'GET' && req.url?.match(/^\/img\/(\d+)$/);
const imageMatch = req.method === 'GET' && req.url?.match(/^\/img\/(\d+)(?:\?.*)?$/);
if (imageMatch) {
const abs = localImages[Number(imageMatch[1])];
if (!abs) { res.writeHead(404); res.end(); return; }
if (!abs || !fs.existsSync(abs)) { res.writeHead(404); res.end(); return; }
const type = abs.endsWith('.webp') ? 'image/webp'
: abs.endsWith('.png') ? 'image/png'
: abs.endsWith('.svg') ? 'image/svg+xml'
@@ -572,6 +849,7 @@ const server = http.createServer((req, res) => {
optionId: parsed.optionId ?? null,
steer: parsed.steer ?? '',
...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}),
...(chosen?.sketch ? { sketch: chosen.sketch } : {}),
});
const isReroll = parsed.optionId === 'reroll';
if (detachedKey) {
+1 -1
View File
@@ -12,7 +12,7 @@
{
"name": "impeccable",
"description": "Design fluency for frontend development. 1 skill with 23 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.",
"version": "4.0.2",
"version": "4.0.3",
"author": {
"name": "Paul Bakaus",
"email": "paul@paulbakaus.com"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "impeccable",
"description": "Design fluency for frontend development. 1 skill with 23 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.",
"version": "4.0.2",
"version": "4.0.3",
"author": {
"name": "Paul Bakaus",
"email": "paul@paulbakaus.com"
+5 -1
View File
@@ -4,7 +4,7 @@ description: Produces clean reusable raster assets from approved Impeccable mock
tools: Read, Write, Edit, Bash, Glob, Grep
model: inherit
effort: medium
maxTurns: 12
maxTurns: 24
---
# Impeccable Asset Producer
@@ -16,6 +16,10 @@ Your job is production cleanup, not new art direction. Work only from the approv
Do not redesign. Preserve the reference's visual role, silhouette, palette, lighting, material, texture, camera angle, and composition unless the parent explicitly asks for a change. Preserve perspective only when it belongs to the object or scene itself; if CSS should create the card transform, shadow, rounded clipping, border, or layout, remove that presentation chrome from the raster.
## Decision Sketches
When the parent hands you a decision card packet instead of an approved mock, the job is one sketch: one card, one file, written to the card's declared `sketch` path the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a sketch is reported back, not padded from imagination. Render through the parent's shared frame: the requested surface's first viewport as a flat, matte design sketch in the card's own palette and type character, deliberately unfinished, no photorealism, no gloss. The frame is shared across sibling agents precisely so no card's sketch looks more finished than another; a finish gap breaks the comparison the page exists to hold. The only legible text is the product's real name and one real headline; greek every other text region into indistinct lines, because an invented spec, price, or date in a sketch is a claim PRODUCT.md never made. Return one line naming the path and any deviation, nothing more.
## Input Contract
Expect:
+3 -1
View File
@@ -4,12 +4,14 @@ description: Records DESIGN.md and its sidecar from a finished Impeccable build,
tools: Read, Write, Bash, Glob, Grep
model: inherit
effort: medium
maxTurns: 12
maxTurns: 30
---
# Impeccable Documenter
You record a project's design system after the build is done. Ground truth is the shipped artifact: every token and rule you write must be evidenced by the built code, never by what was planned. Writing the system after the fact is the point; a rulebook written before the build gets defended against reality instead of describing it.
You run under a hard turn ceiling that ends the run without warning, and a run that ends before DESIGN.md is written has recorded nothing. Batch several Reads into each turn, take `reference/document.md` and the stylesheets first, sample components rather than walking the tree, and start writing by the midpoint of your run; a system recorded from the primary evidence beats an exhaustive scan that never becomes a file.
## Input Contract
Expect: the project root; the artifact path(s); the direction contract text (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; the path to the skill's `reference/document.md`; and the boundary to write at (project or app root). An existing DESIGN.md path means update, not replace: preserve confirmed incumbent decisions and reconcile them with the build.
+11 -5
View File
@@ -4,7 +4,7 @@ description: Reviews a finished Impeccable build against its direction contract,
tools: Read, Bash, Glob, Grep
model: inherit
effort: high
maxTurns: 12
maxTurns: 30
---
# Impeccable Finish Reviewer
@@ -12,20 +12,26 @@ You are the finishing reviewer for an Impeccable build: fresh eyes on a done art
You have no browser. Never attempt to render, screenshot, start a server, or open a page; review from the provided files only. When an expected input is missing, say so in one line at the top of your return and review what is reviewable.
You run under a hard turn ceiling that ends the run without warning, and a run that ends before the five sections are written returns nothing; a review built from what you saw beats a perfect review that never arrives. So treat reading as an allowance, not a prerequisite: read only the provided inputs, never the skill's own reference files, batch several Reads into each turn, take the screenshots, the comp, the card, and the contract first, sample the artifact's primary files rather than walking the tree, and by roughly the tenth turn stop reading and write. Name whatever went unread in the line above the sections.
## Input Contract
Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path. When the harness can view images, open the screenshots, the comp, and the card before judging.
Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: the contract is the builder's abstraction of the comp, and a review anchored on it inherits whatever that abstraction dropped.
## Checks, in order
1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding.
2. **Fidelity.** Against the approved comp: topology, reading order, focal scale, overlaps and z-order, density, signature geometry. Name every drift and classify it intentional or defect. The comp governs composition; it is not a pixel spec for semantics, accessibility, or responsive behavior.
2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement.
3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. The card governs commitment and finish, never composition.
4. **Contract, promise by promise.** First verify FORM carries the seed key the concept roll printed; a contract with no seed key, or one the parent cannot corroborate, means the roll was skipped and that is a material fix ahead of any craft point. Then, for each of the five blocks, does the render keep the promise? Apply the memory test to the first viewport.
5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one.
5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one, and every produced asset visibly present in the screenshots; an asset applied at near-zero opacity or buried behind other paint is a compliance token, not a shipped material.
Do not run a second detector pass; mechanical findings belong to the parent's hooks.
## Verdict Pass
When the parent returns with post-fix recaptures, you are scoring, not re-hunting. For each material fix from your review, one line: resolved, partial, or unresolved, tied to what the new screenshots visibly show; a fix answered mechanically, positions moved but the quality the finding named still absent, is partial at best. Then name at most three regressions the fix batch itself introduced, judged by the same matrix rules, and nothing else; no new hunt, no new checks. Return exactly two sections: `verdict` (the scored list) and `remaining` (what stays open, or "clear").
## Output Contract
Return exactly five sections: `persistence` (pass/fail with specifics), `fidelity` (drift list with intentional/defect per item, or "faithful"), `ceiling` (unused native devices, or "reached"), `material_fixes` (ordered, most material first, each one line tied to a check or contract promise, at most eight), and `keep` (one line naming what must not be diluted while fixing). Missing inputs are named in one line above the sections. No praise, no summary prose.
Return exactly five sections: `persistence` (pass/fail with specifics), `fidelity` (the element matrix: match, adaptation, missing, contradicted, or added without approval per salient element, adaptations citing their evidence, or "faithful"), `ceiling` (unused native devices, or "reached"), `material_fixes` (ordered, most material first, fidelity failures ahead of craft, each one line tied to a check or contract promise, at most eight), and `keep` (one line naming what must not be diluted while fixing). Missing inputs are named in one line above the sections. No praise, no summary prose.
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 4.0.2
version: 4.0.3
user-invocable: true
argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]"
license: Apache 2.0
@@ -23,7 +23,7 @@ Page scaffolds:
- Same-size cards of icon plus heading plus text as the page structure. Cards are the lazy container; nested cards are always wrong.
- The hero-metric template: big number, small label, supporting stats, accent.
- A tracked uppercase eyebrow over every section. One named kicker is a system; an eyebrow everywhere is grammar you did not choose.
- A kicker or eyebrow above a heading. This one is a ban, not a default: no brief earns it back. The heading carries its own weight; delete the label and let the heading speak.
- Section numbers (01 / 02 / 03) unless the sequence itself carries information the reader needs.
- A modal for a task that needs neither interruption nor protected focus.
@@ -11,6 +11,10 @@ Your job is production cleanup, not new art direction. Work only from the approv
Do not redesign. Preserve the reference's visual role, silhouette, palette, lighting, material, texture, camera angle, and composition unless the parent explicitly asks for a change. Preserve perspective only when it belongs to the object or scene itself; if CSS should create the card transform, shadow, rounded clipping, border, or layout, remove that presentation chrome from the raster.
## Decision Sketches
When the parent hands you a decision card packet instead of an approved mock, the job is one sketch: one card, one file, written to the card's declared `sketch` path the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a sketch is reported back, not padded from imagination. Render through the parent's shared frame: the requested surface's first viewport as a flat, matte design sketch in the card's own palette and type character, deliberately unfinished, no photorealism, no gloss. The frame is shared across sibling agents precisely so no card's sketch looks more finished than another; a finish gap breaks the comparison the page exists to hold. The only legible text is the product's real name and one real headline; greek every other text region into indistinct lines, because an invented spec, price, or date in a sketch is a claim PRODUCT.md never made. Return one line naming the path and any deviation, nothing more.
## Input Contract
Expect:
@@ -5,6 +5,8 @@ This harness has no subagent capability, so you are running this role inline. St
You record a project's design system after the build is done. Ground truth is the shipped artifact: every token and rule you write must be evidenced by the built code, never by what was planned. Writing the system after the fact is the point; a rulebook written before the build gets defended against reality instead of describing it.
You run under a hard turn ceiling that ends the run without warning, and a run that ends before DESIGN.md is written has recorded nothing. Batch several Reads into each turn, take `reference/document.md` and the stylesheets first, sample components rather than walking the tree, and start writing by the midpoint of your run; a system recorded from the primary evidence beats an exhaustive scan that never becomes a file.
## Input Contract
Expect: the project root; the artifact path(s); the direction contract text (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; the path to the skill's `reference/document.md`; and the boundary to write at (project or app root). An existing DESIGN.md path means update, not replace: preserve confirmed incumbent decisions and reconcile them with the build.
@@ -7,20 +7,26 @@ You are the finishing reviewer for an Impeccable build: fresh eyes on a done art
You have no browser. Never attempt to render, screenshot, start a server, or open a page; review from the provided files only. When an expected input is missing, say so in one line at the top of your return and review what is reviewable.
You run under a hard turn ceiling that ends the run without warning, and a run that ends before the five sections are written returns nothing; a review built from what you saw beats a perfect review that never arrives. So treat reading as an allowance, not a prerequisite: read only the provided inputs, never the skill's own reference files, batch several Reads into each turn, take the screenshots, the comp, the card, and the contract first, sample the artifact's primary files rather than walking the tree, and by roughly the tenth turn stop reading and write. Name whatever went unread in the line above the sections.
## Input Contract
Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path. When the harness can view images, open the screenshots, the comp, and the card before judging.
Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: the contract is the builder's abstraction of the comp, and a review anchored on it inherits whatever that abstraction dropped.
## Checks, in order
1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding.
2. **Fidelity.** Against the approved comp: topology, reading order, focal scale, overlaps and z-order, density, signature geometry. Name every drift and classify it intentional or defect. The comp governs composition; it is not a pixel spec for semantics, accessibility, or responsive behavior.
2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement.
3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. The card governs commitment and finish, never composition.
4. **Contract, promise by promise.** First verify FORM carries the seed key the concept roll printed; a contract with no seed key, or one the parent cannot corroborate, means the roll was skipped and that is a material fix ahead of any craft point. Then, for each of the five blocks, does the render keep the promise? Apply the memory test to the first viewport.
5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one.
5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one, and every produced asset visibly present in the screenshots; an asset applied at near-zero opacity or buried behind other paint is a compliance token, not a shipped material.
Do not run a second detector pass; mechanical findings belong to the parent's hooks.
## Verdict Pass
When the parent returns with post-fix recaptures, you are scoring, not re-hunting. For each material fix from your review, one line: resolved, partial, or unresolved, tied to what the new screenshots visibly show; a fix answered mechanically, positions moved but the quality the finding named still absent, is partial at best. Then name at most three regressions the fix batch itself introduced, judged by the same matrix rules, and nothing else; no new hunt, no new checks. Return exactly two sections: `verdict` (the scored list) and `remaining` (what stays open, or "clear").
## Output Contract
Return exactly five sections: `persistence` (pass/fail with specifics), `fidelity` (drift list with intentional/defect per item, or "faithful"), `ceiling` (unused native devices, or "reached"), `material_fixes` (ordered, most material first, each one line tied to a check or contract promise, at most eight), and `keep` (one line naming what must not be diluted while fixing). Missing inputs are named in one line above the sections. No praise, no summary prose.
Return exactly five sections: `persistence` (pass/fail with specifics), `fidelity` (the element matrix: match, adaptation, missing, contradicted, or added without approval per salient element, adaptations citing their evidence, or "faithful"), `ceiling` (unused native devices, or "reached"), `material_fixes` (ordered, most material first, fidelity failures ahead of craft, each one line tied to a check or contract promise, at most eight), and `keep` (one line naming what must not be diluted while fixing). Missing inputs are named in one line above the sections. No praise, no summary prose.
@@ -44,9 +44,11 @@ The script assigns which structure gets built: your top-ranked structure is what
2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, the notation, publications, identity programs, data graphics, and interfaces it reads daily, not only its physical objects; a nameable abstract system (a school of poster, a documentation standard, a data-graphic tradition) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; the audience's world is larger than that, so dig until the list spans at least three families.
3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience.
4. Run `node .claude/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode <mode>` and follow what it prints. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point.
5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the one or two fused challengers that survived the weighing as named alternates with a one-line case each, plus re-roll with an optional one-line steer. What you never present is a ranked menu of your own grounded candidates; a lineup of those invites the safest card, while dealt challengers carry no such rut. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option.
5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. What you never present is a ranked menu of your own grounded candidates; a lineup of those invites the safest card, while dealt challengers carry no such rut. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option.
The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Run `node .claude/skills/impeccable/scripts/serve-question.mjs --start --payload <file>` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key <key>`, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry.
The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, so a grounded direction is never a wall of text against rendered art, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you, so it is never yours to recommend. Run `node .claude/skills/impeccable/scripts/serve-question.mjs --start --payload <file>` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key <key>`, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry.
When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land, so generation never delays the question. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once, so the whole set lands in roughly the time of one. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version.
Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images for exactly this reason (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product.
@@ -100,6 +102,6 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi
Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary.
After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, and the QUALITY BAR card and approved comp paths. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. Apply the material fixes in one batch, rebuild once, and stop; the reviewer ran so that you do not re-open your own hunt. Do not run a second detector.
After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, and the QUALITY BAR card and approved comp paths. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. Apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get exactly one more batch, recapture, and verdict; two correction rounds is the ceiling, the second verdict ends the work whatever it says, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included: presenting mechanical confirmation as artistic success is how a failed build gets announced as a finished one. Do not run a second detector.
Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded.
@@ -6,7 +6,7 @@ The purpose of a probe is to test composition, narrative, hierarchy, density, fo
## Generate three compositional options
Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session; record the approved comp's path in the surface brief. Base them on the real content and the surface concepts already developed with the user. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building.
Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session; record the approved comp's path in the surface brief. Base them on the real content and the surface concepts already developed with the user. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. A decision-page sketch is not a probe: it chose the direction at deliberately unfinished fidelity, so the three comps render regardless, and the chosen card's sketch seeds at most one of them.
- When the user shortlisted multiple concepts, spread the three across them.
- When one direction is committed, vary the structural uncertainty an image can resolve: topology, sequence, density, hierarchy, focal composition, or interaction framing.
@@ -25,11 +25,11 @@ After approval, summarize the composition and the parts of the comp that must no
## Inventory implementation fidelity
Before building, inventory the approved comp's major visible ingredients in writing (a short table in the surface brief or working notes; the finish reviewer audits shipped assets against it) and choose an implementation medium for each: semantic HTML/CSS/SVG, existing project asset, generated raster, sourced raster, icon library, canvas/WebGL, or accepted omission. Textures, portraits, and scenes are raster-by-default; a CSS gradient is not a texture medium. Every `produce` entry is produced before the build ships, through the asset producer or in the current thread; an inventory with unproduced entries is an unfinished build, and this gate is where imagery-free pages come from when it is skipped.
Before building, inventory the approved comp's major visible ingredients in writing (a short table in the surface brief or working notes; the finish reviewer audits shipped assets against it) and choose an implementation medium for each: semantic HTML/CSS/SVG, existing project asset, generated raster, sourced raster, icon library, canvas/WebGL, or accepted omission. The same written inventory names the comp's compositional commitments: navigation items and icons, headline levels and their scale relationship, signature geometry such as seams, masks, and overlaps, and each section's arrangement and density. An element never written down is the element the build silently drops, and the direction contract's 150 words cannot carry this list, so this inventory is where it lives. Textures, portraits, and scenes are raster-by-default; a CSS gradient is not a texture medium. Every `produce` entry is produced before the build ships, through the asset producer or in the current thread; an inventory with unproduced entries is an unfinished build, and this gate is where imagery-free pages come from when it is skipped.
Pay special attention to the dominant composition, signature use, image-native content, second-fold system, and any interaction the still image only implies. If the concept depends on a photograph, architectural scene, product object, portrait, or other raster-native material, do not silently replace it with generic CSS scenery.
Treat the comp as a north star, not something to trace. Do not rasterize core UI text or controls. Do not substitute a different visual driver after approval without asking.
Treat the comp as a north star, not something to trace, and know what that allows: translation into semantic, responsive, accessible code, never recomposition. Keeping the palette and mood while redrawing the topology is a second art direction, not an adaptation. Do not rasterize core UI text or controls. Do not substitute a different visual driver after approval without asking.
## Produce only the assets the build needs
@@ -206,7 +206,13 @@ ${grammar}
// returns no staging. Re-rolls exclude every earlier set until the pool runs out.
export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) {
const pool = sourceCompositions ?? requireLocalConcepts().compositions;
// Stagings honour the same breadth gate as worlds: a staging too specific to
// serve an arbitrary build stays approved for direct briefs and leaves the
// challenger pool. Falls back to the full approved set rather than returning
// nothing if every approved staging is marked niche.
let approved = pool.filter(composition => composition.status === 'approved');
const broad = approved.filter(composition => composition.review?.breadth !== 'niche');
if (broad.length > 0) approved = broad;
if (approved.length === 0) return [];
if (mode) {
const matching = approved.filter(composition => composition.surface === mode);
@@ -271,12 +277,19 @@ export function selectApprovedChallengers({ scope, key, reroll = 0, sourceConcep
// graphic systems beside instrument languages and atmosphere worlds, with
// the second pick preferring a different family for diversity. Tier order
// in the rendered list is rolled too, to avoid positional bias.
// Approval ratings weight the draw: a 3-star world earns a second ticket
// (roughly double odds), a 1-star keeps its approval for direct briefs but
// leaves the challenger pool unless a tier has nothing else.
// Two separate axes, and both can exclude. Rating grades quality: a 3-star
// earns a second ticket, a 1-star marginal keep leaves the pool. Breadth says
// whether a world can serve an arbitrary build at all, so a niche world
// leaves the pool however good it is. Breadth was split out of rating because
// the only way to hold a narrow world back used to be calling it marginal,
// which made "excellent but narrow" unrecordable and corrupted the ratings as
// a calibration signal for the next authoring round.
const ticketsFor = pool => pool.flatMap(concept => {
const rating = concept.review?.rating;
if (rating === 1) return [];
// Two independent exclusions: a marginal world is too weak to challenge,
// a niche world too narrow. Either one keeps its approval for direct
// briefs and leaves the pool.
if (rating === 1 || concept.review?.breadth === 'niche') return [];
return rating === 3
? [{ concept, ticket: 0 }, { concept, ticket: 1 }]
: [{ concept, ticket: 0 }];
@@ -1529,7 +1529,7 @@ if (IS_BROWSER) {
addBrowserFindings(groupMap, document.body, typoFindings);
}
const sectionKickerFindings = checkRepeatedSectionKickersDOM()
const sectionKickerFindings = checkKickerAboveHeadingDOM()
.map(f => ({ type: f.id, detail: f.snippet }))
.filter(f => _ruleOk(f.type));
if (sectionKickerFindings.length > 0) {
@@ -309,15 +309,14 @@ const ANTIPATTERNS = [
skillGuideline: 'tiny uppercase tracked label above the hero headline',
},
{
id: 'repeated-section-kickers',
id: 'kicker-above-heading',
category: 'slop',
scopes: ['type'],
severity: 'advisory',
name: 'Repeated section kicker labels',
name: 'Kicker / eyebrow label above heading',
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.',
'A tiny tracked uppercase or small-caps label sitting as its own block directly above a heading is banned outright, repeated or not. Generated kickers never earn their place: the heading carries its own weight. Delete the label and let the heading speak; if the words matter, work them into the heading or the body.',
skillSection: 'Typography',
skillGuideline: 'repeated eyebrow or kicker labels as section scaffolding',
skillGuideline: 'kicker or eyebrow labels above headings',
},
{
id: 'numbered-section-labels',
@@ -1258,12 +1257,15 @@ function checkHeroEyebrow(opts) {
}];
}
function checkRepeatedSectionKickers(opts) {
const { candidates, minCount = 3 } = opts;
if (!Array.isArray(candidates) || candidates.length < minCount) return [];
// Outright ban: one kicker is one too many, so every collected candidate is
// a finding. The judgment lives in the candidate gate (isKickerCandidate) and
// the collector's context skips, not in a repetition count.
function checkKickerAboveHeading(opts) {
const { candidates } = opts;
if (!Array.isArray(candidates)) return [];
return candidates.map(candidate => ({
id: 'repeated-section-kickers',
snippet: `repeated section kicker "${candidate.kickerText}" before ${candidate.headingTag} "${candidate.headingText}" (${candidates.length} on page)`,
id: 'kicker-above-heading',
snippet: `kicker "${candidate.kickerText}" above ${candidate.headingTag} "${candidate.headingText}"`,
}));
}
@@ -3190,7 +3192,7 @@ function parseColorResolved(str, customPropMap) {
return parseAnyColor(resolved);
}
const REPEATED_KICKER_SKIP_SELECTOR = [
const KICKER_SKIP_SELECTOR = [
'nav',
'form',
'table',
@@ -3209,7 +3211,7 @@ const REPEATED_KICKER_SKIP_SELECTOR = [
'[data-impeccable-allow-kickers]',
].join(',');
const REPEATED_KICKER_CARD_CONTEXT_SELECTOR = [
const KICKER_CARD_CONTEXT_SELECTOR = [
'article',
'button',
'a',
@@ -3227,23 +3229,32 @@ function cleanInlineText(el) {
.trim();
}
function isRepeatedKickerCardContext(heading, kicker) {
const item = heading.closest?.(REPEATED_KICKER_CARD_CONTEXT_SELECTOR);
function isKickerCardContext(heading, kicker) {
const item = heading.closest?.(KICKER_CARD_CONTEXT_SELECTOR);
return Boolean(item && (!item.contains || item.contains(kicker)));
}
function isRepeatedKickerCandidate(opts) {
// Meta lines above headlines join category and date (or path crumbs) with
// separator glyphs, or carry a year. A kicker is one short phrase; metadata
// keeps its markers.
const KICKER_META_TEXT_RE = /[·•|]|\s[\/›»>]\s|\b(19|20)\d{2}\b/;
// Legal and document numbering: "Section 4.2", "Article IX", "§ 12.3",
// dotted decimal outlines. The label identifies the clause, so it stays.
const KICKER_DOC_NUMBERING_RE = /^(§|\d+(\.\d+)+\b|(section|article|clause|appendix|exhibit|schedule|chapter|part|rule|title)\s+([\divxlc]+\b|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)\b)/i;
function isKickerCandidate(opts) {
const {
headingTag,
headingLevel,
headingText,
headingFontSize,
kickerTag,
kickerText,
kickerTextTransform,
kickerFontVariant,
kickerFontSize,
kickerLetterSpacing,
} = opts;
if (!['h2', 'h3', 'h4'].includes(headingTag)) return false;
if (!headingLevel || headingLevel > 4) return false;
if (!headingText || headingText.length < 3) return false;
if (/^\/[\w-]+/i.test(headingText.replace(/^"|"$/g, '').trim())) return false;
if (!(headingFontSize >= 20)) return false;
@@ -3251,9 +3262,13 @@ function isRepeatedKickerCandidate(opts) {
if (!['p', 'span', 'div', 'small'].includes(kickerTag)) return false;
if (!kickerText || kickerText.length < 2 || kickerText.length > 34) return false;
if (/^step\s*\d+/i.test(kickerText) || /^\d{1,2}$/.test(kickerText)) return false;
if (KICKER_META_TEXT_RE.test(kickerText)) return false;
if (KICKER_DOC_NUMBERING_RE.test(kickerText)) return false;
const isSmallCaps = /small-caps/.test(kickerFontVariant || '');
const isUppercased = kickerTextTransform === 'uppercase'
|| (/[A-Z]/.test(kickerText) && !/[a-z]/.test(kickerText));
|| (/[A-Z]/.test(kickerText) && !/[a-z]/.test(kickerText))
|| isSmallCaps;
if (!isUppercased) return false;
if (!(kickerFontSize > 0 && kickerFontSize <= 14)) return false;
const minTrackedSpacing = Math.max(1, kickerFontSize * 0.08);
@@ -3261,37 +3276,64 @@ function isRepeatedKickerCandidate(opts) {
return true;
}
function collectRepeatedSectionKickerCandidates(doc, getStyle, resolveLetterSpacing) {
// Resolve a heading level for the anchor element: 1-4 for h1-h4, aria-level
// (default 2) for role="heading" elements, 0 otherwise.
function kickerHeadingLevel(heading) {
const tag = heading.tagName.toLowerCase();
const byTag = /^h([1-6])$/.exec(tag);
if (byTag) return parseInt(byTag[1], 10);
const role = heading.getAttribute?.('role') || '';
if (role.toLowerCase() !== 'heading') return 0;
const ariaLevel = parseInt(heading.getAttribute?.('aria-level') || '', 10);
return Number.isFinite(ariaLevel) && ariaLevel >= 1 ? ariaLevel : 2;
}
function collectKickerCandidates(doc, getStyle, resolveLetterSpacing) {
const candidates = [];
for (const heading of doc.querySelectorAll('h2, h3, h4')) {
if (heading.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
for (const heading of doc.querySelectorAll('h1, h2, h3, h4, [role="heading"]')) {
const headingLevel = kickerHeadingLevel(heading);
if (!headingLevel || headingLevel > 4) continue;
if (heading.closest?.(KICKER_SKIP_SELECTOR)) continue;
// Application contexts (tab panels, dialogs) use compact context labels
// above headings to describe state, not to decorate. Same carve-out the
// hero-eyebrow rule makes.
if (heading.closest?.('[role="tabpanel"], [role="dialog"], [role="application"], dialog')) continue;
const kicker = heading.previousElementSibling;
if (!kicker || kicker.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
if (isRepeatedKickerCardContext(heading, kicker)) continue;
if (!kicker || kicker.closest?.(KICKER_SKIP_SELECTOR)) continue;
if (isKickerCardContext(heading, kicker)) continue;
const headingStyle = getStyle(heading);
const kickerStyle = getStyle(kicker);
const headingTag = heading.tagName.toLowerCase();
const headingText = (heading.textContent || '').replace(/\s+/g, ' ').trim();
const kickerText = cleanInlineText(kicker) || (kicker.textContent || '').replace(/\s+/g, ' ').trim();
const headingFontSize = resolveLetterSpacing(headingStyle.fontSize || '', 16) || parseFloat(headingStyle.fontSize) || 0;
const kickerFontSize = resolveLetterSpacing(kickerStyle.fontSize || '', 16) || parseFloat(kickerStyle.fontSize) || 0;
const kickerLetterSpacing = resolveLetterSpacing(kickerStyle.letterSpacing || '', kickerFontSize);
if (!isRepeatedKickerCandidate({
headingTag: heading.tagName.toLowerCase(),
if (!isKickerCandidate({
headingLevel,
headingText,
headingFontSize,
kickerTag: kicker.tagName.toLowerCase(),
kickerText,
kickerTextTransform: kickerStyle.textTransform || '',
kickerFontVariant: `${kickerStyle.fontVariant || ''} ${kickerStyle.fontVariantCaps || ''}`,
kickerFontSize,
kickerLetterSpacing,
})) {
continue;
}
// A tracked-caps eyebrow above a hero-scale h1 belongs to
// hero-eyebrow-chip (which also covers the accent-bold and dash-prefix
// stylings there). Stand down so one element gets one finding.
if (headingTag === 'h1' && headingFontSize >= 48 && kickerLetterSpacing >= 1.6) {
continue;
}
candidates.push({
headingTag: heading.tagName.toLowerCase(),
headingTag,
headingText: headingText.replace(/^"|"$/g, '').slice(0, 60),
kickerText: kickerText.slice(0, 40),
});
@@ -3299,17 +3341,17 @@ function collectRepeatedSectionKickerCandidates(doc, getStyle, resolveLetterSpac
return candidates;
}
function checkRepeatedSectionKickersDOM() {
const candidates = collectRepeatedSectionKickerCandidates(
function checkKickerAboveHeadingDOM() {
const candidates = collectKickerCandidates(
document,
(el) => getComputedStyle(el),
(value, fontSize) => resolveLengthPx(value, fontSize) || 0,
);
return checkRepeatedSectionKickers({ candidates });
return checkKickerAboveHeading({ candidates });
}
// ── Numbered section labels ─────────────────────────────────────────────────
// Sibling of the repeated-kicker rule: instead of a tracked uppercase word,
// Sibling of the kicker-above-heading rule: instead of a tracked uppercase word,
// the section scaffold is a tiny numeric index riding beside each section
// heading — bare and zero-padded, or an index joined to a short micro-label
// by a separator glyph. The kicker rule deliberately excludes bare 1-2 digit
@@ -3363,7 +3405,7 @@ function collectNumberedSectionLabelCandidates(doc, getStyle, resolveLetterSpaci
const candidates = [];
const seenLabels = new Set();
for (const heading of doc.querySelectorAll('h2, h3, h4')) {
if (heading.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
if (heading.closest?.(KICKER_SKIP_SELECTOR)) continue;
// The index sits either directly before the heading, or before the
// wrapper the heading leads (label | <div><h2>…</h2>…</div>).
let label = heading.previousElementSibling;
@@ -3373,9 +3415,9 @@ function collectNumberedSectionLabelCandidates(doc, getStyle, resolveLetterSpaci
if (firstChild === heading) label = parent.previousElementSibling;
}
if (!label || seenLabels.has(label)) continue;
if (label.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
if (label.closest?.(KICKER_SKIP_SELECTOR)) continue;
if (HEADING_TAGS.has(label.tagName.toLowerCase())) continue;
if (isRepeatedKickerCardContext(heading, label)) continue;
if (isKickerCardContext(heading, label)) continue;
const labelText = cleanInlineText(label) || (label.textContent || '').replace(/\s+/g, ' ').trim();
const parsed = parseNumberedLabelText(labelText);
@@ -4548,13 +4590,13 @@ function checkElementHeroEyebrow(el, style, tag, window, customPropMap) {
});
}
function checkRepeatedSectionKickersFromDoc(doc, win) {
const candidates = collectRepeatedSectionKickerCandidates(
function checkKickerAboveHeadingFromDoc(doc, win) {
const candidates = collectKickerCandidates(
doc,
(el) => win.getComputedStyle(el),
(value, fontSize) => resolveLengthPx(value, fontSize) || 0,
);
return checkRepeatedSectionKickers({ candidates });
return checkKickerAboveHeading({ candidates });
}
function checkElementMotion(tag, style) {
@@ -7712,7 +7754,7 @@ if (IS_BROWSER) {
addBrowserFindings(groupMap, document.body, typoFindings);
}
const sectionKickerFindings = checkRepeatedSectionKickersDOM()
const sectionKickerFindings = checkKickerAboveHeadingDOM()
.map(f => ({ type: f.id, detail: f.snippet }))
.filter(f => _ruleOk(f.type));
if (sectionKickerFindings.length > 0) {
@@ -223,7 +223,7 @@ function unwrapCssAtLayer(source) {
// ---------------------------------------------------------------------------
const STATIC_INHERITED_PROPS = new Set([
'color', 'fontFamily', 'fontSize', 'fontStyle', 'fontWeight',
'color', 'fontFamily', 'fontSize', 'fontStyle', 'fontWeight', 'fontVariant',
'lineHeight', 'letterSpacing', 'textTransform', 'textAlign', 'hyphens',
'webkitHyphens',
]);
@@ -252,6 +252,7 @@ const STATIC_DEFAULT_STYLE = {
fontFamily: '',
fontSize: '16px',
fontStyle: 'normal',
fontVariant: 'normal',
fontWeight: '400',
lineHeight: 'normal',
letterSpacing: 'normal',
@@ -27,11 +27,11 @@ import {
checkElementRadialSpotlight,
checkCreamPalette,
checkHtmlPatterns,
checkKickerAboveHeadingFromDoc,
checkNumberedSectionLabelsFromDoc,
checkPageLayout,
checkPageQualityFromDoc,
checkRepeatedContainerTextFromDoc,
checkRepeatedSectionKickersFromDoc,
resolveBackground,
resolveBorderRadiusPx,
} from '../../rules/checks.mjs';
@@ -202,7 +202,7 @@ async function detectHtml(filePath, options = {}) {
for (const f of runPageCheck('typography-rules', () => checkStaticPageTypography(document, window))) {
findings.push(finding(f.id, filePath, f.snippet));
}
for (const f of runPageCheck('repeated-section-kickers', () => checkRepeatedSectionKickersFromDoc(document, window))) {
for (const f of runPageCheck('kicker-above-heading', () => checkKickerAboveHeadingFromDoc(document, window))) {
findings.push(finding(f.id, filePath, f.snippet));
}
for (const f of runPageCheck('numbered-section-labels', () => checkNumberedSectionLabelsFromDoc(document, window))) {
@@ -198,15 +198,14 @@ const ANTIPATTERNS = [
skillGuideline: 'tiny uppercase tracked label above the hero headline',
},
{
id: 'repeated-section-kickers',
id: 'kicker-above-heading',
category: 'slop',
scopes: ['type'],
severity: 'advisory',
name: 'Repeated section kicker labels',
name: 'Kicker / eyebrow label above heading',
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.',
'A tiny tracked uppercase or small-caps label sitting as its own block directly above a heading is banned outright, repeated or not. Generated kickers never earn their place: the heading carries its own weight. Delete the label and let the heading speak; if the words matter, work them into the heading or the body.',
skillSection: 'Typography',
skillGuideline: 'repeated eyebrow or kicker labels as section scaffolding',
skillGuideline: 'kicker or eyebrow labels above headings',
},
{
id: 'numbered-section-labels',
@@ -456,12 +456,15 @@ function checkHeroEyebrow(opts) {
}];
}
function checkRepeatedSectionKickers(opts) {
const { candidates, minCount = 3 } = opts;
if (!Array.isArray(candidates) || candidates.length < minCount) return [];
// Outright ban: one kicker is one too many, so every collected candidate is
// a finding. The judgment lives in the candidate gate (isKickerCandidate) and
// the collector's context skips, not in a repetition count.
function checkKickerAboveHeading(opts) {
const { candidates } = opts;
if (!Array.isArray(candidates)) return [];
return candidates.map(candidate => ({
id: 'repeated-section-kickers',
snippet: `repeated section kicker "${candidate.kickerText}" before ${candidate.headingTag} "${candidate.headingText}" (${candidates.length} on page)`,
id: 'kicker-above-heading',
snippet: `kicker "${candidate.kickerText}" above ${candidate.headingTag} "${candidate.headingText}"`,
}));
}
@@ -2388,7 +2391,7 @@ function parseColorResolved(str, customPropMap) {
return parseAnyColor(resolved);
}
const REPEATED_KICKER_SKIP_SELECTOR = [
const KICKER_SKIP_SELECTOR = [
'nav',
'form',
'table',
@@ -2407,7 +2410,7 @@ const REPEATED_KICKER_SKIP_SELECTOR = [
'[data-impeccable-allow-kickers]',
].join(',');
const REPEATED_KICKER_CARD_CONTEXT_SELECTOR = [
const KICKER_CARD_CONTEXT_SELECTOR = [
'article',
'button',
'a',
@@ -2425,23 +2428,32 @@ function cleanInlineText(el) {
.trim();
}
function isRepeatedKickerCardContext(heading, kicker) {
const item = heading.closest?.(REPEATED_KICKER_CARD_CONTEXT_SELECTOR);
function isKickerCardContext(heading, kicker) {
const item = heading.closest?.(KICKER_CARD_CONTEXT_SELECTOR);
return Boolean(item && (!item.contains || item.contains(kicker)));
}
function isRepeatedKickerCandidate(opts) {
// Meta lines above headlines join category and date (or path crumbs) with
// separator glyphs, or carry a year. A kicker is one short phrase; metadata
// keeps its markers.
const KICKER_META_TEXT_RE = /[·•|]|\s[\/›»>]\s|\b(19|20)\d{2}\b/;
// Legal and document numbering: "Section 4.2", "Article IX", "§ 12.3",
// dotted decimal outlines. The label identifies the clause, so it stays.
const KICKER_DOC_NUMBERING_RE = /^(§|\d+(\.\d+)+\b|(section|article|clause|appendix|exhibit|schedule|chapter|part|rule|title)\s+([\divxlc]+\b|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)\b)/i;
function isKickerCandidate(opts) {
const {
headingTag,
headingLevel,
headingText,
headingFontSize,
kickerTag,
kickerText,
kickerTextTransform,
kickerFontVariant,
kickerFontSize,
kickerLetterSpacing,
} = opts;
if (!['h2', 'h3', 'h4'].includes(headingTag)) return false;
if (!headingLevel || headingLevel > 4) return false;
if (!headingText || headingText.length < 3) return false;
if (/^\/[\w-]+/i.test(headingText.replace(/^"|"$/g, '').trim())) return false;
if (!(headingFontSize >= 20)) return false;
@@ -2449,9 +2461,13 @@ function isRepeatedKickerCandidate(opts) {
if (!['p', 'span', 'div', 'small'].includes(kickerTag)) return false;
if (!kickerText || kickerText.length < 2 || kickerText.length > 34) return false;
if (/^step\s*\d+/i.test(kickerText) || /^\d{1,2}$/.test(kickerText)) return false;
if (KICKER_META_TEXT_RE.test(kickerText)) return false;
if (KICKER_DOC_NUMBERING_RE.test(kickerText)) return false;
const isSmallCaps = /small-caps/.test(kickerFontVariant || '');
const isUppercased = kickerTextTransform === 'uppercase'
|| (/[A-Z]/.test(kickerText) && !/[a-z]/.test(kickerText));
|| (/[A-Z]/.test(kickerText) && !/[a-z]/.test(kickerText))
|| isSmallCaps;
if (!isUppercased) return false;
if (!(kickerFontSize > 0 && kickerFontSize <= 14)) return false;
const minTrackedSpacing = Math.max(1, kickerFontSize * 0.08);
@@ -2459,37 +2475,64 @@ function isRepeatedKickerCandidate(opts) {
return true;
}
function collectRepeatedSectionKickerCandidates(doc, getStyle, resolveLetterSpacing) {
// Resolve a heading level for the anchor element: 1-4 for h1-h4, aria-level
// (default 2) for role="heading" elements, 0 otherwise.
function kickerHeadingLevel(heading) {
const tag = heading.tagName.toLowerCase();
const byTag = /^h([1-6])$/.exec(tag);
if (byTag) return parseInt(byTag[1], 10);
const role = heading.getAttribute?.('role') || '';
if (role.toLowerCase() !== 'heading') return 0;
const ariaLevel = parseInt(heading.getAttribute?.('aria-level') || '', 10);
return Number.isFinite(ariaLevel) && ariaLevel >= 1 ? ariaLevel : 2;
}
function collectKickerCandidates(doc, getStyle, resolveLetterSpacing) {
const candidates = [];
for (const heading of doc.querySelectorAll('h2, h3, h4')) {
if (heading.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
for (const heading of doc.querySelectorAll('h1, h2, h3, h4, [role="heading"]')) {
const headingLevel = kickerHeadingLevel(heading);
if (!headingLevel || headingLevel > 4) continue;
if (heading.closest?.(KICKER_SKIP_SELECTOR)) continue;
// Application contexts (tab panels, dialogs) use compact context labels
// above headings to describe state, not to decorate. Same carve-out the
// hero-eyebrow rule makes.
if (heading.closest?.('[role="tabpanel"], [role="dialog"], [role="application"], dialog')) continue;
const kicker = heading.previousElementSibling;
if (!kicker || kicker.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
if (isRepeatedKickerCardContext(heading, kicker)) continue;
if (!kicker || kicker.closest?.(KICKER_SKIP_SELECTOR)) continue;
if (isKickerCardContext(heading, kicker)) continue;
const headingStyle = getStyle(heading);
const kickerStyle = getStyle(kicker);
const headingTag = heading.tagName.toLowerCase();
const headingText = (heading.textContent || '').replace(/\s+/g, ' ').trim();
const kickerText = cleanInlineText(kicker) || (kicker.textContent || '').replace(/\s+/g, ' ').trim();
const headingFontSize = resolveLetterSpacing(headingStyle.fontSize || '', 16) || parseFloat(headingStyle.fontSize) || 0;
const kickerFontSize = resolveLetterSpacing(kickerStyle.fontSize || '', 16) || parseFloat(kickerStyle.fontSize) || 0;
const kickerLetterSpacing = resolveLetterSpacing(kickerStyle.letterSpacing || '', kickerFontSize);
if (!isRepeatedKickerCandidate({
headingTag: heading.tagName.toLowerCase(),
if (!isKickerCandidate({
headingLevel,
headingText,
headingFontSize,
kickerTag: kicker.tagName.toLowerCase(),
kickerText,
kickerTextTransform: kickerStyle.textTransform || '',
kickerFontVariant: `${kickerStyle.fontVariant || ''} ${kickerStyle.fontVariantCaps || ''}`,
kickerFontSize,
kickerLetterSpacing,
})) {
continue;
}
// A tracked-caps eyebrow above a hero-scale h1 belongs to
// hero-eyebrow-chip (which also covers the accent-bold and dash-prefix
// stylings there). Stand down so one element gets one finding.
if (headingTag === 'h1' && headingFontSize >= 48 && kickerLetterSpacing >= 1.6) {
continue;
}
candidates.push({
headingTag: heading.tagName.toLowerCase(),
headingTag,
headingText: headingText.replace(/^"|"$/g, '').slice(0, 60),
kickerText: kickerText.slice(0, 40),
});
@@ -2497,17 +2540,17 @@ function collectRepeatedSectionKickerCandidates(doc, getStyle, resolveLetterSpac
return candidates;
}
function checkRepeatedSectionKickersDOM() {
const candidates = collectRepeatedSectionKickerCandidates(
function checkKickerAboveHeadingDOM() {
const candidates = collectKickerCandidates(
document,
(el) => getComputedStyle(el),
(value, fontSize) => resolveLengthPx(value, fontSize) || 0,
);
return checkRepeatedSectionKickers({ candidates });
return checkKickerAboveHeading({ candidates });
}
// ── Numbered section labels ─────────────────────────────────────────────────
// Sibling of the repeated-kicker rule: instead of a tracked uppercase word,
// Sibling of the kicker-above-heading rule: instead of a tracked uppercase word,
// the section scaffold is a tiny numeric index riding beside each section
// heading — bare and zero-padded, or an index joined to a short micro-label
// by a separator glyph. The kicker rule deliberately excludes bare 1-2 digit
@@ -2561,7 +2604,7 @@ function collectNumberedSectionLabelCandidates(doc, getStyle, resolveLetterSpaci
const candidates = [];
const seenLabels = new Set();
for (const heading of doc.querySelectorAll('h2, h3, h4')) {
if (heading.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
if (heading.closest?.(KICKER_SKIP_SELECTOR)) continue;
// The index sits either directly before the heading, or before the
// wrapper the heading leads (label | <div><h2>…</h2>…</div>).
let label = heading.previousElementSibling;
@@ -2571,9 +2614,9 @@ function collectNumberedSectionLabelCandidates(doc, getStyle, resolveLetterSpaci
if (firstChild === heading) label = parent.previousElementSibling;
}
if (!label || seenLabels.has(label)) continue;
if (label.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
if (label.closest?.(KICKER_SKIP_SELECTOR)) continue;
if (HEADING_TAGS.has(label.tagName.toLowerCase())) continue;
if (isRepeatedKickerCardContext(heading, label)) continue;
if (isKickerCardContext(heading, label)) continue;
const labelText = cleanInlineText(label) || (label.textContent || '').replace(/\s+/g, ' ').trim();
const parsed = parseNumberedLabelText(labelText);
@@ -3746,13 +3789,13 @@ function checkElementHeroEyebrow(el, style, tag, window, customPropMap) {
});
}
function checkRepeatedSectionKickersFromDoc(doc, win) {
const candidates = collectRepeatedSectionKickerCandidates(
function checkKickerAboveHeadingFromDoc(doc, win) {
const candidates = collectKickerCandidates(
doc,
(el) => win.getComputedStyle(el),
(value, fontSize) => resolveLengthPx(value, fontSize) || 0,
);
return checkRepeatedSectionKickers({ candidates });
return checkKickerAboveHeading({ candidates });
}
function checkElementMotion(tag, style) {
@@ -5393,7 +5436,7 @@ export {
checkItalicSerif,
isAccentColor,
checkHeroEyebrow,
checkRepeatedSectionKickers,
checkKickerAboveHeading,
checkMotion,
checkGlow,
scanCssTextForGlow,
@@ -5424,9 +5467,9 @@ export {
parseAnyColor,
parseColorResolved,
cleanInlineText,
isRepeatedKickerCandidate,
collectRepeatedSectionKickerCandidates,
checkRepeatedSectionKickersDOM,
isKickerCandidate,
collectKickerCandidates,
checkKickerAboveHeadingDOM,
parseNumberedLabelText,
isNumberedSectionLabelCandidate,
collectNumberedSectionLabelCandidates,
@@ -5458,7 +5501,7 @@ export {
checkElementIconTile,
checkElementItalicSerif,
checkElementHeroEyebrow,
checkRepeatedSectionKickersFromDoc,
checkKickerAboveHeadingFromDoc,
checkElementMotion,
checkElementGlow,
checkTypography,
@@ -24,21 +24,36 @@
* "label": "Fillmore Handbill",
* "kicker": "THE ROLL", // optional badge; the assigned option leads
* "lineage": "1966-71 Fillmore ...", // optional
* "body": "why it fits, first viewport, risk ...", // optional, plain text
* "hero": "https://... or /abs/path.webp", // optional image
* "thesis": "one line: the idea this direction owns", // optional
* "palette": ["#1a2f5e", "oklch(84% .19 80)", ...], // optional, rendered as chips
* "materials": ["letterpress", "newsprint"], // optional, rendered as tags
* "viewport": "one line: the first-viewport composition", // optional
* "case": "one line: the fusion verdict, honest", // optional
* "risk": "one line: the honest risk", // optional
* "body": "fallback prose when the structured fields are absent",
* "sketch": ".impeccable/sketches/assigned.webp", // optional; may not exist
* // yet: the page shimmer-waits and polls the
* // slot until the file lands, so serve first
* // and generate after
* "hero": "https://... or /abs/path.webp", // optional inspiration image;
* // rides picture-in-picture when a sketch exists
* "board": "https://... or /abs/path.webp" // optional secondary image
* }, ...
* ],
* "reroll": true, // adds a re-roll action (returns {"optionId":"reroll"})
* "canon": true, // adds the quiet "Play it straight" standing exit
* // (returns {"optionId":"canon"}); direction rounds only
* "canon": true, // adds the "Play it straight" standing exit;
* // direction rounds only (returns {"optionId":"canon"})
* "canonCard": { ... }, // optional: the standing exit as a full card with the
* // same anatomy (label, thesis, palette, sketch, ...);
* // rendered last and visually subordinate. Without it,
* // canon stays a quiet footer action.
* "steer": true // adds a free-text steer field returned with any answer
* }
*
* Options render as large cards: hero render first when present (the dealt
* catalog worlds already have cards; grounded directions may present text-only
* or a freshly generated mock). Local image paths are served by this server;
* nothing is uploaded anywhere.
* Options render as large cards: the sketch leads when present, with the
* inspiration image picture-in-picture; a hero alone renders full-bleed; a
* text-only direction gets its identity from the palette chips and tags.
* Local image paths are served by this server; nothing is uploaded anywhere.
*
* Modes:
* (default) block until answered; ANSWER on stdout; exit 0.
@@ -107,6 +122,9 @@ function printAnswer(raw) {
if (a.hero || a.board) {
console.log("CHOSEN CARD: open the chosen world's board and hero images now, before any code. When your harness only reads files, or runs sandboxed, download them INTO the workspace and open the relative path; a sandboxed viewer rejects absolute paths outside it. They set the craft bar the build must reach.");
}
if (a.sketch) {
console.log('CHOSEN SKETCH: the decision sketch at that path may seed one comp probe; the comp round still renders its full set, because a sketch chose the direction, not the composition.');
}
if (a.optionId === 'canon') {
console.log('CANON CHOSEN: the user picked the category standard on purpose. Ask once for two or three products this should sit alongside; their craft level becomes the quality bar. Execute the canon at full commitment, conventions embraced without irony or smuggled quirk.');
}
@@ -125,14 +143,15 @@ if (hasFlag('schema')) {
title: 'Choose the visual world',
question: 'The roll assigned Fillmore Handbill. Keep it, take an alternate, or re-roll.',
options: [
{ id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', body: 'Why it fits, the first viewport, the honest risk.', hero: 'https://impeccable.style/worlds/cards/fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/fillmore-handbill.webp' },
{ id: 'challenger-teletext', label: 'Teletext Service', lineage: 'broadcast teletext magazines', body: 'Fused alternate.', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' },
{ id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', sketch: '.impeccable/sketches/assigned.webp', hero: 'https://impeccable.style/worlds/cards/fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/fillmore-handbill.webp' },
{ id: 'challenger-teletext', label: 'Teletext Service', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', case: 'Fuses cleanly: releases map to numbered pages.', sketch: '.impeccable/sketches/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' },
],
reroll: true,
canon: true,
canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', viewport: 'The arrangement a visitor expects, at full craft.', sketch: '.impeccable/sketches/canon.webp' },
steer: true,
}, null, 2));
console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board accept URLs or local paths. canon adds a quiet standing "Play it straight" action for direction decisions: the user\'s explicit door to the category standard. Include it only for visual-direction rounds; never present canon as your own recommendation.');
console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/sketch accept URLs or local paths; sketch slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the sketch, never as the promise of the build. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability.');
process.exit(0);
}
@@ -203,14 +222,25 @@ if (hasFlag('start')) {
const key = arg('key') || Math.random().toString(16).slice(2, 10);
// In start mode the agent is alive and owns browser routing; the server
// only opens the system browser itself when --open forces it.
// The daemon's output lands in a per-key log so a startup failure can say
// what actually went wrong instead of only that it did.
const logFile = path.join(QUESTION_DIR, `${key}.log`);
const logFd = fs.openSync(logFile, 'a');
const child = spawn(process.execPath, [
fileURLToPath(import.meta.url), '--payload', payloadPath, '--detached-serve', '--key', key,
'--timeout', String(timeoutSec), ...(hasFlag('open') ? [] : ['--no-open']),
], { detached: true, stdio: 'ignore' });
], { detached: true, stdio: ['ignore', logFd, logFd] });
child.unref();
fs.closeSync(logFd);
const deadline = Date.now() + 8000;
while (Date.now() < deadline && !fs.existsSync(stateFile(key))) await new Promise((r) => setTimeout(r, 100));
if (!fs.existsSync(stateFile(key))) { console.error('serve-question: server failed to start'); process.exit(1); }
if (!fs.existsSync(stateFile(key))) {
let tail = '';
try { tail = fs.readFileSync(logFile, 'utf8').trim().split('\n').slice(-4).join('\n '); } catch { /* log never written */ }
console.error(`serve-question: server failed to start${tail ? `\n ${tail}` : ''}`);
console.error(`serve-question: log at ${path.relative(process.cwd(), logFile) || logFile}. A sandboxed exec that cannot listen on localhost causes exactly this; rerun this command once through the harness's network-enabled or unsandboxed command tool before falling back.`);
process.exit(1);
}
const state = JSON.parse(fs.readFileSync(stateFile(key), 'utf8'));
console.log(`QUESTION URL: ${state.url}`);
console.log(`QUESTION KEY: ${key}`);
@@ -244,12 +274,28 @@ function loadRound(json) {
localImages.push(abs);
return `/img/${localImages.length - 1}`;
};
// Sketches stream in after the page is served, so their slots register
// whether or not the file exists yet; /img answers 404 until it lands and
// the page polls the slot. Remote sketch URLs pass through untouched.
const sketchSrc = (value) => {
if (!value) return null;
if (/^https?:\/\//.test(value)) return value;
localImages.push(path.resolve(value));
return `/img/${localImages.length - 1}`;
};
payload = parsed;
options = parsed.options.map((option) => ({
const decorate = (option) => ({
...option,
heroSrc: imageSrc(option.hero),
boardSrc: imageSrc(option.board),
}));
sketchSrc: sketchSrc(option.sketch),
});
options = parsed.options.map(decorate);
// The standing exit as a full card: same anatomy, reserved id, rendered
// subordinate by the page. Without it, canon stays the quiet footer action.
if (parsed.canonCard && typeof parsed.canonCard === 'object') {
options = [...options, { ...decorate(parsed.canonCard), id: 'canon', isCanon: true }];
}
}
try { loadRound(raw); } catch (error) { console.error(`serve-question: ${error.message}`); process.exit(1); }
const detachedKey = hasFlag('detached-serve') ? arg('key') : null;
@@ -260,30 +306,81 @@ const esc = (s) => String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<
function page() {
const flipChip = (label) => `<button type="button" class="chip flip" aria-label="Flip the card"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4a8 8 0 1 1-8 8" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/><path d="M4 5.5V12h6.5" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg><span>${label}</span></button>`;
const expandChip = `<button type="button" class="chip expand" aria-label="Expand the image"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 9V4h5M20 15v5h-5M20 9V4h-5M4 15v5h5" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg></button>`;
const cards = options.map((option, index) => `
<article class="card" style="--fan:${index === 0 ? '0deg' : (index % 2 ? '1.4deg' : '-1.2deg')};--deal:${index * 90}ms" data-id="${esc(option.id)}">
<div class="card-inner">
<div class="face front${index === 0 ? ' lead' : ''}${option.heroSrc || option.boardSrc ? '' : ' text-only'}">
${option.kicker ? `<span class="kicker">${esc(option.kicker)}</span>` : ''}
${option.heroSrc || option.boardSrc ? `<div class="media">
// Structured anatomy: chips and one-line facts render when the payload
// carries them; a plain body falls back to the prose block. Palette chips
// and material tags give a text-only direction an immediate identity that
// no generation luck can distort.
const fact = (label, value, cls = '') => value ? `<p class="fact${cls ? ` ${cls}` : ''}"><span class="fact-label">${label}</span>${esc(value)}</p>` : '';
const hasBack = (option) => Boolean(option.viewport || option.case || (option.boardSrc && option.heroSrc));
const anatomy = (option) => {
const rows = [];
if (option.thesis) rows.push(`<p class="thesis">${esc(option.thesis)}</p>`);
const idBits = [];
if (Array.isArray(option.palette) && option.palette.length) {
idBits.push(`<span class="swatches">${option.palette.slice(0, 6).map((c) => `<i style="background:${esc(c)}" title="${esc(c)}"></i>`).join('')}</span>`);
}
if (Array.isArray(option.materials) && option.materials.length) {
idBits.push(option.materials.slice(0, 4).map((m) => `<span class="tag">${esc(m)}</span>`).join(''));
}
if (idBits.length) rows.push(`<div class="identity">${idBits.join('')}</div>`);
// The front carries only what the choice needs: thesis, identity, and the
// honest risk clamped to two lines. First viewport and the case read on
// the card's back; once the sketch lands, the first viewport is a picture.
rows.push(fact('Risk', option.risk, 'clamp'));
if (!option.thesis && option.body) rows.push(`<p class="detail">${esc(option.body)}</p>`);
else if (option.body && option.thesis && !hasBack(option)) rows.push(`<p class="detail more">${esc(option.body)}</p>`);
return rows.join('\n ');
};
const backFacts = (option) => [
fact('First viewport', option.viewport),
fact('The case', option.case),
fact('Risk', option.risk),
option.body && option.thesis ? `<p class="detail more">${esc(option.body)}</p>` : '',
].filter(Boolean).join('\n ');
const media = (option) => {
const inspiration = option.heroSrc ? `<figure class="pip" title="Inspiration: the world this direction draws from. Your page will not look like this image.">
<img src="${esc(option.heroSrc)}" alt="">
<figcaption>inspiration</figcaption>
</figure>` : '';
const details = hasBack(option) ? flipChip('Details') : '';
if (option.sketchSrc) {
return `<div class="media sketching" data-sketch="${esc(option.sketchSrc)}">
<div class="shimmer"><span class="sketch-note">sketching&hellip;</span></div>
<img class="sketch" alt="" hidden>
${inspiration}
<div class="chips">${expandChip}${details}</div>
</div>`;
}
if (option.heroSrc || option.boardSrc) {
return `<div class="media">
<img src="${esc(option.heroSrc || option.boardSrc)}" alt="">
<div class="chips">${expandChip}${option.boardSrc && option.heroSrc ? flipChip('Board') : ''}</div>
</div>` : ''}
<div class="chips">${expandChip}${details}</div>
</div>`;
}
return '';
};
const cards = options.map((option, index) => `
<article class="card${option.isCanon ? ' canon' : ''}" style="--fan:${index === 0 ? '0deg' : (index % 2 ? '1.4deg' : '-1.2deg')};--deal:${index * 90}ms" data-id="${esc(option.id)}">
<div class="card-inner">
<div class="face front${index === 0 ? ' lead' : ''}${media(option) ? '' : ' text-only'}">
${option.kicker ? `<span class="kicker">${esc(option.kicker)}</span>` : option.isCanon ? '<span class="kicker standing">The standing door</span>' : ''}
${media(option)}
<div class="body">
${option.lineage ? `<p class="tier">${esc(option.lineage)}</p>` : ''}
<h2>${esc(option.label)}</h2>
${option.body ? `<p class="detail">${esc(option.body)}</p>` : ''}
<button class="choose" data-id="${esc(option.id)}">Build this</button>
${anatomy(option)}
<button class="choose" data-id="${esc(option.id)}">${option.isCanon ? 'Play it straight' : 'Build this'}</button>
</div>
</div>
${option.boardSrc && option.heroSrc ? `<div class="face back${index === 0 ? ' lead' : ''}">
<div class="media back-media">
${hasBack(option) ? `<div class="face back${index === 0 ? ' lead' : ''}">
${option.boardSrc ? `<div class="media back-media">
<img src="${esc(option.boardSrc)}" alt="">
<div class="chips">${expandChip}${flipChip('Hero')}</div>
</div>
<div class="body back-bar">
<p class="tier">Design-system board &middot; ${esc(option.label)}</p>
<button class="choose" data-id="${esc(option.id)}">Build this</button>
<div class="chips">${expandChip}${flipChip('Front')}</div>
</div>` : `<div class="back-head"><p class="tier">The full read &middot; ${esc(option.label)}</p>${flipChip('Front')}</div>`}
<div class="body back-body">
${option.boardSrc ? `<p class="tier">The full read &middot; ${esc(option.label)}</p>` : ''}
${backFacts(option)}
<button class="choose" data-id="${esc(option.id)}">${option.isCanon ? 'Play it straight' : 'Build this'}</button>
</div>
</div>` : ''}
</div>
@@ -318,7 +415,7 @@ function page() {
--ks-mono: "SFMono-Regular", "Roboto Mono", "JetBrains Mono", Consolas, monospace;
}
* { box-sizing: border-box; margin: 0; }
body { background: var(--ks-lacquer); color: var(--ks-text); font: 15px/1.55 var(--ks-font); padding: 1.8rem clamp(1rem, 5vw, 4rem) 2rem; min-height: 100dvh; display: flex; flex-direction: column; }
body { background: var(--ks-lacquer); color: var(--ks-text); font: 15px/1.55 var(--ks-font); padding: 1.8rem clamp(1rem, 5vw, 4rem) 2rem; min-height: 100dvh; display: flex; flex-direction: column; overflow-x: clip; }
#ambient { position: fixed; inset: -40px; z-index: 0; background-size: cover; background-position: center; filter: blur(34px) saturate(1.05); opacity: 0; transition: opacity .55s ease, background-image .2s; pointer-events: none; }
#scrim { position: fixed; inset: 0; z-index: 0; background: linear-gradient(180deg, oklch(7% 0.006 95 / 0.62), oklch(7% 0.006 95 / 0.78)); pointer-events: none; }
header, main, footer { position: relative; z-index: 1; }
@@ -336,7 +433,42 @@ function page() {
.question { color: var(--ks-text-muted); margin-top: .7rem; max-width: 52rem; }
main { flex: 1; display: flex; align-items: center; width: 100%; max-width: 90rem; margin: 0 auto; }
.stage { width: 100%; display: flex; flex-direction: column; gap: 1.5rem; }
.grid { display: grid; gap: 1.6rem; grid-template-columns: repeat(auto-fit, minmax(min(23rem, 100%), 1fr)); width: 100%; }
/* The deck bleeds to the viewport edges while the first card aligns with the
content column; a carousel cut off at an invisible container edge reads as
a rendering bug, but one cut off at the screen edge reads as more cards. */
.deck-shell { position: relative; width: 100vw; margin-left: calc(50% - 50vw); }
/* One row in a wide viewport, one column in a tall one; the deck scrolls on
its axis with snap points and the arrows page it card by card. */
.grid { --deck-inset: max(clamp(1rem, 5vw, 4rem), calc((100vw - 90rem) / 2)); display: flex; gap: 1.6rem; width: 100%; overflow-x: auto; overflow-y: hidden; scroll-snap-type: x mandatory; scrollbar-width: none; padding: 6px var(--deck-inset); scroll-padding-inline: var(--deck-inset); align-items: stretch; }
.grid::-webkit-scrollbar { display: none; }
.grid > .card { flex: 0 0 clamp(20rem, 27vw, 27rem); scroll-snap-align: center; }
.nav { position: absolute; z-index: 6; width: 42px; height: 42px; display: flex; align-items: center; justify-content: center; border-radius: 50%; background: oklch(7% 0.006 95 / 0.78); border: 1px solid var(--ks-rule); color: var(--ks-kinpaku); cursor: pointer; backdrop-filter: blur(6px); transition: border-color .2s, color .2s, opacity .2s; }
.nav:hover { border-color: var(--ks-kinpaku-deep); color: var(--ks-kinpaku-pale); }
.nav[disabled] { opacity: .25; cursor: default; }
.nav[hidden] { display: none; }
.nav svg { width: 16px; height: 16px; }
.nav.prev { left: 14px; top: 50%; transform: translateY(-50%); }
.nav.next { right: 14px; top: 50%; transform: translateY(-50%); }
/* A side that hides more cards fades out; a hard edge means the end. */
.fade { position: absolute; z-index: 5; pointer-events: none; opacity: 0; transition: opacity .3s ease; }
.fade-prev { left: 0; top: 0; bottom: 0; width: 88px; background: linear-gradient(90deg, var(--ks-lacquer), transparent); }
.fade-next { right: 0; top: 0; bottom: 0; width: 88px; background: linear-gradient(270deg, var(--ks-lacquer), transparent); }
.deck-shell.can-prev .fade-prev { opacity: 1; }
.deck-shell.can-next .fade-next { opacity: 1; }
@media (max-aspect-ratio: 1/1) {
.grid { flex-direction: column; overflow-x: hidden; overflow-y: auto; scroll-snap-type: y mandatory; max-height: min(68dvh, 44rem); scroll-padding-block: 6px; }
.grid > .card { flex: 0 0 auto; }
/* In the vertical deck the pager is the primary way forward, so it grows
into a labeled pill instead of a bare chevron nobody notices. */
.nav { width: auto; height: 38px; border-radius: 19px; padding: 0 16px; gap: 8px; border-color: var(--ks-kinpaku-deep); background: oklch(7% 0.006 95 / 0.88); font-family: var(--ks-mono); font-size: .62rem; letter-spacing: .2em; text-transform: uppercase; }
.nav svg { transform: rotate(90deg); }
.nav.prev::after { content: "Back"; }
.nav.next::after { content: "More"; }
.nav.prev { left: 50%; top: 6px; transform: translate(-50%, 0); }
.nav.next { right: auto; left: 50%; top: auto; bottom: 6px; transform: translate(-50%, 0); }
.fade-prev { top: 0; left: 0; right: 0; bottom: auto; width: auto; height: 72px; background: linear-gradient(180deg, var(--ks-lacquer), transparent); }
.fade-next { top: auto; left: 0; right: 0; bottom: 0; width: auto; height: 72px; background: linear-gradient(0deg, var(--ks-lacquer), transparent); }
}
.card { position: relative; perspective: 1400px; transform: rotate(var(--fan, 0deg)); transition: transform .25s cubic-bezier(.16, 1, .3, 1); }
.card:hover { transform: rotate(0deg) translateY(-4px); }
.card-inner { position: relative; height: 100%; transform-style: preserve-3d; transition: transform .7s cubic-bezier(.16, 1, .3, 1); }
@@ -344,6 +476,12 @@ function page() {
.face { background: var(--ks-lacquer-raised); border: 1px solid var(--ks-rule); border-radius: 10px; box-shadow: 0 18px 40px oklch(0% 0 0 / 0.35); overflow: hidden; display: flex; flex-direction: column; backface-visibility: hidden; -webkit-backface-visibility: hidden; }
.face.front { position: relative; height: 100%; }
.face.back { position: absolute; inset: 0; transform: rotateY(180deg); }
/* Only the visible face is interactive: a hidden backface still hit-tests
in Chrome, so the front's pip would otherwise sit invisibly over the
back's chips, showing its zoom cursor and eating the flip-back click. */
.face.back { pointer-events: none; }
.card.flipped .face.back { pointer-events: auto; }
.card.flipped .face.front { pointer-events: none; }
.face.lead { border-color: var(--ks-kinpaku); box-shadow: 0 0 0 1px var(--ks-kinpaku), 0 18px 40px oklch(0% 0 0 / 0.45); }
.card:hover .face { border-color: var(--ks-kinpaku-deep); }
.card:hover .face.lead { border-color: var(--ks-kinpaku); }
@@ -355,7 +493,7 @@ function page() {
.face.text-only .body { padding-top: 12px; }
.media { position: relative; width: 100%; aspect-ratio: 16/9; flex: none; }
.media img { width: 100%; height: 100%; object-fit: cover; display: block; background: linear-gradient(100deg, var(--ks-graphite) 40%, var(--ks-graphite-2) 50%, var(--ks-graphite) 60%); }
.face.back { background: var(--ks-lacquer-deep); }
.face.back { background: var(--ks-lacquer-raised); }
.back-bar { margin-top: auto; background: var(--ks-lacquer-raised); }
.hero-blank { width: 100%; height: 100%; background: linear-gradient(100deg, var(--ks-graphite) 40%, var(--ks-graphite-2) 50%, var(--ks-graphite) 60%); }
.back-bar { flex: none; flex-direction: row; align-items: center; justify-content: space-between; gap: .8rem; }
@@ -367,6 +505,48 @@ function page() {
.tier { font-family: var(--ks-mono); font-size: .625rem; letter-spacing: .24em; text-transform: uppercase; color: var(--ks-text-faint); }
h2 { font-family: var(--ks-font); font-size: 1.125rem; font-weight: 500; line-height: 1.35; color: var(--ks-champagne); }
.detail { color: var(--ks-text-muted); font-size: .88rem; white-space: pre-wrap; }
.detail.more { font-size: .8rem; color: var(--ks-text-faint); }
.thesis { color: var(--ks-text); font-size: .95rem; line-height: 1.45; }
.identity { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin: 2px 0; }
.swatches { display: inline-flex; gap: 4px; margin-right: 4px; }
.swatches i { width: 18px; height: 18px; border-radius: 5px; border: 1px solid oklch(100% 0 0 / 0.18); box-shadow: inset 0 0 0 1px oklch(0% 0 0 / 0.25); }
.tag { font-family: var(--ks-mono); font-size: .6rem; letter-spacing: .14em; text-transform: uppercase; color: var(--ks-text-muted); border: 1px solid var(--ks-rule); border-radius: 4px; padding: 3px 7px; }
.fact { font-size: .8rem; color: var(--ks-text-muted); line-height: 1.45; }
.fact-label { display: inline-block; font-family: var(--ks-mono); font-size: .6rem; letter-spacing: .18em; text-transform: uppercase; color: var(--ks-text-faint); margin-right: .55em; transform: translateY(-1px); }
.fact.clamp { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
/* The back is the full read: first viewport, the case, the whole risk, and
the board when the world has one. */
.back-head { display: flex; align-items: center; justify-content: space-between; gap: .8rem; padding: 14px 14px 0; }
.media.back-media { aspect-ratio: 16/6; }
.media.back-media img { width: 100%; height: 100%; object-fit: cover; }
.body.back-body { overflow-y: auto; flex: 1; scrollbar-width: thin; }
/* Inspiration rides picture-in-picture: the catalog world explains where the
direction comes from without promising what the build will look like. */
/* Hovering the inspiration takes over the whole media region; the sketch is
the promise, the inspiration is a glance, so the glance must cost nothing. */
.pip { position: absolute; z-index: 2; left: 10px; bottom: 10px; margin: 0; width: 84px; height: 64px; border: 1px solid var(--ks-rule); border-radius: 6px; overflow: hidden; background: var(--ks-lacquer); cursor: zoom-in; transition: left .35s cubic-bezier(.16,1,.3,1), bottom .35s cubic-bezier(.16,1,.3,1), width .35s cubic-bezier(.16,1,.3,1), height .35s cubic-bezier(.16,1,.3,1), border-radius .35s ease; box-shadow: 0 6px 18px oklch(0% 0 0 / 0.45); }
.pip img { display: block; width: 100%; height: 100%; object-fit: cover; }
.pip figcaption { position: absolute; left: 0; right: 0; bottom: 0; font-family: var(--ks-mono); font-size: .5rem; letter-spacing: .2em; text-transform: uppercase; color: var(--ks-text); text-align: center; padding: 3px 0 4px; background: oklch(7% 0.006 95 / 0.72); backdrop-filter: blur(3px); }
.pip:hover { left: 0; bottom: 0; width: 100%; height: 100%; border-radius: 0; z-index: 3; }
.sketch-note { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-family: var(--ks-mono); font-size: .66rem; letter-spacing: .22em; text-transform: uppercase; color: var(--ks-text-faint); }
/* A stand-in is honest about being one: dimmed, labeled, and replaced by
the real sketch whenever it lands. */
.media.stand-in img.sketch { filter: brightness(.72) saturate(.85); }
.media.stand-in .pip { display: none; }
.stand-in-label { position: absolute; z-index: 2; left: 0; right: 0; bottom: 0; margin: 0; font-family: var(--ks-mono); font-size: .56rem; letter-spacing: .2em; text-transform: uppercase; color: var(--ks-text); text-align: center; padding: 4px 0 5px; background: oklch(7% 0.006 95 / 0.78); backdrop-filter: blur(3px); }
.media.sketching { position: relative; }
.media.sketching .shimmer { position: absolute; inset: 0; }
.media img.sketch { position: relative; z-index: 1; }
/* The generic .media img display:block would defeat [hidden] and float an
empty block over the shimmer; an unloaded sketch must truly not render. */
.media img[hidden] { display: none; }
/* The standing exit as a card: present with full anatomy, never dressed as a
contender. Graphite instead of kinpaku, and it never takes the lead ring. */
.card.canon .face { border-color: var(--ks-rule); background: var(--ks-graphite); }
.card.canon:hover .face { border-color: var(--ks-text-faint); }
.card.canon .kicker.standing { background: transparent; border: 1px solid var(--ks-rule); color: var(--ks-text-faint); }
.card.canon button.choose { background: transparent; color: var(--ks-text); border: 1px solid var(--ks-rule); }
.card.canon button.choose:hover { border-color: var(--ks-text-muted); background: var(--ks-graphite-2); }
button.choose { margin-top: auto; align-self: start; background: var(--ks-kinpaku); color: var(--ks-dark-ink); border: 0; font-family: var(--ks-font); font-size: 1rem; font-weight: 500; line-height: 1.35; padding: 10px 38px; border-radius: 6px; cursor: pointer; transition: background .15s; }
button.choose:hover { background: var(--ks-kinpaku-pale); }
footer { width: 100%; max-width: 90rem; margin: 1.6rem auto 0; display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; }
@@ -408,13 +588,19 @@ function page() {
<h1>${esc(payload.title || 'Choose a direction')}</h1>
</div>
${payload.question ? `<p class="question">${esc(payload.question)}</p>` : ''}
<div class="grid">${cards}</div>
<div class="deck-shell">
<div class="grid">${cards}</div>
<div class="fade fade-prev" aria-hidden="true"></div>
<div class="fade fade-next" aria-hidden="true"></div>
<button class="nav prev" hidden aria-label="Previous card"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14.5 5 8 12l6.5 7" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
<button class="nav next" hidden aria-label="Next card"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9.5 5 16 12l-6.5 7" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
</div>
</div>
</main>
<footer>
${payload.steer ? '<input id="steer" placeholder="Optional steer: what should be different or kept?">' : ''}
${payload.reroll ? '<button id="reroll"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="4" fill="none" stroke="currentColor" stroke-width="1.6"/><circle cx="8.4" cy="8.4" r="1.5" fill="currentColor"/><circle cx="15.6" cy="8.4" r="1.5" fill="currentColor"/><circle cx="8.4" cy="15.6" r="1.5" fill="currentColor"/><circle cx="15.6" cy="15.6" r="1.5" fill="currentColor"/><circle cx="12" cy="12" r="1.5" fill="currentColor"/></svg><span>Re-roll</span></button>' : ''}
${payload.canon ? '<button id="canon" title="Skip the roll: build the page this category ships, executed impeccably">Play it straight</button>' : ''}
${payload.canon && !payload.canonCard ? '<button id="canon" title="Skip the roll: build the page this category ships, executed impeccably">Play it straight</button>' : ''}
</footer>
<script>
const steer = () => document.getElementById('steer')?.value || '';
@@ -434,7 +620,11 @@ function page() {
// Deal from the stack: cards begin piled at the grid's center, blurred,
// then travel to their seats with a stagger.
const cards = [...document.querySelectorAll('.card')];
if (!matchMedia('(prefers-reduced-motion: reduce)').matches && cards.length) {
// The deal is decoration: a hidden tab throttles rAF, so never let the
// animation hold the cards at opacity 0. Skip it when hidden, and force
// the final state after a beat no matter what the animation did.
setTimeout(() => cards.forEach(c => { c.style.opacity = ''; c.style.transform = ''; c.style.filter = ''; c.style.transition = ''; c.style.zIndex = ''; }), 1600);
if (!matchMedia('(prefers-reduced-motion: reduce)').matches && cards.length && !document.hidden) {
const grid = document.querySelector('.grid').getBoundingClientRect();
const cx = grid.left + grid.width / 2, cy = grid.top + grid.height / 2;
cards.forEach((card, i) => {
@@ -460,12 +650,99 @@ function page() {
}));
}
// Ambient: the hovered card's hero bleeds into the page ground under a scrim.
// Sketches stream in after the deal: poll each slot until the file lands,
// then swap the shimmer for the image. Generation is genuinely slow and a
// sequential batch puts the last card many minutes out, so patience is the
// default: a slot only shows its inspiration as a stand-in when it has
// waited four minutes AND nothing has landed anywhere for four minutes, the
// stand-in is labeled as such, and polling continues so the real sketch
// still swaps in whenever it arrives. Progress anywhere resets patience.
const landTracker = { last: Date.now() };
document.querySelectorAll('.media.sketching').forEach(m => {
const url = m.dataset.sketch;
const img = m.querySelector('img.sketch');
const note = m.querySelector('.sketch-note');
const started = Date.now();
// A live elapsed count is the difference between "working" and "frozen".
const tick = setInterval(() => { if (note) note.textContent = 'sketching · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000);
const settle = () => { clearInterval(tick); m.classList.remove('sketching', 'stand-in'); m.querySelector('.shimmer')?.remove(); m.querySelector('.stand-in-label')?.remove(); };
const standIn = () => {
const pip = m.querySelector('.pip img');
if (!pip || m.classList.contains('stand-in')) return;
img.src = pip.getAttribute('src'); img.hidden = false;
m.classList.add('stand-in');
m.querySelector('.shimmer')?.remove();
clearInterval(tick);
const label = document.createElement('p');
label.className = 'stand-in-label';
label.textContent = 'inspiration · sketch pending';
m.appendChild(label);
};
const tryLoad = () => {
const probe = new Image();
probe.onload = () => { landTracker.last = Date.now(); img.src = probe.src; img.hidden = false; settle(); };
probe.onerror = () => {
const quiet = Date.now() - landTracker.last > 240000;
if (Date.now() - started > 240000 && quiet) standIn();
setTimeout(tryLoad, m.classList.contains('stand-in') ? 5000 : 2500);
};
probe.src = url + (url.includes('?') ? '&' : '?') + 't=' + Date.now();
};
tryLoad();
});
// Inspiration PIP opens the full catalog card in the lightbox.
document.querySelectorAll('.pip').forEach(p => p.addEventListener('click', (e) => {
e.stopPropagation();
const img = p.querySelector('img');
if (!img) return;
lightboxImg.src = img.getAttribute('src');
lightbox.hidden = false;
requestAnimationFrame(() => lightbox.classList.add('open'));
}));
// Deck paging: arrows appear only when the deck overflows its axis, page
// one card at a time, and follow the aspect-ratio flip between row and column.
const deck = document.querySelector('.grid');
const prevBtn = document.querySelector('.nav.prev');
const nextBtn = document.querySelector('.nav.next');
const vertical = () => matchMedia('(max-aspect-ratio: 1/1)').matches;
function updateNav() {
if (!deck || !prevBtn) return;
const shell = deck.closest('.deck-shell');
const v = vertical();
const overflow = v ? deck.scrollHeight > deck.clientHeight + 4 : deck.scrollWidth > deck.clientWidth + 4;
prevBtn.hidden = nextBtn.hidden = !overflow;
const pos = v ? deck.scrollTop : deck.scrollLeft;
const max = v ? deck.scrollHeight - deck.clientHeight : deck.scrollWidth - deck.clientWidth;
const canPrev = overflow && pos > 2;
const canNext = overflow && pos < max - 2;
prevBtn.toggleAttribute('disabled', !canPrev);
nextBtn.toggleAttribute('disabled', !canNext);
shell?.classList.toggle('can-prev', canPrev);
shell?.classList.toggle('can-next', canNext);
}
function pageDeck(dir) {
const card = deck.querySelector('.card');
if (!card) return;
const r = card.getBoundingClientRect();
const step = (vertical() ? r.height : r.width) + 26;
deck.scrollBy(vertical() ? { top: dir * step, behavior: 'smooth' } : { left: dir * step, behavior: 'smooth' });
}
prevBtn?.addEventListener('click', () => pageDeck(-1));
nextBtn?.addEventListener('click', () => pageDeck(1));
deck?.addEventListener('scroll', updateNav, { passive: true });
addEventListener('resize', updateNav);
updateNav();
// Ambient: the hovered card's visible art bleeds into the page ground.
const ambient = document.getElementById('ambient');
document.querySelectorAll('.card').forEach(card => {
const hero = card.querySelector('.face.front .media img');
if (!hero) return;
card.addEventListener('mouseenter', () => { ambient.style.backgroundImage = 'url("' + hero.getAttribute('src') + '")'; ambient.style.opacity = '1'; });
card.addEventListener('mouseenter', () => {
const art = card.querySelector('.face.front .media img:not([hidden])') || card.querySelector('.face.front .pip img');
if (!art || !art.getAttribute('src')) return;
ambient.style.backgroundImage = 'url("' + art.getAttribute('src') + '")'; ambient.style.opacity = '1';
});
card.addEventListener('mouseleave', () => { ambient.style.opacity = '0'; });
});
@@ -476,8 +753,8 @@ function page() {
e.stopPropagation();
const card = b.closest('.card');
const face = card.classList.contains('flipped') ? '.face.back' : '.face.front';
const img = card.querySelector(face + ' .media img');
if (!img) return;
const img = card.querySelector(face + ' .media img:not([hidden])');
if (!img || !img.getAttribute('src')) return;
lightboxImg.src = img.getAttribute('src');
lightbox.hidden = false;
requestAnimationFrame(() => lightbox.classList.add('open'));
@@ -546,10 +823,10 @@ const server = http.createServer((req, res) => {
res.end(JSON.stringify({ ready: Boolean(pending && fs.existsSync(pending)) }));
return;
}
const imageMatch = req.method === 'GET' && req.url?.match(/^\/img\/(\d+)$/);
const imageMatch = req.method === 'GET' && req.url?.match(/^\/img\/(\d+)(?:\?.*)?$/);
if (imageMatch) {
const abs = localImages[Number(imageMatch[1])];
if (!abs) { res.writeHead(404); res.end(); return; }
if (!abs || !fs.existsSync(abs)) { res.writeHead(404); res.end(); return; }
const type = abs.endsWith('.webp') ? 'image/webp'
: abs.endsWith('.png') ? 'image/png'
: abs.endsWith('.svg') ? 'image/svg+xml'
@@ -572,6 +849,7 @@ const server = http.createServer((req, res) => {
optionId: parsed.optionId ?? null,
steer: parsed.steer ?? '',
...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}),
...(chosen?.sketch ? { sketch: chosen.sketch } : {}),
});
const isReroll = parsed.optionId === 'reroll';
if (detachedKey) {
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 4.0.2
version: 4.0.3
license: Apache 2.0
---
@@ -23,7 +23,7 @@ Page scaffolds:
- Same-size cards of icon plus heading plus text as the page structure. Cards are the lazy container; nested cards are always wrong.
- The hero-metric template: big number, small label, supporting stats, accent.
- A tracked uppercase eyebrow over every section. One named kicker is a system; an eyebrow everywhere is grammar you did not choose.
- A kicker or eyebrow above a heading. This one is a ban, not a default: no brief earns it back. The heading carries its own weight; delete the label and let the heading speak.
- Section numbers (01 / 02 / 03) unless the sequence itself carries information the reader needs.
- A modal for a task that needs neither interruption nor protected focus.
@@ -11,6 +11,10 @@ Your job is production cleanup, not new art direction. Work only from the approv
Do not redesign. Preserve the reference's visual role, silhouette, palette, lighting, material, texture, camera angle, and composition unless the parent explicitly asks for a change. Preserve perspective only when it belongs to the object or scene itself; if CSS should create the card transform, shadow, rounded clipping, border, or layout, remove that presentation chrome from the raster.
## Decision Sketches
When the parent hands you a decision card packet instead of an approved mock, the job is one sketch: one card, one file, written to the card's declared `sketch` path the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a sketch is reported back, not padded from imagination. Render through the parent's shared frame: the requested surface's first viewport as a flat, matte design sketch in the card's own palette and type character, deliberately unfinished, no photorealism, no gloss. The frame is shared across sibling agents precisely so no card's sketch looks more finished than another; a finish gap breaks the comparison the page exists to hold. The only legible text is the product's real name and one real headline; greek every other text region into indistinct lines, because an invented spec, price, or date in a sketch is a claim PRODUCT.md never made. Return one line naming the path and any deviation, nothing more.
## Input Contract
Expect:
@@ -5,6 +5,8 @@ This harness has no subagent capability, so you are running this role inline. St
You record a project's design system after the build is done. Ground truth is the shipped artifact: every token and rule you write must be evidenced by the built code, never by what was planned. Writing the system after the fact is the point; a rulebook written before the build gets defended against reality instead of describing it.
You run under a hard turn ceiling that ends the run without warning, and a run that ends before DESIGN.md is written has recorded nothing. Batch several Reads into each turn, take `reference/document.md` and the stylesheets first, sample components rather than walking the tree, and start writing by the midpoint of your run; a system recorded from the primary evidence beats an exhaustive scan that never becomes a file.
## Input Contract
Expect: the project root; the artifact path(s); the direction contract text (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; the path to the skill's `reference/document.md`; and the boundary to write at (project or app root). An existing DESIGN.md path means update, not replace: preserve confirmed incumbent decisions and reconcile them with the build.
@@ -7,20 +7,26 @@ You are the finishing reviewer for an Impeccable build: fresh eyes on a done art
You have no browser. Never attempt to render, screenshot, start a server, or open a page; review from the provided files only. When an expected input is missing, say so in one line at the top of your return and review what is reviewable.
You run under a hard turn ceiling that ends the run without warning, and a run that ends before the five sections are written returns nothing; a review built from what you saw beats a perfect review that never arrives. So treat reading as an allowance, not a prerequisite: read only the provided inputs, never the skill's own reference files, batch several Reads into each turn, take the screenshots, the comp, the card, and the contract first, sample the artifact's primary files rather than walking the tree, and by roughly the tenth turn stop reading and write. Name whatever went unread in the line above the sections.
## Input Contract
Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path. When the harness can view images, open the screenshots, the comp, and the card before judging.
Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: the contract is the builder's abstraction of the comp, and a review anchored on it inherits whatever that abstraction dropped.
## Checks, in order
1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding.
2. **Fidelity.** Against the approved comp: topology, reading order, focal scale, overlaps and z-order, density, signature geometry. Name every drift and classify it intentional or defect. The comp governs composition; it is not a pixel spec for semantics, accessibility, or responsive behavior.
2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement.
3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. The card governs commitment and finish, never composition.
4. **Contract, promise by promise.** First verify FORM carries the seed key the concept roll printed; a contract with no seed key, or one the parent cannot corroborate, means the roll was skipped and that is a material fix ahead of any craft point. Then, for each of the five blocks, does the render keep the promise? Apply the memory test to the first viewport.
5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one.
5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one, and every produced asset visibly present in the screenshots; an asset applied at near-zero opacity or buried behind other paint is a compliance token, not a shipped material.
Do not run a second detector pass; mechanical findings belong to the parent's hooks.
## Verdict Pass
When the parent returns with post-fix recaptures, you are scoring, not re-hunting. For each material fix from your review, one line: resolved, partial, or unresolved, tied to what the new screenshots visibly show; a fix answered mechanically, positions moved but the quality the finding named still absent, is partial at best. Then name at most three regressions the fix batch itself introduced, judged by the same matrix rules, and nothing else; no new hunt, no new checks. Return exactly two sections: `verdict` (the scored list) and `remaining` (what stays open, or "clear").
## Output Contract
Return exactly five sections: `persistence` (pass/fail with specifics), `fidelity` (drift list with intentional/defect per item, or "faithful"), `ceiling` (unused native devices, or "reached"), `material_fixes` (ordered, most material first, each one line tied to a check or contract promise, at most eight), and `keep` (one line naming what must not be diluted while fixing). Missing inputs are named in one line above the sections. No praise, no summary prose.
Return exactly five sections: `persistence` (pass/fail with specifics), `fidelity` (the element matrix: match, adaptation, missing, contradicted, or added without approval per salient element, adaptations citing their evidence, or "faithful"), `ceiling` (unused native devices, or "reached"), `material_fixes` (ordered, most material first, fidelity failures ahead of craft, each one line tied to a check or contract promise, at most eight), and `keep` (one line naming what must not be diluted while fixing). Missing inputs are named in one line above the sections. No praise, no summary prose.
@@ -44,9 +44,11 @@ The script assigns which structure gets built: your top-ranked structure is what
2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, the notation, publications, identity programs, data graphics, and interfaces it reads daily, not only its physical objects; a nameable abstract system (a school of poster, a documentation standard, a data-graphic tradition) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; the audience's world is larger than that, so dig until the list spans at least three families.
3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience.
4. Run `node .cursor/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode <mode>` and follow what it prints. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point.
5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the one or two fused challengers that survived the weighing as named alternates with a one-line case each, plus re-roll with an optional one-line steer. What you never present is a ranked menu of your own grounded candidates; a lineup of those invites the safest card, while dealt challengers carry no such rut. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option.
5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. What you never present is a ranked menu of your own grounded candidates; a lineup of those invites the safest card, while dealt challengers carry no such rut. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option.
The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Run `node .cursor/skills/impeccable/scripts/serve-question.mjs --start --payload <file>` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key <key>`, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry.
The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, so a grounded direction is never a wall of text against rendered art, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you, so it is never yours to recommend. Run `node .cursor/skills/impeccable/scripts/serve-question.mjs --start --payload <file>` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key <key>`, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry.
When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land, so generation never delays the question. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once, so the whole set lands in roughly the time of one. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version.
Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images for exactly this reason (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product.
@@ -98,6 +100,6 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi
Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary.
After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, and the QUALITY BAR card and approved comp paths. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. Apply the material fixes in one batch, rebuild once, and stop; the reviewer ran so that you do not re-open your own hunt. Do not run a second detector.
After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, and the QUALITY BAR card and approved comp paths. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. Apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get exactly one more batch, recapture, and verdict; two correction rounds is the ceiling, the second verdict ends the work whatever it says, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included: presenting mechanical confirmation as artistic success is how a failed build gets announced as a finished one. Do not run a second detector.
Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded.
@@ -6,7 +6,7 @@ The purpose of a probe is to test composition, narrative, hierarchy, density, fo
## Generate three compositional options
Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session; record the approved comp's path in the surface brief. Base them on the real content and the surface concepts already developed with the user. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building.
Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session; record the approved comp's path in the surface brief. Base them on the real content and the surface concepts already developed with the user. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. A decision-page sketch is not a probe: it chose the direction at deliberately unfinished fidelity, so the three comps render regardless, and the chosen card's sketch seeds at most one of them.
- When the user shortlisted multiple concepts, spread the three across them.
- When one direction is committed, vary the structural uncertainty an image can resolve: topology, sequence, density, hierarchy, focal composition, or interaction framing.
@@ -25,11 +25,11 @@ After approval, summarize the composition and the parts of the comp that must no
## Inventory implementation fidelity
Before building, inventory the approved comp's major visible ingredients in writing (a short table in the surface brief or working notes; the finish reviewer audits shipped assets against it) and choose an implementation medium for each: semantic HTML/CSS/SVG, existing project asset, generated raster, sourced raster, icon library, canvas/WebGL, or accepted omission. Textures, portraits, and scenes are raster-by-default; a CSS gradient is not a texture medium. Every `produce` entry is produced before the build ships, through the asset producer or in the current thread; an inventory with unproduced entries is an unfinished build, and this gate is where imagery-free pages come from when it is skipped.
Before building, inventory the approved comp's major visible ingredients in writing (a short table in the surface brief or working notes; the finish reviewer audits shipped assets against it) and choose an implementation medium for each: semantic HTML/CSS/SVG, existing project asset, generated raster, sourced raster, icon library, canvas/WebGL, or accepted omission. The same written inventory names the comp's compositional commitments: navigation items and icons, headline levels and their scale relationship, signature geometry such as seams, masks, and overlaps, and each section's arrangement and density. An element never written down is the element the build silently drops, and the direction contract's 150 words cannot carry this list, so this inventory is where it lives. Textures, portraits, and scenes are raster-by-default; a CSS gradient is not a texture medium. Every `produce` entry is produced before the build ships, through the asset producer or in the current thread; an inventory with unproduced entries is an unfinished build, and this gate is where imagery-free pages come from when it is skipped.
Pay special attention to the dominant composition, signature use, image-native content, second-fold system, and any interaction the still image only implies. If the concept depends on a photograph, architectural scene, product object, portrait, or other raster-native material, do not silently replace it with generic CSS scenery.
Treat the comp as a north star, not something to trace. Do not rasterize core UI text or controls. Do not substitute a different visual driver after approval without asking.
Treat the comp as a north star, not something to trace, and know what that allows: translation into semantic, responsive, accessible code, never recomposition. Keeping the palette and mood while redrawing the topology is a second art direction, not an adaptation. Do not rasterize core UI text or controls. Do not substitute a different visual driver after approval without asking.
## Produce only the assets the build needs
@@ -206,7 +206,13 @@ ${grammar}
// returns no staging. Re-rolls exclude every earlier set until the pool runs out.
export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) {
const pool = sourceCompositions ?? requireLocalConcepts().compositions;
// Stagings honour the same breadth gate as worlds: a staging too specific to
// serve an arbitrary build stays approved for direct briefs and leaves the
// challenger pool. Falls back to the full approved set rather than returning
// nothing if every approved staging is marked niche.
let approved = pool.filter(composition => composition.status === 'approved');
const broad = approved.filter(composition => composition.review?.breadth !== 'niche');
if (broad.length > 0) approved = broad;
if (approved.length === 0) return [];
if (mode) {
const matching = approved.filter(composition => composition.surface === mode);
@@ -271,12 +277,19 @@ export function selectApprovedChallengers({ scope, key, reroll = 0, sourceConcep
// graphic systems beside instrument languages and atmosphere worlds, with
// the second pick preferring a different family for diversity. Tier order
// in the rendered list is rolled too, to avoid positional bias.
// Approval ratings weight the draw: a 3-star world earns a second ticket
// (roughly double odds), a 1-star keeps its approval for direct briefs but
// leaves the challenger pool unless a tier has nothing else.
// Two separate axes, and both can exclude. Rating grades quality: a 3-star
// earns a second ticket, a 1-star marginal keep leaves the pool. Breadth says
// whether a world can serve an arbitrary build at all, so a niche world
// leaves the pool however good it is. Breadth was split out of rating because
// the only way to hold a narrow world back used to be calling it marginal,
// which made "excellent but narrow" unrecordable and corrupted the ratings as
// a calibration signal for the next authoring round.
const ticketsFor = pool => pool.flatMap(concept => {
const rating = concept.review?.rating;
if (rating === 1) return [];
// Two independent exclusions: a marginal world is too weak to challenge,
// a niche world too narrow. Either one keeps its approval for direct
// briefs and leaves the pool.
if (rating === 1 || concept.review?.breadth === 'niche') return [];
return rating === 3
? [{ concept, ticket: 0 }, { concept, ticket: 1 }]
: [{ concept, ticket: 0 }];
@@ -1529,7 +1529,7 @@ if (IS_BROWSER) {
addBrowserFindings(groupMap, document.body, typoFindings);
}
const sectionKickerFindings = checkRepeatedSectionKickersDOM()
const sectionKickerFindings = checkKickerAboveHeadingDOM()
.map(f => ({ type: f.id, detail: f.snippet }))
.filter(f => _ruleOk(f.type));
if (sectionKickerFindings.length > 0) {
@@ -309,15 +309,14 @@ const ANTIPATTERNS = [
skillGuideline: 'tiny uppercase tracked label above the hero headline',
},
{
id: 'repeated-section-kickers',
id: 'kicker-above-heading',
category: 'slop',
scopes: ['type'],
severity: 'advisory',
name: 'Repeated section kicker labels',
name: 'Kicker / eyebrow label above heading',
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.',
'A tiny tracked uppercase or small-caps label sitting as its own block directly above a heading is banned outright, repeated or not. Generated kickers never earn their place: the heading carries its own weight. Delete the label and let the heading speak; if the words matter, work them into the heading or the body.',
skillSection: 'Typography',
skillGuideline: 'repeated eyebrow or kicker labels as section scaffolding',
skillGuideline: 'kicker or eyebrow labels above headings',
},
{
id: 'numbered-section-labels',
@@ -1258,12 +1257,15 @@ function checkHeroEyebrow(opts) {
}];
}
function checkRepeatedSectionKickers(opts) {
const { candidates, minCount = 3 } = opts;
if (!Array.isArray(candidates) || candidates.length < minCount) return [];
// Outright ban: one kicker is one too many, so every collected candidate is
// a finding. The judgment lives in the candidate gate (isKickerCandidate) and
// the collector's context skips, not in a repetition count.
function checkKickerAboveHeading(opts) {
const { candidates } = opts;
if (!Array.isArray(candidates)) return [];
return candidates.map(candidate => ({
id: 'repeated-section-kickers',
snippet: `repeated section kicker "${candidate.kickerText}" before ${candidate.headingTag} "${candidate.headingText}" (${candidates.length} on page)`,
id: 'kicker-above-heading',
snippet: `kicker "${candidate.kickerText}" above ${candidate.headingTag} "${candidate.headingText}"`,
}));
}
@@ -3190,7 +3192,7 @@ function parseColorResolved(str, customPropMap) {
return parseAnyColor(resolved);
}
const REPEATED_KICKER_SKIP_SELECTOR = [
const KICKER_SKIP_SELECTOR = [
'nav',
'form',
'table',
@@ -3209,7 +3211,7 @@ const REPEATED_KICKER_SKIP_SELECTOR = [
'[data-impeccable-allow-kickers]',
].join(',');
const REPEATED_KICKER_CARD_CONTEXT_SELECTOR = [
const KICKER_CARD_CONTEXT_SELECTOR = [
'article',
'button',
'a',
@@ -3227,23 +3229,32 @@ function cleanInlineText(el) {
.trim();
}
function isRepeatedKickerCardContext(heading, kicker) {
const item = heading.closest?.(REPEATED_KICKER_CARD_CONTEXT_SELECTOR);
function isKickerCardContext(heading, kicker) {
const item = heading.closest?.(KICKER_CARD_CONTEXT_SELECTOR);
return Boolean(item && (!item.contains || item.contains(kicker)));
}
function isRepeatedKickerCandidate(opts) {
// Meta lines above headlines join category and date (or path crumbs) with
// separator glyphs, or carry a year. A kicker is one short phrase; metadata
// keeps its markers.
const KICKER_META_TEXT_RE = /[·•|]|\s[\/›»>]\s|\b(19|20)\d{2}\b/;
// Legal and document numbering: "Section 4.2", "Article IX", "§ 12.3",
// dotted decimal outlines. The label identifies the clause, so it stays.
const KICKER_DOC_NUMBERING_RE = /^(§|\d+(\.\d+)+\b|(section|article|clause|appendix|exhibit|schedule|chapter|part|rule|title)\s+([\divxlc]+\b|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)\b)/i;
function isKickerCandidate(opts) {
const {
headingTag,
headingLevel,
headingText,
headingFontSize,
kickerTag,
kickerText,
kickerTextTransform,
kickerFontVariant,
kickerFontSize,
kickerLetterSpacing,
} = opts;
if (!['h2', 'h3', 'h4'].includes(headingTag)) return false;
if (!headingLevel || headingLevel > 4) return false;
if (!headingText || headingText.length < 3) return false;
if (/^\/[\w-]+/i.test(headingText.replace(/^"|"$/g, '').trim())) return false;
if (!(headingFontSize >= 20)) return false;
@@ -3251,9 +3262,13 @@ function isRepeatedKickerCandidate(opts) {
if (!['p', 'span', 'div', 'small'].includes(kickerTag)) return false;
if (!kickerText || kickerText.length < 2 || kickerText.length > 34) return false;
if (/^step\s*\d+/i.test(kickerText) || /^\d{1,2}$/.test(kickerText)) return false;
if (KICKER_META_TEXT_RE.test(kickerText)) return false;
if (KICKER_DOC_NUMBERING_RE.test(kickerText)) return false;
const isSmallCaps = /small-caps/.test(kickerFontVariant || '');
const isUppercased = kickerTextTransform === 'uppercase'
|| (/[A-Z]/.test(kickerText) && !/[a-z]/.test(kickerText));
|| (/[A-Z]/.test(kickerText) && !/[a-z]/.test(kickerText))
|| isSmallCaps;
if (!isUppercased) return false;
if (!(kickerFontSize > 0 && kickerFontSize <= 14)) return false;
const minTrackedSpacing = Math.max(1, kickerFontSize * 0.08);
@@ -3261,37 +3276,64 @@ function isRepeatedKickerCandidate(opts) {
return true;
}
function collectRepeatedSectionKickerCandidates(doc, getStyle, resolveLetterSpacing) {
// Resolve a heading level for the anchor element: 1-4 for h1-h4, aria-level
// (default 2) for role="heading" elements, 0 otherwise.
function kickerHeadingLevel(heading) {
const tag = heading.tagName.toLowerCase();
const byTag = /^h([1-6])$/.exec(tag);
if (byTag) return parseInt(byTag[1], 10);
const role = heading.getAttribute?.('role') || '';
if (role.toLowerCase() !== 'heading') return 0;
const ariaLevel = parseInt(heading.getAttribute?.('aria-level') || '', 10);
return Number.isFinite(ariaLevel) && ariaLevel >= 1 ? ariaLevel : 2;
}
function collectKickerCandidates(doc, getStyle, resolveLetterSpacing) {
const candidates = [];
for (const heading of doc.querySelectorAll('h2, h3, h4')) {
if (heading.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
for (const heading of doc.querySelectorAll('h1, h2, h3, h4, [role="heading"]')) {
const headingLevel = kickerHeadingLevel(heading);
if (!headingLevel || headingLevel > 4) continue;
if (heading.closest?.(KICKER_SKIP_SELECTOR)) continue;
// Application contexts (tab panels, dialogs) use compact context labels
// above headings to describe state, not to decorate. Same carve-out the
// hero-eyebrow rule makes.
if (heading.closest?.('[role="tabpanel"], [role="dialog"], [role="application"], dialog')) continue;
const kicker = heading.previousElementSibling;
if (!kicker || kicker.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
if (isRepeatedKickerCardContext(heading, kicker)) continue;
if (!kicker || kicker.closest?.(KICKER_SKIP_SELECTOR)) continue;
if (isKickerCardContext(heading, kicker)) continue;
const headingStyle = getStyle(heading);
const kickerStyle = getStyle(kicker);
const headingTag = heading.tagName.toLowerCase();
const headingText = (heading.textContent || '').replace(/\s+/g, ' ').trim();
const kickerText = cleanInlineText(kicker) || (kicker.textContent || '').replace(/\s+/g, ' ').trim();
const headingFontSize = resolveLetterSpacing(headingStyle.fontSize || '', 16) || parseFloat(headingStyle.fontSize) || 0;
const kickerFontSize = resolveLetterSpacing(kickerStyle.fontSize || '', 16) || parseFloat(kickerStyle.fontSize) || 0;
const kickerLetterSpacing = resolveLetterSpacing(kickerStyle.letterSpacing || '', kickerFontSize);
if (!isRepeatedKickerCandidate({
headingTag: heading.tagName.toLowerCase(),
if (!isKickerCandidate({
headingLevel,
headingText,
headingFontSize,
kickerTag: kicker.tagName.toLowerCase(),
kickerText,
kickerTextTransform: kickerStyle.textTransform || '',
kickerFontVariant: `${kickerStyle.fontVariant || ''} ${kickerStyle.fontVariantCaps || ''}`,
kickerFontSize,
kickerLetterSpacing,
})) {
continue;
}
// A tracked-caps eyebrow above a hero-scale h1 belongs to
// hero-eyebrow-chip (which also covers the accent-bold and dash-prefix
// stylings there). Stand down so one element gets one finding.
if (headingTag === 'h1' && headingFontSize >= 48 && kickerLetterSpacing >= 1.6) {
continue;
}
candidates.push({
headingTag: heading.tagName.toLowerCase(),
headingTag,
headingText: headingText.replace(/^"|"$/g, '').slice(0, 60),
kickerText: kickerText.slice(0, 40),
});
@@ -3299,17 +3341,17 @@ function collectRepeatedSectionKickerCandidates(doc, getStyle, resolveLetterSpac
return candidates;
}
function checkRepeatedSectionKickersDOM() {
const candidates = collectRepeatedSectionKickerCandidates(
function checkKickerAboveHeadingDOM() {
const candidates = collectKickerCandidates(
document,
(el) => getComputedStyle(el),
(value, fontSize) => resolveLengthPx(value, fontSize) || 0,
);
return checkRepeatedSectionKickers({ candidates });
return checkKickerAboveHeading({ candidates });
}
// ── Numbered section labels ─────────────────────────────────────────────────
// Sibling of the repeated-kicker rule: instead of a tracked uppercase word,
// Sibling of the kicker-above-heading rule: instead of a tracked uppercase word,
// the section scaffold is a tiny numeric index riding beside each section
// heading — bare and zero-padded, or an index joined to a short micro-label
// by a separator glyph. The kicker rule deliberately excludes bare 1-2 digit
@@ -3363,7 +3405,7 @@ function collectNumberedSectionLabelCandidates(doc, getStyle, resolveLetterSpaci
const candidates = [];
const seenLabels = new Set();
for (const heading of doc.querySelectorAll('h2, h3, h4')) {
if (heading.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
if (heading.closest?.(KICKER_SKIP_SELECTOR)) continue;
// The index sits either directly before the heading, or before the
// wrapper the heading leads (label | <div><h2>…</h2>…</div>).
let label = heading.previousElementSibling;
@@ -3373,9 +3415,9 @@ function collectNumberedSectionLabelCandidates(doc, getStyle, resolveLetterSpaci
if (firstChild === heading) label = parent.previousElementSibling;
}
if (!label || seenLabels.has(label)) continue;
if (label.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
if (label.closest?.(KICKER_SKIP_SELECTOR)) continue;
if (HEADING_TAGS.has(label.tagName.toLowerCase())) continue;
if (isRepeatedKickerCardContext(heading, label)) continue;
if (isKickerCardContext(heading, label)) continue;
const labelText = cleanInlineText(label) || (label.textContent || '').replace(/\s+/g, ' ').trim();
const parsed = parseNumberedLabelText(labelText);
@@ -4548,13 +4590,13 @@ function checkElementHeroEyebrow(el, style, tag, window, customPropMap) {
});
}
function checkRepeatedSectionKickersFromDoc(doc, win) {
const candidates = collectRepeatedSectionKickerCandidates(
function checkKickerAboveHeadingFromDoc(doc, win) {
const candidates = collectKickerCandidates(
doc,
(el) => win.getComputedStyle(el),
(value, fontSize) => resolveLengthPx(value, fontSize) || 0,
);
return checkRepeatedSectionKickers({ candidates });
return checkKickerAboveHeading({ candidates });
}
function checkElementMotion(tag, style) {
@@ -7712,7 +7754,7 @@ if (IS_BROWSER) {
addBrowserFindings(groupMap, document.body, typoFindings);
}
const sectionKickerFindings = checkRepeatedSectionKickersDOM()
const sectionKickerFindings = checkKickerAboveHeadingDOM()
.map(f => ({ type: f.id, detail: f.snippet }))
.filter(f => _ruleOk(f.type));
if (sectionKickerFindings.length > 0) {
@@ -223,7 +223,7 @@ function unwrapCssAtLayer(source) {
// ---------------------------------------------------------------------------
const STATIC_INHERITED_PROPS = new Set([
'color', 'fontFamily', 'fontSize', 'fontStyle', 'fontWeight',
'color', 'fontFamily', 'fontSize', 'fontStyle', 'fontWeight', 'fontVariant',
'lineHeight', 'letterSpacing', 'textTransform', 'textAlign', 'hyphens',
'webkitHyphens',
]);
@@ -252,6 +252,7 @@ const STATIC_DEFAULT_STYLE = {
fontFamily: '',
fontSize: '16px',
fontStyle: 'normal',
fontVariant: 'normal',
fontWeight: '400',
lineHeight: 'normal',
letterSpacing: 'normal',
@@ -27,11 +27,11 @@ import {
checkElementRadialSpotlight,
checkCreamPalette,
checkHtmlPatterns,
checkKickerAboveHeadingFromDoc,
checkNumberedSectionLabelsFromDoc,
checkPageLayout,
checkPageQualityFromDoc,
checkRepeatedContainerTextFromDoc,
checkRepeatedSectionKickersFromDoc,
resolveBackground,
resolveBorderRadiusPx,
} from '../../rules/checks.mjs';
@@ -202,7 +202,7 @@ async function detectHtml(filePath, options = {}) {
for (const f of runPageCheck('typography-rules', () => checkStaticPageTypography(document, window))) {
findings.push(finding(f.id, filePath, f.snippet));
}
for (const f of runPageCheck('repeated-section-kickers', () => checkRepeatedSectionKickersFromDoc(document, window))) {
for (const f of runPageCheck('kicker-above-heading', () => checkKickerAboveHeadingFromDoc(document, window))) {
findings.push(finding(f.id, filePath, f.snippet));
}
for (const f of runPageCheck('numbered-section-labels', () => checkNumberedSectionLabelsFromDoc(document, window))) {
@@ -198,15 +198,14 @@ const ANTIPATTERNS = [
skillGuideline: 'tiny uppercase tracked label above the hero headline',
},
{
id: 'repeated-section-kickers',
id: 'kicker-above-heading',
category: 'slop',
scopes: ['type'],
severity: 'advisory',
name: 'Repeated section kicker labels',
name: 'Kicker / eyebrow label above heading',
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.',
'A tiny tracked uppercase or small-caps label sitting as its own block directly above a heading is banned outright, repeated or not. Generated kickers never earn their place: the heading carries its own weight. Delete the label and let the heading speak; if the words matter, work them into the heading or the body.',
skillSection: 'Typography',
skillGuideline: 'repeated eyebrow or kicker labels as section scaffolding',
skillGuideline: 'kicker or eyebrow labels above headings',
},
{
id: 'numbered-section-labels',
@@ -456,12 +456,15 @@ function checkHeroEyebrow(opts) {
}];
}
function checkRepeatedSectionKickers(opts) {
const { candidates, minCount = 3 } = opts;
if (!Array.isArray(candidates) || candidates.length < minCount) return [];
// Outright ban: one kicker is one too many, so every collected candidate is
// a finding. The judgment lives in the candidate gate (isKickerCandidate) and
// the collector's context skips, not in a repetition count.
function checkKickerAboveHeading(opts) {
const { candidates } = opts;
if (!Array.isArray(candidates)) return [];
return candidates.map(candidate => ({
id: 'repeated-section-kickers',
snippet: `repeated section kicker "${candidate.kickerText}" before ${candidate.headingTag} "${candidate.headingText}" (${candidates.length} on page)`,
id: 'kicker-above-heading',
snippet: `kicker "${candidate.kickerText}" above ${candidate.headingTag} "${candidate.headingText}"`,
}));
}
@@ -2388,7 +2391,7 @@ function parseColorResolved(str, customPropMap) {
return parseAnyColor(resolved);
}
const REPEATED_KICKER_SKIP_SELECTOR = [
const KICKER_SKIP_SELECTOR = [
'nav',
'form',
'table',
@@ -2407,7 +2410,7 @@ const REPEATED_KICKER_SKIP_SELECTOR = [
'[data-impeccable-allow-kickers]',
].join(',');
const REPEATED_KICKER_CARD_CONTEXT_SELECTOR = [
const KICKER_CARD_CONTEXT_SELECTOR = [
'article',
'button',
'a',
@@ -2425,23 +2428,32 @@ function cleanInlineText(el) {
.trim();
}
function isRepeatedKickerCardContext(heading, kicker) {
const item = heading.closest?.(REPEATED_KICKER_CARD_CONTEXT_SELECTOR);
function isKickerCardContext(heading, kicker) {
const item = heading.closest?.(KICKER_CARD_CONTEXT_SELECTOR);
return Boolean(item && (!item.contains || item.contains(kicker)));
}
function isRepeatedKickerCandidate(opts) {
// Meta lines above headlines join category and date (or path crumbs) with
// separator glyphs, or carry a year. A kicker is one short phrase; metadata
// keeps its markers.
const KICKER_META_TEXT_RE = /[·•|]|\s[\/›»>]\s|\b(19|20)\d{2}\b/;
// Legal and document numbering: "Section 4.2", "Article IX", "§ 12.3",
// dotted decimal outlines. The label identifies the clause, so it stays.
const KICKER_DOC_NUMBERING_RE = /^(§|\d+(\.\d+)+\b|(section|article|clause|appendix|exhibit|schedule|chapter|part|rule|title)\s+([\divxlc]+\b|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)\b)/i;
function isKickerCandidate(opts) {
const {
headingTag,
headingLevel,
headingText,
headingFontSize,
kickerTag,
kickerText,
kickerTextTransform,
kickerFontVariant,
kickerFontSize,
kickerLetterSpacing,
} = opts;
if (!['h2', 'h3', 'h4'].includes(headingTag)) return false;
if (!headingLevel || headingLevel > 4) return false;
if (!headingText || headingText.length < 3) return false;
if (/^\/[\w-]+/i.test(headingText.replace(/^"|"$/g, '').trim())) return false;
if (!(headingFontSize >= 20)) return false;
@@ -2449,9 +2461,13 @@ function isRepeatedKickerCandidate(opts) {
if (!['p', 'span', 'div', 'small'].includes(kickerTag)) return false;
if (!kickerText || kickerText.length < 2 || kickerText.length > 34) return false;
if (/^step\s*\d+/i.test(kickerText) || /^\d{1,2}$/.test(kickerText)) return false;
if (KICKER_META_TEXT_RE.test(kickerText)) return false;
if (KICKER_DOC_NUMBERING_RE.test(kickerText)) return false;
const isSmallCaps = /small-caps/.test(kickerFontVariant || '');
const isUppercased = kickerTextTransform === 'uppercase'
|| (/[A-Z]/.test(kickerText) && !/[a-z]/.test(kickerText));
|| (/[A-Z]/.test(kickerText) && !/[a-z]/.test(kickerText))
|| isSmallCaps;
if (!isUppercased) return false;
if (!(kickerFontSize > 0 && kickerFontSize <= 14)) return false;
const minTrackedSpacing = Math.max(1, kickerFontSize * 0.08);
@@ -2459,37 +2475,64 @@ function isRepeatedKickerCandidate(opts) {
return true;
}
function collectRepeatedSectionKickerCandidates(doc, getStyle, resolveLetterSpacing) {
// Resolve a heading level for the anchor element: 1-4 for h1-h4, aria-level
// (default 2) for role="heading" elements, 0 otherwise.
function kickerHeadingLevel(heading) {
const tag = heading.tagName.toLowerCase();
const byTag = /^h([1-6])$/.exec(tag);
if (byTag) return parseInt(byTag[1], 10);
const role = heading.getAttribute?.('role') || '';
if (role.toLowerCase() !== 'heading') return 0;
const ariaLevel = parseInt(heading.getAttribute?.('aria-level') || '', 10);
return Number.isFinite(ariaLevel) && ariaLevel >= 1 ? ariaLevel : 2;
}
function collectKickerCandidates(doc, getStyle, resolveLetterSpacing) {
const candidates = [];
for (const heading of doc.querySelectorAll('h2, h3, h4')) {
if (heading.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
for (const heading of doc.querySelectorAll('h1, h2, h3, h4, [role="heading"]')) {
const headingLevel = kickerHeadingLevel(heading);
if (!headingLevel || headingLevel > 4) continue;
if (heading.closest?.(KICKER_SKIP_SELECTOR)) continue;
// Application contexts (tab panels, dialogs) use compact context labels
// above headings to describe state, not to decorate. Same carve-out the
// hero-eyebrow rule makes.
if (heading.closest?.('[role="tabpanel"], [role="dialog"], [role="application"], dialog')) continue;
const kicker = heading.previousElementSibling;
if (!kicker || kicker.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
if (isRepeatedKickerCardContext(heading, kicker)) continue;
if (!kicker || kicker.closest?.(KICKER_SKIP_SELECTOR)) continue;
if (isKickerCardContext(heading, kicker)) continue;
const headingStyle = getStyle(heading);
const kickerStyle = getStyle(kicker);
const headingTag = heading.tagName.toLowerCase();
const headingText = (heading.textContent || '').replace(/\s+/g, ' ').trim();
const kickerText = cleanInlineText(kicker) || (kicker.textContent || '').replace(/\s+/g, ' ').trim();
const headingFontSize = resolveLetterSpacing(headingStyle.fontSize || '', 16) || parseFloat(headingStyle.fontSize) || 0;
const kickerFontSize = resolveLetterSpacing(kickerStyle.fontSize || '', 16) || parseFloat(kickerStyle.fontSize) || 0;
const kickerLetterSpacing = resolveLetterSpacing(kickerStyle.letterSpacing || '', kickerFontSize);
if (!isRepeatedKickerCandidate({
headingTag: heading.tagName.toLowerCase(),
if (!isKickerCandidate({
headingLevel,
headingText,
headingFontSize,
kickerTag: kicker.tagName.toLowerCase(),
kickerText,
kickerTextTransform: kickerStyle.textTransform || '',
kickerFontVariant: `${kickerStyle.fontVariant || ''} ${kickerStyle.fontVariantCaps || ''}`,
kickerFontSize,
kickerLetterSpacing,
})) {
continue;
}
// A tracked-caps eyebrow above a hero-scale h1 belongs to
// hero-eyebrow-chip (which also covers the accent-bold and dash-prefix
// stylings there). Stand down so one element gets one finding.
if (headingTag === 'h1' && headingFontSize >= 48 && kickerLetterSpacing >= 1.6) {
continue;
}
candidates.push({
headingTag: heading.tagName.toLowerCase(),
headingTag,
headingText: headingText.replace(/^"|"$/g, '').slice(0, 60),
kickerText: kickerText.slice(0, 40),
});
@@ -2497,17 +2540,17 @@ function collectRepeatedSectionKickerCandidates(doc, getStyle, resolveLetterSpac
return candidates;
}
function checkRepeatedSectionKickersDOM() {
const candidates = collectRepeatedSectionKickerCandidates(
function checkKickerAboveHeadingDOM() {
const candidates = collectKickerCandidates(
document,
(el) => getComputedStyle(el),
(value, fontSize) => resolveLengthPx(value, fontSize) || 0,
);
return checkRepeatedSectionKickers({ candidates });
return checkKickerAboveHeading({ candidates });
}
// ── Numbered section labels ─────────────────────────────────────────────────
// Sibling of the repeated-kicker rule: instead of a tracked uppercase word,
// Sibling of the kicker-above-heading rule: instead of a tracked uppercase word,
// the section scaffold is a tiny numeric index riding beside each section
// heading — bare and zero-padded, or an index joined to a short micro-label
// by a separator glyph. The kicker rule deliberately excludes bare 1-2 digit
@@ -2561,7 +2604,7 @@ function collectNumberedSectionLabelCandidates(doc, getStyle, resolveLetterSpaci
const candidates = [];
const seenLabels = new Set();
for (const heading of doc.querySelectorAll('h2, h3, h4')) {
if (heading.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
if (heading.closest?.(KICKER_SKIP_SELECTOR)) continue;
// The index sits either directly before the heading, or before the
// wrapper the heading leads (label | <div><h2>…</h2>…</div>).
let label = heading.previousElementSibling;
@@ -2571,9 +2614,9 @@ function collectNumberedSectionLabelCandidates(doc, getStyle, resolveLetterSpaci
if (firstChild === heading) label = parent.previousElementSibling;
}
if (!label || seenLabels.has(label)) continue;
if (label.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
if (label.closest?.(KICKER_SKIP_SELECTOR)) continue;
if (HEADING_TAGS.has(label.tagName.toLowerCase())) continue;
if (isRepeatedKickerCardContext(heading, label)) continue;
if (isKickerCardContext(heading, label)) continue;
const labelText = cleanInlineText(label) || (label.textContent || '').replace(/\s+/g, ' ').trim();
const parsed = parseNumberedLabelText(labelText);
@@ -3746,13 +3789,13 @@ function checkElementHeroEyebrow(el, style, tag, window, customPropMap) {
});
}
function checkRepeatedSectionKickersFromDoc(doc, win) {
const candidates = collectRepeatedSectionKickerCandidates(
function checkKickerAboveHeadingFromDoc(doc, win) {
const candidates = collectKickerCandidates(
doc,
(el) => win.getComputedStyle(el),
(value, fontSize) => resolveLengthPx(value, fontSize) || 0,
);
return checkRepeatedSectionKickers({ candidates });
return checkKickerAboveHeading({ candidates });
}
function checkElementMotion(tag, style) {
@@ -5393,7 +5436,7 @@ export {
checkItalicSerif,
isAccentColor,
checkHeroEyebrow,
checkRepeatedSectionKickers,
checkKickerAboveHeading,
checkMotion,
checkGlow,
scanCssTextForGlow,
@@ -5424,9 +5467,9 @@ export {
parseAnyColor,
parseColorResolved,
cleanInlineText,
isRepeatedKickerCandidate,
collectRepeatedSectionKickerCandidates,
checkRepeatedSectionKickersDOM,
isKickerCandidate,
collectKickerCandidates,
checkKickerAboveHeadingDOM,
parseNumberedLabelText,
isNumberedSectionLabelCandidate,
collectNumberedSectionLabelCandidates,
@@ -5458,7 +5501,7 @@ export {
checkElementIconTile,
checkElementItalicSerif,
checkElementHeroEyebrow,
checkRepeatedSectionKickersFromDoc,
checkKickerAboveHeadingFromDoc,
checkElementMotion,
checkElementGlow,
checkTypography,
@@ -24,21 +24,36 @@
* "label": "Fillmore Handbill",
* "kicker": "THE ROLL", // optional badge; the assigned option leads
* "lineage": "1966-71 Fillmore ...", // optional
* "body": "why it fits, first viewport, risk ...", // optional, plain text
* "hero": "https://... or /abs/path.webp", // optional image
* "thesis": "one line: the idea this direction owns", // optional
* "palette": ["#1a2f5e", "oklch(84% .19 80)", ...], // optional, rendered as chips
* "materials": ["letterpress", "newsprint"], // optional, rendered as tags
* "viewport": "one line: the first-viewport composition", // optional
* "case": "one line: the fusion verdict, honest", // optional
* "risk": "one line: the honest risk", // optional
* "body": "fallback prose when the structured fields are absent",
* "sketch": ".impeccable/sketches/assigned.webp", // optional; may not exist
* // yet: the page shimmer-waits and polls the
* // slot until the file lands, so serve first
* // and generate after
* "hero": "https://... or /abs/path.webp", // optional inspiration image;
* // rides picture-in-picture when a sketch exists
* "board": "https://... or /abs/path.webp" // optional secondary image
* }, ...
* ],
* "reroll": true, // adds a re-roll action (returns {"optionId":"reroll"})
* "canon": true, // adds the quiet "Play it straight" standing exit
* // (returns {"optionId":"canon"}); direction rounds only
* "canon": true, // adds the "Play it straight" standing exit;
* // direction rounds only (returns {"optionId":"canon"})
* "canonCard": { ... }, // optional: the standing exit as a full card with the
* // same anatomy (label, thesis, palette, sketch, ...);
* // rendered last and visually subordinate. Without it,
* // canon stays a quiet footer action.
* "steer": true // adds a free-text steer field returned with any answer
* }
*
* Options render as large cards: hero render first when present (the dealt
* catalog worlds already have cards; grounded directions may present text-only
* or a freshly generated mock). Local image paths are served by this server;
* nothing is uploaded anywhere.
* Options render as large cards: the sketch leads when present, with the
* inspiration image picture-in-picture; a hero alone renders full-bleed; a
* text-only direction gets its identity from the palette chips and tags.
* Local image paths are served by this server; nothing is uploaded anywhere.
*
* Modes:
* (default) block until answered; ANSWER on stdout; exit 0.
@@ -107,6 +122,9 @@ function printAnswer(raw) {
if (a.hero || a.board) {
console.log("CHOSEN CARD: open the chosen world's board and hero images now, before any code. When your harness only reads files, or runs sandboxed, download them INTO the workspace and open the relative path; a sandboxed viewer rejects absolute paths outside it. They set the craft bar the build must reach.");
}
if (a.sketch) {
console.log('CHOSEN SKETCH: the decision sketch at that path may seed one comp probe; the comp round still renders its full set, because a sketch chose the direction, not the composition.');
}
if (a.optionId === 'canon') {
console.log('CANON CHOSEN: the user picked the category standard on purpose. Ask once for two or three products this should sit alongside; their craft level becomes the quality bar. Execute the canon at full commitment, conventions embraced without irony or smuggled quirk.');
}
@@ -125,14 +143,15 @@ if (hasFlag('schema')) {
title: 'Choose the visual world',
question: 'The roll assigned Fillmore Handbill. Keep it, take an alternate, or re-roll.',
options: [
{ id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', body: 'Why it fits, the first viewport, the honest risk.', hero: 'https://impeccable.style/worlds/cards/fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/fillmore-handbill.webp' },
{ id: 'challenger-teletext', label: 'Teletext Service', lineage: 'broadcast teletext magazines', body: 'Fused alternate.', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' },
{ id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', sketch: '.impeccable/sketches/assigned.webp', hero: 'https://impeccable.style/worlds/cards/fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/fillmore-handbill.webp' },
{ id: 'challenger-teletext', label: 'Teletext Service', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', case: 'Fuses cleanly: releases map to numbered pages.', sketch: '.impeccable/sketches/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' },
],
reroll: true,
canon: true,
canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', viewport: 'The arrangement a visitor expects, at full craft.', sketch: '.impeccable/sketches/canon.webp' },
steer: true,
}, null, 2));
console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board accept URLs or local paths. canon adds a quiet standing "Play it straight" action for direction decisions: the user\'s explicit door to the category standard. Include it only for visual-direction rounds; never present canon as your own recommendation.');
console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/sketch accept URLs or local paths; sketch slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the sketch, never as the promise of the build. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability.');
process.exit(0);
}
@@ -203,14 +222,25 @@ if (hasFlag('start')) {
const key = arg('key') || Math.random().toString(16).slice(2, 10);
// In start mode the agent is alive and owns browser routing; the server
// only opens the system browser itself when --open forces it.
// The daemon's output lands in a per-key log so a startup failure can say
// what actually went wrong instead of only that it did.
const logFile = path.join(QUESTION_DIR, `${key}.log`);
const logFd = fs.openSync(logFile, 'a');
const child = spawn(process.execPath, [
fileURLToPath(import.meta.url), '--payload', payloadPath, '--detached-serve', '--key', key,
'--timeout', String(timeoutSec), ...(hasFlag('open') ? [] : ['--no-open']),
], { detached: true, stdio: 'ignore' });
], { detached: true, stdio: ['ignore', logFd, logFd] });
child.unref();
fs.closeSync(logFd);
const deadline = Date.now() + 8000;
while (Date.now() < deadline && !fs.existsSync(stateFile(key))) await new Promise((r) => setTimeout(r, 100));
if (!fs.existsSync(stateFile(key))) { console.error('serve-question: server failed to start'); process.exit(1); }
if (!fs.existsSync(stateFile(key))) {
let tail = '';
try { tail = fs.readFileSync(logFile, 'utf8').trim().split('\n').slice(-4).join('\n '); } catch { /* log never written */ }
console.error(`serve-question: server failed to start${tail ? `\n ${tail}` : ''}`);
console.error(`serve-question: log at ${path.relative(process.cwd(), logFile) || logFile}. A sandboxed exec that cannot listen on localhost causes exactly this; rerun this command once through the harness's network-enabled or unsandboxed command tool before falling back.`);
process.exit(1);
}
const state = JSON.parse(fs.readFileSync(stateFile(key), 'utf8'));
console.log(`QUESTION URL: ${state.url}`);
console.log(`QUESTION KEY: ${key}`);
@@ -244,12 +274,28 @@ function loadRound(json) {
localImages.push(abs);
return `/img/${localImages.length - 1}`;
};
// Sketches stream in after the page is served, so their slots register
// whether or not the file exists yet; /img answers 404 until it lands and
// the page polls the slot. Remote sketch URLs pass through untouched.
const sketchSrc = (value) => {
if (!value) return null;
if (/^https?:\/\//.test(value)) return value;
localImages.push(path.resolve(value));
return `/img/${localImages.length - 1}`;
};
payload = parsed;
options = parsed.options.map((option) => ({
const decorate = (option) => ({
...option,
heroSrc: imageSrc(option.hero),
boardSrc: imageSrc(option.board),
}));
sketchSrc: sketchSrc(option.sketch),
});
options = parsed.options.map(decorate);
// The standing exit as a full card: same anatomy, reserved id, rendered
// subordinate by the page. Without it, canon stays the quiet footer action.
if (parsed.canonCard && typeof parsed.canonCard === 'object') {
options = [...options, { ...decorate(parsed.canonCard), id: 'canon', isCanon: true }];
}
}
try { loadRound(raw); } catch (error) { console.error(`serve-question: ${error.message}`); process.exit(1); }
const detachedKey = hasFlag('detached-serve') ? arg('key') : null;
@@ -260,30 +306,81 @@ const esc = (s) => String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<
function page() {
const flipChip = (label) => `<button type="button" class="chip flip" aria-label="Flip the card"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4a8 8 0 1 1-8 8" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/><path d="M4 5.5V12h6.5" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg><span>${label}</span></button>`;
const expandChip = `<button type="button" class="chip expand" aria-label="Expand the image"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 9V4h5M20 15v5h-5M20 9V4h-5M4 15v5h5" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg></button>`;
const cards = options.map((option, index) => `
<article class="card" style="--fan:${index === 0 ? '0deg' : (index % 2 ? '1.4deg' : '-1.2deg')};--deal:${index * 90}ms" data-id="${esc(option.id)}">
<div class="card-inner">
<div class="face front${index === 0 ? ' lead' : ''}${option.heroSrc || option.boardSrc ? '' : ' text-only'}">
${option.kicker ? `<span class="kicker">${esc(option.kicker)}</span>` : ''}
${option.heroSrc || option.boardSrc ? `<div class="media">
// Structured anatomy: chips and one-line facts render when the payload
// carries them; a plain body falls back to the prose block. Palette chips
// and material tags give a text-only direction an immediate identity that
// no generation luck can distort.
const fact = (label, value, cls = '') => value ? `<p class="fact${cls ? ` ${cls}` : ''}"><span class="fact-label">${label}</span>${esc(value)}</p>` : '';
const hasBack = (option) => Boolean(option.viewport || option.case || (option.boardSrc && option.heroSrc));
const anatomy = (option) => {
const rows = [];
if (option.thesis) rows.push(`<p class="thesis">${esc(option.thesis)}</p>`);
const idBits = [];
if (Array.isArray(option.palette) && option.palette.length) {
idBits.push(`<span class="swatches">${option.palette.slice(0, 6).map((c) => `<i style="background:${esc(c)}" title="${esc(c)}"></i>`).join('')}</span>`);
}
if (Array.isArray(option.materials) && option.materials.length) {
idBits.push(option.materials.slice(0, 4).map((m) => `<span class="tag">${esc(m)}</span>`).join(''));
}
if (idBits.length) rows.push(`<div class="identity">${idBits.join('')}</div>`);
// The front carries only what the choice needs: thesis, identity, and the
// honest risk clamped to two lines. First viewport and the case read on
// the card's back; once the sketch lands, the first viewport is a picture.
rows.push(fact('Risk', option.risk, 'clamp'));
if (!option.thesis && option.body) rows.push(`<p class="detail">${esc(option.body)}</p>`);
else if (option.body && option.thesis && !hasBack(option)) rows.push(`<p class="detail more">${esc(option.body)}</p>`);
return rows.join('\n ');
};
const backFacts = (option) => [
fact('First viewport', option.viewport),
fact('The case', option.case),
fact('Risk', option.risk),
option.body && option.thesis ? `<p class="detail more">${esc(option.body)}</p>` : '',
].filter(Boolean).join('\n ');
const media = (option) => {
const inspiration = option.heroSrc ? `<figure class="pip" title="Inspiration: the world this direction draws from. Your page will not look like this image.">
<img src="${esc(option.heroSrc)}" alt="">
<figcaption>inspiration</figcaption>
</figure>` : '';
const details = hasBack(option) ? flipChip('Details') : '';
if (option.sketchSrc) {
return `<div class="media sketching" data-sketch="${esc(option.sketchSrc)}">
<div class="shimmer"><span class="sketch-note">sketching&hellip;</span></div>
<img class="sketch" alt="" hidden>
${inspiration}
<div class="chips">${expandChip}${details}</div>
</div>`;
}
if (option.heroSrc || option.boardSrc) {
return `<div class="media">
<img src="${esc(option.heroSrc || option.boardSrc)}" alt="">
<div class="chips">${expandChip}${option.boardSrc && option.heroSrc ? flipChip('Board') : ''}</div>
</div>` : ''}
<div class="chips">${expandChip}${details}</div>
</div>`;
}
return '';
};
const cards = options.map((option, index) => `
<article class="card${option.isCanon ? ' canon' : ''}" style="--fan:${index === 0 ? '0deg' : (index % 2 ? '1.4deg' : '-1.2deg')};--deal:${index * 90}ms" data-id="${esc(option.id)}">
<div class="card-inner">
<div class="face front${index === 0 ? ' lead' : ''}${media(option) ? '' : ' text-only'}">
${option.kicker ? `<span class="kicker">${esc(option.kicker)}</span>` : option.isCanon ? '<span class="kicker standing">The standing door</span>' : ''}
${media(option)}
<div class="body">
${option.lineage ? `<p class="tier">${esc(option.lineage)}</p>` : ''}
<h2>${esc(option.label)}</h2>
${option.body ? `<p class="detail">${esc(option.body)}</p>` : ''}
<button class="choose" data-id="${esc(option.id)}">Build this</button>
${anatomy(option)}
<button class="choose" data-id="${esc(option.id)}">${option.isCanon ? 'Play it straight' : 'Build this'}</button>
</div>
</div>
${option.boardSrc && option.heroSrc ? `<div class="face back${index === 0 ? ' lead' : ''}">
<div class="media back-media">
${hasBack(option) ? `<div class="face back${index === 0 ? ' lead' : ''}">
${option.boardSrc ? `<div class="media back-media">
<img src="${esc(option.boardSrc)}" alt="">
<div class="chips">${expandChip}${flipChip('Hero')}</div>
</div>
<div class="body back-bar">
<p class="tier">Design-system board &middot; ${esc(option.label)}</p>
<button class="choose" data-id="${esc(option.id)}">Build this</button>
<div class="chips">${expandChip}${flipChip('Front')}</div>
</div>` : `<div class="back-head"><p class="tier">The full read &middot; ${esc(option.label)}</p>${flipChip('Front')}</div>`}
<div class="body back-body">
${option.boardSrc ? `<p class="tier">The full read &middot; ${esc(option.label)}</p>` : ''}
${backFacts(option)}
<button class="choose" data-id="${esc(option.id)}">${option.isCanon ? 'Play it straight' : 'Build this'}</button>
</div>
</div>` : ''}
</div>
@@ -318,7 +415,7 @@ function page() {
--ks-mono: "SFMono-Regular", "Roboto Mono", "JetBrains Mono", Consolas, monospace;
}
* { box-sizing: border-box; margin: 0; }
body { background: var(--ks-lacquer); color: var(--ks-text); font: 15px/1.55 var(--ks-font); padding: 1.8rem clamp(1rem, 5vw, 4rem) 2rem; min-height: 100dvh; display: flex; flex-direction: column; }
body { background: var(--ks-lacquer); color: var(--ks-text); font: 15px/1.55 var(--ks-font); padding: 1.8rem clamp(1rem, 5vw, 4rem) 2rem; min-height: 100dvh; display: flex; flex-direction: column; overflow-x: clip; }
#ambient { position: fixed; inset: -40px; z-index: 0; background-size: cover; background-position: center; filter: blur(34px) saturate(1.05); opacity: 0; transition: opacity .55s ease, background-image .2s; pointer-events: none; }
#scrim { position: fixed; inset: 0; z-index: 0; background: linear-gradient(180deg, oklch(7% 0.006 95 / 0.62), oklch(7% 0.006 95 / 0.78)); pointer-events: none; }
header, main, footer { position: relative; z-index: 1; }
@@ -336,7 +433,42 @@ function page() {
.question { color: var(--ks-text-muted); margin-top: .7rem; max-width: 52rem; }
main { flex: 1; display: flex; align-items: center; width: 100%; max-width: 90rem; margin: 0 auto; }
.stage { width: 100%; display: flex; flex-direction: column; gap: 1.5rem; }
.grid { display: grid; gap: 1.6rem; grid-template-columns: repeat(auto-fit, minmax(min(23rem, 100%), 1fr)); width: 100%; }
/* The deck bleeds to the viewport edges while the first card aligns with the
content column; a carousel cut off at an invisible container edge reads as
a rendering bug, but one cut off at the screen edge reads as more cards. */
.deck-shell { position: relative; width: 100vw; margin-left: calc(50% - 50vw); }
/* One row in a wide viewport, one column in a tall one; the deck scrolls on
its axis with snap points and the arrows page it card by card. */
.grid { --deck-inset: max(clamp(1rem, 5vw, 4rem), calc((100vw - 90rem) / 2)); display: flex; gap: 1.6rem; width: 100%; overflow-x: auto; overflow-y: hidden; scroll-snap-type: x mandatory; scrollbar-width: none; padding: 6px var(--deck-inset); scroll-padding-inline: var(--deck-inset); align-items: stretch; }
.grid::-webkit-scrollbar { display: none; }
.grid > .card { flex: 0 0 clamp(20rem, 27vw, 27rem); scroll-snap-align: center; }
.nav { position: absolute; z-index: 6; width: 42px; height: 42px; display: flex; align-items: center; justify-content: center; border-radius: 50%; background: oklch(7% 0.006 95 / 0.78); border: 1px solid var(--ks-rule); color: var(--ks-kinpaku); cursor: pointer; backdrop-filter: blur(6px); transition: border-color .2s, color .2s, opacity .2s; }
.nav:hover { border-color: var(--ks-kinpaku-deep); color: var(--ks-kinpaku-pale); }
.nav[disabled] { opacity: .25; cursor: default; }
.nav[hidden] { display: none; }
.nav svg { width: 16px; height: 16px; }
.nav.prev { left: 14px; top: 50%; transform: translateY(-50%); }
.nav.next { right: 14px; top: 50%; transform: translateY(-50%); }
/* A side that hides more cards fades out; a hard edge means the end. */
.fade { position: absolute; z-index: 5; pointer-events: none; opacity: 0; transition: opacity .3s ease; }
.fade-prev { left: 0; top: 0; bottom: 0; width: 88px; background: linear-gradient(90deg, var(--ks-lacquer), transparent); }
.fade-next { right: 0; top: 0; bottom: 0; width: 88px; background: linear-gradient(270deg, var(--ks-lacquer), transparent); }
.deck-shell.can-prev .fade-prev { opacity: 1; }
.deck-shell.can-next .fade-next { opacity: 1; }
@media (max-aspect-ratio: 1/1) {
.grid { flex-direction: column; overflow-x: hidden; overflow-y: auto; scroll-snap-type: y mandatory; max-height: min(68dvh, 44rem); scroll-padding-block: 6px; }
.grid > .card { flex: 0 0 auto; }
/* In the vertical deck the pager is the primary way forward, so it grows
into a labeled pill instead of a bare chevron nobody notices. */
.nav { width: auto; height: 38px; border-radius: 19px; padding: 0 16px; gap: 8px; border-color: var(--ks-kinpaku-deep); background: oklch(7% 0.006 95 / 0.88); font-family: var(--ks-mono); font-size: .62rem; letter-spacing: .2em; text-transform: uppercase; }
.nav svg { transform: rotate(90deg); }
.nav.prev::after { content: "Back"; }
.nav.next::after { content: "More"; }
.nav.prev { left: 50%; top: 6px; transform: translate(-50%, 0); }
.nav.next { right: auto; left: 50%; top: auto; bottom: 6px; transform: translate(-50%, 0); }
.fade-prev { top: 0; left: 0; right: 0; bottom: auto; width: auto; height: 72px; background: linear-gradient(180deg, var(--ks-lacquer), transparent); }
.fade-next { top: auto; left: 0; right: 0; bottom: 0; width: auto; height: 72px; background: linear-gradient(0deg, var(--ks-lacquer), transparent); }
}
.card { position: relative; perspective: 1400px; transform: rotate(var(--fan, 0deg)); transition: transform .25s cubic-bezier(.16, 1, .3, 1); }
.card:hover { transform: rotate(0deg) translateY(-4px); }
.card-inner { position: relative; height: 100%; transform-style: preserve-3d; transition: transform .7s cubic-bezier(.16, 1, .3, 1); }
@@ -344,6 +476,12 @@ function page() {
.face { background: var(--ks-lacquer-raised); border: 1px solid var(--ks-rule); border-radius: 10px; box-shadow: 0 18px 40px oklch(0% 0 0 / 0.35); overflow: hidden; display: flex; flex-direction: column; backface-visibility: hidden; -webkit-backface-visibility: hidden; }
.face.front { position: relative; height: 100%; }
.face.back { position: absolute; inset: 0; transform: rotateY(180deg); }
/* Only the visible face is interactive: a hidden backface still hit-tests
in Chrome, so the front's pip would otherwise sit invisibly over the
back's chips, showing its zoom cursor and eating the flip-back click. */
.face.back { pointer-events: none; }
.card.flipped .face.back { pointer-events: auto; }
.card.flipped .face.front { pointer-events: none; }
.face.lead { border-color: var(--ks-kinpaku); box-shadow: 0 0 0 1px var(--ks-kinpaku), 0 18px 40px oklch(0% 0 0 / 0.45); }
.card:hover .face { border-color: var(--ks-kinpaku-deep); }
.card:hover .face.lead { border-color: var(--ks-kinpaku); }
@@ -355,7 +493,7 @@ function page() {
.face.text-only .body { padding-top: 12px; }
.media { position: relative; width: 100%; aspect-ratio: 16/9; flex: none; }
.media img { width: 100%; height: 100%; object-fit: cover; display: block; background: linear-gradient(100deg, var(--ks-graphite) 40%, var(--ks-graphite-2) 50%, var(--ks-graphite) 60%); }
.face.back { background: var(--ks-lacquer-deep); }
.face.back { background: var(--ks-lacquer-raised); }
.back-bar { margin-top: auto; background: var(--ks-lacquer-raised); }
.hero-blank { width: 100%; height: 100%; background: linear-gradient(100deg, var(--ks-graphite) 40%, var(--ks-graphite-2) 50%, var(--ks-graphite) 60%); }
.back-bar { flex: none; flex-direction: row; align-items: center; justify-content: space-between; gap: .8rem; }
@@ -367,6 +505,48 @@ function page() {
.tier { font-family: var(--ks-mono); font-size: .625rem; letter-spacing: .24em; text-transform: uppercase; color: var(--ks-text-faint); }
h2 { font-family: var(--ks-font); font-size: 1.125rem; font-weight: 500; line-height: 1.35; color: var(--ks-champagne); }
.detail { color: var(--ks-text-muted); font-size: .88rem; white-space: pre-wrap; }
.detail.more { font-size: .8rem; color: var(--ks-text-faint); }
.thesis { color: var(--ks-text); font-size: .95rem; line-height: 1.45; }
.identity { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin: 2px 0; }
.swatches { display: inline-flex; gap: 4px; margin-right: 4px; }
.swatches i { width: 18px; height: 18px; border-radius: 5px; border: 1px solid oklch(100% 0 0 / 0.18); box-shadow: inset 0 0 0 1px oklch(0% 0 0 / 0.25); }
.tag { font-family: var(--ks-mono); font-size: .6rem; letter-spacing: .14em; text-transform: uppercase; color: var(--ks-text-muted); border: 1px solid var(--ks-rule); border-radius: 4px; padding: 3px 7px; }
.fact { font-size: .8rem; color: var(--ks-text-muted); line-height: 1.45; }
.fact-label { display: inline-block; font-family: var(--ks-mono); font-size: .6rem; letter-spacing: .18em; text-transform: uppercase; color: var(--ks-text-faint); margin-right: .55em; transform: translateY(-1px); }
.fact.clamp { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
/* The back is the full read: first viewport, the case, the whole risk, and
the board when the world has one. */
.back-head { display: flex; align-items: center; justify-content: space-between; gap: .8rem; padding: 14px 14px 0; }
.media.back-media { aspect-ratio: 16/6; }
.media.back-media img { width: 100%; height: 100%; object-fit: cover; }
.body.back-body { overflow-y: auto; flex: 1; scrollbar-width: thin; }
/* Inspiration rides picture-in-picture: the catalog world explains where the
direction comes from without promising what the build will look like. */
/* Hovering the inspiration takes over the whole media region; the sketch is
the promise, the inspiration is a glance, so the glance must cost nothing. */
.pip { position: absolute; z-index: 2; left: 10px; bottom: 10px; margin: 0; width: 84px; height: 64px; border: 1px solid var(--ks-rule); border-radius: 6px; overflow: hidden; background: var(--ks-lacquer); cursor: zoom-in; transition: left .35s cubic-bezier(.16,1,.3,1), bottom .35s cubic-bezier(.16,1,.3,1), width .35s cubic-bezier(.16,1,.3,1), height .35s cubic-bezier(.16,1,.3,1), border-radius .35s ease; box-shadow: 0 6px 18px oklch(0% 0 0 / 0.45); }
.pip img { display: block; width: 100%; height: 100%; object-fit: cover; }
.pip figcaption { position: absolute; left: 0; right: 0; bottom: 0; font-family: var(--ks-mono); font-size: .5rem; letter-spacing: .2em; text-transform: uppercase; color: var(--ks-text); text-align: center; padding: 3px 0 4px; background: oklch(7% 0.006 95 / 0.72); backdrop-filter: blur(3px); }
.pip:hover { left: 0; bottom: 0; width: 100%; height: 100%; border-radius: 0; z-index: 3; }
.sketch-note { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-family: var(--ks-mono); font-size: .66rem; letter-spacing: .22em; text-transform: uppercase; color: var(--ks-text-faint); }
/* A stand-in is honest about being one: dimmed, labeled, and replaced by
the real sketch whenever it lands. */
.media.stand-in img.sketch { filter: brightness(.72) saturate(.85); }
.media.stand-in .pip { display: none; }
.stand-in-label { position: absolute; z-index: 2; left: 0; right: 0; bottom: 0; margin: 0; font-family: var(--ks-mono); font-size: .56rem; letter-spacing: .2em; text-transform: uppercase; color: var(--ks-text); text-align: center; padding: 4px 0 5px; background: oklch(7% 0.006 95 / 0.78); backdrop-filter: blur(3px); }
.media.sketching { position: relative; }
.media.sketching .shimmer { position: absolute; inset: 0; }
.media img.sketch { position: relative; z-index: 1; }
/* The generic .media img display:block would defeat [hidden] and float an
empty block over the shimmer; an unloaded sketch must truly not render. */
.media img[hidden] { display: none; }
/* The standing exit as a card: present with full anatomy, never dressed as a
contender. Graphite instead of kinpaku, and it never takes the lead ring. */
.card.canon .face { border-color: var(--ks-rule); background: var(--ks-graphite); }
.card.canon:hover .face { border-color: var(--ks-text-faint); }
.card.canon .kicker.standing { background: transparent; border: 1px solid var(--ks-rule); color: var(--ks-text-faint); }
.card.canon button.choose { background: transparent; color: var(--ks-text); border: 1px solid var(--ks-rule); }
.card.canon button.choose:hover { border-color: var(--ks-text-muted); background: var(--ks-graphite-2); }
button.choose { margin-top: auto; align-self: start; background: var(--ks-kinpaku); color: var(--ks-dark-ink); border: 0; font-family: var(--ks-font); font-size: 1rem; font-weight: 500; line-height: 1.35; padding: 10px 38px; border-radius: 6px; cursor: pointer; transition: background .15s; }
button.choose:hover { background: var(--ks-kinpaku-pale); }
footer { width: 100%; max-width: 90rem; margin: 1.6rem auto 0; display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; }
@@ -408,13 +588,19 @@ function page() {
<h1>${esc(payload.title || 'Choose a direction')}</h1>
</div>
${payload.question ? `<p class="question">${esc(payload.question)}</p>` : ''}
<div class="grid">${cards}</div>
<div class="deck-shell">
<div class="grid">${cards}</div>
<div class="fade fade-prev" aria-hidden="true"></div>
<div class="fade fade-next" aria-hidden="true"></div>
<button class="nav prev" hidden aria-label="Previous card"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14.5 5 8 12l6.5 7" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
<button class="nav next" hidden aria-label="Next card"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9.5 5 16 12l-6.5 7" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
</div>
</div>
</main>
<footer>
${payload.steer ? '<input id="steer" placeholder="Optional steer: what should be different or kept?">' : ''}
${payload.reroll ? '<button id="reroll"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="4" fill="none" stroke="currentColor" stroke-width="1.6"/><circle cx="8.4" cy="8.4" r="1.5" fill="currentColor"/><circle cx="15.6" cy="8.4" r="1.5" fill="currentColor"/><circle cx="8.4" cy="15.6" r="1.5" fill="currentColor"/><circle cx="15.6" cy="15.6" r="1.5" fill="currentColor"/><circle cx="12" cy="12" r="1.5" fill="currentColor"/></svg><span>Re-roll</span></button>' : ''}
${payload.canon ? '<button id="canon" title="Skip the roll: build the page this category ships, executed impeccably">Play it straight</button>' : ''}
${payload.canon && !payload.canonCard ? '<button id="canon" title="Skip the roll: build the page this category ships, executed impeccably">Play it straight</button>' : ''}
</footer>
<script>
const steer = () => document.getElementById('steer')?.value || '';
@@ -434,7 +620,11 @@ function page() {
// Deal from the stack: cards begin piled at the grid's center, blurred,
// then travel to their seats with a stagger.
const cards = [...document.querySelectorAll('.card')];
if (!matchMedia('(prefers-reduced-motion: reduce)').matches && cards.length) {
// The deal is decoration: a hidden tab throttles rAF, so never let the
// animation hold the cards at opacity 0. Skip it when hidden, and force
// the final state after a beat no matter what the animation did.
setTimeout(() => cards.forEach(c => { c.style.opacity = ''; c.style.transform = ''; c.style.filter = ''; c.style.transition = ''; c.style.zIndex = ''; }), 1600);
if (!matchMedia('(prefers-reduced-motion: reduce)').matches && cards.length && !document.hidden) {
const grid = document.querySelector('.grid').getBoundingClientRect();
const cx = grid.left + grid.width / 2, cy = grid.top + grid.height / 2;
cards.forEach((card, i) => {
@@ -460,12 +650,99 @@ function page() {
}));
}
// Ambient: the hovered card's hero bleeds into the page ground under a scrim.
// Sketches stream in after the deal: poll each slot until the file lands,
// then swap the shimmer for the image. Generation is genuinely slow and a
// sequential batch puts the last card many minutes out, so patience is the
// default: a slot only shows its inspiration as a stand-in when it has
// waited four minutes AND nothing has landed anywhere for four minutes, the
// stand-in is labeled as such, and polling continues so the real sketch
// still swaps in whenever it arrives. Progress anywhere resets patience.
const landTracker = { last: Date.now() };
document.querySelectorAll('.media.sketching').forEach(m => {
const url = m.dataset.sketch;
const img = m.querySelector('img.sketch');
const note = m.querySelector('.sketch-note');
const started = Date.now();
// A live elapsed count is the difference between "working" and "frozen".
const tick = setInterval(() => { if (note) note.textContent = 'sketching · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000);
const settle = () => { clearInterval(tick); m.classList.remove('sketching', 'stand-in'); m.querySelector('.shimmer')?.remove(); m.querySelector('.stand-in-label')?.remove(); };
const standIn = () => {
const pip = m.querySelector('.pip img');
if (!pip || m.classList.contains('stand-in')) return;
img.src = pip.getAttribute('src'); img.hidden = false;
m.classList.add('stand-in');
m.querySelector('.shimmer')?.remove();
clearInterval(tick);
const label = document.createElement('p');
label.className = 'stand-in-label';
label.textContent = 'inspiration · sketch pending';
m.appendChild(label);
};
const tryLoad = () => {
const probe = new Image();
probe.onload = () => { landTracker.last = Date.now(); img.src = probe.src; img.hidden = false; settle(); };
probe.onerror = () => {
const quiet = Date.now() - landTracker.last > 240000;
if (Date.now() - started > 240000 && quiet) standIn();
setTimeout(tryLoad, m.classList.contains('stand-in') ? 5000 : 2500);
};
probe.src = url + (url.includes('?') ? '&' : '?') + 't=' + Date.now();
};
tryLoad();
});
// Inspiration PIP opens the full catalog card in the lightbox.
document.querySelectorAll('.pip').forEach(p => p.addEventListener('click', (e) => {
e.stopPropagation();
const img = p.querySelector('img');
if (!img) return;
lightboxImg.src = img.getAttribute('src');
lightbox.hidden = false;
requestAnimationFrame(() => lightbox.classList.add('open'));
}));
// Deck paging: arrows appear only when the deck overflows its axis, page
// one card at a time, and follow the aspect-ratio flip between row and column.
const deck = document.querySelector('.grid');
const prevBtn = document.querySelector('.nav.prev');
const nextBtn = document.querySelector('.nav.next');
const vertical = () => matchMedia('(max-aspect-ratio: 1/1)').matches;
function updateNav() {
if (!deck || !prevBtn) return;
const shell = deck.closest('.deck-shell');
const v = vertical();
const overflow = v ? deck.scrollHeight > deck.clientHeight + 4 : deck.scrollWidth > deck.clientWidth + 4;
prevBtn.hidden = nextBtn.hidden = !overflow;
const pos = v ? deck.scrollTop : deck.scrollLeft;
const max = v ? deck.scrollHeight - deck.clientHeight : deck.scrollWidth - deck.clientWidth;
const canPrev = overflow && pos > 2;
const canNext = overflow && pos < max - 2;
prevBtn.toggleAttribute('disabled', !canPrev);
nextBtn.toggleAttribute('disabled', !canNext);
shell?.classList.toggle('can-prev', canPrev);
shell?.classList.toggle('can-next', canNext);
}
function pageDeck(dir) {
const card = deck.querySelector('.card');
if (!card) return;
const r = card.getBoundingClientRect();
const step = (vertical() ? r.height : r.width) + 26;
deck.scrollBy(vertical() ? { top: dir * step, behavior: 'smooth' } : { left: dir * step, behavior: 'smooth' });
}
prevBtn?.addEventListener('click', () => pageDeck(-1));
nextBtn?.addEventListener('click', () => pageDeck(1));
deck?.addEventListener('scroll', updateNav, { passive: true });
addEventListener('resize', updateNav);
updateNav();
// Ambient: the hovered card's visible art bleeds into the page ground.
const ambient = document.getElementById('ambient');
document.querySelectorAll('.card').forEach(card => {
const hero = card.querySelector('.face.front .media img');
if (!hero) return;
card.addEventListener('mouseenter', () => { ambient.style.backgroundImage = 'url("' + hero.getAttribute('src') + '")'; ambient.style.opacity = '1'; });
card.addEventListener('mouseenter', () => {
const art = card.querySelector('.face.front .media img:not([hidden])') || card.querySelector('.face.front .pip img');
if (!art || !art.getAttribute('src')) return;
ambient.style.backgroundImage = 'url("' + art.getAttribute('src') + '")'; ambient.style.opacity = '1';
});
card.addEventListener('mouseleave', () => { ambient.style.opacity = '0'; });
});
@@ -476,8 +753,8 @@ function page() {
e.stopPropagation();
const card = b.closest('.card');
const face = card.classList.contains('flipped') ? '.face.back' : '.face.front';
const img = card.querySelector(face + ' .media img');
if (!img) return;
const img = card.querySelector(face + ' .media img:not([hidden])');
if (!img || !img.getAttribute('src')) return;
lightboxImg.src = img.getAttribute('src');
lightbox.hidden = false;
requestAnimationFrame(() => lightbox.classList.add('open'));
@@ -546,10 +823,10 @@ const server = http.createServer((req, res) => {
res.end(JSON.stringify({ ready: Boolean(pending && fs.existsSync(pending)) }));
return;
}
const imageMatch = req.method === 'GET' && req.url?.match(/^\/img\/(\d+)$/);
const imageMatch = req.method === 'GET' && req.url?.match(/^\/img\/(\d+)(?:\?.*)?$/);
if (imageMatch) {
const abs = localImages[Number(imageMatch[1])];
if (!abs) { res.writeHead(404); res.end(); return; }
if (!abs || !fs.existsSync(abs)) { res.writeHead(404); res.end(); return; }
const type = abs.endsWith('.webp') ? 'image/webp'
: abs.endsWith('.png') ? 'image/png'
: abs.endsWith('.svg') ? 'image/svg+xml'
@@ -572,6 +849,7 @@ const server = http.createServer((req, res) => {
optionId: parsed.optionId ?? null,
steer: parsed.steer ?? '',
...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}),
...(chosen?.sketch ? { sketch: chosen.sketch } : {}),
});
const isReroll = parsed.optionId === 'reroll';
if (detachedKey) {
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 4.0.2
version: 4.0.3
---
This skill gives you the tools and permission to create design that earns to be called out-of-distribution craft: Whereas before, your design work would have been safe, timid and measured, you now approach every design task as a award-winning design director with impeccable understanding for what makes exceptional design work: production-grade code, peak creativity, a clear POV, deep understanding of the needs of the client and users, and exceptional craft.
@@ -23,7 +23,7 @@ Page scaffolds:
- Same-size cards of icon plus heading plus text as the page structure. Cards are the lazy container; nested cards are always wrong.
- The hero-metric template: big number, small label, supporting stats, accent.
- A tracked uppercase eyebrow over every section. One named kicker is a system; an eyebrow everywhere is grammar you did not choose.
- A kicker or eyebrow above a heading. This one is a ban, not a default: no brief earns it back. The heading carries its own weight; delete the label and let the heading speak.
- Section numbers (01 / 02 / 03) unless the sequence itself carries information the reader needs.
- A modal for a task that needs neither interruption nor protected focus.
@@ -11,6 +11,10 @@ Your job is production cleanup, not new art direction. Work only from the approv
Do not redesign. Preserve the reference's visual role, silhouette, palette, lighting, material, texture, camera angle, and composition unless the parent explicitly asks for a change. Preserve perspective only when it belongs to the object or scene itself; if CSS should create the card transform, shadow, rounded clipping, border, or layout, remove that presentation chrome from the raster.
## Decision Sketches
When the parent hands you a decision card packet instead of an approved mock, the job is one sketch: one card, one file, written to the card's declared `sketch` path the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a sketch is reported back, not padded from imagination. Render through the parent's shared frame: the requested surface's first viewport as a flat, matte design sketch in the card's own palette and type character, deliberately unfinished, no photorealism, no gloss. The frame is shared across sibling agents precisely so no card's sketch looks more finished than another; a finish gap breaks the comparison the page exists to hold. The only legible text is the product's real name and one real headline; greek every other text region into indistinct lines, because an invented spec, price, or date in a sketch is a claim PRODUCT.md never made. Return one line naming the path and any deviation, nothing more.
## Input Contract
Expect:
@@ -5,6 +5,8 @@ This harness has no subagent capability, so you are running this role inline. St
You record a project's design system after the build is done. Ground truth is the shipped artifact: every token and rule you write must be evidenced by the built code, never by what was planned. Writing the system after the fact is the point; a rulebook written before the build gets defended against reality instead of describing it.
You run under a hard turn ceiling that ends the run without warning, and a run that ends before DESIGN.md is written has recorded nothing. Batch several Reads into each turn, take `reference/document.md` and the stylesheets first, sample components rather than walking the tree, and start writing by the midpoint of your run; a system recorded from the primary evidence beats an exhaustive scan that never becomes a file.
## Input Contract
Expect: the project root; the artifact path(s); the direction contract text (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; the path to the skill's `reference/document.md`; and the boundary to write at (project or app root). An existing DESIGN.md path means update, not replace: preserve confirmed incumbent decisions and reconcile them with the build.
@@ -7,20 +7,26 @@ You are the finishing reviewer for an Impeccable build: fresh eyes on a done art
You have no browser. Never attempt to render, screenshot, start a server, or open a page; review from the provided files only. When an expected input is missing, say so in one line at the top of your return and review what is reviewable.
You run under a hard turn ceiling that ends the run without warning, and a run that ends before the five sections are written returns nothing; a review built from what you saw beats a perfect review that never arrives. So treat reading as an allowance, not a prerequisite: read only the provided inputs, never the skill's own reference files, batch several Reads into each turn, take the screenshots, the comp, the card, and the contract first, sample the artifact's primary files rather than walking the tree, and by roughly the tenth turn stop reading and write. Name whatever went unread in the line above the sections.
## Input Contract
Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path. When the harness can view images, open the screenshots, the comp, and the card before judging.
Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: the contract is the builder's abstraction of the comp, and a review anchored on it inherits whatever that abstraction dropped.
## Checks, in order
1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding.
2. **Fidelity.** Against the approved comp: topology, reading order, focal scale, overlaps and z-order, density, signature geometry. Name every drift and classify it intentional or defect. The comp governs composition; it is not a pixel spec for semantics, accessibility, or responsive behavior.
2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement.
3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. The card governs commitment and finish, never composition.
4. **Contract, promise by promise.** First verify FORM carries the seed key the concept roll printed; a contract with no seed key, or one the parent cannot corroborate, means the roll was skipped and that is a material fix ahead of any craft point. Then, for each of the five blocks, does the render keep the promise? Apply the memory test to the first viewport.
5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one.
5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one, and every produced asset visibly present in the screenshots; an asset applied at near-zero opacity or buried behind other paint is a compliance token, not a shipped material.
Do not run a second detector pass; mechanical findings belong to the parent's hooks.
## Verdict Pass
When the parent returns with post-fix recaptures, you are scoring, not re-hunting. For each material fix from your review, one line: resolved, partial, or unresolved, tied to what the new screenshots visibly show; a fix answered mechanically, positions moved but the quality the finding named still absent, is partial at best. Then name at most three regressions the fix batch itself introduced, judged by the same matrix rules, and nothing else; no new hunt, no new checks. Return exactly two sections: `verdict` (the scored list) and `remaining` (what stays open, or "clear").
## Output Contract
Return exactly five sections: `persistence` (pass/fail with specifics), `fidelity` (drift list with intentional/defect per item, or "faithful"), `ceiling` (unused native devices, or "reached"), `material_fixes` (ordered, most material first, each one line tied to a check or contract promise, at most eight), and `keep` (one line naming what must not be diluted while fixing). Missing inputs are named in one line above the sections. No praise, no summary prose.
Return exactly five sections: `persistence` (pass/fail with specifics), `fidelity` (the element matrix: match, adaptation, missing, contradicted, or added without approval per salient element, adaptations citing their evidence, or "faithful"), `ceiling` (unused native devices, or "reached"), `material_fixes` (ordered, most material first, fidelity failures ahead of craft, each one line tied to a check or contract promise, at most eight), and `keep` (one line naming what must not be diluted while fixing). Missing inputs are named in one line above the sections. No praise, no summary prose.
@@ -44,9 +44,11 @@ The script assigns which structure gets built: your top-ranked structure is what
2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, the notation, publications, identity programs, data graphics, and interfaces it reads daily, not only its physical objects; a nameable abstract system (a school of poster, a documentation standard, a data-graphic tradition) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; the audience's world is larger than that, so dig until the list spans at least three families.
3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience.
4. Run `node .gemini/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode <mode>` and follow what it prints. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point.
5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the one or two fused challengers that survived the weighing as named alternates with a one-line case each, plus re-roll with an optional one-line steer. What you never present is a ranked menu of your own grounded candidates; a lineup of those invites the safest card, while dealt challengers carry no such rut. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option.
5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. What you never present is a ranked menu of your own grounded candidates; a lineup of those invites the safest card, while dealt challengers carry no such rut. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option.
The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Run `node .gemini/skills/impeccable/scripts/serve-question.mjs --start --payload <file>` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key <key>`, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry.
The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, so a grounded direction is never a wall of text against rendered art, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you, so it is never yours to recommend. Run `node .gemini/skills/impeccable/scripts/serve-question.mjs --start --payload <file>` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key <key>`, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry.
When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land, so generation never delays the question. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once, so the whole set lands in roughly the time of one. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version.
Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images for exactly this reason (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product.
@@ -98,6 +100,6 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi
Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary.
After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, and the QUALITY BAR card and approved comp paths. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. Apply the material fixes in one batch, rebuild once, and stop; the reviewer ran so that you do not re-open your own hunt. Do not run a second detector.
After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, and the QUALITY BAR card and approved comp paths. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. Apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get exactly one more batch, recapture, and verdict; two correction rounds is the ceiling, the second verdict ends the work whatever it says, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included: presenting mechanical confirmation as artistic success is how a failed build gets announced as a finished one. Do not run a second detector.
Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded.
@@ -6,7 +6,7 @@ The purpose of a probe is to test composition, narrative, hierarchy, density, fo
## Generate three compositional options
Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session; record the approved comp's path in the surface brief. Base them on the real content and the surface concepts already developed with the user. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building.
Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session; record the approved comp's path in the surface brief. Base them on the real content and the surface concepts already developed with the user. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. A decision-page sketch is not a probe: it chose the direction at deliberately unfinished fidelity, so the three comps render regardless, and the chosen card's sketch seeds at most one of them.
- When the user shortlisted multiple concepts, spread the three across them.
- When one direction is committed, vary the structural uncertainty an image can resolve: topology, sequence, density, hierarchy, focal composition, or interaction framing.
@@ -25,11 +25,11 @@ After approval, summarize the composition and the parts of the comp that must no
## Inventory implementation fidelity
Before building, inventory the approved comp's major visible ingredients in writing (a short table in the surface brief or working notes; the finish reviewer audits shipped assets against it) and choose an implementation medium for each: semantic HTML/CSS/SVG, existing project asset, generated raster, sourced raster, icon library, canvas/WebGL, or accepted omission. Textures, portraits, and scenes are raster-by-default; a CSS gradient is not a texture medium. Every `produce` entry is produced before the build ships, through the asset producer or in the current thread; an inventory with unproduced entries is an unfinished build, and this gate is where imagery-free pages come from when it is skipped.
Before building, inventory the approved comp's major visible ingredients in writing (a short table in the surface brief or working notes; the finish reviewer audits shipped assets against it) and choose an implementation medium for each: semantic HTML/CSS/SVG, existing project asset, generated raster, sourced raster, icon library, canvas/WebGL, or accepted omission. The same written inventory names the comp's compositional commitments: navigation items and icons, headline levels and their scale relationship, signature geometry such as seams, masks, and overlaps, and each section's arrangement and density. An element never written down is the element the build silently drops, and the direction contract's 150 words cannot carry this list, so this inventory is where it lives. Textures, portraits, and scenes are raster-by-default; a CSS gradient is not a texture medium. Every `produce` entry is produced before the build ships, through the asset producer or in the current thread; an inventory with unproduced entries is an unfinished build, and this gate is where imagery-free pages come from when it is skipped.
Pay special attention to the dominant composition, signature use, image-native content, second-fold system, and any interaction the still image only implies. If the concept depends on a photograph, architectural scene, product object, portrait, or other raster-native material, do not silently replace it with generic CSS scenery.
Treat the comp as a north star, not something to trace. Do not rasterize core UI text or controls. Do not substitute a different visual driver after approval without asking.
Treat the comp as a north star, not something to trace, and know what that allows: translation into semantic, responsive, accessible code, never recomposition. Keeping the palette and mood while redrawing the topology is a second art direction, not an adaptation. Do not rasterize core UI text or controls. Do not substitute a different visual driver after approval without asking.
## Produce only the assets the build needs
@@ -206,7 +206,13 @@ ${grammar}
// returns no staging. Re-rolls exclude every earlier set until the pool runs out.
export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) {
const pool = sourceCompositions ?? requireLocalConcepts().compositions;
// Stagings honour the same breadth gate as worlds: a staging too specific to
// serve an arbitrary build stays approved for direct briefs and leaves the
// challenger pool. Falls back to the full approved set rather than returning
// nothing if every approved staging is marked niche.
let approved = pool.filter(composition => composition.status === 'approved');
const broad = approved.filter(composition => composition.review?.breadth !== 'niche');
if (broad.length > 0) approved = broad;
if (approved.length === 0) return [];
if (mode) {
const matching = approved.filter(composition => composition.surface === mode);
@@ -271,12 +277,19 @@ export function selectApprovedChallengers({ scope, key, reroll = 0, sourceConcep
// graphic systems beside instrument languages and atmosphere worlds, with
// the second pick preferring a different family for diversity. Tier order
// in the rendered list is rolled too, to avoid positional bias.
// Approval ratings weight the draw: a 3-star world earns a second ticket
// (roughly double odds), a 1-star keeps its approval for direct briefs but
// leaves the challenger pool unless a tier has nothing else.
// Two separate axes, and both can exclude. Rating grades quality: a 3-star
// earns a second ticket, a 1-star marginal keep leaves the pool. Breadth says
// whether a world can serve an arbitrary build at all, so a niche world
// leaves the pool however good it is. Breadth was split out of rating because
// the only way to hold a narrow world back used to be calling it marginal,
// which made "excellent but narrow" unrecordable and corrupted the ratings as
// a calibration signal for the next authoring round.
const ticketsFor = pool => pool.flatMap(concept => {
const rating = concept.review?.rating;
if (rating === 1) return [];
// Two independent exclusions: a marginal world is too weak to challenge,
// a niche world too narrow. Either one keeps its approval for direct
// briefs and leaves the pool.
if (rating === 1 || concept.review?.breadth === 'niche') return [];
return rating === 3
? [{ concept, ticket: 0 }, { concept, ticket: 1 }]
: [{ concept, ticket: 0 }];
@@ -1529,7 +1529,7 @@ if (IS_BROWSER) {
addBrowserFindings(groupMap, document.body, typoFindings);
}
const sectionKickerFindings = checkRepeatedSectionKickersDOM()
const sectionKickerFindings = checkKickerAboveHeadingDOM()
.map(f => ({ type: f.id, detail: f.snippet }))
.filter(f => _ruleOk(f.type));
if (sectionKickerFindings.length > 0) {
@@ -309,15 +309,14 @@ const ANTIPATTERNS = [
skillGuideline: 'tiny uppercase tracked label above the hero headline',
},
{
id: 'repeated-section-kickers',
id: 'kicker-above-heading',
category: 'slop',
scopes: ['type'],
severity: 'advisory',
name: 'Repeated section kicker labels',
name: 'Kicker / eyebrow label above heading',
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.',
'A tiny tracked uppercase or small-caps label sitting as its own block directly above a heading is banned outright, repeated or not. Generated kickers never earn their place: the heading carries its own weight. Delete the label and let the heading speak; if the words matter, work them into the heading or the body.',
skillSection: 'Typography',
skillGuideline: 'repeated eyebrow or kicker labels as section scaffolding',
skillGuideline: 'kicker or eyebrow labels above headings',
},
{
id: 'numbered-section-labels',
@@ -1258,12 +1257,15 @@ function checkHeroEyebrow(opts) {
}];
}
function checkRepeatedSectionKickers(opts) {
const { candidates, minCount = 3 } = opts;
if (!Array.isArray(candidates) || candidates.length < minCount) return [];
// Outright ban: one kicker is one too many, so every collected candidate is
// a finding. The judgment lives in the candidate gate (isKickerCandidate) and
// the collector's context skips, not in a repetition count.
function checkKickerAboveHeading(opts) {
const { candidates } = opts;
if (!Array.isArray(candidates)) return [];
return candidates.map(candidate => ({
id: 'repeated-section-kickers',
snippet: `repeated section kicker "${candidate.kickerText}" before ${candidate.headingTag} "${candidate.headingText}" (${candidates.length} on page)`,
id: 'kicker-above-heading',
snippet: `kicker "${candidate.kickerText}" above ${candidate.headingTag} "${candidate.headingText}"`,
}));
}
@@ -3190,7 +3192,7 @@ function parseColorResolved(str, customPropMap) {
return parseAnyColor(resolved);
}
const REPEATED_KICKER_SKIP_SELECTOR = [
const KICKER_SKIP_SELECTOR = [
'nav',
'form',
'table',
@@ -3209,7 +3211,7 @@ const REPEATED_KICKER_SKIP_SELECTOR = [
'[data-impeccable-allow-kickers]',
].join(',');
const REPEATED_KICKER_CARD_CONTEXT_SELECTOR = [
const KICKER_CARD_CONTEXT_SELECTOR = [
'article',
'button',
'a',
@@ -3227,23 +3229,32 @@ function cleanInlineText(el) {
.trim();
}
function isRepeatedKickerCardContext(heading, kicker) {
const item = heading.closest?.(REPEATED_KICKER_CARD_CONTEXT_SELECTOR);
function isKickerCardContext(heading, kicker) {
const item = heading.closest?.(KICKER_CARD_CONTEXT_SELECTOR);
return Boolean(item && (!item.contains || item.contains(kicker)));
}
function isRepeatedKickerCandidate(opts) {
// Meta lines above headlines join category and date (or path crumbs) with
// separator glyphs, or carry a year. A kicker is one short phrase; metadata
// keeps its markers.
const KICKER_META_TEXT_RE = /[·•|]|\s[\/›»>]\s|\b(19|20)\d{2}\b/;
// Legal and document numbering: "Section 4.2", "Article IX", "§ 12.3",
// dotted decimal outlines. The label identifies the clause, so it stays.
const KICKER_DOC_NUMBERING_RE = /^(§|\d+(\.\d+)+\b|(section|article|clause|appendix|exhibit|schedule|chapter|part|rule|title)\s+([\divxlc]+\b|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)\b)/i;
function isKickerCandidate(opts) {
const {
headingTag,
headingLevel,
headingText,
headingFontSize,
kickerTag,
kickerText,
kickerTextTransform,
kickerFontVariant,
kickerFontSize,
kickerLetterSpacing,
} = opts;
if (!['h2', 'h3', 'h4'].includes(headingTag)) return false;
if (!headingLevel || headingLevel > 4) return false;
if (!headingText || headingText.length < 3) return false;
if (/^\/[\w-]+/i.test(headingText.replace(/^"|"$/g, '').trim())) return false;
if (!(headingFontSize >= 20)) return false;
@@ -3251,9 +3262,13 @@ function isRepeatedKickerCandidate(opts) {
if (!['p', 'span', 'div', 'small'].includes(kickerTag)) return false;
if (!kickerText || kickerText.length < 2 || kickerText.length > 34) return false;
if (/^step\s*\d+/i.test(kickerText) || /^\d{1,2}$/.test(kickerText)) return false;
if (KICKER_META_TEXT_RE.test(kickerText)) return false;
if (KICKER_DOC_NUMBERING_RE.test(kickerText)) return false;
const isSmallCaps = /small-caps/.test(kickerFontVariant || '');
const isUppercased = kickerTextTransform === 'uppercase'
|| (/[A-Z]/.test(kickerText) && !/[a-z]/.test(kickerText));
|| (/[A-Z]/.test(kickerText) && !/[a-z]/.test(kickerText))
|| isSmallCaps;
if (!isUppercased) return false;
if (!(kickerFontSize > 0 && kickerFontSize <= 14)) return false;
const minTrackedSpacing = Math.max(1, kickerFontSize * 0.08);
@@ -3261,37 +3276,64 @@ function isRepeatedKickerCandidate(opts) {
return true;
}
function collectRepeatedSectionKickerCandidates(doc, getStyle, resolveLetterSpacing) {
// Resolve a heading level for the anchor element: 1-4 for h1-h4, aria-level
// (default 2) for role="heading" elements, 0 otherwise.
function kickerHeadingLevel(heading) {
const tag = heading.tagName.toLowerCase();
const byTag = /^h([1-6])$/.exec(tag);
if (byTag) return parseInt(byTag[1], 10);
const role = heading.getAttribute?.('role') || '';
if (role.toLowerCase() !== 'heading') return 0;
const ariaLevel = parseInt(heading.getAttribute?.('aria-level') || '', 10);
return Number.isFinite(ariaLevel) && ariaLevel >= 1 ? ariaLevel : 2;
}
function collectKickerCandidates(doc, getStyle, resolveLetterSpacing) {
const candidates = [];
for (const heading of doc.querySelectorAll('h2, h3, h4')) {
if (heading.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
for (const heading of doc.querySelectorAll('h1, h2, h3, h4, [role="heading"]')) {
const headingLevel = kickerHeadingLevel(heading);
if (!headingLevel || headingLevel > 4) continue;
if (heading.closest?.(KICKER_SKIP_SELECTOR)) continue;
// Application contexts (tab panels, dialogs) use compact context labels
// above headings to describe state, not to decorate. Same carve-out the
// hero-eyebrow rule makes.
if (heading.closest?.('[role="tabpanel"], [role="dialog"], [role="application"], dialog')) continue;
const kicker = heading.previousElementSibling;
if (!kicker || kicker.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
if (isRepeatedKickerCardContext(heading, kicker)) continue;
if (!kicker || kicker.closest?.(KICKER_SKIP_SELECTOR)) continue;
if (isKickerCardContext(heading, kicker)) continue;
const headingStyle = getStyle(heading);
const kickerStyle = getStyle(kicker);
const headingTag = heading.tagName.toLowerCase();
const headingText = (heading.textContent || '').replace(/\s+/g, ' ').trim();
const kickerText = cleanInlineText(kicker) || (kicker.textContent || '').replace(/\s+/g, ' ').trim();
const headingFontSize = resolveLetterSpacing(headingStyle.fontSize || '', 16) || parseFloat(headingStyle.fontSize) || 0;
const kickerFontSize = resolveLetterSpacing(kickerStyle.fontSize || '', 16) || parseFloat(kickerStyle.fontSize) || 0;
const kickerLetterSpacing = resolveLetterSpacing(kickerStyle.letterSpacing || '', kickerFontSize);
if (!isRepeatedKickerCandidate({
headingTag: heading.tagName.toLowerCase(),
if (!isKickerCandidate({
headingLevel,
headingText,
headingFontSize,
kickerTag: kicker.tagName.toLowerCase(),
kickerText,
kickerTextTransform: kickerStyle.textTransform || '',
kickerFontVariant: `${kickerStyle.fontVariant || ''} ${kickerStyle.fontVariantCaps || ''}`,
kickerFontSize,
kickerLetterSpacing,
})) {
continue;
}
// A tracked-caps eyebrow above a hero-scale h1 belongs to
// hero-eyebrow-chip (which also covers the accent-bold and dash-prefix
// stylings there). Stand down so one element gets one finding.
if (headingTag === 'h1' && headingFontSize >= 48 && kickerLetterSpacing >= 1.6) {
continue;
}
candidates.push({
headingTag: heading.tagName.toLowerCase(),
headingTag,
headingText: headingText.replace(/^"|"$/g, '').slice(0, 60),
kickerText: kickerText.slice(0, 40),
});
@@ -3299,17 +3341,17 @@ function collectRepeatedSectionKickerCandidates(doc, getStyle, resolveLetterSpac
return candidates;
}
function checkRepeatedSectionKickersDOM() {
const candidates = collectRepeatedSectionKickerCandidates(
function checkKickerAboveHeadingDOM() {
const candidates = collectKickerCandidates(
document,
(el) => getComputedStyle(el),
(value, fontSize) => resolveLengthPx(value, fontSize) || 0,
);
return checkRepeatedSectionKickers({ candidates });
return checkKickerAboveHeading({ candidates });
}
// ── Numbered section labels ─────────────────────────────────────────────────
// Sibling of the repeated-kicker rule: instead of a tracked uppercase word,
// Sibling of the kicker-above-heading rule: instead of a tracked uppercase word,
// the section scaffold is a tiny numeric index riding beside each section
// heading — bare and zero-padded, or an index joined to a short micro-label
// by a separator glyph. The kicker rule deliberately excludes bare 1-2 digit
@@ -3363,7 +3405,7 @@ function collectNumberedSectionLabelCandidates(doc, getStyle, resolveLetterSpaci
const candidates = [];
const seenLabels = new Set();
for (const heading of doc.querySelectorAll('h2, h3, h4')) {
if (heading.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
if (heading.closest?.(KICKER_SKIP_SELECTOR)) continue;
// The index sits either directly before the heading, or before the
// wrapper the heading leads (label | <div><h2>…</h2>…</div>).
let label = heading.previousElementSibling;
@@ -3373,9 +3415,9 @@ function collectNumberedSectionLabelCandidates(doc, getStyle, resolveLetterSpaci
if (firstChild === heading) label = parent.previousElementSibling;
}
if (!label || seenLabels.has(label)) continue;
if (label.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
if (label.closest?.(KICKER_SKIP_SELECTOR)) continue;
if (HEADING_TAGS.has(label.tagName.toLowerCase())) continue;
if (isRepeatedKickerCardContext(heading, label)) continue;
if (isKickerCardContext(heading, label)) continue;
const labelText = cleanInlineText(label) || (label.textContent || '').replace(/\s+/g, ' ').trim();
const parsed = parseNumberedLabelText(labelText);
@@ -4548,13 +4590,13 @@ function checkElementHeroEyebrow(el, style, tag, window, customPropMap) {
});
}
function checkRepeatedSectionKickersFromDoc(doc, win) {
const candidates = collectRepeatedSectionKickerCandidates(
function checkKickerAboveHeadingFromDoc(doc, win) {
const candidates = collectKickerCandidates(
doc,
(el) => win.getComputedStyle(el),
(value, fontSize) => resolveLengthPx(value, fontSize) || 0,
);
return checkRepeatedSectionKickers({ candidates });
return checkKickerAboveHeading({ candidates });
}
function checkElementMotion(tag, style) {
@@ -7712,7 +7754,7 @@ if (IS_BROWSER) {
addBrowserFindings(groupMap, document.body, typoFindings);
}
const sectionKickerFindings = checkRepeatedSectionKickersDOM()
const sectionKickerFindings = checkKickerAboveHeadingDOM()
.map(f => ({ type: f.id, detail: f.snippet }))
.filter(f => _ruleOk(f.type));
if (sectionKickerFindings.length > 0) {
@@ -223,7 +223,7 @@ function unwrapCssAtLayer(source) {
// ---------------------------------------------------------------------------
const STATIC_INHERITED_PROPS = new Set([
'color', 'fontFamily', 'fontSize', 'fontStyle', 'fontWeight',
'color', 'fontFamily', 'fontSize', 'fontStyle', 'fontWeight', 'fontVariant',
'lineHeight', 'letterSpacing', 'textTransform', 'textAlign', 'hyphens',
'webkitHyphens',
]);
@@ -252,6 +252,7 @@ const STATIC_DEFAULT_STYLE = {
fontFamily: '',
fontSize: '16px',
fontStyle: 'normal',
fontVariant: 'normal',
fontWeight: '400',
lineHeight: 'normal',
letterSpacing: 'normal',
@@ -27,11 +27,11 @@ import {
checkElementRadialSpotlight,
checkCreamPalette,
checkHtmlPatterns,
checkKickerAboveHeadingFromDoc,
checkNumberedSectionLabelsFromDoc,
checkPageLayout,
checkPageQualityFromDoc,
checkRepeatedContainerTextFromDoc,
checkRepeatedSectionKickersFromDoc,
resolveBackground,
resolveBorderRadiusPx,
} from '../../rules/checks.mjs';
@@ -202,7 +202,7 @@ async function detectHtml(filePath, options = {}) {
for (const f of runPageCheck('typography-rules', () => checkStaticPageTypography(document, window))) {
findings.push(finding(f.id, filePath, f.snippet));
}
for (const f of runPageCheck('repeated-section-kickers', () => checkRepeatedSectionKickersFromDoc(document, window))) {
for (const f of runPageCheck('kicker-above-heading', () => checkKickerAboveHeadingFromDoc(document, window))) {
findings.push(finding(f.id, filePath, f.snippet));
}
for (const f of runPageCheck('numbered-section-labels', () => checkNumberedSectionLabelsFromDoc(document, window))) {
@@ -198,15 +198,14 @@ const ANTIPATTERNS = [
skillGuideline: 'tiny uppercase tracked label above the hero headline',
},
{
id: 'repeated-section-kickers',
id: 'kicker-above-heading',
category: 'slop',
scopes: ['type'],
severity: 'advisory',
name: 'Repeated section kicker labels',
name: 'Kicker / eyebrow label above heading',
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.',
'A tiny tracked uppercase or small-caps label sitting as its own block directly above a heading is banned outright, repeated or not. Generated kickers never earn their place: the heading carries its own weight. Delete the label and let the heading speak; if the words matter, work them into the heading or the body.',
skillSection: 'Typography',
skillGuideline: 'repeated eyebrow or kicker labels as section scaffolding',
skillGuideline: 'kicker or eyebrow labels above headings',
},
{
id: 'numbered-section-labels',
@@ -456,12 +456,15 @@ function checkHeroEyebrow(opts) {
}];
}
function checkRepeatedSectionKickers(opts) {
const { candidates, minCount = 3 } = opts;
if (!Array.isArray(candidates) || candidates.length < minCount) return [];
// Outright ban: one kicker is one too many, so every collected candidate is
// a finding. The judgment lives in the candidate gate (isKickerCandidate) and
// the collector's context skips, not in a repetition count.
function checkKickerAboveHeading(opts) {
const { candidates } = opts;
if (!Array.isArray(candidates)) return [];
return candidates.map(candidate => ({
id: 'repeated-section-kickers',
snippet: `repeated section kicker "${candidate.kickerText}" before ${candidate.headingTag} "${candidate.headingText}" (${candidates.length} on page)`,
id: 'kicker-above-heading',
snippet: `kicker "${candidate.kickerText}" above ${candidate.headingTag} "${candidate.headingText}"`,
}));
}
@@ -2388,7 +2391,7 @@ function parseColorResolved(str, customPropMap) {
return parseAnyColor(resolved);
}
const REPEATED_KICKER_SKIP_SELECTOR = [
const KICKER_SKIP_SELECTOR = [
'nav',
'form',
'table',
@@ -2407,7 +2410,7 @@ const REPEATED_KICKER_SKIP_SELECTOR = [
'[data-impeccable-allow-kickers]',
].join(',');
const REPEATED_KICKER_CARD_CONTEXT_SELECTOR = [
const KICKER_CARD_CONTEXT_SELECTOR = [
'article',
'button',
'a',
@@ -2425,23 +2428,32 @@ function cleanInlineText(el) {
.trim();
}
function isRepeatedKickerCardContext(heading, kicker) {
const item = heading.closest?.(REPEATED_KICKER_CARD_CONTEXT_SELECTOR);
function isKickerCardContext(heading, kicker) {
const item = heading.closest?.(KICKER_CARD_CONTEXT_SELECTOR);
return Boolean(item && (!item.contains || item.contains(kicker)));
}
function isRepeatedKickerCandidate(opts) {
// Meta lines above headlines join category and date (or path crumbs) with
// separator glyphs, or carry a year. A kicker is one short phrase; metadata
// keeps its markers.
const KICKER_META_TEXT_RE = /[·•|]|\s[\/›»>]\s|\b(19|20)\d{2}\b/;
// Legal and document numbering: "Section 4.2", "Article IX", "§ 12.3",
// dotted decimal outlines. The label identifies the clause, so it stays.
const KICKER_DOC_NUMBERING_RE = /^(§|\d+(\.\d+)+\b|(section|article|clause|appendix|exhibit|schedule|chapter|part|rule|title)\s+([\divxlc]+\b|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)\b)/i;
function isKickerCandidate(opts) {
const {
headingTag,
headingLevel,
headingText,
headingFontSize,
kickerTag,
kickerText,
kickerTextTransform,
kickerFontVariant,
kickerFontSize,
kickerLetterSpacing,
} = opts;
if (!['h2', 'h3', 'h4'].includes(headingTag)) return false;
if (!headingLevel || headingLevel > 4) return false;
if (!headingText || headingText.length < 3) return false;
if (/^\/[\w-]+/i.test(headingText.replace(/^"|"$/g, '').trim())) return false;
if (!(headingFontSize >= 20)) return false;
@@ -2449,9 +2461,13 @@ function isRepeatedKickerCandidate(opts) {
if (!['p', 'span', 'div', 'small'].includes(kickerTag)) return false;
if (!kickerText || kickerText.length < 2 || kickerText.length > 34) return false;
if (/^step\s*\d+/i.test(kickerText) || /^\d{1,2}$/.test(kickerText)) return false;
if (KICKER_META_TEXT_RE.test(kickerText)) return false;
if (KICKER_DOC_NUMBERING_RE.test(kickerText)) return false;
const isSmallCaps = /small-caps/.test(kickerFontVariant || '');
const isUppercased = kickerTextTransform === 'uppercase'
|| (/[A-Z]/.test(kickerText) && !/[a-z]/.test(kickerText));
|| (/[A-Z]/.test(kickerText) && !/[a-z]/.test(kickerText))
|| isSmallCaps;
if (!isUppercased) return false;
if (!(kickerFontSize > 0 && kickerFontSize <= 14)) return false;
const minTrackedSpacing = Math.max(1, kickerFontSize * 0.08);
@@ -2459,37 +2475,64 @@ function isRepeatedKickerCandidate(opts) {
return true;
}
function collectRepeatedSectionKickerCandidates(doc, getStyle, resolveLetterSpacing) {
// Resolve a heading level for the anchor element: 1-4 for h1-h4, aria-level
// (default 2) for role="heading" elements, 0 otherwise.
function kickerHeadingLevel(heading) {
const tag = heading.tagName.toLowerCase();
const byTag = /^h([1-6])$/.exec(tag);
if (byTag) return parseInt(byTag[1], 10);
const role = heading.getAttribute?.('role') || '';
if (role.toLowerCase() !== 'heading') return 0;
const ariaLevel = parseInt(heading.getAttribute?.('aria-level') || '', 10);
return Number.isFinite(ariaLevel) && ariaLevel >= 1 ? ariaLevel : 2;
}
function collectKickerCandidates(doc, getStyle, resolveLetterSpacing) {
const candidates = [];
for (const heading of doc.querySelectorAll('h2, h3, h4')) {
if (heading.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
for (const heading of doc.querySelectorAll('h1, h2, h3, h4, [role="heading"]')) {
const headingLevel = kickerHeadingLevel(heading);
if (!headingLevel || headingLevel > 4) continue;
if (heading.closest?.(KICKER_SKIP_SELECTOR)) continue;
// Application contexts (tab panels, dialogs) use compact context labels
// above headings to describe state, not to decorate. Same carve-out the
// hero-eyebrow rule makes.
if (heading.closest?.('[role="tabpanel"], [role="dialog"], [role="application"], dialog')) continue;
const kicker = heading.previousElementSibling;
if (!kicker || kicker.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
if (isRepeatedKickerCardContext(heading, kicker)) continue;
if (!kicker || kicker.closest?.(KICKER_SKIP_SELECTOR)) continue;
if (isKickerCardContext(heading, kicker)) continue;
const headingStyle = getStyle(heading);
const kickerStyle = getStyle(kicker);
const headingTag = heading.tagName.toLowerCase();
const headingText = (heading.textContent || '').replace(/\s+/g, ' ').trim();
const kickerText = cleanInlineText(kicker) || (kicker.textContent || '').replace(/\s+/g, ' ').trim();
const headingFontSize = resolveLetterSpacing(headingStyle.fontSize || '', 16) || parseFloat(headingStyle.fontSize) || 0;
const kickerFontSize = resolveLetterSpacing(kickerStyle.fontSize || '', 16) || parseFloat(kickerStyle.fontSize) || 0;
const kickerLetterSpacing = resolveLetterSpacing(kickerStyle.letterSpacing || '', kickerFontSize);
if (!isRepeatedKickerCandidate({
headingTag: heading.tagName.toLowerCase(),
if (!isKickerCandidate({
headingLevel,
headingText,
headingFontSize,
kickerTag: kicker.tagName.toLowerCase(),
kickerText,
kickerTextTransform: kickerStyle.textTransform || '',
kickerFontVariant: `${kickerStyle.fontVariant || ''} ${kickerStyle.fontVariantCaps || ''}`,
kickerFontSize,
kickerLetterSpacing,
})) {
continue;
}
// A tracked-caps eyebrow above a hero-scale h1 belongs to
// hero-eyebrow-chip (which also covers the accent-bold and dash-prefix
// stylings there). Stand down so one element gets one finding.
if (headingTag === 'h1' && headingFontSize >= 48 && kickerLetterSpacing >= 1.6) {
continue;
}
candidates.push({
headingTag: heading.tagName.toLowerCase(),
headingTag,
headingText: headingText.replace(/^"|"$/g, '').slice(0, 60),
kickerText: kickerText.slice(0, 40),
});
@@ -2497,17 +2540,17 @@ function collectRepeatedSectionKickerCandidates(doc, getStyle, resolveLetterSpac
return candidates;
}
function checkRepeatedSectionKickersDOM() {
const candidates = collectRepeatedSectionKickerCandidates(
function checkKickerAboveHeadingDOM() {
const candidates = collectKickerCandidates(
document,
(el) => getComputedStyle(el),
(value, fontSize) => resolveLengthPx(value, fontSize) || 0,
);
return checkRepeatedSectionKickers({ candidates });
return checkKickerAboveHeading({ candidates });
}
// ── Numbered section labels ─────────────────────────────────────────────────
// Sibling of the repeated-kicker rule: instead of a tracked uppercase word,
// Sibling of the kicker-above-heading rule: instead of a tracked uppercase word,
// the section scaffold is a tiny numeric index riding beside each section
// heading — bare and zero-padded, or an index joined to a short micro-label
// by a separator glyph. The kicker rule deliberately excludes bare 1-2 digit
@@ -2561,7 +2604,7 @@ function collectNumberedSectionLabelCandidates(doc, getStyle, resolveLetterSpaci
const candidates = [];
const seenLabels = new Set();
for (const heading of doc.querySelectorAll('h2, h3, h4')) {
if (heading.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
if (heading.closest?.(KICKER_SKIP_SELECTOR)) continue;
// The index sits either directly before the heading, or before the
// wrapper the heading leads (label | <div><h2>…</h2>…</div>).
let label = heading.previousElementSibling;
@@ -2571,9 +2614,9 @@ function collectNumberedSectionLabelCandidates(doc, getStyle, resolveLetterSpaci
if (firstChild === heading) label = parent.previousElementSibling;
}
if (!label || seenLabels.has(label)) continue;
if (label.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
if (label.closest?.(KICKER_SKIP_SELECTOR)) continue;
if (HEADING_TAGS.has(label.tagName.toLowerCase())) continue;
if (isRepeatedKickerCardContext(heading, label)) continue;
if (isKickerCardContext(heading, label)) continue;
const labelText = cleanInlineText(label) || (label.textContent || '').replace(/\s+/g, ' ').trim();
const parsed = parseNumberedLabelText(labelText);
@@ -3746,13 +3789,13 @@ function checkElementHeroEyebrow(el, style, tag, window, customPropMap) {
});
}
function checkRepeatedSectionKickersFromDoc(doc, win) {
const candidates = collectRepeatedSectionKickerCandidates(
function checkKickerAboveHeadingFromDoc(doc, win) {
const candidates = collectKickerCandidates(
doc,
(el) => win.getComputedStyle(el),
(value, fontSize) => resolveLengthPx(value, fontSize) || 0,
);
return checkRepeatedSectionKickers({ candidates });
return checkKickerAboveHeading({ candidates });
}
function checkElementMotion(tag, style) {
@@ -5393,7 +5436,7 @@ export {
checkItalicSerif,
isAccentColor,
checkHeroEyebrow,
checkRepeatedSectionKickers,
checkKickerAboveHeading,
checkMotion,
checkGlow,
scanCssTextForGlow,
@@ -5424,9 +5467,9 @@ export {
parseAnyColor,
parseColorResolved,
cleanInlineText,
isRepeatedKickerCandidate,
collectRepeatedSectionKickerCandidates,
checkRepeatedSectionKickersDOM,
isKickerCandidate,
collectKickerCandidates,
checkKickerAboveHeadingDOM,
parseNumberedLabelText,
isNumberedSectionLabelCandidate,
collectNumberedSectionLabelCandidates,
@@ -5458,7 +5501,7 @@ export {
checkElementIconTile,
checkElementItalicSerif,
checkElementHeroEyebrow,
checkRepeatedSectionKickersFromDoc,
checkKickerAboveHeadingFromDoc,
checkElementMotion,
checkElementGlow,
checkTypography,
@@ -24,21 +24,36 @@
* "label": "Fillmore Handbill",
* "kicker": "THE ROLL", // optional badge; the assigned option leads
* "lineage": "1966-71 Fillmore ...", // optional
* "body": "why it fits, first viewport, risk ...", // optional, plain text
* "hero": "https://... or /abs/path.webp", // optional image
* "thesis": "one line: the idea this direction owns", // optional
* "palette": ["#1a2f5e", "oklch(84% .19 80)", ...], // optional, rendered as chips
* "materials": ["letterpress", "newsprint"], // optional, rendered as tags
* "viewport": "one line: the first-viewport composition", // optional
* "case": "one line: the fusion verdict, honest", // optional
* "risk": "one line: the honest risk", // optional
* "body": "fallback prose when the structured fields are absent",
* "sketch": ".impeccable/sketches/assigned.webp", // optional; may not exist
* // yet: the page shimmer-waits and polls the
* // slot until the file lands, so serve first
* // and generate after
* "hero": "https://... or /abs/path.webp", // optional inspiration image;
* // rides picture-in-picture when a sketch exists
* "board": "https://... or /abs/path.webp" // optional secondary image
* }, ...
* ],
* "reroll": true, // adds a re-roll action (returns {"optionId":"reroll"})
* "canon": true, // adds the quiet "Play it straight" standing exit
* // (returns {"optionId":"canon"}); direction rounds only
* "canon": true, // adds the "Play it straight" standing exit;
* // direction rounds only (returns {"optionId":"canon"})
* "canonCard": { ... }, // optional: the standing exit as a full card with the
* // same anatomy (label, thesis, palette, sketch, ...);
* // rendered last and visually subordinate. Without it,
* // canon stays a quiet footer action.
* "steer": true // adds a free-text steer field returned with any answer
* }
*
* Options render as large cards: hero render first when present (the dealt
* catalog worlds already have cards; grounded directions may present text-only
* or a freshly generated mock). Local image paths are served by this server;
* nothing is uploaded anywhere.
* Options render as large cards: the sketch leads when present, with the
* inspiration image picture-in-picture; a hero alone renders full-bleed; a
* text-only direction gets its identity from the palette chips and tags.
* Local image paths are served by this server; nothing is uploaded anywhere.
*
* Modes:
* (default) block until answered; ANSWER on stdout; exit 0.
@@ -107,6 +122,9 @@ function printAnswer(raw) {
if (a.hero || a.board) {
console.log("CHOSEN CARD: open the chosen world's board and hero images now, before any code. When your harness only reads files, or runs sandboxed, download them INTO the workspace and open the relative path; a sandboxed viewer rejects absolute paths outside it. They set the craft bar the build must reach.");
}
if (a.sketch) {
console.log('CHOSEN SKETCH: the decision sketch at that path may seed one comp probe; the comp round still renders its full set, because a sketch chose the direction, not the composition.');
}
if (a.optionId === 'canon') {
console.log('CANON CHOSEN: the user picked the category standard on purpose. Ask once for two or three products this should sit alongside; their craft level becomes the quality bar. Execute the canon at full commitment, conventions embraced without irony or smuggled quirk.');
}
@@ -125,14 +143,15 @@ if (hasFlag('schema')) {
title: 'Choose the visual world',
question: 'The roll assigned Fillmore Handbill. Keep it, take an alternate, or re-roll.',
options: [
{ id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', body: 'Why it fits, the first viewport, the honest risk.', hero: 'https://impeccable.style/worlds/cards/fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/fillmore-handbill.webp' },
{ id: 'challenger-teletext', label: 'Teletext Service', lineage: 'broadcast teletext magazines', body: 'Fused alternate.', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' },
{ id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', sketch: '.impeccable/sketches/assigned.webp', hero: 'https://impeccable.style/worlds/cards/fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/fillmore-handbill.webp' },
{ id: 'challenger-teletext', label: 'Teletext Service', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', case: 'Fuses cleanly: releases map to numbered pages.', sketch: '.impeccable/sketches/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' },
],
reroll: true,
canon: true,
canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', viewport: 'The arrangement a visitor expects, at full craft.', sketch: '.impeccable/sketches/canon.webp' },
steer: true,
}, null, 2));
console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board accept URLs or local paths. canon adds a quiet standing "Play it straight" action for direction decisions: the user\'s explicit door to the category standard. Include it only for visual-direction rounds; never present canon as your own recommendation.');
console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/sketch accept URLs or local paths; sketch slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the sketch, never as the promise of the build. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability.');
process.exit(0);
}
@@ -203,14 +222,25 @@ if (hasFlag('start')) {
const key = arg('key') || Math.random().toString(16).slice(2, 10);
// In start mode the agent is alive and owns browser routing; the server
// only opens the system browser itself when --open forces it.
// The daemon's output lands in a per-key log so a startup failure can say
// what actually went wrong instead of only that it did.
const logFile = path.join(QUESTION_DIR, `${key}.log`);
const logFd = fs.openSync(logFile, 'a');
const child = spawn(process.execPath, [
fileURLToPath(import.meta.url), '--payload', payloadPath, '--detached-serve', '--key', key,
'--timeout', String(timeoutSec), ...(hasFlag('open') ? [] : ['--no-open']),
], { detached: true, stdio: 'ignore' });
], { detached: true, stdio: ['ignore', logFd, logFd] });
child.unref();
fs.closeSync(logFd);
const deadline = Date.now() + 8000;
while (Date.now() < deadline && !fs.existsSync(stateFile(key))) await new Promise((r) => setTimeout(r, 100));
if (!fs.existsSync(stateFile(key))) { console.error('serve-question: server failed to start'); process.exit(1); }
if (!fs.existsSync(stateFile(key))) {
let tail = '';
try { tail = fs.readFileSync(logFile, 'utf8').trim().split('\n').slice(-4).join('\n '); } catch { /* log never written */ }
console.error(`serve-question: server failed to start${tail ? `\n ${tail}` : ''}`);
console.error(`serve-question: log at ${path.relative(process.cwd(), logFile) || logFile}. A sandboxed exec that cannot listen on localhost causes exactly this; rerun this command once through the harness's network-enabled or unsandboxed command tool before falling back.`);
process.exit(1);
}
const state = JSON.parse(fs.readFileSync(stateFile(key), 'utf8'));
console.log(`QUESTION URL: ${state.url}`);
console.log(`QUESTION KEY: ${key}`);
@@ -244,12 +274,28 @@ function loadRound(json) {
localImages.push(abs);
return `/img/${localImages.length - 1}`;
};
// Sketches stream in after the page is served, so their slots register
// whether or not the file exists yet; /img answers 404 until it lands and
// the page polls the slot. Remote sketch URLs pass through untouched.
const sketchSrc = (value) => {
if (!value) return null;
if (/^https?:\/\//.test(value)) return value;
localImages.push(path.resolve(value));
return `/img/${localImages.length - 1}`;
};
payload = parsed;
options = parsed.options.map((option) => ({
const decorate = (option) => ({
...option,
heroSrc: imageSrc(option.hero),
boardSrc: imageSrc(option.board),
}));
sketchSrc: sketchSrc(option.sketch),
});
options = parsed.options.map(decorate);
// The standing exit as a full card: same anatomy, reserved id, rendered
// subordinate by the page. Without it, canon stays the quiet footer action.
if (parsed.canonCard && typeof parsed.canonCard === 'object') {
options = [...options, { ...decorate(parsed.canonCard), id: 'canon', isCanon: true }];
}
}
try { loadRound(raw); } catch (error) { console.error(`serve-question: ${error.message}`); process.exit(1); }
const detachedKey = hasFlag('detached-serve') ? arg('key') : null;
@@ -260,30 +306,81 @@ const esc = (s) => String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<
function page() {
const flipChip = (label) => `<button type="button" class="chip flip" aria-label="Flip the card"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4a8 8 0 1 1-8 8" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/><path d="M4 5.5V12h6.5" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg><span>${label}</span></button>`;
const expandChip = `<button type="button" class="chip expand" aria-label="Expand the image"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 9V4h5M20 15v5h-5M20 9V4h-5M4 15v5h5" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg></button>`;
const cards = options.map((option, index) => `
<article class="card" style="--fan:${index === 0 ? '0deg' : (index % 2 ? '1.4deg' : '-1.2deg')};--deal:${index * 90}ms" data-id="${esc(option.id)}">
<div class="card-inner">
<div class="face front${index === 0 ? ' lead' : ''}${option.heroSrc || option.boardSrc ? '' : ' text-only'}">
${option.kicker ? `<span class="kicker">${esc(option.kicker)}</span>` : ''}
${option.heroSrc || option.boardSrc ? `<div class="media">
// Structured anatomy: chips and one-line facts render when the payload
// carries them; a plain body falls back to the prose block. Palette chips
// and material tags give a text-only direction an immediate identity that
// no generation luck can distort.
const fact = (label, value, cls = '') => value ? `<p class="fact${cls ? ` ${cls}` : ''}"><span class="fact-label">${label}</span>${esc(value)}</p>` : '';
const hasBack = (option) => Boolean(option.viewport || option.case || (option.boardSrc && option.heroSrc));
const anatomy = (option) => {
const rows = [];
if (option.thesis) rows.push(`<p class="thesis">${esc(option.thesis)}</p>`);
const idBits = [];
if (Array.isArray(option.palette) && option.palette.length) {
idBits.push(`<span class="swatches">${option.palette.slice(0, 6).map((c) => `<i style="background:${esc(c)}" title="${esc(c)}"></i>`).join('')}</span>`);
}
if (Array.isArray(option.materials) && option.materials.length) {
idBits.push(option.materials.slice(0, 4).map((m) => `<span class="tag">${esc(m)}</span>`).join(''));
}
if (idBits.length) rows.push(`<div class="identity">${idBits.join('')}</div>`);
// The front carries only what the choice needs: thesis, identity, and the
// honest risk clamped to two lines. First viewport and the case read on
// the card's back; once the sketch lands, the first viewport is a picture.
rows.push(fact('Risk', option.risk, 'clamp'));
if (!option.thesis && option.body) rows.push(`<p class="detail">${esc(option.body)}</p>`);
else if (option.body && option.thesis && !hasBack(option)) rows.push(`<p class="detail more">${esc(option.body)}</p>`);
return rows.join('\n ');
};
const backFacts = (option) => [
fact('First viewport', option.viewport),
fact('The case', option.case),
fact('Risk', option.risk),
option.body && option.thesis ? `<p class="detail more">${esc(option.body)}</p>` : '',
].filter(Boolean).join('\n ');
const media = (option) => {
const inspiration = option.heroSrc ? `<figure class="pip" title="Inspiration: the world this direction draws from. Your page will not look like this image.">
<img src="${esc(option.heroSrc)}" alt="">
<figcaption>inspiration</figcaption>
</figure>` : '';
const details = hasBack(option) ? flipChip('Details') : '';
if (option.sketchSrc) {
return `<div class="media sketching" data-sketch="${esc(option.sketchSrc)}">
<div class="shimmer"><span class="sketch-note">sketching&hellip;</span></div>
<img class="sketch" alt="" hidden>
${inspiration}
<div class="chips">${expandChip}${details}</div>
</div>`;
}
if (option.heroSrc || option.boardSrc) {
return `<div class="media">
<img src="${esc(option.heroSrc || option.boardSrc)}" alt="">
<div class="chips">${expandChip}${option.boardSrc && option.heroSrc ? flipChip('Board') : ''}</div>
</div>` : ''}
<div class="chips">${expandChip}${details}</div>
</div>`;
}
return '';
};
const cards = options.map((option, index) => `
<article class="card${option.isCanon ? ' canon' : ''}" style="--fan:${index === 0 ? '0deg' : (index % 2 ? '1.4deg' : '-1.2deg')};--deal:${index * 90}ms" data-id="${esc(option.id)}">
<div class="card-inner">
<div class="face front${index === 0 ? ' lead' : ''}${media(option) ? '' : ' text-only'}">
${option.kicker ? `<span class="kicker">${esc(option.kicker)}</span>` : option.isCanon ? '<span class="kicker standing">The standing door</span>' : ''}
${media(option)}
<div class="body">
${option.lineage ? `<p class="tier">${esc(option.lineage)}</p>` : ''}
<h2>${esc(option.label)}</h2>
${option.body ? `<p class="detail">${esc(option.body)}</p>` : ''}
<button class="choose" data-id="${esc(option.id)}">Build this</button>
${anatomy(option)}
<button class="choose" data-id="${esc(option.id)}">${option.isCanon ? 'Play it straight' : 'Build this'}</button>
</div>
</div>
${option.boardSrc && option.heroSrc ? `<div class="face back${index === 0 ? ' lead' : ''}">
<div class="media back-media">
${hasBack(option) ? `<div class="face back${index === 0 ? ' lead' : ''}">
${option.boardSrc ? `<div class="media back-media">
<img src="${esc(option.boardSrc)}" alt="">
<div class="chips">${expandChip}${flipChip('Hero')}</div>
</div>
<div class="body back-bar">
<p class="tier">Design-system board &middot; ${esc(option.label)}</p>
<button class="choose" data-id="${esc(option.id)}">Build this</button>
<div class="chips">${expandChip}${flipChip('Front')}</div>
</div>` : `<div class="back-head"><p class="tier">The full read &middot; ${esc(option.label)}</p>${flipChip('Front')}</div>`}
<div class="body back-body">
${option.boardSrc ? `<p class="tier">The full read &middot; ${esc(option.label)}</p>` : ''}
${backFacts(option)}
<button class="choose" data-id="${esc(option.id)}">${option.isCanon ? 'Play it straight' : 'Build this'}</button>
</div>
</div>` : ''}
</div>
@@ -318,7 +415,7 @@ function page() {
--ks-mono: "SFMono-Regular", "Roboto Mono", "JetBrains Mono", Consolas, monospace;
}
* { box-sizing: border-box; margin: 0; }
body { background: var(--ks-lacquer); color: var(--ks-text); font: 15px/1.55 var(--ks-font); padding: 1.8rem clamp(1rem, 5vw, 4rem) 2rem; min-height: 100dvh; display: flex; flex-direction: column; }
body { background: var(--ks-lacquer); color: var(--ks-text); font: 15px/1.55 var(--ks-font); padding: 1.8rem clamp(1rem, 5vw, 4rem) 2rem; min-height: 100dvh; display: flex; flex-direction: column; overflow-x: clip; }
#ambient { position: fixed; inset: -40px; z-index: 0; background-size: cover; background-position: center; filter: blur(34px) saturate(1.05); opacity: 0; transition: opacity .55s ease, background-image .2s; pointer-events: none; }
#scrim { position: fixed; inset: 0; z-index: 0; background: linear-gradient(180deg, oklch(7% 0.006 95 / 0.62), oklch(7% 0.006 95 / 0.78)); pointer-events: none; }
header, main, footer { position: relative; z-index: 1; }
@@ -336,7 +433,42 @@ function page() {
.question { color: var(--ks-text-muted); margin-top: .7rem; max-width: 52rem; }
main { flex: 1; display: flex; align-items: center; width: 100%; max-width: 90rem; margin: 0 auto; }
.stage { width: 100%; display: flex; flex-direction: column; gap: 1.5rem; }
.grid { display: grid; gap: 1.6rem; grid-template-columns: repeat(auto-fit, minmax(min(23rem, 100%), 1fr)); width: 100%; }
/* The deck bleeds to the viewport edges while the first card aligns with the
content column; a carousel cut off at an invisible container edge reads as
a rendering bug, but one cut off at the screen edge reads as more cards. */
.deck-shell { position: relative; width: 100vw; margin-left: calc(50% - 50vw); }
/* One row in a wide viewport, one column in a tall one; the deck scrolls on
its axis with snap points and the arrows page it card by card. */
.grid { --deck-inset: max(clamp(1rem, 5vw, 4rem), calc((100vw - 90rem) / 2)); display: flex; gap: 1.6rem; width: 100%; overflow-x: auto; overflow-y: hidden; scroll-snap-type: x mandatory; scrollbar-width: none; padding: 6px var(--deck-inset); scroll-padding-inline: var(--deck-inset); align-items: stretch; }
.grid::-webkit-scrollbar { display: none; }
.grid > .card { flex: 0 0 clamp(20rem, 27vw, 27rem); scroll-snap-align: center; }
.nav { position: absolute; z-index: 6; width: 42px; height: 42px; display: flex; align-items: center; justify-content: center; border-radius: 50%; background: oklch(7% 0.006 95 / 0.78); border: 1px solid var(--ks-rule); color: var(--ks-kinpaku); cursor: pointer; backdrop-filter: blur(6px); transition: border-color .2s, color .2s, opacity .2s; }
.nav:hover { border-color: var(--ks-kinpaku-deep); color: var(--ks-kinpaku-pale); }
.nav[disabled] { opacity: .25; cursor: default; }
.nav[hidden] { display: none; }
.nav svg { width: 16px; height: 16px; }
.nav.prev { left: 14px; top: 50%; transform: translateY(-50%); }
.nav.next { right: 14px; top: 50%; transform: translateY(-50%); }
/* A side that hides more cards fades out; a hard edge means the end. */
.fade { position: absolute; z-index: 5; pointer-events: none; opacity: 0; transition: opacity .3s ease; }
.fade-prev { left: 0; top: 0; bottom: 0; width: 88px; background: linear-gradient(90deg, var(--ks-lacquer), transparent); }
.fade-next { right: 0; top: 0; bottom: 0; width: 88px; background: linear-gradient(270deg, var(--ks-lacquer), transparent); }
.deck-shell.can-prev .fade-prev { opacity: 1; }
.deck-shell.can-next .fade-next { opacity: 1; }
@media (max-aspect-ratio: 1/1) {
.grid { flex-direction: column; overflow-x: hidden; overflow-y: auto; scroll-snap-type: y mandatory; max-height: min(68dvh, 44rem); scroll-padding-block: 6px; }
.grid > .card { flex: 0 0 auto; }
/* In the vertical deck the pager is the primary way forward, so it grows
into a labeled pill instead of a bare chevron nobody notices. */
.nav { width: auto; height: 38px; border-radius: 19px; padding: 0 16px; gap: 8px; border-color: var(--ks-kinpaku-deep); background: oklch(7% 0.006 95 / 0.88); font-family: var(--ks-mono); font-size: .62rem; letter-spacing: .2em; text-transform: uppercase; }
.nav svg { transform: rotate(90deg); }
.nav.prev::after { content: "Back"; }
.nav.next::after { content: "More"; }
.nav.prev { left: 50%; top: 6px; transform: translate(-50%, 0); }
.nav.next { right: auto; left: 50%; top: auto; bottom: 6px; transform: translate(-50%, 0); }
.fade-prev { top: 0; left: 0; right: 0; bottom: auto; width: auto; height: 72px; background: linear-gradient(180deg, var(--ks-lacquer), transparent); }
.fade-next { top: auto; left: 0; right: 0; bottom: 0; width: auto; height: 72px; background: linear-gradient(0deg, var(--ks-lacquer), transparent); }
}
.card { position: relative; perspective: 1400px; transform: rotate(var(--fan, 0deg)); transition: transform .25s cubic-bezier(.16, 1, .3, 1); }
.card:hover { transform: rotate(0deg) translateY(-4px); }
.card-inner { position: relative; height: 100%; transform-style: preserve-3d; transition: transform .7s cubic-bezier(.16, 1, .3, 1); }
@@ -344,6 +476,12 @@ function page() {
.face { background: var(--ks-lacquer-raised); border: 1px solid var(--ks-rule); border-radius: 10px; box-shadow: 0 18px 40px oklch(0% 0 0 / 0.35); overflow: hidden; display: flex; flex-direction: column; backface-visibility: hidden; -webkit-backface-visibility: hidden; }
.face.front { position: relative; height: 100%; }
.face.back { position: absolute; inset: 0; transform: rotateY(180deg); }
/* Only the visible face is interactive: a hidden backface still hit-tests
in Chrome, so the front's pip would otherwise sit invisibly over the
back's chips, showing its zoom cursor and eating the flip-back click. */
.face.back { pointer-events: none; }
.card.flipped .face.back { pointer-events: auto; }
.card.flipped .face.front { pointer-events: none; }
.face.lead { border-color: var(--ks-kinpaku); box-shadow: 0 0 0 1px var(--ks-kinpaku), 0 18px 40px oklch(0% 0 0 / 0.45); }
.card:hover .face { border-color: var(--ks-kinpaku-deep); }
.card:hover .face.lead { border-color: var(--ks-kinpaku); }
@@ -355,7 +493,7 @@ function page() {
.face.text-only .body { padding-top: 12px; }
.media { position: relative; width: 100%; aspect-ratio: 16/9; flex: none; }
.media img { width: 100%; height: 100%; object-fit: cover; display: block; background: linear-gradient(100deg, var(--ks-graphite) 40%, var(--ks-graphite-2) 50%, var(--ks-graphite) 60%); }
.face.back { background: var(--ks-lacquer-deep); }
.face.back { background: var(--ks-lacquer-raised); }
.back-bar { margin-top: auto; background: var(--ks-lacquer-raised); }
.hero-blank { width: 100%; height: 100%; background: linear-gradient(100deg, var(--ks-graphite) 40%, var(--ks-graphite-2) 50%, var(--ks-graphite) 60%); }
.back-bar { flex: none; flex-direction: row; align-items: center; justify-content: space-between; gap: .8rem; }
@@ -367,6 +505,48 @@ function page() {
.tier { font-family: var(--ks-mono); font-size: .625rem; letter-spacing: .24em; text-transform: uppercase; color: var(--ks-text-faint); }
h2 { font-family: var(--ks-font); font-size: 1.125rem; font-weight: 500; line-height: 1.35; color: var(--ks-champagne); }
.detail { color: var(--ks-text-muted); font-size: .88rem; white-space: pre-wrap; }
.detail.more { font-size: .8rem; color: var(--ks-text-faint); }
.thesis { color: var(--ks-text); font-size: .95rem; line-height: 1.45; }
.identity { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin: 2px 0; }
.swatches { display: inline-flex; gap: 4px; margin-right: 4px; }
.swatches i { width: 18px; height: 18px; border-radius: 5px; border: 1px solid oklch(100% 0 0 / 0.18); box-shadow: inset 0 0 0 1px oklch(0% 0 0 / 0.25); }
.tag { font-family: var(--ks-mono); font-size: .6rem; letter-spacing: .14em; text-transform: uppercase; color: var(--ks-text-muted); border: 1px solid var(--ks-rule); border-radius: 4px; padding: 3px 7px; }
.fact { font-size: .8rem; color: var(--ks-text-muted); line-height: 1.45; }
.fact-label { display: inline-block; font-family: var(--ks-mono); font-size: .6rem; letter-spacing: .18em; text-transform: uppercase; color: var(--ks-text-faint); margin-right: .55em; transform: translateY(-1px); }
.fact.clamp { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
/* The back is the full read: first viewport, the case, the whole risk, and
the board when the world has one. */
.back-head { display: flex; align-items: center; justify-content: space-between; gap: .8rem; padding: 14px 14px 0; }
.media.back-media { aspect-ratio: 16/6; }
.media.back-media img { width: 100%; height: 100%; object-fit: cover; }
.body.back-body { overflow-y: auto; flex: 1; scrollbar-width: thin; }
/* Inspiration rides picture-in-picture: the catalog world explains where the
direction comes from without promising what the build will look like. */
/* Hovering the inspiration takes over the whole media region; the sketch is
the promise, the inspiration is a glance, so the glance must cost nothing. */
.pip { position: absolute; z-index: 2; left: 10px; bottom: 10px; margin: 0; width: 84px; height: 64px; border: 1px solid var(--ks-rule); border-radius: 6px; overflow: hidden; background: var(--ks-lacquer); cursor: zoom-in; transition: left .35s cubic-bezier(.16,1,.3,1), bottom .35s cubic-bezier(.16,1,.3,1), width .35s cubic-bezier(.16,1,.3,1), height .35s cubic-bezier(.16,1,.3,1), border-radius .35s ease; box-shadow: 0 6px 18px oklch(0% 0 0 / 0.45); }
.pip img { display: block; width: 100%; height: 100%; object-fit: cover; }
.pip figcaption { position: absolute; left: 0; right: 0; bottom: 0; font-family: var(--ks-mono); font-size: .5rem; letter-spacing: .2em; text-transform: uppercase; color: var(--ks-text); text-align: center; padding: 3px 0 4px; background: oklch(7% 0.006 95 / 0.72); backdrop-filter: blur(3px); }
.pip:hover { left: 0; bottom: 0; width: 100%; height: 100%; border-radius: 0; z-index: 3; }
.sketch-note { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-family: var(--ks-mono); font-size: .66rem; letter-spacing: .22em; text-transform: uppercase; color: var(--ks-text-faint); }
/* A stand-in is honest about being one: dimmed, labeled, and replaced by
the real sketch whenever it lands. */
.media.stand-in img.sketch { filter: brightness(.72) saturate(.85); }
.media.stand-in .pip { display: none; }
.stand-in-label { position: absolute; z-index: 2; left: 0; right: 0; bottom: 0; margin: 0; font-family: var(--ks-mono); font-size: .56rem; letter-spacing: .2em; text-transform: uppercase; color: var(--ks-text); text-align: center; padding: 4px 0 5px; background: oklch(7% 0.006 95 / 0.78); backdrop-filter: blur(3px); }
.media.sketching { position: relative; }
.media.sketching .shimmer { position: absolute; inset: 0; }
.media img.sketch { position: relative; z-index: 1; }
/* The generic .media img display:block would defeat [hidden] and float an
empty block over the shimmer; an unloaded sketch must truly not render. */
.media img[hidden] { display: none; }
/* The standing exit as a card: present with full anatomy, never dressed as a
contender. Graphite instead of kinpaku, and it never takes the lead ring. */
.card.canon .face { border-color: var(--ks-rule); background: var(--ks-graphite); }
.card.canon:hover .face { border-color: var(--ks-text-faint); }
.card.canon .kicker.standing { background: transparent; border: 1px solid var(--ks-rule); color: var(--ks-text-faint); }
.card.canon button.choose { background: transparent; color: var(--ks-text); border: 1px solid var(--ks-rule); }
.card.canon button.choose:hover { border-color: var(--ks-text-muted); background: var(--ks-graphite-2); }
button.choose { margin-top: auto; align-self: start; background: var(--ks-kinpaku); color: var(--ks-dark-ink); border: 0; font-family: var(--ks-font); font-size: 1rem; font-weight: 500; line-height: 1.35; padding: 10px 38px; border-radius: 6px; cursor: pointer; transition: background .15s; }
button.choose:hover { background: var(--ks-kinpaku-pale); }
footer { width: 100%; max-width: 90rem; margin: 1.6rem auto 0; display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; }
@@ -408,13 +588,19 @@ function page() {
<h1>${esc(payload.title || 'Choose a direction')}</h1>
</div>
${payload.question ? `<p class="question">${esc(payload.question)}</p>` : ''}
<div class="grid">${cards}</div>
<div class="deck-shell">
<div class="grid">${cards}</div>
<div class="fade fade-prev" aria-hidden="true"></div>
<div class="fade fade-next" aria-hidden="true"></div>
<button class="nav prev" hidden aria-label="Previous card"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14.5 5 8 12l6.5 7" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
<button class="nav next" hidden aria-label="Next card"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9.5 5 16 12l-6.5 7" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
</div>
</div>
</main>
<footer>
${payload.steer ? '<input id="steer" placeholder="Optional steer: what should be different or kept?">' : ''}
${payload.reroll ? '<button id="reroll"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="4" fill="none" stroke="currentColor" stroke-width="1.6"/><circle cx="8.4" cy="8.4" r="1.5" fill="currentColor"/><circle cx="15.6" cy="8.4" r="1.5" fill="currentColor"/><circle cx="8.4" cy="15.6" r="1.5" fill="currentColor"/><circle cx="15.6" cy="15.6" r="1.5" fill="currentColor"/><circle cx="12" cy="12" r="1.5" fill="currentColor"/></svg><span>Re-roll</span></button>' : ''}
${payload.canon ? '<button id="canon" title="Skip the roll: build the page this category ships, executed impeccably">Play it straight</button>' : ''}
${payload.canon && !payload.canonCard ? '<button id="canon" title="Skip the roll: build the page this category ships, executed impeccably">Play it straight</button>' : ''}
</footer>
<script>
const steer = () => document.getElementById('steer')?.value || '';
@@ -434,7 +620,11 @@ function page() {
// Deal from the stack: cards begin piled at the grid's center, blurred,
// then travel to their seats with a stagger.
const cards = [...document.querySelectorAll('.card')];
if (!matchMedia('(prefers-reduced-motion: reduce)').matches && cards.length) {
// The deal is decoration: a hidden tab throttles rAF, so never let the
// animation hold the cards at opacity 0. Skip it when hidden, and force
// the final state after a beat no matter what the animation did.
setTimeout(() => cards.forEach(c => { c.style.opacity = ''; c.style.transform = ''; c.style.filter = ''; c.style.transition = ''; c.style.zIndex = ''; }), 1600);
if (!matchMedia('(prefers-reduced-motion: reduce)').matches && cards.length && !document.hidden) {
const grid = document.querySelector('.grid').getBoundingClientRect();
const cx = grid.left + grid.width / 2, cy = grid.top + grid.height / 2;
cards.forEach((card, i) => {
@@ -460,12 +650,99 @@ function page() {
}));
}
// Ambient: the hovered card's hero bleeds into the page ground under a scrim.
// Sketches stream in after the deal: poll each slot until the file lands,
// then swap the shimmer for the image. Generation is genuinely slow and a
// sequential batch puts the last card many minutes out, so patience is the
// default: a slot only shows its inspiration as a stand-in when it has
// waited four minutes AND nothing has landed anywhere for four minutes, the
// stand-in is labeled as such, and polling continues so the real sketch
// still swaps in whenever it arrives. Progress anywhere resets patience.
const landTracker = { last: Date.now() };
document.querySelectorAll('.media.sketching').forEach(m => {
const url = m.dataset.sketch;
const img = m.querySelector('img.sketch');
const note = m.querySelector('.sketch-note');
const started = Date.now();
// A live elapsed count is the difference between "working" and "frozen".
const tick = setInterval(() => { if (note) note.textContent = 'sketching · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000);
const settle = () => { clearInterval(tick); m.classList.remove('sketching', 'stand-in'); m.querySelector('.shimmer')?.remove(); m.querySelector('.stand-in-label')?.remove(); };
const standIn = () => {
const pip = m.querySelector('.pip img');
if (!pip || m.classList.contains('stand-in')) return;
img.src = pip.getAttribute('src'); img.hidden = false;
m.classList.add('stand-in');
m.querySelector('.shimmer')?.remove();
clearInterval(tick);
const label = document.createElement('p');
label.className = 'stand-in-label';
label.textContent = 'inspiration · sketch pending';
m.appendChild(label);
};
const tryLoad = () => {
const probe = new Image();
probe.onload = () => { landTracker.last = Date.now(); img.src = probe.src; img.hidden = false; settle(); };
probe.onerror = () => {
const quiet = Date.now() - landTracker.last > 240000;
if (Date.now() - started > 240000 && quiet) standIn();
setTimeout(tryLoad, m.classList.contains('stand-in') ? 5000 : 2500);
};
probe.src = url + (url.includes('?') ? '&' : '?') + 't=' + Date.now();
};
tryLoad();
});
// Inspiration PIP opens the full catalog card in the lightbox.
document.querySelectorAll('.pip').forEach(p => p.addEventListener('click', (e) => {
e.stopPropagation();
const img = p.querySelector('img');
if (!img) return;
lightboxImg.src = img.getAttribute('src');
lightbox.hidden = false;
requestAnimationFrame(() => lightbox.classList.add('open'));
}));
// Deck paging: arrows appear only when the deck overflows its axis, page
// one card at a time, and follow the aspect-ratio flip between row and column.
const deck = document.querySelector('.grid');
const prevBtn = document.querySelector('.nav.prev');
const nextBtn = document.querySelector('.nav.next');
const vertical = () => matchMedia('(max-aspect-ratio: 1/1)').matches;
function updateNav() {
if (!deck || !prevBtn) return;
const shell = deck.closest('.deck-shell');
const v = vertical();
const overflow = v ? deck.scrollHeight > deck.clientHeight + 4 : deck.scrollWidth > deck.clientWidth + 4;
prevBtn.hidden = nextBtn.hidden = !overflow;
const pos = v ? deck.scrollTop : deck.scrollLeft;
const max = v ? deck.scrollHeight - deck.clientHeight : deck.scrollWidth - deck.clientWidth;
const canPrev = overflow && pos > 2;
const canNext = overflow && pos < max - 2;
prevBtn.toggleAttribute('disabled', !canPrev);
nextBtn.toggleAttribute('disabled', !canNext);
shell?.classList.toggle('can-prev', canPrev);
shell?.classList.toggle('can-next', canNext);
}
function pageDeck(dir) {
const card = deck.querySelector('.card');
if (!card) return;
const r = card.getBoundingClientRect();
const step = (vertical() ? r.height : r.width) + 26;
deck.scrollBy(vertical() ? { top: dir * step, behavior: 'smooth' } : { left: dir * step, behavior: 'smooth' });
}
prevBtn?.addEventListener('click', () => pageDeck(-1));
nextBtn?.addEventListener('click', () => pageDeck(1));
deck?.addEventListener('scroll', updateNav, { passive: true });
addEventListener('resize', updateNav);
updateNav();
// Ambient: the hovered card's visible art bleeds into the page ground.
const ambient = document.getElementById('ambient');
document.querySelectorAll('.card').forEach(card => {
const hero = card.querySelector('.face.front .media img');
if (!hero) return;
card.addEventListener('mouseenter', () => { ambient.style.backgroundImage = 'url("' + hero.getAttribute('src') + '")'; ambient.style.opacity = '1'; });
card.addEventListener('mouseenter', () => {
const art = card.querySelector('.face.front .media img:not([hidden])') || card.querySelector('.face.front .pip img');
if (!art || !art.getAttribute('src')) return;
ambient.style.backgroundImage = 'url("' + art.getAttribute('src') + '")'; ambient.style.opacity = '1';
});
card.addEventListener('mouseleave', () => { ambient.style.opacity = '0'; });
});
@@ -476,8 +753,8 @@ function page() {
e.stopPropagation();
const card = b.closest('.card');
const face = card.classList.contains('flipped') ? '.face.back' : '.face.front';
const img = card.querySelector(face + ' .media img');
if (!img) return;
const img = card.querySelector(face + ' .media img:not([hidden])');
if (!img || !img.getAttribute('src')) return;
lightboxImg.src = img.getAttribute('src');
lightbox.hidden = false;
requestAnimationFrame(() => lightbox.classList.add('open'));
@@ -546,10 +823,10 @@ const server = http.createServer((req, res) => {
res.end(JSON.stringify({ ready: Boolean(pending && fs.existsSync(pending)) }));
return;
}
const imageMatch = req.method === 'GET' && req.url?.match(/^\/img\/(\d+)$/);
const imageMatch = req.method === 'GET' && req.url?.match(/^\/img\/(\d+)(?:\?.*)?$/);
if (imageMatch) {
const abs = localImages[Number(imageMatch[1])];
if (!abs) { res.writeHead(404); res.end(); return; }
if (!abs || !fs.existsSync(abs)) { res.writeHead(404); res.end(); return; }
const type = abs.endsWith('.webp') ? 'image/webp'
: abs.endsWith('.png') ? 'image/png'
: abs.endsWith('.svg') ? 'image/svg+xml'
@@ -572,6 +849,7 @@ const server = http.createServer((req, res) => {
optionId: parsed.optionId ?? null,
steer: parsed.steer ?? '',
...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}),
...(chosen?.sketch ? { sketch: chosen.sketch } : {}),
});
const isReroll = parsed.optionId === 'reroll';
if (detachedKey) {
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 4.0.2
version: 4.0.3
user-invocable: true
argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]"
license: Apache 2.0
@@ -23,7 +23,7 @@ Page scaffolds:
- Same-size cards of icon plus heading plus text as the page structure. Cards are the lazy container; nested cards are always wrong.
- The hero-metric template: big number, small label, supporting stats, accent.
- A tracked uppercase eyebrow over every section. One named kicker is a system; an eyebrow everywhere is grammar you did not choose.
- A kicker or eyebrow above a heading. This one is a ban, not a default: no brief earns it back. The heading carries its own weight; delete the label and let the heading speak.
- Section numbers (01 / 02 / 03) unless the sequence itself carries information the reader needs.
- A modal for a task that needs neither interruption nor protected focus.
@@ -11,6 +11,10 @@ Your job is production cleanup, not new art direction. Work only from the approv
Do not redesign. Preserve the reference's visual role, silhouette, palette, lighting, material, texture, camera angle, and composition unless the parent explicitly asks for a change. Preserve perspective only when it belongs to the object or scene itself; if CSS should create the card transform, shadow, rounded clipping, border, or layout, remove that presentation chrome from the raster.
## Decision Sketches
When the parent hands you a decision card packet instead of an approved mock, the job is one sketch: one card, one file, written to the card's declared `sketch` path the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a sketch is reported back, not padded from imagination. Render through the parent's shared frame: the requested surface's first viewport as a flat, matte design sketch in the card's own palette and type character, deliberately unfinished, no photorealism, no gloss. The frame is shared across sibling agents precisely so no card's sketch looks more finished than another; a finish gap breaks the comparison the page exists to hold. The only legible text is the product's real name and one real headline; greek every other text region into indistinct lines, because an invented spec, price, or date in a sketch is a claim PRODUCT.md never made. Return one line naming the path and any deviation, nothing more.
## Input Contract
Expect:
@@ -5,6 +5,8 @@ This harness has no subagent capability, so you are running this role inline. St
You record a project's design system after the build is done. Ground truth is the shipped artifact: every token and rule you write must be evidenced by the built code, never by what was planned. Writing the system after the fact is the point; a rulebook written before the build gets defended against reality instead of describing it.
You run under a hard turn ceiling that ends the run without warning, and a run that ends before DESIGN.md is written has recorded nothing. Batch several Reads into each turn, take `reference/document.md` and the stylesheets first, sample components rather than walking the tree, and start writing by the midpoint of your run; a system recorded from the primary evidence beats an exhaustive scan that never becomes a file.
## Input Contract
Expect: the project root; the artifact path(s); the direction contract text (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; the path to the skill's `reference/document.md`; and the boundary to write at (project or app root). An existing DESIGN.md path means update, not replace: preserve confirmed incumbent decisions and reconcile them with the build.
@@ -7,20 +7,26 @@ You are the finishing reviewer for an Impeccable build: fresh eyes on a done art
You have no browser. Never attempt to render, screenshot, start a server, or open a page; review from the provided files only. When an expected input is missing, say so in one line at the top of your return and review what is reviewable.
You run under a hard turn ceiling that ends the run without warning, and a run that ends before the five sections are written returns nothing; a review built from what you saw beats a perfect review that never arrives. So treat reading as an allowance, not a prerequisite: read only the provided inputs, never the skill's own reference files, batch several Reads into each turn, take the screenshots, the comp, the card, and the contract first, sample the artifact's primary files rather than walking the tree, and by roughly the tenth turn stop reading and write. Name whatever went unread in the line above the sections.
## Input Contract
Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path. When the harness can view images, open the screenshots, the comp, and the card before judging.
Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: the contract is the builder's abstraction of the comp, and a review anchored on it inherits whatever that abstraction dropped.
## Checks, in order
1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding.
2. **Fidelity.** Against the approved comp: topology, reading order, focal scale, overlaps and z-order, density, signature geometry. Name every drift and classify it intentional or defect. The comp governs composition; it is not a pixel spec for semantics, accessibility, or responsive behavior.
2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement.
3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. The card governs commitment and finish, never composition.
4. **Contract, promise by promise.** First verify FORM carries the seed key the concept roll printed; a contract with no seed key, or one the parent cannot corroborate, means the roll was skipped and that is a material fix ahead of any craft point. Then, for each of the five blocks, does the render keep the promise? Apply the memory test to the first viewport.
5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one.
5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one, and every produced asset visibly present in the screenshots; an asset applied at near-zero opacity or buried behind other paint is a compliance token, not a shipped material.
Do not run a second detector pass; mechanical findings belong to the parent's hooks.
## Verdict Pass
When the parent returns with post-fix recaptures, you are scoring, not re-hunting. For each material fix from your review, one line: resolved, partial, or unresolved, tied to what the new screenshots visibly show; a fix answered mechanically, positions moved but the quality the finding named still absent, is partial at best. Then name at most three regressions the fix batch itself introduced, judged by the same matrix rules, and nothing else; no new hunt, no new checks. Return exactly two sections: `verdict` (the scored list) and `remaining` (what stays open, or "clear").
## Output Contract
Return exactly five sections: `persistence` (pass/fail with specifics), `fidelity` (drift list with intentional/defect per item, or "faithful"), `ceiling` (unused native devices, or "reached"), `material_fixes` (ordered, most material first, each one line tied to a check or contract promise, at most eight), and `keep` (one line naming what must not be diluted while fixing). Missing inputs are named in one line above the sections. No praise, no summary prose.
Return exactly five sections: `persistence` (pass/fail with specifics), `fidelity` (the element matrix: match, adaptation, missing, contradicted, or added without approval per salient element, adaptations citing their evidence, or "faithful"), `ceiling` (unused native devices, or "reached"), `material_fixes` (ordered, most material first, fidelity failures ahead of craft, each one line tied to a check or contract promise, at most eight), and `keep` (one line naming what must not be diluted while fixing). Missing inputs are named in one line above the sections. No praise, no summary prose.
@@ -44,9 +44,11 @@ The script assigns which structure gets built: your top-ranked structure is what
2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, the notation, publications, identity programs, data graphics, and interfaces it reads daily, not only its physical objects; a nameable abstract system (a school of poster, a documentation standard, a data-graphic tradition) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; the audience's world is larger than that, so dig until the list spans at least three families.
3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience.
4. Run `node .github/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode <mode>` and follow what it prints. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point.
5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the one or two fused challengers that survived the weighing as named alternates with a one-line case each, plus re-roll with an optional one-line steer. What you never present is a ranked menu of your own grounded candidates; a lineup of those invites the safest card, while dealt challengers carry no such rut. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option.
5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. What you never present is a ranked menu of your own grounded candidates; a lineup of those invites the safest card, while dealt challengers carry no such rut. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option.
The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Run `node .github/skills/impeccable/scripts/serve-question.mjs --start --payload <file>` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key <key>`, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry.
The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, so a grounded direction is never a wall of text against rendered art, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you, so it is never yours to recommend. Run `node .github/skills/impeccable/scripts/serve-question.mjs --start --payload <file>` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key <key>`, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry.
When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land, so generation never delays the question. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once, so the whole set lands in roughly the time of one. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version.
Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images for exactly this reason (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product.
@@ -98,6 +100,6 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi
Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary.
After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, and the QUALITY BAR card and approved comp paths. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. Apply the material fixes in one batch, rebuild once, and stop; the reviewer ran so that you do not re-open your own hunt. Do not run a second detector.
After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, and the QUALITY BAR card and approved comp paths. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. Apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get exactly one more batch, recapture, and verdict; two correction rounds is the ceiling, the second verdict ends the work whatever it says, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included: presenting mechanical confirmation as artistic success is how a failed build gets announced as a finished one. Do not run a second detector.
Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded.
@@ -6,7 +6,7 @@ The purpose of a probe is to test composition, narrative, hierarchy, density, fo
## Generate three compositional options
Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session; record the approved comp's path in the surface brief. Base them on the real content and the surface concepts already developed with the user. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building.
Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session; record the approved comp's path in the surface brief. Base them on the real content and the surface concepts already developed with the user. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. A decision-page sketch is not a probe: it chose the direction at deliberately unfinished fidelity, so the three comps render regardless, and the chosen card's sketch seeds at most one of them.
- When the user shortlisted multiple concepts, spread the three across them.
- When one direction is committed, vary the structural uncertainty an image can resolve: topology, sequence, density, hierarchy, focal composition, or interaction framing.
@@ -25,11 +25,11 @@ After approval, summarize the composition and the parts of the comp that must no
## Inventory implementation fidelity
Before building, inventory the approved comp's major visible ingredients in writing (a short table in the surface brief or working notes; the finish reviewer audits shipped assets against it) and choose an implementation medium for each: semantic HTML/CSS/SVG, existing project asset, generated raster, sourced raster, icon library, canvas/WebGL, or accepted omission. Textures, portraits, and scenes are raster-by-default; a CSS gradient is not a texture medium. Every `produce` entry is produced before the build ships, through the asset producer or in the current thread; an inventory with unproduced entries is an unfinished build, and this gate is where imagery-free pages come from when it is skipped.
Before building, inventory the approved comp's major visible ingredients in writing (a short table in the surface brief or working notes; the finish reviewer audits shipped assets against it) and choose an implementation medium for each: semantic HTML/CSS/SVG, existing project asset, generated raster, sourced raster, icon library, canvas/WebGL, or accepted omission. The same written inventory names the comp's compositional commitments: navigation items and icons, headline levels and their scale relationship, signature geometry such as seams, masks, and overlaps, and each section's arrangement and density. An element never written down is the element the build silently drops, and the direction contract's 150 words cannot carry this list, so this inventory is where it lives. Textures, portraits, and scenes are raster-by-default; a CSS gradient is not a texture medium. Every `produce` entry is produced before the build ships, through the asset producer or in the current thread; an inventory with unproduced entries is an unfinished build, and this gate is where imagery-free pages come from when it is skipped.
Pay special attention to the dominant composition, signature use, image-native content, second-fold system, and any interaction the still image only implies. If the concept depends on a photograph, architectural scene, product object, portrait, or other raster-native material, do not silently replace it with generic CSS scenery.
Treat the comp as a north star, not something to trace. Do not rasterize core UI text or controls. Do not substitute a different visual driver after approval without asking.
Treat the comp as a north star, not something to trace, and know what that allows: translation into semantic, responsive, accessible code, never recomposition. Keeping the palette and mood while redrawing the topology is a second art direction, not an adaptation. Do not rasterize core UI text or controls. Do not substitute a different visual driver after approval without asking.
## Produce only the assets the build needs
@@ -206,7 +206,13 @@ ${grammar}
// returns no staging. Re-rolls exclude every earlier set until the pool runs out.
export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) {
const pool = sourceCompositions ?? requireLocalConcepts().compositions;
// Stagings honour the same breadth gate as worlds: a staging too specific to
// serve an arbitrary build stays approved for direct briefs and leaves the
// challenger pool. Falls back to the full approved set rather than returning
// nothing if every approved staging is marked niche.
let approved = pool.filter(composition => composition.status === 'approved');
const broad = approved.filter(composition => composition.review?.breadth !== 'niche');
if (broad.length > 0) approved = broad;
if (approved.length === 0) return [];
if (mode) {
const matching = approved.filter(composition => composition.surface === mode);
@@ -271,12 +277,19 @@ export function selectApprovedChallengers({ scope, key, reroll = 0, sourceConcep
// graphic systems beside instrument languages and atmosphere worlds, with
// the second pick preferring a different family for diversity. Tier order
// in the rendered list is rolled too, to avoid positional bias.
// Approval ratings weight the draw: a 3-star world earns a second ticket
// (roughly double odds), a 1-star keeps its approval for direct briefs but
// leaves the challenger pool unless a tier has nothing else.
// Two separate axes, and both can exclude. Rating grades quality: a 3-star
// earns a second ticket, a 1-star marginal keep leaves the pool. Breadth says
// whether a world can serve an arbitrary build at all, so a niche world
// leaves the pool however good it is. Breadth was split out of rating because
// the only way to hold a narrow world back used to be calling it marginal,
// which made "excellent but narrow" unrecordable and corrupted the ratings as
// a calibration signal for the next authoring round.
const ticketsFor = pool => pool.flatMap(concept => {
const rating = concept.review?.rating;
if (rating === 1) return [];
// Two independent exclusions: a marginal world is too weak to challenge,
// a niche world too narrow. Either one keeps its approval for direct
// briefs and leaves the pool.
if (rating === 1 || concept.review?.breadth === 'niche') return [];
return rating === 3
? [{ concept, ticket: 0 }, { concept, ticket: 1 }]
: [{ concept, ticket: 0 }];
@@ -1529,7 +1529,7 @@ if (IS_BROWSER) {
addBrowserFindings(groupMap, document.body, typoFindings);
}
const sectionKickerFindings = checkRepeatedSectionKickersDOM()
const sectionKickerFindings = checkKickerAboveHeadingDOM()
.map(f => ({ type: f.id, detail: f.snippet }))
.filter(f => _ruleOk(f.type));
if (sectionKickerFindings.length > 0) {
@@ -309,15 +309,14 @@ const ANTIPATTERNS = [
skillGuideline: 'tiny uppercase tracked label above the hero headline',
},
{
id: 'repeated-section-kickers',
id: 'kicker-above-heading',
category: 'slop',
scopes: ['type'],
severity: 'advisory',
name: 'Repeated section kicker labels',
name: 'Kicker / eyebrow label above heading',
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.',
'A tiny tracked uppercase or small-caps label sitting as its own block directly above a heading is banned outright, repeated or not. Generated kickers never earn their place: the heading carries its own weight. Delete the label and let the heading speak; if the words matter, work them into the heading or the body.',
skillSection: 'Typography',
skillGuideline: 'repeated eyebrow or kicker labels as section scaffolding',
skillGuideline: 'kicker or eyebrow labels above headings',
},
{
id: 'numbered-section-labels',
@@ -1258,12 +1257,15 @@ function checkHeroEyebrow(opts) {
}];
}
function checkRepeatedSectionKickers(opts) {
const { candidates, minCount = 3 } = opts;
if (!Array.isArray(candidates) || candidates.length < minCount) return [];
// Outright ban: one kicker is one too many, so every collected candidate is
// a finding. The judgment lives in the candidate gate (isKickerCandidate) and
// the collector's context skips, not in a repetition count.
function checkKickerAboveHeading(opts) {
const { candidates } = opts;
if (!Array.isArray(candidates)) return [];
return candidates.map(candidate => ({
id: 'repeated-section-kickers',
snippet: `repeated section kicker "${candidate.kickerText}" before ${candidate.headingTag} "${candidate.headingText}" (${candidates.length} on page)`,
id: 'kicker-above-heading',
snippet: `kicker "${candidate.kickerText}" above ${candidate.headingTag} "${candidate.headingText}"`,
}));
}
@@ -3190,7 +3192,7 @@ function parseColorResolved(str, customPropMap) {
return parseAnyColor(resolved);
}
const REPEATED_KICKER_SKIP_SELECTOR = [
const KICKER_SKIP_SELECTOR = [
'nav',
'form',
'table',
@@ -3209,7 +3211,7 @@ const REPEATED_KICKER_SKIP_SELECTOR = [
'[data-impeccable-allow-kickers]',
].join(',');
const REPEATED_KICKER_CARD_CONTEXT_SELECTOR = [
const KICKER_CARD_CONTEXT_SELECTOR = [
'article',
'button',
'a',
@@ -3227,23 +3229,32 @@ function cleanInlineText(el) {
.trim();
}
function isRepeatedKickerCardContext(heading, kicker) {
const item = heading.closest?.(REPEATED_KICKER_CARD_CONTEXT_SELECTOR);
function isKickerCardContext(heading, kicker) {
const item = heading.closest?.(KICKER_CARD_CONTEXT_SELECTOR);
return Boolean(item && (!item.contains || item.contains(kicker)));
}
function isRepeatedKickerCandidate(opts) {
// Meta lines above headlines join category and date (or path crumbs) with
// separator glyphs, or carry a year. A kicker is one short phrase; metadata
// keeps its markers.
const KICKER_META_TEXT_RE = /[·•|]|\s[\/›»>]\s|\b(19|20)\d{2}\b/;
// Legal and document numbering: "Section 4.2", "Article IX", "§ 12.3",
// dotted decimal outlines. The label identifies the clause, so it stays.
const KICKER_DOC_NUMBERING_RE = /^(§|\d+(\.\d+)+\b|(section|article|clause|appendix|exhibit|schedule|chapter|part|rule|title)\s+([\divxlc]+\b|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)\b)/i;
function isKickerCandidate(opts) {
const {
headingTag,
headingLevel,
headingText,
headingFontSize,
kickerTag,
kickerText,
kickerTextTransform,
kickerFontVariant,
kickerFontSize,
kickerLetterSpacing,
} = opts;
if (!['h2', 'h3', 'h4'].includes(headingTag)) return false;
if (!headingLevel || headingLevel > 4) return false;
if (!headingText || headingText.length < 3) return false;
if (/^\/[\w-]+/i.test(headingText.replace(/^"|"$/g, '').trim())) return false;
if (!(headingFontSize >= 20)) return false;
@@ -3251,9 +3262,13 @@ function isRepeatedKickerCandidate(opts) {
if (!['p', 'span', 'div', 'small'].includes(kickerTag)) return false;
if (!kickerText || kickerText.length < 2 || kickerText.length > 34) return false;
if (/^step\s*\d+/i.test(kickerText) || /^\d{1,2}$/.test(kickerText)) return false;
if (KICKER_META_TEXT_RE.test(kickerText)) return false;
if (KICKER_DOC_NUMBERING_RE.test(kickerText)) return false;
const isSmallCaps = /small-caps/.test(kickerFontVariant || '');
const isUppercased = kickerTextTransform === 'uppercase'
|| (/[A-Z]/.test(kickerText) && !/[a-z]/.test(kickerText));
|| (/[A-Z]/.test(kickerText) && !/[a-z]/.test(kickerText))
|| isSmallCaps;
if (!isUppercased) return false;
if (!(kickerFontSize > 0 && kickerFontSize <= 14)) return false;
const minTrackedSpacing = Math.max(1, kickerFontSize * 0.08);
@@ -3261,37 +3276,64 @@ function isRepeatedKickerCandidate(opts) {
return true;
}
function collectRepeatedSectionKickerCandidates(doc, getStyle, resolveLetterSpacing) {
// Resolve a heading level for the anchor element: 1-4 for h1-h4, aria-level
// (default 2) for role="heading" elements, 0 otherwise.
function kickerHeadingLevel(heading) {
const tag = heading.tagName.toLowerCase();
const byTag = /^h([1-6])$/.exec(tag);
if (byTag) return parseInt(byTag[1], 10);
const role = heading.getAttribute?.('role') || '';
if (role.toLowerCase() !== 'heading') return 0;
const ariaLevel = parseInt(heading.getAttribute?.('aria-level') || '', 10);
return Number.isFinite(ariaLevel) && ariaLevel >= 1 ? ariaLevel : 2;
}
function collectKickerCandidates(doc, getStyle, resolveLetterSpacing) {
const candidates = [];
for (const heading of doc.querySelectorAll('h2, h3, h4')) {
if (heading.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
for (const heading of doc.querySelectorAll('h1, h2, h3, h4, [role="heading"]')) {
const headingLevel = kickerHeadingLevel(heading);
if (!headingLevel || headingLevel > 4) continue;
if (heading.closest?.(KICKER_SKIP_SELECTOR)) continue;
// Application contexts (tab panels, dialogs) use compact context labels
// above headings to describe state, not to decorate. Same carve-out the
// hero-eyebrow rule makes.
if (heading.closest?.('[role="tabpanel"], [role="dialog"], [role="application"], dialog')) continue;
const kicker = heading.previousElementSibling;
if (!kicker || kicker.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
if (isRepeatedKickerCardContext(heading, kicker)) continue;
if (!kicker || kicker.closest?.(KICKER_SKIP_SELECTOR)) continue;
if (isKickerCardContext(heading, kicker)) continue;
const headingStyle = getStyle(heading);
const kickerStyle = getStyle(kicker);
const headingTag = heading.tagName.toLowerCase();
const headingText = (heading.textContent || '').replace(/\s+/g, ' ').trim();
const kickerText = cleanInlineText(kicker) || (kicker.textContent || '').replace(/\s+/g, ' ').trim();
const headingFontSize = resolveLetterSpacing(headingStyle.fontSize || '', 16) || parseFloat(headingStyle.fontSize) || 0;
const kickerFontSize = resolveLetterSpacing(kickerStyle.fontSize || '', 16) || parseFloat(kickerStyle.fontSize) || 0;
const kickerLetterSpacing = resolveLetterSpacing(kickerStyle.letterSpacing || '', kickerFontSize);
if (!isRepeatedKickerCandidate({
headingTag: heading.tagName.toLowerCase(),
if (!isKickerCandidate({
headingLevel,
headingText,
headingFontSize,
kickerTag: kicker.tagName.toLowerCase(),
kickerText,
kickerTextTransform: kickerStyle.textTransform || '',
kickerFontVariant: `${kickerStyle.fontVariant || ''} ${kickerStyle.fontVariantCaps || ''}`,
kickerFontSize,
kickerLetterSpacing,
})) {
continue;
}
// A tracked-caps eyebrow above a hero-scale h1 belongs to
// hero-eyebrow-chip (which also covers the accent-bold and dash-prefix
// stylings there). Stand down so one element gets one finding.
if (headingTag === 'h1' && headingFontSize >= 48 && kickerLetterSpacing >= 1.6) {
continue;
}
candidates.push({
headingTag: heading.tagName.toLowerCase(),
headingTag,
headingText: headingText.replace(/^"|"$/g, '').slice(0, 60),
kickerText: kickerText.slice(0, 40),
});
@@ -3299,17 +3341,17 @@ function collectRepeatedSectionKickerCandidates(doc, getStyle, resolveLetterSpac
return candidates;
}
function checkRepeatedSectionKickersDOM() {
const candidates = collectRepeatedSectionKickerCandidates(
function checkKickerAboveHeadingDOM() {
const candidates = collectKickerCandidates(
document,
(el) => getComputedStyle(el),
(value, fontSize) => resolveLengthPx(value, fontSize) || 0,
);
return checkRepeatedSectionKickers({ candidates });
return checkKickerAboveHeading({ candidates });
}
// ── Numbered section labels ─────────────────────────────────────────────────
// Sibling of the repeated-kicker rule: instead of a tracked uppercase word,
// Sibling of the kicker-above-heading rule: instead of a tracked uppercase word,
// the section scaffold is a tiny numeric index riding beside each section
// heading — bare and zero-padded, or an index joined to a short micro-label
// by a separator glyph. The kicker rule deliberately excludes bare 1-2 digit
@@ -3363,7 +3405,7 @@ function collectNumberedSectionLabelCandidates(doc, getStyle, resolveLetterSpaci
const candidates = [];
const seenLabels = new Set();
for (const heading of doc.querySelectorAll('h2, h3, h4')) {
if (heading.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
if (heading.closest?.(KICKER_SKIP_SELECTOR)) continue;
// The index sits either directly before the heading, or before the
// wrapper the heading leads (label | <div><h2>…</h2>…</div>).
let label = heading.previousElementSibling;
@@ -3373,9 +3415,9 @@ function collectNumberedSectionLabelCandidates(doc, getStyle, resolveLetterSpaci
if (firstChild === heading) label = parent.previousElementSibling;
}
if (!label || seenLabels.has(label)) continue;
if (label.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
if (label.closest?.(KICKER_SKIP_SELECTOR)) continue;
if (HEADING_TAGS.has(label.tagName.toLowerCase())) continue;
if (isRepeatedKickerCardContext(heading, label)) continue;
if (isKickerCardContext(heading, label)) continue;
const labelText = cleanInlineText(label) || (label.textContent || '').replace(/\s+/g, ' ').trim();
const parsed = parseNumberedLabelText(labelText);
@@ -4548,13 +4590,13 @@ function checkElementHeroEyebrow(el, style, tag, window, customPropMap) {
});
}
function checkRepeatedSectionKickersFromDoc(doc, win) {
const candidates = collectRepeatedSectionKickerCandidates(
function checkKickerAboveHeadingFromDoc(doc, win) {
const candidates = collectKickerCandidates(
doc,
(el) => win.getComputedStyle(el),
(value, fontSize) => resolveLengthPx(value, fontSize) || 0,
);
return checkRepeatedSectionKickers({ candidates });
return checkKickerAboveHeading({ candidates });
}
function checkElementMotion(tag, style) {
@@ -7712,7 +7754,7 @@ if (IS_BROWSER) {
addBrowserFindings(groupMap, document.body, typoFindings);
}
const sectionKickerFindings = checkRepeatedSectionKickersDOM()
const sectionKickerFindings = checkKickerAboveHeadingDOM()
.map(f => ({ type: f.id, detail: f.snippet }))
.filter(f => _ruleOk(f.type));
if (sectionKickerFindings.length > 0) {
@@ -223,7 +223,7 @@ function unwrapCssAtLayer(source) {
// ---------------------------------------------------------------------------
const STATIC_INHERITED_PROPS = new Set([
'color', 'fontFamily', 'fontSize', 'fontStyle', 'fontWeight',
'color', 'fontFamily', 'fontSize', 'fontStyle', 'fontWeight', 'fontVariant',
'lineHeight', 'letterSpacing', 'textTransform', 'textAlign', 'hyphens',
'webkitHyphens',
]);
@@ -252,6 +252,7 @@ const STATIC_DEFAULT_STYLE = {
fontFamily: '',
fontSize: '16px',
fontStyle: 'normal',
fontVariant: 'normal',
fontWeight: '400',
lineHeight: 'normal',
letterSpacing: 'normal',
@@ -27,11 +27,11 @@ import {
checkElementRadialSpotlight,
checkCreamPalette,
checkHtmlPatterns,
checkKickerAboveHeadingFromDoc,
checkNumberedSectionLabelsFromDoc,
checkPageLayout,
checkPageQualityFromDoc,
checkRepeatedContainerTextFromDoc,
checkRepeatedSectionKickersFromDoc,
resolveBackground,
resolveBorderRadiusPx,
} from '../../rules/checks.mjs';
@@ -202,7 +202,7 @@ async function detectHtml(filePath, options = {}) {
for (const f of runPageCheck('typography-rules', () => checkStaticPageTypography(document, window))) {
findings.push(finding(f.id, filePath, f.snippet));
}
for (const f of runPageCheck('repeated-section-kickers', () => checkRepeatedSectionKickersFromDoc(document, window))) {
for (const f of runPageCheck('kicker-above-heading', () => checkKickerAboveHeadingFromDoc(document, window))) {
findings.push(finding(f.id, filePath, f.snippet));
}
for (const f of runPageCheck('numbered-section-labels', () => checkNumberedSectionLabelsFromDoc(document, window))) {
@@ -198,15 +198,14 @@ const ANTIPATTERNS = [
skillGuideline: 'tiny uppercase tracked label above the hero headline',
},
{
id: 'repeated-section-kickers',
id: 'kicker-above-heading',
category: 'slop',
scopes: ['type'],
severity: 'advisory',
name: 'Repeated section kicker labels',
name: 'Kicker / eyebrow label above heading',
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.',
'A tiny tracked uppercase or small-caps label sitting as its own block directly above a heading is banned outright, repeated or not. Generated kickers never earn their place: the heading carries its own weight. Delete the label and let the heading speak; if the words matter, work them into the heading or the body.',
skillSection: 'Typography',
skillGuideline: 'repeated eyebrow or kicker labels as section scaffolding',
skillGuideline: 'kicker or eyebrow labels above headings',
},
{
id: 'numbered-section-labels',
@@ -456,12 +456,15 @@ function checkHeroEyebrow(opts) {
}];
}
function checkRepeatedSectionKickers(opts) {
const { candidates, minCount = 3 } = opts;
if (!Array.isArray(candidates) || candidates.length < minCount) return [];
// Outright ban: one kicker is one too many, so every collected candidate is
// a finding. The judgment lives in the candidate gate (isKickerCandidate) and
// the collector's context skips, not in a repetition count.
function checkKickerAboveHeading(opts) {
const { candidates } = opts;
if (!Array.isArray(candidates)) return [];
return candidates.map(candidate => ({
id: 'repeated-section-kickers',
snippet: `repeated section kicker "${candidate.kickerText}" before ${candidate.headingTag} "${candidate.headingText}" (${candidates.length} on page)`,
id: 'kicker-above-heading',
snippet: `kicker "${candidate.kickerText}" above ${candidate.headingTag} "${candidate.headingText}"`,
}));
}
@@ -2388,7 +2391,7 @@ function parseColorResolved(str, customPropMap) {
return parseAnyColor(resolved);
}
const REPEATED_KICKER_SKIP_SELECTOR = [
const KICKER_SKIP_SELECTOR = [
'nav',
'form',
'table',
@@ -2407,7 +2410,7 @@ const REPEATED_KICKER_SKIP_SELECTOR = [
'[data-impeccable-allow-kickers]',
].join(',');
const REPEATED_KICKER_CARD_CONTEXT_SELECTOR = [
const KICKER_CARD_CONTEXT_SELECTOR = [
'article',
'button',
'a',
@@ -2425,23 +2428,32 @@ function cleanInlineText(el) {
.trim();
}
function isRepeatedKickerCardContext(heading, kicker) {
const item = heading.closest?.(REPEATED_KICKER_CARD_CONTEXT_SELECTOR);
function isKickerCardContext(heading, kicker) {
const item = heading.closest?.(KICKER_CARD_CONTEXT_SELECTOR);
return Boolean(item && (!item.contains || item.contains(kicker)));
}
function isRepeatedKickerCandidate(opts) {
// Meta lines above headlines join category and date (or path crumbs) with
// separator glyphs, or carry a year. A kicker is one short phrase; metadata
// keeps its markers.
const KICKER_META_TEXT_RE = /[·•|]|\s[\/›»>]\s|\b(19|20)\d{2}\b/;
// Legal and document numbering: "Section 4.2", "Article IX", "§ 12.3",
// dotted decimal outlines. The label identifies the clause, so it stays.
const KICKER_DOC_NUMBERING_RE = /^(§|\d+(\.\d+)+\b|(section|article|clause|appendix|exhibit|schedule|chapter|part|rule|title)\s+([\divxlc]+\b|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)\b)/i;
function isKickerCandidate(opts) {
const {
headingTag,
headingLevel,
headingText,
headingFontSize,
kickerTag,
kickerText,
kickerTextTransform,
kickerFontVariant,
kickerFontSize,
kickerLetterSpacing,
} = opts;
if (!['h2', 'h3', 'h4'].includes(headingTag)) return false;
if (!headingLevel || headingLevel > 4) return false;
if (!headingText || headingText.length < 3) return false;
if (/^\/[\w-]+/i.test(headingText.replace(/^"|"$/g, '').trim())) return false;
if (!(headingFontSize >= 20)) return false;
@@ -2449,9 +2461,13 @@ function isRepeatedKickerCandidate(opts) {
if (!['p', 'span', 'div', 'small'].includes(kickerTag)) return false;
if (!kickerText || kickerText.length < 2 || kickerText.length > 34) return false;
if (/^step\s*\d+/i.test(kickerText) || /^\d{1,2}$/.test(kickerText)) return false;
if (KICKER_META_TEXT_RE.test(kickerText)) return false;
if (KICKER_DOC_NUMBERING_RE.test(kickerText)) return false;
const isSmallCaps = /small-caps/.test(kickerFontVariant || '');
const isUppercased = kickerTextTransform === 'uppercase'
|| (/[A-Z]/.test(kickerText) && !/[a-z]/.test(kickerText));
|| (/[A-Z]/.test(kickerText) && !/[a-z]/.test(kickerText))
|| isSmallCaps;
if (!isUppercased) return false;
if (!(kickerFontSize > 0 && kickerFontSize <= 14)) return false;
const minTrackedSpacing = Math.max(1, kickerFontSize * 0.08);
@@ -2459,37 +2475,64 @@ function isRepeatedKickerCandidate(opts) {
return true;
}
function collectRepeatedSectionKickerCandidates(doc, getStyle, resolveLetterSpacing) {
// Resolve a heading level for the anchor element: 1-4 for h1-h4, aria-level
// (default 2) for role="heading" elements, 0 otherwise.
function kickerHeadingLevel(heading) {
const tag = heading.tagName.toLowerCase();
const byTag = /^h([1-6])$/.exec(tag);
if (byTag) return parseInt(byTag[1], 10);
const role = heading.getAttribute?.('role') || '';
if (role.toLowerCase() !== 'heading') return 0;
const ariaLevel = parseInt(heading.getAttribute?.('aria-level') || '', 10);
return Number.isFinite(ariaLevel) && ariaLevel >= 1 ? ariaLevel : 2;
}
function collectKickerCandidates(doc, getStyle, resolveLetterSpacing) {
const candidates = [];
for (const heading of doc.querySelectorAll('h2, h3, h4')) {
if (heading.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
for (const heading of doc.querySelectorAll('h1, h2, h3, h4, [role="heading"]')) {
const headingLevel = kickerHeadingLevel(heading);
if (!headingLevel || headingLevel > 4) continue;
if (heading.closest?.(KICKER_SKIP_SELECTOR)) continue;
// Application contexts (tab panels, dialogs) use compact context labels
// above headings to describe state, not to decorate. Same carve-out the
// hero-eyebrow rule makes.
if (heading.closest?.('[role="tabpanel"], [role="dialog"], [role="application"], dialog')) continue;
const kicker = heading.previousElementSibling;
if (!kicker || kicker.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
if (isRepeatedKickerCardContext(heading, kicker)) continue;
if (!kicker || kicker.closest?.(KICKER_SKIP_SELECTOR)) continue;
if (isKickerCardContext(heading, kicker)) continue;
const headingStyle = getStyle(heading);
const kickerStyle = getStyle(kicker);
const headingTag = heading.tagName.toLowerCase();
const headingText = (heading.textContent || '').replace(/\s+/g, ' ').trim();
const kickerText = cleanInlineText(kicker) || (kicker.textContent || '').replace(/\s+/g, ' ').trim();
const headingFontSize = resolveLetterSpacing(headingStyle.fontSize || '', 16) || parseFloat(headingStyle.fontSize) || 0;
const kickerFontSize = resolveLetterSpacing(kickerStyle.fontSize || '', 16) || parseFloat(kickerStyle.fontSize) || 0;
const kickerLetterSpacing = resolveLetterSpacing(kickerStyle.letterSpacing || '', kickerFontSize);
if (!isRepeatedKickerCandidate({
headingTag: heading.tagName.toLowerCase(),
if (!isKickerCandidate({
headingLevel,
headingText,
headingFontSize,
kickerTag: kicker.tagName.toLowerCase(),
kickerText,
kickerTextTransform: kickerStyle.textTransform || '',
kickerFontVariant: `${kickerStyle.fontVariant || ''} ${kickerStyle.fontVariantCaps || ''}`,
kickerFontSize,
kickerLetterSpacing,
})) {
continue;
}
// A tracked-caps eyebrow above a hero-scale h1 belongs to
// hero-eyebrow-chip (which also covers the accent-bold and dash-prefix
// stylings there). Stand down so one element gets one finding.
if (headingTag === 'h1' && headingFontSize >= 48 && kickerLetterSpacing >= 1.6) {
continue;
}
candidates.push({
headingTag: heading.tagName.toLowerCase(),
headingTag,
headingText: headingText.replace(/^"|"$/g, '').slice(0, 60),
kickerText: kickerText.slice(0, 40),
});
@@ -2497,17 +2540,17 @@ function collectRepeatedSectionKickerCandidates(doc, getStyle, resolveLetterSpac
return candidates;
}
function checkRepeatedSectionKickersDOM() {
const candidates = collectRepeatedSectionKickerCandidates(
function checkKickerAboveHeadingDOM() {
const candidates = collectKickerCandidates(
document,
(el) => getComputedStyle(el),
(value, fontSize) => resolveLengthPx(value, fontSize) || 0,
);
return checkRepeatedSectionKickers({ candidates });
return checkKickerAboveHeading({ candidates });
}
// ── Numbered section labels ─────────────────────────────────────────────────
// Sibling of the repeated-kicker rule: instead of a tracked uppercase word,
// Sibling of the kicker-above-heading rule: instead of a tracked uppercase word,
// the section scaffold is a tiny numeric index riding beside each section
// heading — bare and zero-padded, or an index joined to a short micro-label
// by a separator glyph. The kicker rule deliberately excludes bare 1-2 digit
@@ -2561,7 +2604,7 @@ function collectNumberedSectionLabelCandidates(doc, getStyle, resolveLetterSpaci
const candidates = [];
const seenLabels = new Set();
for (const heading of doc.querySelectorAll('h2, h3, h4')) {
if (heading.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
if (heading.closest?.(KICKER_SKIP_SELECTOR)) continue;
// The index sits either directly before the heading, or before the
// wrapper the heading leads (label | <div><h2>…</h2>…</div>).
let label = heading.previousElementSibling;
@@ -2571,9 +2614,9 @@ function collectNumberedSectionLabelCandidates(doc, getStyle, resolveLetterSpaci
if (firstChild === heading) label = parent.previousElementSibling;
}
if (!label || seenLabels.has(label)) continue;
if (label.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
if (label.closest?.(KICKER_SKIP_SELECTOR)) continue;
if (HEADING_TAGS.has(label.tagName.toLowerCase())) continue;
if (isRepeatedKickerCardContext(heading, label)) continue;
if (isKickerCardContext(heading, label)) continue;
const labelText = cleanInlineText(label) || (label.textContent || '').replace(/\s+/g, ' ').trim();
const parsed = parseNumberedLabelText(labelText);
@@ -3746,13 +3789,13 @@ function checkElementHeroEyebrow(el, style, tag, window, customPropMap) {
});
}
function checkRepeatedSectionKickersFromDoc(doc, win) {
const candidates = collectRepeatedSectionKickerCandidates(
function checkKickerAboveHeadingFromDoc(doc, win) {
const candidates = collectKickerCandidates(
doc,
(el) => win.getComputedStyle(el),
(value, fontSize) => resolveLengthPx(value, fontSize) || 0,
);
return checkRepeatedSectionKickers({ candidates });
return checkKickerAboveHeading({ candidates });
}
function checkElementMotion(tag, style) {
@@ -5393,7 +5436,7 @@ export {
checkItalicSerif,
isAccentColor,
checkHeroEyebrow,
checkRepeatedSectionKickers,
checkKickerAboveHeading,
checkMotion,
checkGlow,
scanCssTextForGlow,
@@ -5424,9 +5467,9 @@ export {
parseAnyColor,
parseColorResolved,
cleanInlineText,
isRepeatedKickerCandidate,
collectRepeatedSectionKickerCandidates,
checkRepeatedSectionKickersDOM,
isKickerCandidate,
collectKickerCandidates,
checkKickerAboveHeadingDOM,
parseNumberedLabelText,
isNumberedSectionLabelCandidate,
collectNumberedSectionLabelCandidates,
@@ -5458,7 +5501,7 @@ export {
checkElementIconTile,
checkElementItalicSerif,
checkElementHeroEyebrow,
checkRepeatedSectionKickersFromDoc,
checkKickerAboveHeadingFromDoc,
checkElementMotion,
checkElementGlow,
checkTypography,
@@ -24,21 +24,36 @@
* "label": "Fillmore Handbill",
* "kicker": "THE ROLL", // optional badge; the assigned option leads
* "lineage": "1966-71 Fillmore ...", // optional
* "body": "why it fits, first viewport, risk ...", // optional, plain text
* "hero": "https://... or /abs/path.webp", // optional image
* "thesis": "one line: the idea this direction owns", // optional
* "palette": ["#1a2f5e", "oklch(84% .19 80)", ...], // optional, rendered as chips
* "materials": ["letterpress", "newsprint"], // optional, rendered as tags
* "viewport": "one line: the first-viewport composition", // optional
* "case": "one line: the fusion verdict, honest", // optional
* "risk": "one line: the honest risk", // optional
* "body": "fallback prose when the structured fields are absent",
* "sketch": ".impeccable/sketches/assigned.webp", // optional; may not exist
* // yet: the page shimmer-waits and polls the
* // slot until the file lands, so serve first
* // and generate after
* "hero": "https://... or /abs/path.webp", // optional inspiration image;
* // rides picture-in-picture when a sketch exists
* "board": "https://... or /abs/path.webp" // optional secondary image
* }, ...
* ],
* "reroll": true, // adds a re-roll action (returns {"optionId":"reroll"})
* "canon": true, // adds the quiet "Play it straight" standing exit
* // (returns {"optionId":"canon"}); direction rounds only
* "canon": true, // adds the "Play it straight" standing exit;
* // direction rounds only (returns {"optionId":"canon"})
* "canonCard": { ... }, // optional: the standing exit as a full card with the
* // same anatomy (label, thesis, palette, sketch, ...);
* // rendered last and visually subordinate. Without it,
* // canon stays a quiet footer action.
* "steer": true // adds a free-text steer field returned with any answer
* }
*
* Options render as large cards: hero render first when present (the dealt
* catalog worlds already have cards; grounded directions may present text-only
* or a freshly generated mock). Local image paths are served by this server;
* nothing is uploaded anywhere.
* Options render as large cards: the sketch leads when present, with the
* inspiration image picture-in-picture; a hero alone renders full-bleed; a
* text-only direction gets its identity from the palette chips and tags.
* Local image paths are served by this server; nothing is uploaded anywhere.
*
* Modes:
* (default) block until answered; ANSWER on stdout; exit 0.
@@ -107,6 +122,9 @@ function printAnswer(raw) {
if (a.hero || a.board) {
console.log("CHOSEN CARD: open the chosen world's board and hero images now, before any code. When your harness only reads files, or runs sandboxed, download them INTO the workspace and open the relative path; a sandboxed viewer rejects absolute paths outside it. They set the craft bar the build must reach.");
}
if (a.sketch) {
console.log('CHOSEN SKETCH: the decision sketch at that path may seed one comp probe; the comp round still renders its full set, because a sketch chose the direction, not the composition.');
}
if (a.optionId === 'canon') {
console.log('CANON CHOSEN: the user picked the category standard on purpose. Ask once for two or three products this should sit alongside; their craft level becomes the quality bar. Execute the canon at full commitment, conventions embraced without irony or smuggled quirk.');
}
@@ -125,14 +143,15 @@ if (hasFlag('schema')) {
title: 'Choose the visual world',
question: 'The roll assigned Fillmore Handbill. Keep it, take an alternate, or re-roll.',
options: [
{ id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', body: 'Why it fits, the first viewport, the honest risk.', hero: 'https://impeccable.style/worlds/cards/fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/fillmore-handbill.webp' },
{ id: 'challenger-teletext', label: 'Teletext Service', lineage: 'broadcast teletext magazines', body: 'Fused alternate.', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' },
{ id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', sketch: '.impeccable/sketches/assigned.webp', hero: 'https://impeccable.style/worlds/cards/fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/fillmore-handbill.webp' },
{ id: 'challenger-teletext', label: 'Teletext Service', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', case: 'Fuses cleanly: releases map to numbered pages.', sketch: '.impeccable/sketches/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' },
],
reroll: true,
canon: true,
canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', viewport: 'The arrangement a visitor expects, at full craft.', sketch: '.impeccable/sketches/canon.webp' },
steer: true,
}, null, 2));
console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board accept URLs or local paths. canon adds a quiet standing "Play it straight" action for direction decisions: the user\'s explicit door to the category standard. Include it only for visual-direction rounds; never present canon as your own recommendation.');
console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/sketch accept URLs or local paths; sketch slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the sketch, never as the promise of the build. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability.');
process.exit(0);
}
@@ -203,14 +222,25 @@ if (hasFlag('start')) {
const key = arg('key') || Math.random().toString(16).slice(2, 10);
// In start mode the agent is alive and owns browser routing; the server
// only opens the system browser itself when --open forces it.
// The daemon's output lands in a per-key log so a startup failure can say
// what actually went wrong instead of only that it did.
const logFile = path.join(QUESTION_DIR, `${key}.log`);
const logFd = fs.openSync(logFile, 'a');
const child = spawn(process.execPath, [
fileURLToPath(import.meta.url), '--payload', payloadPath, '--detached-serve', '--key', key,
'--timeout', String(timeoutSec), ...(hasFlag('open') ? [] : ['--no-open']),
], { detached: true, stdio: 'ignore' });
], { detached: true, stdio: ['ignore', logFd, logFd] });
child.unref();
fs.closeSync(logFd);
const deadline = Date.now() + 8000;
while (Date.now() < deadline && !fs.existsSync(stateFile(key))) await new Promise((r) => setTimeout(r, 100));
if (!fs.existsSync(stateFile(key))) { console.error('serve-question: server failed to start'); process.exit(1); }
if (!fs.existsSync(stateFile(key))) {
let tail = '';
try { tail = fs.readFileSync(logFile, 'utf8').trim().split('\n').slice(-4).join('\n '); } catch { /* log never written */ }
console.error(`serve-question: server failed to start${tail ? `\n ${tail}` : ''}`);
console.error(`serve-question: log at ${path.relative(process.cwd(), logFile) || logFile}. A sandboxed exec that cannot listen on localhost causes exactly this; rerun this command once through the harness's network-enabled or unsandboxed command tool before falling back.`);
process.exit(1);
}
const state = JSON.parse(fs.readFileSync(stateFile(key), 'utf8'));
console.log(`QUESTION URL: ${state.url}`);
console.log(`QUESTION KEY: ${key}`);
@@ -244,12 +274,28 @@ function loadRound(json) {
localImages.push(abs);
return `/img/${localImages.length - 1}`;
};
// Sketches stream in after the page is served, so their slots register
// whether or not the file exists yet; /img answers 404 until it lands and
// the page polls the slot. Remote sketch URLs pass through untouched.
const sketchSrc = (value) => {
if (!value) return null;
if (/^https?:\/\//.test(value)) return value;
localImages.push(path.resolve(value));
return `/img/${localImages.length - 1}`;
};
payload = parsed;
options = parsed.options.map((option) => ({
const decorate = (option) => ({
...option,
heroSrc: imageSrc(option.hero),
boardSrc: imageSrc(option.board),
}));
sketchSrc: sketchSrc(option.sketch),
});
options = parsed.options.map(decorate);
// The standing exit as a full card: same anatomy, reserved id, rendered
// subordinate by the page. Without it, canon stays the quiet footer action.
if (parsed.canonCard && typeof parsed.canonCard === 'object') {
options = [...options, { ...decorate(parsed.canonCard), id: 'canon', isCanon: true }];
}
}
try { loadRound(raw); } catch (error) { console.error(`serve-question: ${error.message}`); process.exit(1); }
const detachedKey = hasFlag('detached-serve') ? arg('key') : null;
@@ -260,30 +306,81 @@ const esc = (s) => String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<
function page() {
const flipChip = (label) => `<button type="button" class="chip flip" aria-label="Flip the card"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4a8 8 0 1 1-8 8" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/><path d="M4 5.5V12h6.5" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg><span>${label}</span></button>`;
const expandChip = `<button type="button" class="chip expand" aria-label="Expand the image"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 9V4h5M20 15v5h-5M20 9V4h-5M4 15v5h5" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg></button>`;
const cards = options.map((option, index) => `
<article class="card" style="--fan:${index === 0 ? '0deg' : (index % 2 ? '1.4deg' : '-1.2deg')};--deal:${index * 90}ms" data-id="${esc(option.id)}">
<div class="card-inner">
<div class="face front${index === 0 ? ' lead' : ''}${option.heroSrc || option.boardSrc ? '' : ' text-only'}">
${option.kicker ? `<span class="kicker">${esc(option.kicker)}</span>` : ''}
${option.heroSrc || option.boardSrc ? `<div class="media">
// Structured anatomy: chips and one-line facts render when the payload
// carries them; a plain body falls back to the prose block. Palette chips
// and material tags give a text-only direction an immediate identity that
// no generation luck can distort.
const fact = (label, value, cls = '') => value ? `<p class="fact${cls ? ` ${cls}` : ''}"><span class="fact-label">${label}</span>${esc(value)}</p>` : '';
const hasBack = (option) => Boolean(option.viewport || option.case || (option.boardSrc && option.heroSrc));
const anatomy = (option) => {
const rows = [];
if (option.thesis) rows.push(`<p class="thesis">${esc(option.thesis)}</p>`);
const idBits = [];
if (Array.isArray(option.palette) && option.palette.length) {
idBits.push(`<span class="swatches">${option.palette.slice(0, 6).map((c) => `<i style="background:${esc(c)}" title="${esc(c)}"></i>`).join('')}</span>`);
}
if (Array.isArray(option.materials) && option.materials.length) {
idBits.push(option.materials.slice(0, 4).map((m) => `<span class="tag">${esc(m)}</span>`).join(''));
}
if (idBits.length) rows.push(`<div class="identity">${idBits.join('')}</div>`);
// The front carries only what the choice needs: thesis, identity, and the
// honest risk clamped to two lines. First viewport and the case read on
// the card's back; once the sketch lands, the first viewport is a picture.
rows.push(fact('Risk', option.risk, 'clamp'));
if (!option.thesis && option.body) rows.push(`<p class="detail">${esc(option.body)}</p>`);
else if (option.body && option.thesis && !hasBack(option)) rows.push(`<p class="detail more">${esc(option.body)}</p>`);
return rows.join('\n ');
};
const backFacts = (option) => [
fact('First viewport', option.viewport),
fact('The case', option.case),
fact('Risk', option.risk),
option.body && option.thesis ? `<p class="detail more">${esc(option.body)}</p>` : '',
].filter(Boolean).join('\n ');
const media = (option) => {
const inspiration = option.heroSrc ? `<figure class="pip" title="Inspiration: the world this direction draws from. Your page will not look like this image.">
<img src="${esc(option.heroSrc)}" alt="">
<figcaption>inspiration</figcaption>
</figure>` : '';
const details = hasBack(option) ? flipChip('Details') : '';
if (option.sketchSrc) {
return `<div class="media sketching" data-sketch="${esc(option.sketchSrc)}">
<div class="shimmer"><span class="sketch-note">sketching&hellip;</span></div>
<img class="sketch" alt="" hidden>
${inspiration}
<div class="chips">${expandChip}${details}</div>
</div>`;
}
if (option.heroSrc || option.boardSrc) {
return `<div class="media">
<img src="${esc(option.heroSrc || option.boardSrc)}" alt="">
<div class="chips">${expandChip}${option.boardSrc && option.heroSrc ? flipChip('Board') : ''}</div>
</div>` : ''}
<div class="chips">${expandChip}${details}</div>
</div>`;
}
return '';
};
const cards = options.map((option, index) => `
<article class="card${option.isCanon ? ' canon' : ''}" style="--fan:${index === 0 ? '0deg' : (index % 2 ? '1.4deg' : '-1.2deg')};--deal:${index * 90}ms" data-id="${esc(option.id)}">
<div class="card-inner">
<div class="face front${index === 0 ? ' lead' : ''}${media(option) ? '' : ' text-only'}">
${option.kicker ? `<span class="kicker">${esc(option.kicker)}</span>` : option.isCanon ? '<span class="kicker standing">The standing door</span>' : ''}
${media(option)}
<div class="body">
${option.lineage ? `<p class="tier">${esc(option.lineage)}</p>` : ''}
<h2>${esc(option.label)}</h2>
${option.body ? `<p class="detail">${esc(option.body)}</p>` : ''}
<button class="choose" data-id="${esc(option.id)}">Build this</button>
${anatomy(option)}
<button class="choose" data-id="${esc(option.id)}">${option.isCanon ? 'Play it straight' : 'Build this'}</button>
</div>
</div>
${option.boardSrc && option.heroSrc ? `<div class="face back${index === 0 ? ' lead' : ''}">
<div class="media back-media">
${hasBack(option) ? `<div class="face back${index === 0 ? ' lead' : ''}">
${option.boardSrc ? `<div class="media back-media">
<img src="${esc(option.boardSrc)}" alt="">
<div class="chips">${expandChip}${flipChip('Hero')}</div>
</div>
<div class="body back-bar">
<p class="tier">Design-system board &middot; ${esc(option.label)}</p>
<button class="choose" data-id="${esc(option.id)}">Build this</button>
<div class="chips">${expandChip}${flipChip('Front')}</div>
</div>` : `<div class="back-head"><p class="tier">The full read &middot; ${esc(option.label)}</p>${flipChip('Front')}</div>`}
<div class="body back-body">
${option.boardSrc ? `<p class="tier">The full read &middot; ${esc(option.label)}</p>` : ''}
${backFacts(option)}
<button class="choose" data-id="${esc(option.id)}">${option.isCanon ? 'Play it straight' : 'Build this'}</button>
</div>
</div>` : ''}
</div>
@@ -318,7 +415,7 @@ function page() {
--ks-mono: "SFMono-Regular", "Roboto Mono", "JetBrains Mono", Consolas, monospace;
}
* { box-sizing: border-box; margin: 0; }
body { background: var(--ks-lacquer); color: var(--ks-text); font: 15px/1.55 var(--ks-font); padding: 1.8rem clamp(1rem, 5vw, 4rem) 2rem; min-height: 100dvh; display: flex; flex-direction: column; }
body { background: var(--ks-lacquer); color: var(--ks-text); font: 15px/1.55 var(--ks-font); padding: 1.8rem clamp(1rem, 5vw, 4rem) 2rem; min-height: 100dvh; display: flex; flex-direction: column; overflow-x: clip; }
#ambient { position: fixed; inset: -40px; z-index: 0; background-size: cover; background-position: center; filter: blur(34px) saturate(1.05); opacity: 0; transition: opacity .55s ease, background-image .2s; pointer-events: none; }
#scrim { position: fixed; inset: 0; z-index: 0; background: linear-gradient(180deg, oklch(7% 0.006 95 / 0.62), oklch(7% 0.006 95 / 0.78)); pointer-events: none; }
header, main, footer { position: relative; z-index: 1; }
@@ -336,7 +433,42 @@ function page() {
.question { color: var(--ks-text-muted); margin-top: .7rem; max-width: 52rem; }
main { flex: 1; display: flex; align-items: center; width: 100%; max-width: 90rem; margin: 0 auto; }
.stage { width: 100%; display: flex; flex-direction: column; gap: 1.5rem; }
.grid { display: grid; gap: 1.6rem; grid-template-columns: repeat(auto-fit, minmax(min(23rem, 100%), 1fr)); width: 100%; }
/* The deck bleeds to the viewport edges while the first card aligns with the
content column; a carousel cut off at an invisible container edge reads as
a rendering bug, but one cut off at the screen edge reads as more cards. */
.deck-shell { position: relative; width: 100vw; margin-left: calc(50% - 50vw); }
/* One row in a wide viewport, one column in a tall one; the deck scrolls on
its axis with snap points and the arrows page it card by card. */
.grid { --deck-inset: max(clamp(1rem, 5vw, 4rem), calc((100vw - 90rem) / 2)); display: flex; gap: 1.6rem; width: 100%; overflow-x: auto; overflow-y: hidden; scroll-snap-type: x mandatory; scrollbar-width: none; padding: 6px var(--deck-inset); scroll-padding-inline: var(--deck-inset); align-items: stretch; }
.grid::-webkit-scrollbar { display: none; }
.grid > .card { flex: 0 0 clamp(20rem, 27vw, 27rem); scroll-snap-align: center; }
.nav { position: absolute; z-index: 6; width: 42px; height: 42px; display: flex; align-items: center; justify-content: center; border-radius: 50%; background: oklch(7% 0.006 95 / 0.78); border: 1px solid var(--ks-rule); color: var(--ks-kinpaku); cursor: pointer; backdrop-filter: blur(6px); transition: border-color .2s, color .2s, opacity .2s; }
.nav:hover { border-color: var(--ks-kinpaku-deep); color: var(--ks-kinpaku-pale); }
.nav[disabled] { opacity: .25; cursor: default; }
.nav[hidden] { display: none; }
.nav svg { width: 16px; height: 16px; }
.nav.prev { left: 14px; top: 50%; transform: translateY(-50%); }
.nav.next { right: 14px; top: 50%; transform: translateY(-50%); }
/* A side that hides more cards fades out; a hard edge means the end. */
.fade { position: absolute; z-index: 5; pointer-events: none; opacity: 0; transition: opacity .3s ease; }
.fade-prev { left: 0; top: 0; bottom: 0; width: 88px; background: linear-gradient(90deg, var(--ks-lacquer), transparent); }
.fade-next { right: 0; top: 0; bottom: 0; width: 88px; background: linear-gradient(270deg, var(--ks-lacquer), transparent); }
.deck-shell.can-prev .fade-prev { opacity: 1; }
.deck-shell.can-next .fade-next { opacity: 1; }
@media (max-aspect-ratio: 1/1) {
.grid { flex-direction: column; overflow-x: hidden; overflow-y: auto; scroll-snap-type: y mandatory; max-height: min(68dvh, 44rem); scroll-padding-block: 6px; }
.grid > .card { flex: 0 0 auto; }
/* In the vertical deck the pager is the primary way forward, so it grows
into a labeled pill instead of a bare chevron nobody notices. */
.nav { width: auto; height: 38px; border-radius: 19px; padding: 0 16px; gap: 8px; border-color: var(--ks-kinpaku-deep); background: oklch(7% 0.006 95 / 0.88); font-family: var(--ks-mono); font-size: .62rem; letter-spacing: .2em; text-transform: uppercase; }
.nav svg { transform: rotate(90deg); }
.nav.prev::after { content: "Back"; }
.nav.next::after { content: "More"; }
.nav.prev { left: 50%; top: 6px; transform: translate(-50%, 0); }
.nav.next { right: auto; left: 50%; top: auto; bottom: 6px; transform: translate(-50%, 0); }
.fade-prev { top: 0; left: 0; right: 0; bottom: auto; width: auto; height: 72px; background: linear-gradient(180deg, var(--ks-lacquer), transparent); }
.fade-next { top: auto; left: 0; right: 0; bottom: 0; width: auto; height: 72px; background: linear-gradient(0deg, var(--ks-lacquer), transparent); }
}
.card { position: relative; perspective: 1400px; transform: rotate(var(--fan, 0deg)); transition: transform .25s cubic-bezier(.16, 1, .3, 1); }
.card:hover { transform: rotate(0deg) translateY(-4px); }
.card-inner { position: relative; height: 100%; transform-style: preserve-3d; transition: transform .7s cubic-bezier(.16, 1, .3, 1); }
@@ -344,6 +476,12 @@ function page() {
.face { background: var(--ks-lacquer-raised); border: 1px solid var(--ks-rule); border-radius: 10px; box-shadow: 0 18px 40px oklch(0% 0 0 / 0.35); overflow: hidden; display: flex; flex-direction: column; backface-visibility: hidden; -webkit-backface-visibility: hidden; }
.face.front { position: relative; height: 100%; }
.face.back { position: absolute; inset: 0; transform: rotateY(180deg); }
/* Only the visible face is interactive: a hidden backface still hit-tests
in Chrome, so the front's pip would otherwise sit invisibly over the
back's chips, showing its zoom cursor and eating the flip-back click. */
.face.back { pointer-events: none; }
.card.flipped .face.back { pointer-events: auto; }
.card.flipped .face.front { pointer-events: none; }
.face.lead { border-color: var(--ks-kinpaku); box-shadow: 0 0 0 1px var(--ks-kinpaku), 0 18px 40px oklch(0% 0 0 / 0.45); }
.card:hover .face { border-color: var(--ks-kinpaku-deep); }
.card:hover .face.lead { border-color: var(--ks-kinpaku); }
@@ -355,7 +493,7 @@ function page() {
.face.text-only .body { padding-top: 12px; }
.media { position: relative; width: 100%; aspect-ratio: 16/9; flex: none; }
.media img { width: 100%; height: 100%; object-fit: cover; display: block; background: linear-gradient(100deg, var(--ks-graphite) 40%, var(--ks-graphite-2) 50%, var(--ks-graphite) 60%); }
.face.back { background: var(--ks-lacquer-deep); }
.face.back { background: var(--ks-lacquer-raised); }
.back-bar { margin-top: auto; background: var(--ks-lacquer-raised); }
.hero-blank { width: 100%; height: 100%; background: linear-gradient(100deg, var(--ks-graphite) 40%, var(--ks-graphite-2) 50%, var(--ks-graphite) 60%); }
.back-bar { flex: none; flex-direction: row; align-items: center; justify-content: space-between; gap: .8rem; }
@@ -367,6 +505,48 @@ function page() {
.tier { font-family: var(--ks-mono); font-size: .625rem; letter-spacing: .24em; text-transform: uppercase; color: var(--ks-text-faint); }
h2 { font-family: var(--ks-font); font-size: 1.125rem; font-weight: 500; line-height: 1.35; color: var(--ks-champagne); }
.detail { color: var(--ks-text-muted); font-size: .88rem; white-space: pre-wrap; }
.detail.more { font-size: .8rem; color: var(--ks-text-faint); }
.thesis { color: var(--ks-text); font-size: .95rem; line-height: 1.45; }
.identity { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin: 2px 0; }
.swatches { display: inline-flex; gap: 4px; margin-right: 4px; }
.swatches i { width: 18px; height: 18px; border-radius: 5px; border: 1px solid oklch(100% 0 0 / 0.18); box-shadow: inset 0 0 0 1px oklch(0% 0 0 / 0.25); }
.tag { font-family: var(--ks-mono); font-size: .6rem; letter-spacing: .14em; text-transform: uppercase; color: var(--ks-text-muted); border: 1px solid var(--ks-rule); border-radius: 4px; padding: 3px 7px; }
.fact { font-size: .8rem; color: var(--ks-text-muted); line-height: 1.45; }
.fact-label { display: inline-block; font-family: var(--ks-mono); font-size: .6rem; letter-spacing: .18em; text-transform: uppercase; color: var(--ks-text-faint); margin-right: .55em; transform: translateY(-1px); }
.fact.clamp { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
/* The back is the full read: first viewport, the case, the whole risk, and
the board when the world has one. */
.back-head { display: flex; align-items: center; justify-content: space-between; gap: .8rem; padding: 14px 14px 0; }
.media.back-media { aspect-ratio: 16/6; }
.media.back-media img { width: 100%; height: 100%; object-fit: cover; }
.body.back-body { overflow-y: auto; flex: 1; scrollbar-width: thin; }
/* Inspiration rides picture-in-picture: the catalog world explains where the
direction comes from without promising what the build will look like. */
/* Hovering the inspiration takes over the whole media region; the sketch is
the promise, the inspiration is a glance, so the glance must cost nothing. */
.pip { position: absolute; z-index: 2; left: 10px; bottom: 10px; margin: 0; width: 84px; height: 64px; border: 1px solid var(--ks-rule); border-radius: 6px; overflow: hidden; background: var(--ks-lacquer); cursor: zoom-in; transition: left .35s cubic-bezier(.16,1,.3,1), bottom .35s cubic-bezier(.16,1,.3,1), width .35s cubic-bezier(.16,1,.3,1), height .35s cubic-bezier(.16,1,.3,1), border-radius .35s ease; box-shadow: 0 6px 18px oklch(0% 0 0 / 0.45); }
.pip img { display: block; width: 100%; height: 100%; object-fit: cover; }
.pip figcaption { position: absolute; left: 0; right: 0; bottom: 0; font-family: var(--ks-mono); font-size: .5rem; letter-spacing: .2em; text-transform: uppercase; color: var(--ks-text); text-align: center; padding: 3px 0 4px; background: oklch(7% 0.006 95 / 0.72); backdrop-filter: blur(3px); }
.pip:hover { left: 0; bottom: 0; width: 100%; height: 100%; border-radius: 0; z-index: 3; }
.sketch-note { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-family: var(--ks-mono); font-size: .66rem; letter-spacing: .22em; text-transform: uppercase; color: var(--ks-text-faint); }
/* A stand-in is honest about being one: dimmed, labeled, and replaced by
the real sketch whenever it lands. */
.media.stand-in img.sketch { filter: brightness(.72) saturate(.85); }
.media.stand-in .pip { display: none; }
.stand-in-label { position: absolute; z-index: 2; left: 0; right: 0; bottom: 0; margin: 0; font-family: var(--ks-mono); font-size: .56rem; letter-spacing: .2em; text-transform: uppercase; color: var(--ks-text); text-align: center; padding: 4px 0 5px; background: oklch(7% 0.006 95 / 0.78); backdrop-filter: blur(3px); }
.media.sketching { position: relative; }
.media.sketching .shimmer { position: absolute; inset: 0; }
.media img.sketch { position: relative; z-index: 1; }
/* The generic .media img display:block would defeat [hidden] and float an
empty block over the shimmer; an unloaded sketch must truly not render. */
.media img[hidden] { display: none; }
/* The standing exit as a card: present with full anatomy, never dressed as a
contender. Graphite instead of kinpaku, and it never takes the lead ring. */
.card.canon .face { border-color: var(--ks-rule); background: var(--ks-graphite); }
.card.canon:hover .face { border-color: var(--ks-text-faint); }
.card.canon .kicker.standing { background: transparent; border: 1px solid var(--ks-rule); color: var(--ks-text-faint); }
.card.canon button.choose { background: transparent; color: var(--ks-text); border: 1px solid var(--ks-rule); }
.card.canon button.choose:hover { border-color: var(--ks-text-muted); background: var(--ks-graphite-2); }
button.choose { margin-top: auto; align-self: start; background: var(--ks-kinpaku); color: var(--ks-dark-ink); border: 0; font-family: var(--ks-font); font-size: 1rem; font-weight: 500; line-height: 1.35; padding: 10px 38px; border-radius: 6px; cursor: pointer; transition: background .15s; }
button.choose:hover { background: var(--ks-kinpaku-pale); }
footer { width: 100%; max-width: 90rem; margin: 1.6rem auto 0; display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; }
@@ -408,13 +588,19 @@ function page() {
<h1>${esc(payload.title || 'Choose a direction')}</h1>
</div>
${payload.question ? `<p class="question">${esc(payload.question)}</p>` : ''}
<div class="grid">${cards}</div>
<div class="deck-shell">
<div class="grid">${cards}</div>
<div class="fade fade-prev" aria-hidden="true"></div>
<div class="fade fade-next" aria-hidden="true"></div>
<button class="nav prev" hidden aria-label="Previous card"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14.5 5 8 12l6.5 7" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
<button class="nav next" hidden aria-label="Next card"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9.5 5 16 12l-6.5 7" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
</div>
</div>
</main>
<footer>
${payload.steer ? '<input id="steer" placeholder="Optional steer: what should be different or kept?">' : ''}
${payload.reroll ? '<button id="reroll"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="4" fill="none" stroke="currentColor" stroke-width="1.6"/><circle cx="8.4" cy="8.4" r="1.5" fill="currentColor"/><circle cx="15.6" cy="8.4" r="1.5" fill="currentColor"/><circle cx="8.4" cy="15.6" r="1.5" fill="currentColor"/><circle cx="15.6" cy="15.6" r="1.5" fill="currentColor"/><circle cx="12" cy="12" r="1.5" fill="currentColor"/></svg><span>Re-roll</span></button>' : ''}
${payload.canon ? '<button id="canon" title="Skip the roll: build the page this category ships, executed impeccably">Play it straight</button>' : ''}
${payload.canon && !payload.canonCard ? '<button id="canon" title="Skip the roll: build the page this category ships, executed impeccably">Play it straight</button>' : ''}
</footer>
<script>
const steer = () => document.getElementById('steer')?.value || '';
@@ -434,7 +620,11 @@ function page() {
// Deal from the stack: cards begin piled at the grid's center, blurred,
// then travel to their seats with a stagger.
const cards = [...document.querySelectorAll('.card')];
if (!matchMedia('(prefers-reduced-motion: reduce)').matches && cards.length) {
// The deal is decoration: a hidden tab throttles rAF, so never let the
// animation hold the cards at opacity 0. Skip it when hidden, and force
// the final state after a beat no matter what the animation did.
setTimeout(() => cards.forEach(c => { c.style.opacity = ''; c.style.transform = ''; c.style.filter = ''; c.style.transition = ''; c.style.zIndex = ''; }), 1600);
if (!matchMedia('(prefers-reduced-motion: reduce)').matches && cards.length && !document.hidden) {
const grid = document.querySelector('.grid').getBoundingClientRect();
const cx = grid.left + grid.width / 2, cy = grid.top + grid.height / 2;
cards.forEach((card, i) => {
@@ -460,12 +650,99 @@ function page() {
}));
}
// Ambient: the hovered card's hero bleeds into the page ground under a scrim.
// Sketches stream in after the deal: poll each slot until the file lands,
// then swap the shimmer for the image. Generation is genuinely slow and a
// sequential batch puts the last card many minutes out, so patience is the
// default: a slot only shows its inspiration as a stand-in when it has
// waited four minutes AND nothing has landed anywhere for four minutes, the
// stand-in is labeled as such, and polling continues so the real sketch
// still swaps in whenever it arrives. Progress anywhere resets patience.
const landTracker = { last: Date.now() };
document.querySelectorAll('.media.sketching').forEach(m => {
const url = m.dataset.sketch;
const img = m.querySelector('img.sketch');
const note = m.querySelector('.sketch-note');
const started = Date.now();
// A live elapsed count is the difference between "working" and "frozen".
const tick = setInterval(() => { if (note) note.textContent = 'sketching · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000);
const settle = () => { clearInterval(tick); m.classList.remove('sketching', 'stand-in'); m.querySelector('.shimmer')?.remove(); m.querySelector('.stand-in-label')?.remove(); };
const standIn = () => {
const pip = m.querySelector('.pip img');
if (!pip || m.classList.contains('stand-in')) return;
img.src = pip.getAttribute('src'); img.hidden = false;
m.classList.add('stand-in');
m.querySelector('.shimmer')?.remove();
clearInterval(tick);
const label = document.createElement('p');
label.className = 'stand-in-label';
label.textContent = 'inspiration · sketch pending';
m.appendChild(label);
};
const tryLoad = () => {
const probe = new Image();
probe.onload = () => { landTracker.last = Date.now(); img.src = probe.src; img.hidden = false; settle(); };
probe.onerror = () => {
const quiet = Date.now() - landTracker.last > 240000;
if (Date.now() - started > 240000 && quiet) standIn();
setTimeout(tryLoad, m.classList.contains('stand-in') ? 5000 : 2500);
};
probe.src = url + (url.includes('?') ? '&' : '?') + 't=' + Date.now();
};
tryLoad();
});
// Inspiration PIP opens the full catalog card in the lightbox.
document.querySelectorAll('.pip').forEach(p => p.addEventListener('click', (e) => {
e.stopPropagation();
const img = p.querySelector('img');
if (!img) return;
lightboxImg.src = img.getAttribute('src');
lightbox.hidden = false;
requestAnimationFrame(() => lightbox.classList.add('open'));
}));
// Deck paging: arrows appear only when the deck overflows its axis, page
// one card at a time, and follow the aspect-ratio flip between row and column.
const deck = document.querySelector('.grid');
const prevBtn = document.querySelector('.nav.prev');
const nextBtn = document.querySelector('.nav.next');
const vertical = () => matchMedia('(max-aspect-ratio: 1/1)').matches;
function updateNav() {
if (!deck || !prevBtn) return;
const shell = deck.closest('.deck-shell');
const v = vertical();
const overflow = v ? deck.scrollHeight > deck.clientHeight + 4 : deck.scrollWidth > deck.clientWidth + 4;
prevBtn.hidden = nextBtn.hidden = !overflow;
const pos = v ? deck.scrollTop : deck.scrollLeft;
const max = v ? deck.scrollHeight - deck.clientHeight : deck.scrollWidth - deck.clientWidth;
const canPrev = overflow && pos > 2;
const canNext = overflow && pos < max - 2;
prevBtn.toggleAttribute('disabled', !canPrev);
nextBtn.toggleAttribute('disabled', !canNext);
shell?.classList.toggle('can-prev', canPrev);
shell?.classList.toggle('can-next', canNext);
}
function pageDeck(dir) {
const card = deck.querySelector('.card');
if (!card) return;
const r = card.getBoundingClientRect();
const step = (vertical() ? r.height : r.width) + 26;
deck.scrollBy(vertical() ? { top: dir * step, behavior: 'smooth' } : { left: dir * step, behavior: 'smooth' });
}
prevBtn?.addEventListener('click', () => pageDeck(-1));
nextBtn?.addEventListener('click', () => pageDeck(1));
deck?.addEventListener('scroll', updateNav, { passive: true });
addEventListener('resize', updateNav);
updateNav();
// Ambient: the hovered card's visible art bleeds into the page ground.
const ambient = document.getElementById('ambient');
document.querySelectorAll('.card').forEach(card => {
const hero = card.querySelector('.face.front .media img');
if (!hero) return;
card.addEventListener('mouseenter', () => { ambient.style.backgroundImage = 'url("' + hero.getAttribute('src') + '")'; ambient.style.opacity = '1'; });
card.addEventListener('mouseenter', () => {
const art = card.querySelector('.face.front .media img:not([hidden])') || card.querySelector('.face.front .pip img');
if (!art || !art.getAttribute('src')) return;
ambient.style.backgroundImage = 'url("' + art.getAttribute('src') + '")'; ambient.style.opacity = '1';
});
card.addEventListener('mouseleave', () => { ambient.style.opacity = '0'; });
});
@@ -476,8 +753,8 @@ function page() {
e.stopPropagation();
const card = b.closest('.card');
const face = card.classList.contains('flipped') ? '.face.back' : '.face.front';
const img = card.querySelector(face + ' .media img');
if (!img) return;
const img = card.querySelector(face + ' .media img:not([hidden])');
if (!img || !img.getAttribute('src')) return;
lightboxImg.src = img.getAttribute('src');
lightbox.hidden = false;
requestAnimationFrame(() => lightbox.classList.add('open'));
@@ -546,10 +823,10 @@ const server = http.createServer((req, res) => {
res.end(JSON.stringify({ ready: Boolean(pending && fs.existsSync(pending)) }));
return;
}
const imageMatch = req.method === 'GET' && req.url?.match(/^\/img\/(\d+)$/);
const imageMatch = req.method === 'GET' && req.url?.match(/^\/img\/(\d+)(?:\?.*)?$/);
if (imageMatch) {
const abs = localImages[Number(imageMatch[1])];
if (!abs) { res.writeHead(404); res.end(); return; }
if (!abs || !fs.existsSync(abs)) { res.writeHead(404); res.end(); return; }
const type = abs.endsWith('.webp') ? 'image/webp'
: abs.endsWith('.png') ? 'image/png'
: abs.endsWith('.svg') ? 'image/svg+xml'
@@ -572,6 +849,7 @@ const server = http.createServer((req, res) => {
optionId: parsed.optionId ?? null,
steer: parsed.steer ?? '',
...(chosen?.hero || chosen?.board ? { hero: chosen.hero ?? null, board: chosen.board ?? null } : {}),
...(chosen?.sketch ? { sketch: chosen.sketch } : {}),
});
const isReroll = parsed.optionId === 'reroll';
if (detachedKey) {
+1 -1
View File
@@ -101,7 +101,7 @@ jobs:
# Pinned for reproducible CI. Fails on AMO errors; innerHTML style
# warnings in the panel renderer are non-blocking and not promoted to
# errors here.
run: npx --yes web-ext@10 lint --source-dir dist/extension-firefox
run: npx --yes web-ext@8 lint --source-dir dist/extension-firefox
- name: Verify generated tracked outputs
run: git diff --exit-code -- .agents .claude .cursor .gemini .github/skills plugin cli/engine/detect-antipatterns-browser.js extension/detector
+5 -1
View File
@@ -4,7 +4,7 @@ description: Produces clean reusable raster assets from approved Impeccable mock
tools: Read, Write, Edit, Bash, Glob, Grep
model: inherit
effort: medium
maxTurns: 12
maxTurns: 24
---
# Impeccable Asset Producer
@@ -16,6 +16,10 @@ Your job is production cleanup, not new art direction. Work only from the approv
Do not redesign. Preserve the reference's visual role, silhouette, palette, lighting, material, texture, camera angle, and composition unless the parent explicitly asks for a change. Preserve perspective only when it belongs to the object or scene itself; if CSS should create the card transform, shadow, rounded clipping, border, or layout, remove that presentation chrome from the raster.
## Decision Sketches
When the parent hands you a decision card packet instead of an approved mock, the job is one sketch: one card, one file, written to the card's declared `sketch` path the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a sketch is reported back, not padded from imagination. Render through the parent's shared frame: the requested surface's first viewport as a flat, matte design sketch in the card's own palette and type character, deliberately unfinished, no photorealism, no gloss. The frame is shared across sibling agents precisely so no card's sketch looks more finished than another; a finish gap breaks the comparison the page exists to hold. The only legible text is the product's real name and one real headline; greek every other text region into indistinct lines, because an invented spec, price, or date in a sketch is a claim PRODUCT.md never made. Return one line naming the path and any deviation, nothing more.
## Input Contract
Expect:
+3 -1
View File
@@ -4,12 +4,14 @@ description: Records DESIGN.md and its sidecar from a finished Impeccable build,
tools: Read, Write, Bash, Glob, Grep
model: inherit
effort: medium
maxTurns: 12
maxTurns: 30
---
# Impeccable Documenter
You record a project's design system after the build is done. Ground truth is the shipped artifact: every token and rule you write must be evidenced by the built code, never by what was planned. Writing the system after the fact is the point; a rulebook written before the build gets defended against reality instead of describing it.
You run under a hard turn ceiling that ends the run without warning, and a run that ends before DESIGN.md is written has recorded nothing. Batch several Reads into each turn, take `reference/document.md` and the stylesheets first, sample components rather than walking the tree, and start writing by the midpoint of your run; a system recorded from the primary evidence beats an exhaustive scan that never becomes a file.
## Input Contract
Expect: the project root; the artifact path(s); the direction contract text (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; the path to the skill's `reference/document.md`; and the boundary to write at (project or app root). An existing DESIGN.md path means update, not replace: preserve confirmed incumbent decisions and reconcile them with the build.
+11 -5
View File
@@ -4,7 +4,7 @@ description: Reviews a finished Impeccable build against its direction contract,
tools: Read, Bash, Glob, Grep
model: inherit
effort: high
maxTurns: 12
maxTurns: 30
---
# Impeccable Finish Reviewer
@@ -12,20 +12,26 @@ You are the finishing reviewer for an Impeccable build: fresh eyes on a done art
You have no browser. Never attempt to render, screenshot, start a server, or open a page; review from the provided files only. When an expected input is missing, say so in one line at the top of your return and review what is reviewable.
You run under a hard turn ceiling that ends the run without warning, and a run that ends before the five sections are written returns nothing; a review built from what you saw beats a perfect review that never arrives. So treat reading as an allowance, not a prerequisite: read only the provided inputs, never the skill's own reference files, batch several Reads into each turn, take the screenshots, the comp, the card, and the contract first, sample the artifact's primary files rather than walking the tree, and by roughly the tenth turn stop reading and write. Name whatever went unread in the line above the sections.
## Input Contract
Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path. When the harness can view images, open the screenshots, the comp, and the card before judging.
Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: the contract is the builder's abstraction of the comp, and a review anchored on it inherits whatever that abstraction dropped.
## Checks, in order
1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding.
2. **Fidelity.** Against the approved comp: topology, reading order, focal scale, overlaps and z-order, density, signature geometry. Name every drift and classify it intentional or defect. The comp governs composition; it is not a pixel spec for semantics, accessibility, or responsive behavior.
2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement.
3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. The card governs commitment and finish, never composition.
4. **Contract, promise by promise.** First verify FORM carries the seed key the concept roll printed; a contract with no seed key, or one the parent cannot corroborate, means the roll was skipped and that is a material fix ahead of any craft point. Then, for each of the five blocks, does the render keep the promise? Apply the memory test to the first viewport.
5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one.
5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one, and every produced asset visibly present in the screenshots; an asset applied at near-zero opacity or buried behind other paint is a compliance token, not a shipped material.
Do not run a second detector pass; mechanical findings belong to the parent's hooks.
## Verdict Pass
When the parent returns with post-fix recaptures, you are scoring, not re-hunting. For each material fix from your review, one line: resolved, partial, or unresolved, tied to what the new screenshots visibly show; a fix answered mechanically, positions moved but the quality the finding named still absent, is partial at best. Then name at most three regressions the fix batch itself introduced, judged by the same matrix rules, and nothing else; no new hunt, no new checks. Return exactly two sections: `verdict` (the scored list) and `remaining` (what stays open, or "clear").
## Output Contract
Return exactly five sections: `persistence` (pass/fail with specifics), `fidelity` (drift list with intentional/defect per item, or "faithful"), `ceiling` (unused native devices, or "reached"), `material_fixes` (ordered, most material first, each one line tied to a check or contract promise, at most eight), and `keep` (one line naming what must not be diluted while fixing). Missing inputs are named in one line above the sections. No praise, no summary prose.
Return exactly five sections: `persistence` (pass/fail with specifics), `fidelity` (the element matrix: match, adaptation, missing, contradicted, or added without approval per salient element, adaptations citing their evidence, or "faithful"), `ceiling` (unused native devices, or "reached"), `material_fixes` (ordered, most material first, fidelity failures ahead of craft, each one line tied to a check or contract promise, at most eight), and `keep` (one line naming what must not be diluted while fixing). Missing inputs are named in one line above the sections. No praise, no summary prose.
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
version: 4.0.2
version: 4.0.3
user-invocable: true
argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]"
license: Apache 2.0
@@ -23,7 +23,7 @@ Page scaffolds:
- Same-size cards of icon plus heading plus text as the page structure. Cards are the lazy container; nested cards are always wrong.
- The hero-metric template: big number, small label, supporting stats, accent.
- A tracked uppercase eyebrow over every section. One named kicker is a system; an eyebrow everywhere is grammar you did not choose.
- A kicker or eyebrow above a heading. This one is a ban, not a default: no brief earns it back. The heading carries its own weight; delete the label and let the heading speak.
- Section numbers (01 / 02 / 03) unless the sequence itself carries information the reader needs.
- A modal for a task that needs neither interruption nor protected focus.
@@ -11,6 +11,10 @@ Your job is production cleanup, not new art direction. Work only from the approv
Do not redesign. Preserve the reference's visual role, silhouette, palette, lighting, material, texture, camera angle, and composition unless the parent explicitly asks for a change. Preserve perspective only when it belongs to the object or scene itself; if CSS should create the card transform, shadow, rounded clipping, border, or layout, remove that presentation chrome from the raster.
## Decision Sketches
When the parent hands you a decision card packet instead of an approved mock, the job is one sketch: one card, one file, written to the card's declared `sketch` path the moment it renders. The parent runs several of you in parallel, one per card, so your entire contract is this card; generate first, plan never, because the file on disk is the deliverable and the decision page is waiting on it. Work from the card's structured fields and PRODUCT.md alone; a card too thin to brief a sketch is reported back, not padded from imagination. Render through the parent's shared frame: the requested surface's first viewport as a flat, matte design sketch in the card's own palette and type character, deliberately unfinished, no photorealism, no gloss. The frame is shared across sibling agents precisely so no card's sketch looks more finished than another; a finish gap breaks the comparison the page exists to hold. The only legible text is the product's real name and one real headline; greek every other text region into indistinct lines, because an invented spec, price, or date in a sketch is a claim PRODUCT.md never made. Return one line naming the path and any deviation, nothing more.
## Input Contract
Expect:
@@ -5,6 +5,8 @@ This harness has no subagent capability, so you are running this role inline. St
You record a project's design system after the build is done. Ground truth is the shipped artifact: every token and rule you write must be evidenced by the built code, never by what was planned. Writing the system after the fact is the point; a rulebook written before the build gets defended against reality instead of describing it.
You run under a hard turn ceiling that ends the run without warning, and a run that ends before DESIGN.md is written has recorded nothing. Batch several Reads into each turn, take `reference/document.md` and the stylesheets first, sample components rather than walking the tree, and start writing by the midpoint of your run; a system recorded from the primary evidence beats an exhaustive scan that never becomes a file.
## Input Contract
Expect: the project root; the artifact path(s); the direction contract text (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; the path to the skill's `reference/document.md`; and the boundary to write at (project or app root). An existing DESIGN.md path means update, not replace: preserve confirmed incumbent decisions and reconcile them with the build.
@@ -7,20 +7,26 @@ You are the finishing reviewer for an Impeccable build: fresh eyes on a done art
You have no browser. Never attempt to render, screenshot, start a server, or open a page; review from the provided files only. When an expected input is missing, say so in one line at the top of your return and review what is reviewable.
You run under a hard turn ceiling that ends the run without warning, and a run that ends before the five sections are written returns nothing; a review built from what you saw beats a perfect review that never arrives. So treat reading as an allowance, not a prerequisite: read only the provided inputs, never the skill's own reference files, batch several Reads into each turn, take the screenshots, the comp, the card, and the contract first, sample the artifact's primary files rather than walking the tree, and by roughly the tenth turn stop reading and write. Name whatever went unread in the line above the sections.
## Input Contract
Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path. When the harness can view images, open the screenshots, the comp, and the card before judging.
Expect: the original request; the confirmed user answers; the artifact path(s); desktop and mobile screenshot paths captured by the parent; the direction contract (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; existing hook or detector findings; the chosen world's QUALITY BAR card paths and the approved comp path. When the harness can view images, open the screenshots, the comp, and the card first, and inventory the comp's salient elements in your own words before reading the direction contract or any builder-authored summary: the contract is the builder's abstraction of the comp, and a review anchored on it inherits whatever that abstraction dropped.
## Checks, in order
1. **Persistence.** PRODUCT.md exists. When DESIGN.md predates this build (an extension or redesign), it matches the built world; on a new world it is written after this review by the documenter, so its absence here is not a finding.
2. **Fidelity.** Against the approved comp: topology, reading order, focal scale, overlaps and z-order, density, signature geometry. Name every drift and classify it intentional or defect. The comp governs composition; it is not a pixel spec for semantics, accessibility, or responsive behavior.
2. **Fidelity.** Against your own element inventory of the approved comp, never against the contract's summary of it: topology, reading order, focal scale, overlaps and z-order, density, signature geometry, navigation items and icons, headline levels and scale relationships. Classify every salient element: match, acceptable adaptation, missing, contradicted, or added without approval. Two rows are mandatory in every matrix. TYPE: the display lettering's character, compression, width, weight, contrast, terminals, against the comp's; a face of a different character is contradicted however the layout matches. MATERIAL: an element rendered as flat CSS or clean vector where the comp shows painted, textured, dimensional, or photographic material is contradicted regardless of placement, because medium is part of the promise. An adaptation counts as intentional only when it cites the user answer, surface brief, accessibility need, or product truth that forced it; an uncited deviation is a defect. A missing signature element, a changed topology, or content added without approval fails fidelity and outranks every craft point in material_fixes. The comp is the spec for composition, topology, element inventory, density, lettering character, and material; it is not a pixel spec for semantics, accessibility, or responsive reflow, and that allowance covers translation, never replacement.
3. **Ceiling.** Against the QUALITY BAR card: name the world's native devices the build left unused, frame, depth, lettering treatment, ornament density, motion. The card governs commitment and finish, never composition.
4. **Contract, promise by promise.** First verify FORM carries the seed key the concept roll printed; a contract with no seed key, or one the parent cannot corroborate, means the roll was skipped and that is a material fix ahead of any craft point. Then, for each of the five blocks, does the render keep the promise? Apply the memory test to the first viewport.
5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one.
5. **Truth.** Demonstration data authored and labeled synthetic; no invented commercial claims; unanswered claims present as marked placeholders, not omissions. Every image-native region of the approved comp shipped as a real asset, not a gradient standing in for one, and every produced asset visibly present in the screenshots; an asset applied at near-zero opacity or buried behind other paint is a compliance token, not a shipped material.
Do not run a second detector pass; mechanical findings belong to the parent's hooks.
## Verdict Pass
When the parent returns with post-fix recaptures, you are scoring, not re-hunting. For each material fix from your review, one line: resolved, partial, or unresolved, tied to what the new screenshots visibly show; a fix answered mechanically, positions moved but the quality the finding named still absent, is partial at best. Then name at most three regressions the fix batch itself introduced, judged by the same matrix rules, and nothing else; no new hunt, no new checks. Return exactly two sections: `verdict` (the scored list) and `remaining` (what stays open, or "clear").
## Output Contract
Return exactly five sections: `persistence` (pass/fail with specifics), `fidelity` (drift list with intentional/defect per item, or "faithful"), `ceiling` (unused native devices, or "reached"), `material_fixes` (ordered, most material first, each one line tied to a check or contract promise, at most eight), and `keep` (one line naming what must not be diluted while fixing). Missing inputs are named in one line above the sections. No praise, no summary prose.
Return exactly five sections: `persistence` (pass/fail with specifics), `fidelity` (the element matrix: match, adaptation, missing, contradicted, or added without approval per salient element, adaptations citing their evidence, or "faithful"), `ceiling` (unused native devices, or "reached"), `material_fixes` (ordered, most material first, fidelity failures ahead of craft, each one line tied to a check or contract promise, at most eight), and `keep` (one line naming what must not be diluted while fixing). Missing inputs are named in one line above the sections. No praise, no summary prose.
@@ -44,9 +44,11 @@ The script assigns which structure gets built: your top-ranked structure is what
2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, the notation, publications, identity programs, data graphics, and interfaces it reads daily, not only its physical objects; a nameable abstract system (a school of poster, a documentation standard, a data-graphic tradition) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; the audience's world is larger than that, so dig until the list spans at least three families.
3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience.
4. Run `node .grok/skills/impeccable/scripts/concept-seed.mjs --scope direction --mode <mode>` and follow what it prints. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point.
5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the one or two fused challengers that survived the weighing as named alternates with a one-line case each, plus re-roll with an optional one-line steer. What you never present is a ranked menu of your own grounded candidates; a lineup of those invites the safest card, while dealt challengers carry no such rut. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option.
5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the hand's challengers as named alternates, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it. A hand holds at most three challengers: when the roll deals more, the three strongest join the hand and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. What you never present is a ranked menu of your own grounded candidates; a lineup of those invites the safest card, while dealt challengers carry no such rut. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option.
The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Run `node .grok/skills/impeccable/scripts/serve-question.mjs --start --payload <file>` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key <key>`, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry.
The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, so a grounded direction is never a wall of text against rendered art, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you, so it is never yours to recommend. Run `node .grok/skills/impeccable/scripts/serve-question.mjs --start --payload <file>` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key <key>`, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry.
When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land, so generation never delays the question. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. The only legible text in a sketch is the product's real name and one real headline; every other text region is greeked, indistinct lines standing where copy will go, because a sketch that renders invented specs, prices, or dates puts claims in front of the user that PRODUCT.md never made. Produce in the order the user reads: the assigned card, then the hand, then canon, each file written the moment it is done. When the harness runs subagents in parallel, fan the set out as one agent per card: each spawn is the shipped asset producer with a single-sketch packet, that card's fields, PRODUCT.md, the shared frame, and the card's declared path, up to four in flight at once, so the whole set lands in roughly the time of one. A slot still empty when its agent returns is regenerated inline, and a slot still empty when the user answers is dropped without ceremony; no other supervision is owed. Without parallel subagents, generate in the main thread after serving, in the same reading order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version.
Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images for exactly this reason (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product.
@@ -98,6 +100,6 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi
Inspect desktop and mobile in one batched screenshot round, critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary.
After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, and the QUALITY BAR card and approved comp paths. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. Apply the material fixes in one batch, rebuild once, and stop; the reviewer ran so that you do not re-open your own hunt. Do not run a second detector.
After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. Capture desktop and mobile screenshots to files, then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccable_finish_reviewer` in codex), with the original request, confirmed answers, the artifact path, the screenshot paths, its direction contract, existing hook findings, and the QUALITY BAR card and approved comp paths. The reviewer has no browser; screenshots you fail to pass are checks it cannot run. Verify its return carries the five contract sections; on an empty or thrashed return, respawn once with the same inputs before doing anything else. This review never runs inside the build thread. Only a harness whose tool surface has no subagent capability at all substitutes a fresh in-thread pass after stepping fully out of the build context, run from [degraded/finish-reviewer.md](degraded/finish-reviewer.md), and a substituted or failed-and-replaced review is disclosed in one line at finish, never silently. Apply the material fixes in one batch, rebuild once, and recapture the same viewports. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get exactly one more batch, recapture, and verdict; two correction rounds is the ceiling, the second verdict ends the work whatever it says, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Report the final verdict table to the user as it stands, open items included: presenting mechanical confirmation as artistic success is how a failed build gets announced as a finished one. Do not run a second detector.
Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded.
@@ -6,7 +6,7 @@ The purpose of a probe is to test composition, narrative, hierarchy, density, fo
## Generate three compositional options
Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session; record the approved comp's path in the surface brief. Base them on the real content and the surface concepts already developed with the user. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building.
Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session; record the approved comp's path in the surface brief. Base them on the real content and the surface concepts already developed with the user. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. A decision-page sketch is not a probe: it chose the direction at deliberately unfinished fidelity, so the three comps render regardless, and the chosen card's sketch seeds at most one of them.
- When the user shortlisted multiple concepts, spread the three across them.
- When one direction is committed, vary the structural uncertainty an image can resolve: topology, sequence, density, hierarchy, focal composition, or interaction framing.
@@ -25,11 +25,11 @@ After approval, summarize the composition and the parts of the comp that must no
## Inventory implementation fidelity
Before building, inventory the approved comp's major visible ingredients in writing (a short table in the surface brief or working notes; the finish reviewer audits shipped assets against it) and choose an implementation medium for each: semantic HTML/CSS/SVG, existing project asset, generated raster, sourced raster, icon library, canvas/WebGL, or accepted omission. Textures, portraits, and scenes are raster-by-default; a CSS gradient is not a texture medium. Every `produce` entry is produced before the build ships, through the asset producer or in the current thread; an inventory with unproduced entries is an unfinished build, and this gate is where imagery-free pages come from when it is skipped.
Before building, inventory the approved comp's major visible ingredients in writing (a short table in the surface brief or working notes; the finish reviewer audits shipped assets against it) and choose an implementation medium for each: semantic HTML/CSS/SVG, existing project asset, generated raster, sourced raster, icon library, canvas/WebGL, or accepted omission. The same written inventory names the comp's compositional commitments: navigation items and icons, headline levels and their scale relationship, signature geometry such as seams, masks, and overlaps, and each section's arrangement and density. An element never written down is the element the build silently drops, and the direction contract's 150 words cannot carry this list, so this inventory is where it lives. Textures, portraits, and scenes are raster-by-default; a CSS gradient is not a texture medium. Every `produce` entry is produced before the build ships, through the asset producer or in the current thread; an inventory with unproduced entries is an unfinished build, and this gate is where imagery-free pages come from when it is skipped.
Pay special attention to the dominant composition, signature use, image-native content, second-fold system, and any interaction the still image only implies. If the concept depends on a photograph, architectural scene, product object, portrait, or other raster-native material, do not silently replace it with generic CSS scenery.
Treat the comp as a north star, not something to trace. Do not rasterize core UI text or controls. Do not substitute a different visual driver after approval without asking.
Treat the comp as a north star, not something to trace, and know what that allows: translation into semantic, responsive, accessible code, never recomposition. Keeping the palette and mood while redrawing the topology is a second art direction, not an adaptation. Do not rasterize core UI text or controls. Do not substitute a different visual driver after approval without asking.
## Produce only the assets the build needs
@@ -206,7 +206,13 @@ ${grammar}
// returns no staging. Re-rolls exclude every earlier set until the pool runs out.
export function selectApprovedStagings({ scope, key, reroll = 0, mode = null, sourceCompositions = null, count = 3 }) {
const pool = sourceCompositions ?? requireLocalConcepts().compositions;
// Stagings honour the same breadth gate as worlds: a staging too specific to
// serve an arbitrary build stays approved for direct briefs and leaves the
// challenger pool. Falls back to the full approved set rather than returning
// nothing if every approved staging is marked niche.
let approved = pool.filter(composition => composition.status === 'approved');
const broad = approved.filter(composition => composition.review?.breadth !== 'niche');
if (broad.length > 0) approved = broad;
if (approved.length === 0) return [];
if (mode) {
const matching = approved.filter(composition => composition.surface === mode);
@@ -271,12 +277,19 @@ export function selectApprovedChallengers({ scope, key, reroll = 0, sourceConcep
// graphic systems beside instrument languages and atmosphere worlds, with
// the second pick preferring a different family for diversity. Tier order
// in the rendered list is rolled too, to avoid positional bias.
// Approval ratings weight the draw: a 3-star world earns a second ticket
// (roughly double odds), a 1-star keeps its approval for direct briefs but
// leaves the challenger pool unless a tier has nothing else.
// Two separate axes, and both can exclude. Rating grades quality: a 3-star
// earns a second ticket, a 1-star marginal keep leaves the pool. Breadth says
// whether a world can serve an arbitrary build at all, so a niche world
// leaves the pool however good it is. Breadth was split out of rating because
// the only way to hold a narrow world back used to be calling it marginal,
// which made "excellent but narrow" unrecordable and corrupted the ratings as
// a calibration signal for the next authoring round.
const ticketsFor = pool => pool.flatMap(concept => {
const rating = concept.review?.rating;
if (rating === 1) return [];
// Two independent exclusions: a marginal world is too weak to challenge,
// a niche world too narrow. Either one keeps its approval for direct
// briefs and leaves the pool.
if (rating === 1 || concept.review?.breadth === 'niche') return [];
return rating === 3
? [{ concept, ticket: 0 }, { concept, ticket: 1 }]
: [{ concept, ticket: 0 }];
@@ -1529,7 +1529,7 @@ if (IS_BROWSER) {
addBrowserFindings(groupMap, document.body, typoFindings);
}
const sectionKickerFindings = checkRepeatedSectionKickersDOM()
const sectionKickerFindings = checkKickerAboveHeadingDOM()
.map(f => ({ type: f.id, detail: f.snippet }))
.filter(f => _ruleOk(f.type));
if (sectionKickerFindings.length > 0) {
@@ -309,15 +309,14 @@ const ANTIPATTERNS = [
skillGuideline: 'tiny uppercase tracked label above the hero headline',
},
{
id: 'repeated-section-kickers',
id: 'kicker-above-heading',
category: 'slop',
scopes: ['type'],
severity: 'advisory',
name: 'Repeated section kicker labels',
name: 'Kicker / eyebrow label above heading',
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.',
'A tiny tracked uppercase or small-caps label sitting as its own block directly above a heading is banned outright, repeated or not. Generated kickers never earn their place: the heading carries its own weight. Delete the label and let the heading speak; if the words matter, work them into the heading or the body.',
skillSection: 'Typography',
skillGuideline: 'repeated eyebrow or kicker labels as section scaffolding',
skillGuideline: 'kicker or eyebrow labels above headings',
},
{
id: 'numbered-section-labels',
@@ -1258,12 +1257,15 @@ function checkHeroEyebrow(opts) {
}];
}
function checkRepeatedSectionKickers(opts) {
const { candidates, minCount = 3 } = opts;
if (!Array.isArray(candidates) || candidates.length < minCount) return [];
// Outright ban: one kicker is one too many, so every collected candidate is
// a finding. The judgment lives in the candidate gate (isKickerCandidate) and
// the collector's context skips, not in a repetition count.
function checkKickerAboveHeading(opts) {
const { candidates } = opts;
if (!Array.isArray(candidates)) return [];
return candidates.map(candidate => ({
id: 'repeated-section-kickers',
snippet: `repeated section kicker "${candidate.kickerText}" before ${candidate.headingTag} "${candidate.headingText}" (${candidates.length} on page)`,
id: 'kicker-above-heading',
snippet: `kicker "${candidate.kickerText}" above ${candidate.headingTag} "${candidate.headingText}"`,
}));
}
@@ -3190,7 +3192,7 @@ function parseColorResolved(str, customPropMap) {
return parseAnyColor(resolved);
}
const REPEATED_KICKER_SKIP_SELECTOR = [
const KICKER_SKIP_SELECTOR = [
'nav',
'form',
'table',
@@ -3209,7 +3211,7 @@ const REPEATED_KICKER_SKIP_SELECTOR = [
'[data-impeccable-allow-kickers]',
].join(',');
const REPEATED_KICKER_CARD_CONTEXT_SELECTOR = [
const KICKER_CARD_CONTEXT_SELECTOR = [
'article',
'button',
'a',
@@ -3227,23 +3229,32 @@ function cleanInlineText(el) {
.trim();
}
function isRepeatedKickerCardContext(heading, kicker) {
const item = heading.closest?.(REPEATED_KICKER_CARD_CONTEXT_SELECTOR);
function isKickerCardContext(heading, kicker) {
const item = heading.closest?.(KICKER_CARD_CONTEXT_SELECTOR);
return Boolean(item && (!item.contains || item.contains(kicker)));
}
function isRepeatedKickerCandidate(opts) {
// Meta lines above headlines join category and date (or path crumbs) with
// separator glyphs, or carry a year. A kicker is one short phrase; metadata
// keeps its markers.
const KICKER_META_TEXT_RE = /[·•|]|\s[\/›»>]\s|\b(19|20)\d{2}\b/;
// Legal and document numbering: "Section 4.2", "Article IX", "§ 12.3",
// dotted decimal outlines. The label identifies the clause, so it stays.
const KICKER_DOC_NUMBERING_RE = /^(§|\d+(\.\d+)+\b|(section|article|clause|appendix|exhibit|schedule|chapter|part|rule|title)\s+([\divxlc]+\b|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)\b)/i;
function isKickerCandidate(opts) {
const {
headingTag,
headingLevel,
headingText,
headingFontSize,
kickerTag,
kickerText,
kickerTextTransform,
kickerFontVariant,
kickerFontSize,
kickerLetterSpacing,
} = opts;
if (!['h2', 'h3', 'h4'].includes(headingTag)) return false;
if (!headingLevel || headingLevel > 4) return false;
if (!headingText || headingText.length < 3) return false;
if (/^\/[\w-]+/i.test(headingText.replace(/^"|"$/g, '').trim())) return false;
if (!(headingFontSize >= 20)) return false;
@@ -3251,9 +3262,13 @@ function isRepeatedKickerCandidate(opts) {
if (!['p', 'span', 'div', 'small'].includes(kickerTag)) return false;
if (!kickerText || kickerText.length < 2 || kickerText.length > 34) return false;
if (/^step\s*\d+/i.test(kickerText) || /^\d{1,2}$/.test(kickerText)) return false;
if (KICKER_META_TEXT_RE.test(kickerText)) return false;
if (KICKER_DOC_NUMBERING_RE.test(kickerText)) return false;
const isSmallCaps = /small-caps/.test(kickerFontVariant || '');
const isUppercased = kickerTextTransform === 'uppercase'
|| (/[A-Z]/.test(kickerText) && !/[a-z]/.test(kickerText));
|| (/[A-Z]/.test(kickerText) && !/[a-z]/.test(kickerText))
|| isSmallCaps;
if (!isUppercased) return false;
if (!(kickerFontSize > 0 && kickerFontSize <= 14)) return false;
const minTrackedSpacing = Math.max(1, kickerFontSize * 0.08);
@@ -3261,37 +3276,64 @@ function isRepeatedKickerCandidate(opts) {
return true;
}
function collectRepeatedSectionKickerCandidates(doc, getStyle, resolveLetterSpacing) {
// Resolve a heading level for the anchor element: 1-4 for h1-h4, aria-level
// (default 2) for role="heading" elements, 0 otherwise.
function kickerHeadingLevel(heading) {
const tag = heading.tagName.toLowerCase();
const byTag = /^h([1-6])$/.exec(tag);
if (byTag) return parseInt(byTag[1], 10);
const role = heading.getAttribute?.('role') || '';
if (role.toLowerCase() !== 'heading') return 0;
const ariaLevel = parseInt(heading.getAttribute?.('aria-level') || '', 10);
return Number.isFinite(ariaLevel) && ariaLevel >= 1 ? ariaLevel : 2;
}
function collectKickerCandidates(doc, getStyle, resolveLetterSpacing) {
const candidates = [];
for (const heading of doc.querySelectorAll('h2, h3, h4')) {
if (heading.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
for (const heading of doc.querySelectorAll('h1, h2, h3, h4, [role="heading"]')) {
const headingLevel = kickerHeadingLevel(heading);
if (!headingLevel || headingLevel > 4) continue;
if (heading.closest?.(KICKER_SKIP_SELECTOR)) continue;
// Application contexts (tab panels, dialogs) use compact context labels
// above headings to describe state, not to decorate. Same carve-out the
// hero-eyebrow rule makes.
if (heading.closest?.('[role="tabpanel"], [role="dialog"], [role="application"], dialog')) continue;
const kicker = heading.previousElementSibling;
if (!kicker || kicker.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
if (isRepeatedKickerCardContext(heading, kicker)) continue;
if (!kicker || kicker.closest?.(KICKER_SKIP_SELECTOR)) continue;
if (isKickerCardContext(heading, kicker)) continue;
const headingStyle = getStyle(heading);
const kickerStyle = getStyle(kicker);
const headingTag = heading.tagName.toLowerCase();
const headingText = (heading.textContent || '').replace(/\s+/g, ' ').trim();
const kickerText = cleanInlineText(kicker) || (kicker.textContent || '').replace(/\s+/g, ' ').trim();
const headingFontSize = resolveLetterSpacing(headingStyle.fontSize || '', 16) || parseFloat(headingStyle.fontSize) || 0;
const kickerFontSize = resolveLetterSpacing(kickerStyle.fontSize || '', 16) || parseFloat(kickerStyle.fontSize) || 0;
const kickerLetterSpacing = resolveLetterSpacing(kickerStyle.letterSpacing || '', kickerFontSize);
if (!isRepeatedKickerCandidate({
headingTag: heading.tagName.toLowerCase(),
if (!isKickerCandidate({
headingLevel,
headingText,
headingFontSize,
kickerTag: kicker.tagName.toLowerCase(),
kickerText,
kickerTextTransform: kickerStyle.textTransform || '',
kickerFontVariant: `${kickerStyle.fontVariant || ''} ${kickerStyle.fontVariantCaps || ''}`,
kickerFontSize,
kickerLetterSpacing,
})) {
continue;
}
// A tracked-caps eyebrow above a hero-scale h1 belongs to
// hero-eyebrow-chip (which also covers the accent-bold and dash-prefix
// stylings there). Stand down so one element gets one finding.
if (headingTag === 'h1' && headingFontSize >= 48 && kickerLetterSpacing >= 1.6) {
continue;
}
candidates.push({
headingTag: heading.tagName.toLowerCase(),
headingTag,
headingText: headingText.replace(/^"|"$/g, '').slice(0, 60),
kickerText: kickerText.slice(0, 40),
});
@@ -3299,17 +3341,17 @@ function collectRepeatedSectionKickerCandidates(doc, getStyle, resolveLetterSpac
return candidates;
}
function checkRepeatedSectionKickersDOM() {
const candidates = collectRepeatedSectionKickerCandidates(
function checkKickerAboveHeadingDOM() {
const candidates = collectKickerCandidates(
document,
(el) => getComputedStyle(el),
(value, fontSize) => resolveLengthPx(value, fontSize) || 0,
);
return checkRepeatedSectionKickers({ candidates });
return checkKickerAboveHeading({ candidates });
}
// ── Numbered section labels ─────────────────────────────────────────────────
// Sibling of the repeated-kicker rule: instead of a tracked uppercase word,
// Sibling of the kicker-above-heading rule: instead of a tracked uppercase word,
// the section scaffold is a tiny numeric index riding beside each section
// heading — bare and zero-padded, or an index joined to a short micro-label
// by a separator glyph. The kicker rule deliberately excludes bare 1-2 digit
@@ -3363,7 +3405,7 @@ function collectNumberedSectionLabelCandidates(doc, getStyle, resolveLetterSpaci
const candidates = [];
const seenLabels = new Set();
for (const heading of doc.querySelectorAll('h2, h3, h4')) {
if (heading.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
if (heading.closest?.(KICKER_SKIP_SELECTOR)) continue;
// The index sits either directly before the heading, or before the
// wrapper the heading leads (label | <div><h2>…</h2>…</div>).
let label = heading.previousElementSibling;
@@ -3373,9 +3415,9 @@ function collectNumberedSectionLabelCandidates(doc, getStyle, resolveLetterSpaci
if (firstChild === heading) label = parent.previousElementSibling;
}
if (!label || seenLabels.has(label)) continue;
if (label.closest?.(REPEATED_KICKER_SKIP_SELECTOR)) continue;
if (label.closest?.(KICKER_SKIP_SELECTOR)) continue;
if (HEADING_TAGS.has(label.tagName.toLowerCase())) continue;
if (isRepeatedKickerCardContext(heading, label)) continue;
if (isKickerCardContext(heading, label)) continue;
const labelText = cleanInlineText(label) || (label.textContent || '').replace(/\s+/g, ' ').trim();
const parsed = parseNumberedLabelText(labelText);
@@ -4548,13 +4590,13 @@ function checkElementHeroEyebrow(el, style, tag, window, customPropMap) {
});
}
function checkRepeatedSectionKickersFromDoc(doc, win) {
const candidates = collectRepeatedSectionKickerCandidates(
function checkKickerAboveHeadingFromDoc(doc, win) {
const candidates = collectKickerCandidates(
doc,
(el) => win.getComputedStyle(el),
(value, fontSize) => resolveLengthPx(value, fontSize) || 0,
);
return checkRepeatedSectionKickers({ candidates });
return checkKickerAboveHeading({ candidates });
}
function checkElementMotion(tag, style) {
@@ -7712,7 +7754,7 @@ if (IS_BROWSER) {
addBrowserFindings(groupMap, document.body, typoFindings);
}
const sectionKickerFindings = checkRepeatedSectionKickersDOM()
const sectionKickerFindings = checkKickerAboveHeadingDOM()
.map(f => ({ type: f.id, detail: f.snippet }))
.filter(f => _ruleOk(f.type));
if (sectionKickerFindings.length > 0) {
@@ -223,7 +223,7 @@ function unwrapCssAtLayer(source) {
// ---------------------------------------------------------------------------
const STATIC_INHERITED_PROPS = new Set([
'color', 'fontFamily', 'fontSize', 'fontStyle', 'fontWeight',
'color', 'fontFamily', 'fontSize', 'fontStyle', 'fontWeight', 'fontVariant',
'lineHeight', 'letterSpacing', 'textTransform', 'textAlign', 'hyphens',
'webkitHyphens',
]);
@@ -252,6 +252,7 @@ const STATIC_DEFAULT_STYLE = {
fontFamily: '',
fontSize: '16px',
fontStyle: 'normal',
fontVariant: 'normal',
fontWeight: '400',
lineHeight: 'normal',
letterSpacing: 'normal',
@@ -27,11 +27,11 @@ import {
checkElementRadialSpotlight,
checkCreamPalette,
checkHtmlPatterns,
checkKickerAboveHeadingFromDoc,
checkNumberedSectionLabelsFromDoc,
checkPageLayout,
checkPageQualityFromDoc,
checkRepeatedContainerTextFromDoc,
checkRepeatedSectionKickersFromDoc,
resolveBackground,
resolveBorderRadiusPx,
} from '../../rules/checks.mjs';
@@ -202,7 +202,7 @@ async function detectHtml(filePath, options = {}) {
for (const f of runPageCheck('typography-rules', () => checkStaticPageTypography(document, window))) {
findings.push(finding(f.id, filePath, f.snippet));
}
for (const f of runPageCheck('repeated-section-kickers', () => checkRepeatedSectionKickersFromDoc(document, window))) {
for (const f of runPageCheck('kicker-above-heading', () => checkKickerAboveHeadingFromDoc(document, window))) {
findings.push(finding(f.id, filePath, f.snippet));
}
for (const f of runPageCheck('numbered-section-labels', () => checkNumberedSectionLabelsFromDoc(document, window))) {
@@ -198,15 +198,14 @@ const ANTIPATTERNS = [
skillGuideline: 'tiny uppercase tracked label above the hero headline',
},
{
id: 'repeated-section-kickers',
id: 'kicker-above-heading',
category: 'slop',
scopes: ['type'],
severity: 'advisory',
name: 'Repeated section kicker labels',
name: 'Kicker / eyebrow label above heading',
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.',
'A tiny tracked uppercase or small-caps label sitting as its own block directly above a heading is banned outright, repeated or not. Generated kickers never earn their place: the heading carries its own weight. Delete the label and let the heading speak; if the words matter, work them into the heading or the body.',
skillSection: 'Typography',
skillGuideline: 'repeated eyebrow or kicker labels as section scaffolding',
skillGuideline: 'kicker or eyebrow labels above headings',
},
{
id: 'numbered-section-labels',

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