/1t1scopy
Bring a project page at <route> to pixel-identical parity with a reference (live original site and/or downloaded HTML mirror). The skill captures three versions via Playwright, diffs them, migrates legacy global CSS into CSS Modules per the FSD layout, and iterates until parity.
Usage
/1t1scopy <route> # e.g. /1t1scopy /blog/some-post
/1t1scopy <route> --orig <url> # original site URL (skip the question)
/1t1scopy <route> --ref <path|url> # downloaded reference (file path or :4444 URL)
/1t1scopy <route> --ours <url> # ours (default http://localhost:3000<route>)
/1t1scopy <route> --viewports 1920,1024,540,390 # default
/1t1scopy <route> --no-migrate # diff only, don't write code
/1t1scopy <route> --max-iters 4 # parity loop cap (default 3)
When to invoke
Invoke this skill when the user types /1t1scopy … or asks to "port the page", "make it pixel-identical to reference/site", "compare with reference and fix", "migrate styles for this page", or any equivalent in Russian ("сравни с референсом", "приведи к идентичности", "перенеси стили в модули для страницы X").
Before doing anything else: prerequisites + collect sources.
Step 0 — Prerequisites & sources
Required MCP: Playwright MCP must be available. Tool names start with mcp__playwright__… (deferred — load via ToolSearch with query: "select:mcp__playwright__browser_navigate,mcp__playwright__browser_take_screenshot,mcp__playwright__browser_resize,mcp__playwright__browser_evaluate,mcp__playwright__browser_snapshot,mcp__playwright__browser_console_messages,mcp__playwright__browser_close"). If unavailable → tell user to install @playwright/mcp and stop.
Parse args: extract <route> (required, must start with /). Parse optional flags.
Resolve "ours" URL:
- Default:
http://localhost:3000<route>.
- Check parallel-session rule from project memory
[[project_phase2_componentization]]: if a parallel claude session might be running, ours may be on :3100 with NEXT_DIST_DIR=.next-parallel. Probe curl -sI http://localhost:3000<route> first; if it fails or returns 4xx/5xx, probe :3100. If neither responds, ask user to start dev (npm run dev) and wait.
Resolve "orig" URL (the live original site):
- If
--orig provided → use it.
- Else: ask user — "Какой URL оригинала для этого роута? (например
https://your-site.com<route>)". Accept "skip" to omit.
Resolve "ref" reference:
- If
--ref provided → use it directly.
- Else: probe local mirror —
curl -sI http://localhost:4444<route> (and <route>/index.html). If it responds → use that URL.
- Else: probe
reference/site/your-site.com<route>/index.html and reference/site/your-site.com<route>.html on disk. If found → tell user we'll serve it via Playwright file:// or via a python3 -m http.server 4444 started in reference/site/your-site.com/.
- Else: ask user — "Есть ли скачанный референс? Дай путь или URL, или 'skip'."
Verify at least one of (orig, ref) is available. If both skipped, stop — there's nothing to compare against.
Memory checks — read project memories under $HOME/.claude/projects/<your-project-memory-slug>/memory/ that match these slugs (load them up-front, they encode landmines):
feedback_use_playwright
feedback_css_migration_pitfalls
feedback_slider_bm_bridge
feedback_premium_css_header_bridge
reference_4444_font_artifact (mirror lacks webfont → don't trust glyph-level diffs, compare layout)
reference_premium_new_folder (some refs live in new/ subfolder)
project_phase2_componentization
project_legacy_css_teardown
Step 1 — Capture (Playwright MCP)
For each available source (ours, orig, ref) capture at each viewport (default 1920×1080, 1024×768, 540×900, 390×844):
For each (source, viewport):
mcp__playwright__browser_resize to {width, height}.
mcp__playwright__browser_navigate to the URL.
- Wait for
networkidle (or 2s timeout fallback).
mcp__playwright__browser_take_screenshot → save to .1t1scopy/<route-slug>/<source>__<viewport>.png (create directory if missing).
mcp__playwright__browser_snapshot → capture accessibility tree (use this, not raw DOM, for structural diff — it's smaller and semantic).
mcp__playwright__browser_evaluate to extract a "layout fingerprint":() => {
const nodes = [...document.querySelectorAll('main *, header *, footer *')].slice(0, 400);
return nodes.map(el => {
const r = el.getBoundingClientRect();
const cs = getComputedStyle(el);
return {
tag: el.tagName.toLowerCase(),
cls: el.className?.toString?.() ?? '',
id: el.id || null,
box: { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) },
text: (el.textContent || '').trim().slice(0, 80),
style: {
display: cs.display, position: cs.position,
font: `${cs.fontFamily}/${cs.fontSize}/${cs.fontWeight}/${cs.lineHeight}`,
color: cs.color, bg: cs.backgroundColor,
margin: cs.margin, padding: cs.padding,
border: cs.border, borderRadius: cs.borderRadius,
},
};
});
}
Save to .1t1scopy/<route-slug>/<source>__<viewport>.json.
mcp__playwright__browser_console_messages → save errors/warnings (helps catch missing assets, CSP, hydration).
After capturing all sources, write a small .1t1scopy/<route-slug>/index.md index linking the artifacts so the user can browse.
Glyph trap — if ref is from local :4444 mirror, the webfont is missing ([[reference_4444_font_artifact]]). Treat text-bounding-box width/height diffs vs ours as expected; compare structural layout, not glyph metrics. For text comparisons, prefer the raw HTML from the reference file or the orig live capture.
Step 2 — Diff
Produce a structured diff report at .1t1scopy/<route-slug>/DIFF.md. Compare in this priority order:
- Structural (high signal) — accessibility-tree node sequences. List nodes present in
ref/orig but missing in ours, and vice versa. Include their text content for grep.
- Layout (high signal) — for the K=20 largest visible boxes per viewport, compute
(x, y, w, h) deltas between ours and ref (or orig). Flag any delta > 8px on width/height or > 16px on position.
- Typography — diff
fontSize, lineHeight, fontWeight, letterSpacing per matched node. Ignore fontFamily if ref is :4444.
- Color/decoration —
color, backgroundColor, border, borderRadius, boxShadow.
- Spacing —
margin, padding.
- Console — errors only in
ours are migration regressions; flag them.
For each finding, write one line: [severity] <viewport> <selector> — <metric>: ours=<x> ref=<y>. Severity: block (missing element, wrong color, broken layout > 20px), warn (spacing/font 4-20px off), note (sub-pixel).
End the diff with a Parity score = 1 - (block*1 + warn*0.3 + note*0.05) / nodes_compared. Aim for ≥ 0.97 to declare parity.
Step 3 — Plan migration
Read DIFF.md and produce .1t1scopy/<route-slug>/PLAN.md. The plan must be specific:
- Components to create/modify: list FSD slices (
widgets/<x>, views/<y>) that own each missing or off element. Prefer reusing existing slices over new ones — [[project_phase2_componentization]] already mapped most body_html sections.
- Styles to migrate: for each off rule, name the source (legacy file + selector) and target (
<slice>/ui/<Component>.module.css + class name). Follow [[feedback_css_migration_pitfalls]]:
- GREP CHECKLIST — search
src/shared/styles/legacy-overrides.css, src/shared/styles/legacy-theme.css, and src/shared/styles/content-sections.css for every BEM-ish class name found in the reference. Extractor doesn't see overrides — must grep.
- Cascade trap — module-scoped rules win over
legacy-overrides for the same specificity. When porting, also DELETE the old rule from overrides, or boost the module rule's specificity beyond it.
- Compound selectors — if legacy uses
.parent .child--mod, ensure the React tree still emits parent and child--mod BEM classes alongside CSS-module classes, or rewrite to module-only.
- Legacy classes to remove: list rules in
legacy-*.css that become dead after migration (so we don't carry duplicates).
- Risk notes: anything the user should confirm before edits (e.g. "this class is also used on 12 other pages — confirm scope").
Show the plan, get user ok (unless --no-migrate).
Step 4 — Apply changes
Execute the plan in small commits:
- Create/edit
<Component>.module.css with ported rules.
- Wire
styles.<class> into the JSX (preserve original BEM classes as data hooks if any compound selector still depends on them — see compound-selector trap above).
- Remove dead rules from
legacy-overrides.css / legacy-theme.css.
- After each logical group of changes, run a fast quality probe:
npm run typecheck && npm run lint
(Skip full npm run check — too slow for iteration; user runs it before commit.)
- Tests: only update co-located
*.test.tsx if behavior actually changed. Remember [[reference_vitest_css_false]] — vitest strips CSS, never assert on styles.* class names; assert on data-testid / semantics.
Do not auto-commit. Stop after each iteration and let user inspect.
Step 5 — Verify & loop
Re-capture only ours for all viewports (orig/ref capture stays). Re-run Step 2 diff. Append result to DIFF.md under ## Iteration N.
- If Parity score ≥ 0.97 → declare done, show user the screenshots side-by-side and the final parity score.
- Else if iteration count <
--max-iters (default 3) → return to Step 3 with the residual diff.
- Else → stop, list remaining gaps and explicitly ask user how to proceed (some gaps may be intentional / accepted).
Output contract
By the end of a run, the following must exist:
.1t1scopy/<route-slug>/
ours__1920.png ours__1024.png ours__540.png ours__390.png
orig__*.png (if orig was used)
ref__*.png (if ref was used)
ours__*.json (layout fingerprints)
orig__*.json (...)
ref__*.json (...)
index.md (browseable index)
DIFF.md (initial + per-iteration diffs)
PLAN.md (migration plan)
.1t1scopy/ is gitignored (add to .gitignore if missing — local working dir only).
The final user-facing message must include: parity score, screenshot paths, and a one-line summary of what changed in code.
Hard rules
- Don't trust :4444 fonts. Layout diffs from
:4444 reference are real; glyph-metric diffs aren't. Use orig for text-metric truth when available.
- Don't run
npm run check mid-iteration — it includes vitest and is too slow. Use typecheck && lint only.
- Don't introduce new
content.ts or dangerouslySetInnerHTML (project rule, CLAUDE.md).
- Don't auto-commit. User commits when satisfied.
- Don't rename
views/ → pages/. It's deliberate ([[project_phase2_componentization]]).
- All prose in Russian per
[[feedback_respond_in_russian]]. Code, file names, commit messages stay as-is.
- One slice per content type, not per page — when creating new components, reuse the type-level slice if the route shares it (city/country/blog-post/etc.).
1---2name: 1t1scopy3description: Pixel-parity port of a page from a reference (original site or local HTML mirror) to the current Next.js/FSD project. Uses Playwright MCP to capture original + ours at multiple viewports, diffs layout/DOM/computed styles, then migrates legacy CSS to CSS Modules until parity. Trigger: /1t1scopy <route>4---56# /1t1scopy78Bring a project page at `<route>` to pixel-identical parity with a reference (live original site and/or downloaded HTML mirror). The skill captures three versions via Playwright, diffs them, migrates legacy global CSS into CSS Modules per the FSD layout, and iterates until parity.910## Usage1112```13/1t1scopy <route> # e.g. /1t1scopy /blog/some-post14/1t1scopy <route> --orig <url> # original site URL (skip the question)15/1t1scopy <route> --ref <path|url> # downloaded reference (file path or :4444 URL)16/1t1scopy <route> --ours <url> # ours (default http://localhost:3000<route>)17/1t1scopy <route> --viewports 1920,1024,540,390 # default18/1t1scopy <route> --no-migrate # diff only, don't write code19/1t1scopy <route> --max-iters 4 # parity loop cap (default 3)20```2122## When to invoke2324Invoke this skill when the user types `/1t1scopy …` or asks to "port the page", "make it pixel-identical to reference/site", "compare with reference and fix", "migrate styles for this page", or any equivalent in Russian ("сравни с референсом", "приведи к идентичности", "перенеси стили в модули для страницы X").2526Before doing anything else: prerequisites + collect sources.2728---2930## Step 0 — Prerequisites & sources31321. **Required MCP**: Playwright MCP must be available. Tool names start with `mcp__playwright__…` (deferred — load via `ToolSearch` with `query: "select:mcp__playwright__browser_navigate,mcp__playwright__browser_take_screenshot,mcp__playwright__browser_resize,mcp__playwright__browser_evaluate,mcp__playwright__browser_snapshot,mcp__playwright__browser_console_messages,mcp__playwright__browser_close"`). If unavailable → tell user to install `@playwright/mcp` and stop.33342. **Parse args**: extract `<route>` (required, must start with `/`). Parse optional flags.35363. **Resolve "ours" URL**:37 - Default: `http://localhost:3000<route>`.38 - Check parallel-session rule from project memory `[[project_phase2_componentization]]`: if a parallel claude session might be running, ours may be on `:3100` with `NEXT_DIST_DIR=.next-parallel`. Probe `curl -sI http://localhost:3000<route>` first; if it fails or returns 4xx/5xx, probe `:3100`. If neither responds, ask user to start dev (`npm run dev`) and wait.39404. **Resolve "orig" URL** (the live original site):41 - If `--orig` provided → use it.42 - Else: ask user — *"Какой URL оригинала для этого роута? (например `https://your-site.com<route>`)"*. Accept "skip" to omit.43445. **Resolve "ref" reference**:45 - If `--ref` provided → use it directly.46 - Else: probe local mirror — `curl -sI http://localhost:4444<route>` (and `<route>/index.html`). If it responds → use that URL.47 - Else: probe `reference/site/your-site.com<route>/index.html` and `reference/site/your-site.com<route>.html` on disk. If found → tell user we'll serve it via Playwright `file://` or via a `python3 -m http.server 4444` started in `reference/site/your-site.com/`.48 - Else: ask user — *"Есть ли скачанный референс? Дай путь или URL, или 'skip'."*49506. **Verify at least one of (orig, ref) is available.** If both skipped, stop — there's nothing to compare against.51527. **Memory checks** — read project memories under `$HOME/.claude/projects/<your-project-memory-slug>/memory/` that match these slugs (load them up-front, they encode landmines):53 - `feedback_use_playwright`54 - `feedback_css_migration_pitfalls`55 - `feedback_slider_bm_bridge`56 - `feedback_premium_css_header_bridge`57 - `reference_4444_font_artifact` (mirror lacks webfont → don't trust glyph-level diffs, compare layout)58 - `reference_premium_new_folder` (some refs live in `new/` subfolder)59 - `project_phase2_componentization`60 - `project_legacy_css_teardown`6162---6364## Step 1 — Capture (Playwright MCP)6566For each available source (`ours`, `orig`, `ref`) capture at each viewport (default `1920×1080, 1024×768, 540×900, 390×844`):6768For each `(source, viewport)`:69701. `mcp__playwright__browser_resize` to `{width, height}`.712. `mcp__playwright__browser_navigate` to the URL.723. Wait for `networkidle` (or 2s timeout fallback).734. `mcp__playwright__browser_take_screenshot` → save to `.1t1scopy/<route-slug>/<source>__<viewport>.png` (create directory if missing).745. `mcp__playwright__browser_snapshot` → capture accessibility tree (use this, not raw DOM, for structural diff — it's smaller and semantic).756. `mcp__playwright__browser_evaluate` to extract a "layout fingerprint":76 ```js77 () => {78 const nodes = [...document.querySelectorAll('main *, header *, footer *')].slice(0, 400);79 return nodes.map(el => {80 const r = el.getBoundingClientRect();81 const cs = getComputedStyle(el);82 return {83 tag: el.tagName.toLowerCase(),84 cls: el.className?.toString?.() ?? '',85 id: el.id || null,86 box: { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) },87 text: (el.textContent || '').trim().slice(0, 80),88 style: {89 display: cs.display, position: cs.position,90 font: `${cs.fontFamily}/${cs.fontSize}/${cs.fontWeight}/${cs.lineHeight}`,91 color: cs.color, bg: cs.backgroundColor,92 margin: cs.margin, padding: cs.padding,93 border: cs.border, borderRadius: cs.borderRadius,94 },95 };96 });97 }98 ```99 Save to `.1t1scopy/<route-slug>/<source>__<viewport>.json`.1007. `mcp__playwright__browser_console_messages` → save errors/warnings (helps catch missing assets, CSP, hydration).101102After capturing all sources, write a small `.1t1scopy/<route-slug>/index.md` index linking the artifacts so the user can browse.103104> **Glyph trap** — if `ref` is from local `:4444` mirror, the webfont is missing (`[[reference_4444_font_artifact]]`). Treat text-bounding-box width/height diffs vs ours as expected; compare structural layout, not glyph metrics. For text comparisons, prefer the raw HTML from the reference file or the `orig` live capture.105106---107108## Step 2 — Diff109110Produce a structured diff report at `.1t1scopy/<route-slug>/DIFF.md`. Compare in this priority order:1111121. **Structural (high signal)** — accessibility-tree node sequences. List nodes present in `ref/orig` but missing in `ours`, and vice versa. Include their text content for grep.1132. **Layout (high signal)** — for the K=20 largest visible boxes per viewport, compute `(x, y, w, h)` deltas between `ours` and `ref` (or `orig`). Flag any delta > 8px on width/height or > 16px on position.1143. **Typography** — diff `fontSize`, `lineHeight`, `fontWeight`, `letterSpacing` per matched node. Ignore `fontFamily` if `ref` is `:4444`.1154. **Color/decoration** — `color`, `backgroundColor`, `border`, `borderRadius`, `boxShadow`.1165. **Spacing** — `margin`, `padding`.1176. **Console** — errors only in `ours` are migration regressions; flag them.118119For each finding, write one line: `[severity] <viewport> <selector> — <metric>: ours=<x> ref=<y>`. Severity: `block` (missing element, wrong color, broken layout > 20px), `warn` (spacing/font 4-20px off), `note` (sub-pixel).120121End the diff with a **Parity score** = `1 - (block*1 + warn*0.3 + note*0.05) / nodes_compared`. Aim for ≥ 0.97 to declare parity.122123---124125## Step 3 — Plan migration126127Read `DIFF.md` and produce `.1t1scopy/<route-slug>/PLAN.md`. The plan must be specific:128129- **Components to create/modify**: list FSD slices (`widgets/<x>`, `views/<y>`) that own each missing or off element. Prefer reusing existing slices over new ones — `[[project_phase2_componentization]]` already mapped most body_html sections.130- **Styles to migrate**: for each off rule, name the source (legacy file + selector) and target (`<slice>/ui/<Component>.module.css` + class name). Follow `[[feedback_css_migration_pitfalls]]`:131 - **GREP CHECKLIST** — search `src/shared/styles/legacy-overrides.css`, `src/shared/styles/legacy-theme.css`, and `src/shared/styles/content-sections.css` for every BEM-ish class name found in the reference. Extractor doesn't see overrides — must grep.132 - **Cascade trap** — module-scoped rules win over `legacy-overrides` for the same specificity. When porting, also DELETE the old rule from overrides, or boost the module rule's specificity beyond it.133 - **Compound selectors** — if legacy uses `.parent .child--mod`, ensure the React tree still emits `parent` and `child--mod` BEM classes alongside CSS-module classes, or rewrite to module-only.134- **Legacy classes to remove**: list rules in `legacy-*.css` that become dead after migration (so we don't carry duplicates).135- **Risk notes**: anything the user should confirm before edits (e.g. "this class is also used on 12 other pages — confirm scope").136137Show the plan, get user `ok` (unless `--no-migrate`).138139---140141## Step 4 — Apply changes142143Execute the plan in small commits:1441451. Create/edit `<Component>.module.css` with ported rules.1462. Wire `styles.<class>` into the JSX (preserve original BEM classes as data hooks if any compound selector still depends on them — see compound-selector trap above).1473. Remove dead rules from `legacy-overrides.css` / `legacy-theme.css`.1484. After each logical group of changes, run a fast quality probe:149 ```bash150 npm run typecheck && npm run lint151 ```152 (Skip full `npm run check` — too slow for iteration; user runs it before commit.)1535. Tests: only update co-located `*.test.tsx` if behavior actually changed. Remember `[[reference_vitest_css_false]]` — vitest strips CSS, never assert on `styles.*` class names; assert on `data-testid` / semantics.154155Do not auto-commit. Stop after each iteration and let user inspect.156157---158159## Step 5 — Verify & loop160161Re-capture only `ours` for all viewports (orig/ref capture stays). Re-run Step 2 diff. Append result to `DIFF.md` under `## Iteration N`.162163- If Parity score ≥ 0.97 → declare done, show user the screenshots side-by-side and the final parity score.164- Else if iteration count < `--max-iters` (default 3) → return to Step 3 with the residual diff.165- Else → stop, list remaining gaps and explicitly ask user how to proceed (some gaps may be intentional / accepted).166167---168169## Output contract170171By the end of a run, the following must exist:172173```174.1t1scopy/<route-slug>/175 ours__1920.png ours__1024.png ours__540.png ours__390.png176 orig__*.png (if orig was used)177 ref__*.png (if ref was used)178 ours__*.json (layout fingerprints)179 orig__*.json (...)180 ref__*.json (...)181 index.md (browseable index)182 DIFF.md (initial + per-iteration diffs)183 PLAN.md (migration plan)184```185186`.1t1scopy/` is gitignored (add to `.gitignore` if missing — local working dir only).187188The final user-facing message must include: parity score, screenshot paths, and a one-line summary of what changed in code.189190---191192## Hard rules193194- **Don't trust :4444 fonts.** Layout diffs from `:4444` reference are real; glyph-metric diffs aren't. Use `orig` for text-metric truth when available.195- **Don't run `npm run check` mid-iteration** — it includes vitest and is too slow. Use `typecheck && lint` only.196- **Don't introduce new `content.ts`** or `dangerouslySetInnerHTML` (project rule, CLAUDE.md).197- **Don't auto-commit.** User commits when satisfied.198- **Don't rename `views/` → `pages/`.** It's deliberate (`[[project_phase2_componentization]]`).199- **All prose in Russian** per `[[feedback_respond_in_russian]]`. Code, file names, commit messages stay as-is.200- **One slice per content type, not per page** — when creating new components, reuse the type-level slice if the route shares it (city/country/blog-post/etc.).