Redesign an existing website to make it better. Built on top of impeccable.
71
89%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
Replica's Phase 3: author one clean prototype per page type that the source-fidelity gate can pass. This is recreation, not redesign — the craft skill's redesign gates (critique, anti-template, divergence, distinctiveness) do not apply and must not be invoked; the gate replaces them. The failure mode this doc exists to prevent is taste leaking in: any "improvement" without an inconsistency-register entry is a fidelity bug.
Per archetype, in this order — the order matters because each step removes a class of guesswork before the next begins:
stardust/current/pages/<slug>.json (headings, body, CTAs with hrefs,
alt text, metadata). Content-preservation rules
(../../migrate/reference/content-preservation.md) apply from line one —
including its paragraph-boundary rule: paragraphs come from the source's
block-level nodes, never from splitting captured text on newlines.
Reconcile COUNTS before authoring: group the captured components by
section and count them (n widget instances, n cards each) against what
you are about to author. Vendor-template pages repeat whole widgets
verbatim (recorded: the same 20-card widget twice plus a 6-card "wide"
variant on one page — the first build authored ONE instance and the gate
read a 1559px height deficit as a missing section). A large height delta
with matching section ORDER is usually a duplicated instance, not a
missing one.source-fidelity-gate.md) — do not eyeball-polish first; the first gate
run is the map of what's wrong.Keep the markup clean: semantic elements, BEM-ish classes, CSS custom
properties for the lifted tokens, no JS unless a section's initial state
requires computing it (see § Carousels) or the live chrome morphs with
scroll (see § Fixed and sticky chrome — the one instrument-induced
exception). Adopt the live page's content-root
class on the prototype's main wrapper so one --main selector scopes both
sides of the diff symmetrically.
Every archetype keeps its own standalone reference prototype — never skip
to direct platform authoring for a new archetype "because the blocks
already exist". Field evidence (a financial-services site, 8 pages): the prototyped
archetype reached 3.5%/5.6% pixel diff and stayed the quality ceiling for
its conversion; pages authored directly on the platform plateaued at
8–16%. Prototypes are cumulative: each new one imports the shared
layers earlier prototypes already gated — tokens, chrome, shared module
CSS, the interaction spec — and iterates only on its NEW modules.
Concretely, split the prototype CSS into a shared canon file plus a
per-archetype file; the platform conversion inherits both; and the
prototype remains the per-archetype fidelity reference (full gate: ≤10%,
Δ≤8px, 0 structural red) that the published page is judged against
(source-fidelity-gate.md § The published-origin gate).
Canon chrome is a snapshot — re-verify it against EACH new archetype's
live page before iterating page content. Header/footer vary per template
family and the live site drifts continuously (recorded: the gated canon
header rendered 123px vs 101px live on a second template, and the canon
footer still carried pre-drift metrics — the two "trusted" regions consumed
the new page's first two hot bands). Open every new archetype's gate with
the chrome crop gate (source-fidelity-gate.md § Pass bar, item 5) against
the NEW page's live chrome, and flag any page-level compensation for
back-port into the canon files so later archetypes don't re-discover it.
(Prior art: an earlier airport-site migration's improvement notes §3.6; re-confirmed in UC1-E1 where per-element computed-style capture "did most of the work".)
Before any screenshot-eyeball tuning:
capture/tokens.json
pattern): container max-widths, the full type ramp (family / size /
line-height / letter-spacing / weight per level), button specs (border,
radius, padding — the whole spec, not just color), section paddings,
radii, shadows, hero heights, breakpoint values — and the
text-rendering group: text-rendering, -webkit-font-smoothing,
font-synthesis, font-variant-numeric, font-kerning. Sites commonly
set these globally, and the ramp alone doesn't carry them: a ±1%
glyph-width difference from a mismatched rendering mode produces
systematic one-line-fewer/more wraps that present as inexplicable
per-section height errors at every breakpoint. Diagnostic when wraps
disagree at identical computed family/size/width: measure a literal
string's rendered width on both sides (canvas.measureText or an
offscreen span) — it settles whether the fork is metric or layout.@media geometry (container model, hidden/restacked blocks, mobile nav,
grid collapse rules) up front and build 360 against it. Capture
per-element computed styles at 360 (and any other gate width) BEFORE
authoring — the 360 gate map is not the moment to discover the mobile
container model. Recorded twice: a design-furniture site's 1440-lifted prototype
converged desktop in one iteration but opened mobile at 26.8% (an
overflow:hidden whose only layout effect is margin-collapse containment
at mobile, a different mobile footer container model, a block hidden at
mobile — all sitting in the source CSS, discoverable up front); and
a fashion retailer, where an essentially unbuilt 360 layout measured
−1600px height delta at 360 vs −169px at 1440 — a desktop-only
recreation doesn't degrade gracefully at mobile, it collapses. With
per-breakpoint lifting, mobile converges in 1–2 iterations; without it,
expect the full iteration cap.This converts 3–4 guess-and-screenshot loops into one. Eyeballing is for step 4 of the authoring order only — and even then, the gate's instruments outrank the eye.
No foundation text-wrap: balance on headings. The redesign
prototype's refined pass prescribes h1–h6 { text-wrap: balance }; live
sites almost never use it, and under it multi-line card titles and band
headings re-wrap differently from the original at identical width and font
— persistent 10–20% band diffs that look like font or width errors
(recorded). Replica prototypes and their block CSS leave text-wrap at the
source's computed value (initial unless the lift says otherwise); when a
heading wraps differently at matched width/font, check text-wrap before
anything else.
Both were caught only by the gate in UC1-E1; check for them proactively:
background-image layer list first;
luminance-fit only when the scrim isn't there. A photo band that resists
offset/scale fixes (recorded: 30–40% band diff with position, scale and
copy numerically aligned, cross-correlation dy≈0) usually carries its
scrim as a gradient layer in the live element's background-image stack
(linear-gradient(rgba(0,0,0,.1), rgba(0,0,0,.5)), url(…)) — invisible in
crawl JSON and to the eye, one getComputedStyle(el).backgroundImage
line to find, and worth 4 pixel points on its own. The CSS lift (step 2)
captures every layer (gradients + url + size/position per layer) of any
element carrying a photo. Only when a scrim is present in rendered pixels
with NO discoverable element, pseudo-element, filter, backdrop-filter, or
mask, recover it empirically by per-row luminance
fitting: compare per-row luminance of the live capture region vs the
decoded raw image, fit the ratio curve to a gradient (UC1-E1's hero fit:
linear-gradient(transparent 68%, rgba(0,0,0,.45) 80%, #000 100%)), apply,
and let the pixel probe confirm the fit.A computed-style lift records width: 720px from an element whose authored
rule is width: 50%. At the gate widths the two are byte-identical — BOTH
gate breakpoints render them the same — so the frozen value ships invisibly
and diverges only on wider screens (recorded: a live hero card 940px at
1920 vs the frozen 720px; the CTA row wrapped as a side effect). The same
trap applies to DOM: capturing the 1440 layout OUTCOME (a button row that
wrapped 3+1, authored as two rows / styled with an .x + .x sibling rule)
instead of the layout MODEL (one wrapping flex row) freezes a
viewport-specific artifact into content and CSS.
% / vw / max-width model) in the
source CSS and encode the RULE, never the resolved px. Boxes that hold
constant are legitimately fixed. Section heights and overlaps get the
same test: a fixed-height hero gated pixel-perfect at 1440 read "10px
off" to a reviewer browsing at 1512 because live scaled it with the
viewport (recorded); when a height or overlap scales, encode it
vw-proportional (px@1440 ÷ 14.4 = vw, or the authored vh/% rule).source-fidelity-gate.md § Wide-viewport fluid check) — but the check
only catches what this rule prevents; lifting the model up front is the
cheap half.Sites built as "cards on a canvas" — white/tinted wrap sections floating on a page background — tempt the recreation into margin-based boxes, and margins collapse: two adjacent wraps each carrying a section-pad margin lose one pad at every junction (recorded: 96px designed → 48px rendered), a systematic per-junction height error the anchor probe reads as every section top drifting further down the page. Two safe constructions:
:has()-based wrap rules they override, or the wrap rule silently
wins and the junction re-collapses.The inverse case — the SOURCE's margins do NOT collapse where a clean
recreation's would: AEM-classic (and any clearfix-era) components wrap each
section in a clearfix, whose block formatting context contains child margins.
display: flow-root on the recreated section wrapper reproduces that
containment exactly — one rule fixed systematic −48/−20px per-section errors
across a whole page in the field. Corollary for responsive work: a column
that is floated on desktop loses its BFC when a media query un-floats it, and
the last child's margin escapes at mobile only — add flow-root to the
un-floating override.
CDN-defended sites (Akamai/Cloudflare/Demandware) 403 direct asset requests,
font files, and even in-page fetch() from a headless client. What works:
svg.outerHTML (plus <img src$=".svg"> and mask-image urls) near the
matched text or aria-label. Six hand-drawn lookalikes (person, globe,
bell, clock, swap-arrows, check) passed every block gate and read wrong to
the eye; all six were extractable verbatim in one probe (recorded). A
hand-drawn icon is the fallback only when no live vector exists — and it
is a ledger entry.Capture-state policy — ground truth is the page as observable by the instrument. Two recurring cases:
complete && naturalWidth > 0 (fooling load checks). Do NOT force
data-src→src swaps — the CDN 403s the forced renditions and you get
broken-image icons, worse than placeholders. Replicate the site's own
placeholder assets where the live capture shows placeholders, and log
each instance in the progress ledger flagged for the delivery phase
(real renditions get wired when authors upload media).:hover-styled element under the resting cursor is a false-measurement
trap (recorded: a consent click left the cursor over a hero whose
a.box-hover:hover img{opacity:.4} shipped the entire live capture with
the hero dimmed). Instrument hardening includes parking the pointer —
stitch-shot.mjs does it after consent dismissal; mirror it in any ad-hoc
capture that clicks anything.scrollHeight differs before vs after the settle pass (recorded: 3183 vs
3093 at 360 — entrance translate3d(0,90px,0) transforms inflate the
document until elements go inview). stitch-shot's settle handles it; any
ad-hoc probe that reads document height — the diagnosis probes this skill
encourages included — must settle first too.Zeroing content-diff against a live page requires mirroring the live DOM's node granularity, which is in tension with clean re-authoring. Policy — mirror these classes rather than fighting per-page false-reds:
<main>,
carousel clone slides, hidden tab-panel links, sr-only labels ("Old
price"), even server-truncated strings. Content parity means DOM parity,
not visible-text parity — reproduce them hidden, exactly as captured.<p><br>\r\n </p> and renders TWO line boxes (the <br> plus the still-
collapsible space); headings lead with <br> (<h1><br>\r\nTitle); a
trailing after an inline close is a REAL extra line. Approximating
these as clean <p><br></p> measures 20–36px short per instance. Mirror
the byte patterns as captured; when a wrap-count mismatch survives width
parity, diff innerHTML — the byte-level difference is usually the cause.Widgets are implemented, not justified away. Mirroring is the default for widget content, hidden or not — and beyond mirroring the DOM, widgets must WORK wherever the live site's do: a live carousel becomes a working carousel, a live select carries its real option list, tabs switch, accordions open. Justifying a whole widget away as a class-level residual is not acceptable; confirm-justify stays reserved for genuinely unreachable or nondeterministic content (ticker values, personalization — § Asset harvest, capture-state policy). The interaction-parity pass (§ Interaction parity) is what makes this affordable: probe the live behavior cheaply, replicate it, and mirror the full widget DOM so content-diff stays at zero structural red.
content-diff classifies every string by DOM wrapping + computed style +
heading level, never by text alone: a string inside an <a> is a CTA, an
uppercase small-type node is an eyebrow, an <h3> is not an <h2>. So a
recreation that carries every string verbatim can still open with dozens of
structural 🔴 — recorded (furniture retailer, iteration 1): 43 CTAs vs 58 and 12
eyebrows vs 6, all role swaps, zero dropped copy — the live page
wrapped labels in anchors where the recreation used spans, and vice versa.
Policy:
<a> vs
<button> vs <span>), its heading LEVEL, and the eyebrow-style
signature (uppercase + small size) — not just the visible text. This is
the same discipline as § Granularity parity, one level up: granularity
parity mirrors how text is split, role parity mirrors what it is wrapped
in.2×perView clones before and after the track, clones carry no text nodes,
alt="", no href, no role/aria-* (aria-hidden/inert do not
help — readability checkers read textContent). A faithful clone of the
canon's DOM turns a 5-card rail into 17 cards of text and halves the
page's AI-readability score (deploy/reference/ai-readability.md).The gate measures static pixels at t=0 only: every scroll-entrance animation, header scroll-morph, hover transition, and secondary carousel is invisible to all four probes — and users notice them immediately on first manual review. After the static gate passes (never before — interaction work on unconverged geometry is rework), run the interaction-parity pass. It is a REQUIRED per-archetype gate output (SKILL.md Phase 4 — the motion inventory), not an optional post-pass: in the field it was skipped on 5 of 7 archetypes, including under parallel sub-agent briefs, and every skipped archetype shipped visibly static. Any parallel archetype fan-out brief must carry this section's evidence rule and instrument invocation verbatim — this is precisely the step agents skip when unprompted.
The evidence rule: motion is OBSERVED at runtime, never inferred from
static classes or CSS rules. Run ../scripts/motion-observe.mjs ONCE per
archetype live URL (full down+up scroll traversal; --click each
carousel/widget control; --hover each distinct card/teaser/button
family; reuse the JSON — observation costs live hits like any probe).
Implement ONLY behaviors that measurably fired, with the recorded trigger
mechanism, durations, and thresholds. Static source CSS is then the
authority for the exact keyframe/easing VALUES of those fired animations.
A behavior implemented without a runtime trace naming it is a fidelity bug
— same severity as an unregistered design change.
Static lifting INVENTS motion three distinct ways (all field-recorded, all caught in user review — this is why the evidence rule exists):
.animate etc.) to only SOME of them (recorded: of ~8
caption-class families carrying an entrance class, 2 ever fired; 3 whole
page types had ZERO firing entrances despite fully classed markup).
Tagging from static classes animates elements the real site never
animates. A live-classed-but-dead behavior is recorded as NOT
implemented — that is the correct replica of a dead class.:hover rule can be dead at runtime (scoping condition,
specificity loser, wrong variant) — and the inverse: the fired hover may
move a different element than the rule suggests (recorded: media-card
hover scaled the caption, not the card). Only a measured hover diff
justifies a hover rule in the prototype.The two probe patterns the instrument wraps (both cheap, generic, no source
JS needed): hover diff (--hover) — computed
background/color/shadow/transform on the element + key sub-elements,
before vs after a real pointer hover, plus transition-*; overlays
dismissed first (an overlay intercepts the pointer and the probe reads no
change), mouse parked between probes; the changed-property list translates
directly to :hover CSS. Behavior diff (--click) — click the
control, sample the animated property mid-flight and settled
(transform/scrollLeft + computed transition-*): pitch, easing,
duration. Map observed elements to prototype counterparts by the TEXT
SNIPPET in the event log, never by class names (prototype classes are
clean re-authored names).
Swiper-lock semantics (the dominant carousel library): Swiper hides its
controls and disables dragging when the content fits the viewport
(swiper-lock/watch-overflow), so per-breakpoint the same widget is
sometimes a carousel, sometimes a static row. A scroll-based replica
reproduces the entire behavior with no DOM restructuring: arrows drive
scrollTo on the already-overflow:hidden track, position count =
round((scrollWidth − clientWidth) / pitch) + 1, and controls
hide when scrollWidth <= clientWidth — which auto-degrades to the static
case exactly where the live widget locks. Late re-renders (e.g. re-check at
400/1200/3000ms and window load) are needed when the platform decorates
DOM after the widget initializes — the overflow measurement taken at
decorate time is stale by first interaction.
Implementation pattern — ONE shared motion layer per project, never
per-page forks: css/motion.css (lifted keyframes verbatim; trigger
rules reusing the LIVE class names — that keeps the capture instruments
symmetric, since stitch-shot's animation handling already keys on the live
conventions; hover rules hover-diff-verified only; chrome-morph classes;
indicator transitions) + js/motion.js (an IntersectionObserver adding
the live trigger class — threshold as measured, typically ~0.15, once —
to a tagging map of runtime-FIRED selectors only; the chrome state machine
on scroll with the measured direction + thresholds; widget drivers
mirroring observed mechanics). Gate-safety is by construction: entrance
animations only run once the trigger class is added — verify the live site
has no pre-animate hidden state (if it does, stitch-shot's entrance-state
forcing covers it); chrome morph is inert at y=0; hovers need a pointer;
stitch-shot clears timers so autoplay stays at t=0. Widget DOM stays fully
mirrored so content-diff holds at zero structural red (§ Granularity
parity — widgets are implemented, not justified away).
Verification protocol, both directions: (1) no pixel regression — re-run pixel-compare per touched archetype at the gate breakpoints; the number must return to (± noise of) the gated value (recorded: 1.01% gated → 1.06% with invented motion → 1.01% exact after the evidence-only rewrite). The drift itself is the smell test: motion code that changes t=0 is wrong. (2) behavior match — a headless run against the PROTOTYPE asserting, per page: tagged-element count == live fired count; chrome state at {top, scrolled-down, scrolled-up, back-to-top} == the live headerTimeline states; zero pageerrors. This is the motion analog of the anchor probe, trivial to script from the observe JSON.
Pitfalls (each field-recorded):
--click rather than waiting for autoplay events.left/transform
where an equivalent rendered effect in the recreation animates
width/height — equivalence of the RENDERED effect is the bar, but the
duration/easing must be the measured ones.prefers-reduced-motion: mirror the live site's handling — do not
"improve" by adding it where live has none; that's an
inconsistency-register item, not a freebie.Log each implemented interaction in the progress ledger the way a CSS portation is logged; the static gate is then re-run per the verification protocol above (markup rarely changes — hover CSS and trigger JS are capture-invisible under the freeze, and the pixel re-run proves it).
position: fixed/sticky chrome interacts with the stitched capture in
three ways, each observed live on the first fresh-site run (a design-furniture site):
--vh px).
Instrument behavior, not a page defect — but only while it is symmetric.body.header-minimized 55px
hamburger bar once scrolled, so chunks 2+ carry different chrome than
chunk 1 — the stitched live capture contains BOTH states.Resolution — symmetry, including the scroll-state trigger. Replicate the fixed chrome AS fixed (never flattened to static/in-flow — that changes both the geometry and the seam behavior), and when the live chrome morphs with scroll, give the prototype the SAME morph so chunks 2+ match.
This is the one sanctioned exception to the "no JS unless a section's initial state requires computing it" rule, and the tension resolves cleanly: that rule guards against behavior for its own sake, but scroll-state chrome is instrument-induced state — the capture instrument scrolls, so the instrument itself puts the live page into the morphed state, and a static prototype can never measure symmetric against it. Minimal prototype JS for scroll-state chrome is therefore permitted, tightly bounded: a few lines toggling the same class at the same scroll threshold as the live site (lift both the class and the threshold from the source JS/CSS — never guess them), no frameworks, no other behavior. Log the addition in the progress ledger the way a CSS portation is logged.
Reading the diff: any height delta between the captures de-aligns the seams, turning every seam repeat into a ghost band in the pixel diff (observed: seam ghosting of a fixed newsletter tab, plus a hot band exactly at a chunk boundary). Fix the height delta first — seam ghosts below the first hot band are offset contamination, not chrome bugs.
Re-authoring is the mainline — validated as sufficient for every section type on a strict design-system site. Porting the source's own CSS rules is the reserve, admissible for a SECTION only when it hits one of:
Rules when it fires: port the minimal rule set for that section, tree-shaken to used rules, scoped under the section's class; record the portation (and why) in the progress ledger. Never page-level — page-level portation carries the source's CSS debt into the blocks, defeats "better implementation of key pages", and is fragile under block-class scoping. A page that would need page-level portation is a page that should be flagged to the user as a snowflake-overlay candidate (byte-preservation escape hatch, outside this skill).
.tessl-plugin
skills
audit
reference
deploy
diff
direct
extract
migrate
prepare-migration
prototype
replica
reskin
rollout
stardust
uplift
reference