figma-to-html-pixel-perfect 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILL.md CHANGED
@@ -9,1579 +9,157 @@ description: >
9
9
  accessibility, animations, and visual consistency.
10
10
  ---
11
11
 
12
- # Figma to HTML/CSS — Pixel-Perfect Implementation Skill
12
+ # Figma HTML/CSS, pixel-perfect
13
13
 
14
- ## 0. Setup (one-time, before first use)
14
+ This file is the protocol. Detail lives in `references/` — **read the named reference at
15
+ each step; they are part of the protocol, not optional background.**
15
16
 
16
- The skill needs read access to the Figma file. Walk the user through this once; do not
17
- guess or skip it.
17
+ ## THE NUMBER RULE the one non-negotiable contract
18
18
 
19
- ### 0.1 Figma personal access token (required)
19
+ **The node JSON is the complete design. Every px value you write into CSS must be a
20
+ number read from the node JSON, or arithmetic on such numbers, written down at the
21
+ moment you write the rule.** There is no legitimate third source of a value.
20
22
 
21
- The REST API is the source of every value (geometry, tokens, text, image refs). It is not
22
- seat-limited like the MCP server, but it IS quota-limited per plan: **every endpoint —
23
- including `/v1/files` (node JSON) — can 429 with a Retry-After of hours to days** (observed
24
- live: /files 429 Retry-After 22,734s while the fill-URL endpoint still answered 200).
25
- Therefore work **cache-first**: pull each node's JSON ONCE to `figma/nodes/`, then every
26
- script reads the disk cache — never the API. `figma_pull.py` skips nodes already on disk
27
- (`--force` to refresh). Scarcity order: `/v1/images` (renders) most scarce → `/v1/files`
28
- also quota'd → `/v1/files/:key/images` (fill URLs) most tolerant → the S3 asset bytes
29
- themselves are not metered at all.
23
+ The only permitted workflow for creating OR fixing any spacing/size/position:
30
24
 
31
- 1. figma.com **Settings Security → Personal access tokens** → generate
32
- 2. Scope: **`File content: Read`**
33
- 3. Store it *outside* the transcript never ask the user to paste it into chat:
25
+ 1. Dump the numbers: `python3 scripts/figma_spec.py figma/nodes/<section>.json`
26
+ 2. Compute the CSS value from those numbers (`margin-top = child.y − (prev.y + prev.h)`).
27
+ 3. Write the rule with the derivation in a comment: `margin-top: 24px; /* 404−(146+248−14lead) */`
28
+ 4. Verify with the report once.
34
29
 
35
- ```bash
36
- echo 'YOUR_TOKEN' > ~/.figma_token && chmod 600 ~/.figma_token
37
- ```
30
+ Forbidden, no exceptions, regardless of time pressure:
38
31
 
39
- `scripts/figma_pull.py` reads `FIGMA_TOKEN` or `~/.figma_token`.
32
+ - **Nudge-and-remeasure.** The report prints `[design y…]` beside every delta precisely
33
+ so the correct edit is a subtraction, not an experiment.
34
+ - **Reading only the delta.** `y +8` means: find WHICH gap in the JSON chain is wrong and
35
+ set THAT gap to the design's number — not "subtract 8 somewhere".
36
+ - **Skipping step 1 because you are in a hurry.** Skipping it under time pressure is what
37
+ produced the slowest sessions on record. The fast path IS the extraction path.
40
38
 
41
- ### 0.2 File export permission (required)
39
+ Tooling enforces this: `figma_report.py` auto-runs the duplicate-selector check (FM123)
40
+ every regen and prints design targets beside deltas; `figma_lint.py` fails on any CSS
41
+ value that exists nowhere in the design. If a guard fires, fixing it is the next action.
42
42
 
43
- Run the preflight (§6.5.1). If it returns `403 "File not exportable"`, the file owner has
44
- disabled export/copy/share. **No token, seat, plan, duplicate-to-drafts or manual export
45
- will bypass it.** Only the owner (or someone with edit access) can re-enable
46
- *"Allow viewers to copy, share and export"* in the Share dialog. Say so and stop.
43
+ ## Modes
47
44
 
48
- ### 0.3 Fontsask for the file, every time
49
-
50
- Figma renders with the real font; you almost never have it. **Ask for the font file up
51
- front, as a required input, not a nice-to-have.** It cannot be recovered any other way:
52
-
53
- - the REST API does not serve font binaries;
54
- - an SVG export with "Outline text" contains no font data at all;
55
- - an SVG export *without* outlining only names the family — it does not embed it.
56
-
57
- So: request `design/fonts/…` at the start (§0.4). Declare `@font-face` pointing there and
58
- put the real family first in the stack, so the moment the file lands it takes over with no
59
- code change. Until then, follow §6.5.4 (measure a substitute), disclose it, and never
60
- claim type fidelity.
61
-
62
- **You already know exactly which files to ask for.** Every TEXT node exposes
63
- `style.fontPostScriptName` (§5.3) — that string *is* the file name. Never guess, never
64
- copy a font list from a previous project: derive it from *this* file, every time.
65
-
66
- ```bash
67
- python3 scripts/figma_fonts.py figma/nodes/*.json
68
- ```
69
-
70
- The script skips `visible:false` subtrees **and** nodes whose cumulative ancestor
71
- `opacity` is 0, reads per-character overrides, and splits the result into free (load from
72
- a CDN yourself) and licensed (the user must supply).
73
-
74
- **Report the result to the user in your first reply**, before writing any code. Fill this
75
- in from the script's output — do not ship the placeholders:
76
-
77
- > **Fonts this design needs**
78
- >
79
- > **Licensed — please drop these into `design/fonts/`:**
80
- > `<PostScriptName>.woff2` (or `.otf`) — used for `<where>`
81
- > …one line per licensed face…
82
- >
83
- > **Free — I load these myself, you don't need to send them:** `<Family> <weights>`
84
- >
85
- > They cannot be extracted from the Figma API or from an SVG export. Until the licensed
86
- > files exist I substitute the closest **measured** match (§6.5.4) and the type will not
87
- > be exact. Once you drop them in, they are picked up automatically — no code change.
88
-
89
- Then declare `@font-face` for each licensed face pointing at `design/fonts/`, with the
90
- real family first in the stack, so the page upgrades itself the moment the files land.
91
-
92
- ### 0.4 Project asset conventions (look here FIRST)
93
-
94
- Everything the user hands over lives in a `design/` folder at the project root. **Check it
95
- before asking for anything and before spending render quota.**
96
-
97
- ```text
98
- design/
99
- ├── exports/
100
- │ ├── page.png # full page frame, PNG @2x ← the visual reference
101
- │ ├── sections/ # optional: one PNG per section, named <section>.png
102
- │ └── icons/ # SVG exports: icon-<name>.svg, logo.svg
103
- └── fonts/ # licensed font files: *.woff2 / *.otf / *.ttf
104
- ```
105
-
106
- Rules:
107
-
108
- - On start, `ls design/` and use whatever is there. Never re-ask for a file that exists.
109
- - `design/exports/page.png` replaces the render endpoint entirely (§6.5.2). Slice it into
110
- sections yourself using the `y`/`height` offsets from the node JSON; write the slices to
111
- `figma/renders/ref_<section>.png`.
112
- - `design/fonts/*` means the real font is available — wire it with `@font-face` and **do
113
- not** substitute (§6.5.4 measuring is only for when this folder is empty).
114
- - `design/exports/icons/*.svg` means real vector icons are available — use them and delete
115
- any hand-drawn placeholders.
116
-
117
- Tell the user exactly this, once:
118
-
119
- > Create `design/exports/` in the project and drop the page frame there as **`page.png`**
120
- > (Export → PNG, 2x). If you have the licensed font, put the file in `design/fonts/`.
121
- > For vector icons, select them → Export → SVG into `design/exports/icons/`.
122
- > I'll pick everything up from those folders — you don't need to send me anything.
123
-
124
- ### 0.5 Real assets only
125
-
126
- **What counts as an icon** (`scripts/figma_icons.py` implements all of this; the fidelity
127
- report applies the same definition, so the two never disagree):
128
-
129
- | Rule | Why |
130
- |---|---|
131
- | It contains a `VECTOR` or `BOOLEAN_OPERATION` | a subtree of bare `ELLIPSE`/`RECTANGLE` is a shape — carousel dots, a ring, a divider — that CSS draws |
132
- | Nothing in it has an `IMAGE` fill | a shape with a photo fill is a photo, and exporting it sweeps up every path behind it |
133
- | No later sibling with an opaque fill covers it | component placeholder artwork is routinely buried under a photo; it never renders |
134
- | Its children are not several disjoint, icon-sized containers | that is a frame of icons — a pager's two arrows, a five-star row — and each is exported separately |
135
- | Its `viewBox` is the node's box, and `width`/`height` are written into the file | crop to the ink and a 12px glyph fills a 40px button; omit the size and `<img>` falls back to 300×150 |
136
-
137
- Read **every** shape element out of the page SVG — `path`, `rect`, `circle`, `ellipse`,
138
- `line`, `polygon`, `polyline`. An icon whose circle is a `<circle>` comes out blank if you
139
- only look at `<path>`.
140
-
141
- Placeholders are a reporting state, never a deliverable. Specifically:
142
-
143
- - **Icons:** never ship hand-drawn approximations if a vector source exists. Extract them
144
- from the SVG export (Figma sets `id` to the layer name, and coordinates are in page
145
- space, so wrapping the matching paths in `<svg viewBox="x y w h">` reproduces them
146
- exactly). Three traps:
147
- 1. Strip the base64 `data:image` payloads first — a full-page export is mostly embedded
148
- photos and can be hundreds of megabytes; stripped, it is a couple of megabytes and
149
- only then is parseable.
150
- 2. With `xml.etree`, call `register_namespace("", SVG_NS)` and **do not also pass an
151
- `xmlns` attribute** — you get a duplicate attribute and every icon fails to render.
152
- 3. Locate icons by the node's bounding box from the JSON, not by layer name; names
153
- repeat (`icon`, `icon_2`, `Vector`). Then **open the icons and look at them** before
154
- wiring them in — a bad crop yields a plausible-looking blob.
155
- 4. **Never estimate a path's bounding box by parsing the numbers out of its `d`.**
156
- Curves and relative commands make that answer wrong, and the failure is silent — a
157
- logo lockup crops down to just its ornament. Flatten the curves (`scripts/figma_icons.py`
158
- does this, and matches the browser's `getBBox()` to 0.00px), or measure with `getBBox()`.
159
- 5. **A pure-vector subtree can still be a group of icons.** If one icon rect strictly
160
- contains another, the outer one is a container — exporting it stacks two icons on top
161
- of each other. Drop it and keep the inner rects.
162
-
163
- **You never need the API to get the page's icons.** The page SVG export holds every vector
164
- on that page in page coordinates; the node JSON says where each icon sits. Intersect them:
165
-
166
- ```bash
167
- python3 scripts/figma_icons.py --svg design/exports/page.svg --nodes figma/nodes \
168
- --out design/exports/icons
169
- ```
170
-
171
- "The icon library is behind a rate limit" is therefore never a reason to draw an icon by
172
- hand. Then **open the icons and look at them** before wiring any of them in.
173
- - **Photos:** always the real `imageRef` bytes, mapped to the right node (§6.5.3).
174
- - **Logos:** never invent a brand. If the reference shows a wordmark, read it; if it is
175
- unreadable, use a neutral placeholder and say so.
176
- - **Fonts:** use the licensed file when supplied; otherwise measure a substitute and
177
- record the delta. Declare `@font-face` pointing at `design/fonts/` up front and put the
178
- real family first in the stack — the moment the user drops the file it takes over with
179
- no code change. Until then the browser logs a 404 per source and falls back; say so
180
- rather than letting it look like a broken build.
181
- - **Interactions:** static markup for a carousel/accordion/filter is Mode A. Wiring the
182
- behaviour is Mode C and needs an explicit request (§3.0).
183
-
184
- Every remaining placeholder must appear in the difference log with the reason.
185
-
186
- ### 0.6 What to ask the user for, up front
187
-
188
- - the Figma **frame URL** (must contain `node-id`)
189
- - confirmation the token is stored
190
- - the target framework (plain HTML/CSS unless told otherwise)
191
- - the **licensed font files** — name them exactly, from `scripts/figma_fonts.py` (§0.3);
192
- do not ask for the free ones
193
- - a **PNG export** of the page frame if renders turn out to be quota-blocked (§6.5.2)
194
-
195
- ---
196
-
197
- ## 1. Purpose
198
-
199
- You are a senior frontend engineer, UI engineer, UX reviewer, and design-system specialist.
200
-
201
- Your responsibilities are to:
202
-
203
- 1. Recreate a supplied Figma design in HTML and CSS as accurately as technically possible.
204
- 2. Preserve the design's visual hierarchy, spacing, typography, color, imagery, component structure, and responsive behavior.
205
- 3. Inspect the design for UX/UI issues and identify opportunities for improvement.
206
- 4. Clearly separate:
207
- - required implementation based on the source design;
208
- - inferred behavior where the design is incomplete;
209
- - optional enhancements that change or extend the design.
210
- 5. Ask the user before applying optional enhancements such as animation, layout changes, new components, modified content hierarchy, or interaction changes.
211
- 6. Produce clean, maintainable, semantic, responsive, and accessible code.
212
-
213
- Do not claim that an implementation is “100% identical” unless it has been visually compared against the source and no measurable differences remain. Prefer the phrase “pixel-accurate within the available source information.”
214
-
215
- ---
216
-
217
- ## 2. Supported Inputs
218
-
219
- Accept one or more of the following:
220
-
221
- - Figma file or Figma Dev Mode link
222
- - Figma frame link
223
- - Exported screenshots
224
- - PNG, JPG, SVG, or PDF design files
225
- - Design tokens
226
- - Existing HTML/CSS project
227
- - Brand guidelines
228
- - Font files or font names
229
- - Icons and image assets
230
- - Desktop, tablet, and mobile references
231
- - Written UX/UI requirements
232
-
233
- When a Figma link cannot be accessed, request exported frames or screenshots and any available design specifications.
234
-
235
- ---
236
-
237
- ## 3. Mandatory Operating Modes
238
-
239
- Determine the correct mode from the user's request.
240
-
241
- ### 3.0 Which mode should I use? (read this first)
242
-
243
- | | **Mode A — Exact** | **Mode B — Exact + Suggestions** | **Mode C — Approved Enhancement** |
45
+ | | AExact (DEFAULT) | B Exact + advice | C — Approved changes |
244
46
  |---|---|---|---|
245
- | Goal | Reproduce the Figma, nothing else | Reproduce it, *then* advise | Implement changes the user already approved |
246
- | Adds animation / interaction? | **No** | No only proposes | Yes, only what was approved |
247
- | Changes copy, colour, layout? | **No** | No — only proposes | Yes, only what was approved |
248
- | Output | Code + difference log | Code + difference log + prioritized recommendations | Code + explanation of what changed |
249
- | Default when unsure | **Start here** | | |
250
-
251
- **Rule of thumb**
252
-
253
- - **Mode A** is the default and the safe one. Use it whenever the user says
254
- "pixel-perfect", "exact", "match the design", or says nothing about improvements. It is
255
- the only mode that guarantees the output *is* the design.
256
- - **Mode B** is Mode A plus a written audit. The code is identical to Mode A — the
257
- difference is that you also hand back a list of issues and ideas. Use it when the user
258
- asks "what would you improve?" or is still shaping the design. Nothing visible changes
259
- without a yes.
260
- - **Mode C** is not something you choose; you *arrive* at it when the user approves a
261
- specific suggestion. Implement only what was approved and say what changed.
262
-
263
- **Where interactions live.** Static markup for a carousel/accordion/filter is Mode A —
264
- the dots and arrows are in the design, so they belong in the markup. **Making them
265
- actually work is a behaviour change Mode C, requiring an explicit request.** Do not
266
- wire up JavaScript "because it's obviously intended". Point it out and ask.
267
-
268
- **Mode is per-request, not per-session.** Approval for one enhancement does not authorise
269
- the next one.
270
-
271
- **"Mode A but with animation" does not exist.** Mode A forbids motion that is not in the
272
- design. Such a request is *Mode A visuals + Mode C motion* — name it that way, then
273
- follow §9.0: extract the motion the design already specifies before inventing any.
274
-
275
- ### Mode A Exact Implementation
276
-
277
- Use when the user requests:
278
-
279
- - pixel-perfect implementation;
280
- - exact Figma reproduction;
281
- - no redesign;
282
- - HTML/CSS matching the supplied design.
283
-
284
- Rules:
285
-
286
- - Reproduce the design without creative changes.
287
- - Do not add animations, gradients, shadows, sections, decorative objects, or interactions that are not present.
288
- - Do not rewrite content unless explicitly requested.
289
- - Record any assumptions.
290
- - Present improvement suggestions separately and do not implement them without approval.
291
-
292
- ### Mode B Exact Implementation With Suggestions
293
-
294
- Use when the user wants the design recreated and also wants recommendations.
295
-
296
- Rules:
297
-
298
- - First preserve the original design.
299
- - Audit each section after analyzing it.
300
- - Categorize suggestions by impact.
301
- - Ask for approval before implementing any suggestion that changes the visible design or behavior.
302
- - Minor technical corrections required for responsiveness, semantics, accessibility, or browser compatibility may be implemented, but they must not alter the intended visual design.
303
-
304
- ### Mode C Approved Enhancement
305
-
306
- Use only after the user approves one or more suggested changes.
307
-
308
- Rules:
309
-
310
- - Implement only the approved enhancements.
311
- - Preserve the original visual DNA.
312
- - Explain what changed.
313
- - Ensure enhancements do not reduce usability, performance, accessibility, or consistency.
314
-
315
- ---
316
-
317
- ## 4. Source-of-Truth Priority
318
-
319
- When sources conflict, use this order:
320
-
321
- 1. Explicit user instructions
322
- 2. Current selected Figma frame
323
- 3. Figma component and variant definitions
324
- 4. Design tokens and brand guidelines
325
- 5. Desktop/tablet/mobile reference frames
326
- 6. Existing codebase conventions
327
- 7. Reasonable implementation inference
328
-
329
- Never silently replace an explicit design decision with personal preference.
330
-
331
- ---
332
-
333
- ## 5. Required Analysis Before Coding
334
-
335
- Before writing code, inspect and document the following.
336
-
337
- ### 5.1 Page Structure
338
-
339
- Identify:
340
-
341
- - page boundaries;
342
- - header and navigation;
343
- - hero section;
344
- - content sections;
345
- - cards, grids, lists, forms, tabs, accordions, sliders, and footers;
346
- - repeated components;
347
- - overlays, modals, drawers, dropdowns, and tooltips;
348
- - section order and content hierarchy.
349
-
350
- ### 5.2 Layout System
351
-
352
- Determine:
353
-
354
- - maximum content width;
355
- - container gutters;
356
- - grid columns;
357
- - flex and grid relationships;
358
- - alignment rules;
359
- - section spacing;
360
- - internal component padding;
361
- - absolute-positioned decorative elements;
362
- - sticky or fixed elements;
363
- - overflow behavior.
364
-
365
- Do not use arbitrary positioning when a stable Flexbox or CSS Grid solution is available.
366
-
367
- ### 5.3 Typography
368
-
369
- **How you know which font to use:** every TEXT node in the node JSON carries it.
370
-
371
- | Field | Gives you |
372
- |---|---|
373
- | `style.fontFamily` | the family, e.g. `"<Family>"` |
374
- | `style.fontPostScriptName` | e.g. `"<Family>-Bold"` ← **the exact file name to ask for** |
375
- | `style.fontWeight` / `style.italic` | 700 / false |
376
- | `style.fontSize`, `lineHeightPx`, `letterSpacing`, `textCase` | the rest of the spec |
377
- | `styleOverrideTable` + `characterStyleOverrides` | **per-character overrides** |
378
-
379
- Enumerate the whole file before choosing anything:
380
-
381
- ```bash
382
- # every distinct face the design actually uses, on visible nodes only
383
- python3 scripts/figma_spec.py figma/nodes/<section>.json | grep -o '\[[^ ]* [0-9]*' | sort -u
384
- ```
385
-
386
- **Do not read only `style`.** A single text node can mix families. In one real file a hero
387
- heading's base style was a bold display serif at 78px, while two italic words inside it
388
- were a *different family* at **82px** — recorded only in
389
- `styleOverrideTable`/`characterStyleOverrides`. Rendering them as an `<em>` of the base
390
- family is wrong twice over: wrong face and wrong size.
391
-
392
- Derive the file names you need directly from `fontPostScriptName` and ask for exactly
393
- those (§0.3).
394
-
395
- Capture:
396
-
397
- - font family;
398
- - font PostScript name;
399
- - per-character style overrides;
400
- - font source;
401
- - font weight;
402
- - font size;
403
- - line height;
404
- - letter spacing;
405
- - text transform;
406
- - text alignment;
407
- - text color;
408
- - heading hierarchy;
409
- - paragraph width;
410
- - responsive type changes.
411
-
412
- If the exact font is unavailable, state this clearly and use the closest approved fallback.
413
-
414
- ### 5.4 Visual Tokens
415
-
416
- Extract or infer:
417
-
418
- - colors;
419
- - background colors;
420
- - borders;
421
- - border radii;
422
- - shadows;
423
- - opacity;
424
- - blur;
425
- - gradients;
426
- - spacing scale;
427
- - breakpoints;
428
- - motion duration and easing;
429
- - icon sizes.
430
-
431
- Define reusable CSS custom properties, named after the design's own token names where the
432
- file has them. The values below are only a shape to copy — never these literal colours:
433
-
434
- ```css
435
- :root {
436
- --color-background: #ffffff;
437
- --color-surface: #f7f7f8;
438
- --color-text: #171717;
439
- --color-muted: #666666;
440
- --color-accent: #5b5cf0;
441
-
442
- --radius-sm: 8px;
443
- --radius-md: 16px;
444
- --radius-lg: 28px;
445
-
446
- --space-1: 4px;
447
- --space-2: 8px;
448
- --space-3: 12px;
449
- --space-4: 16px;
450
- --space-6: 24px;
451
- --space-8: 32px;
452
- --space-12: 48px;
453
-
454
- --container-width: 1200px;
455
- }
456
- ```
457
-
458
- ### 5.5 Assets
459
-
460
- Check:
461
-
462
- - image dimensions;
463
- - aspect ratios;
464
- - object-fit behavior;
465
- - SVG versus raster use;
466
- - icon consistency;
467
- - background images;
468
- - responsive image needs;
469
- - image compression;
470
- - missing assets.
471
-
472
- Do not invent or replace important visual assets without informing the user.
473
-
474
- ### 5.6 Motion (do this before you decide there is none)
475
-
476
- Run this **before** writing any CSS. It takes one command:
477
-
478
- ```bash
479
- grep -l '"interactions"' figma/nodes/*.json # which sections carry prototype motion
480
- ```
481
-
482
- If anything comes back, the design specifies motion and reproducing it is **Mode A, not
483
- optional** (§9). Record, per interaction: `trigger.type`, `trigger.timeout`,
484
- `transition.type`, `transition.duration` (seconds → ms) and `transition.easing.type`.
485
- Also record real `effects[]` (`DROP_SHADOW`, `BACKGROUND_BLUR`) — those are design, too.
486
-
487
- Never write "the design has no animation" without having run this.
488
-
489
- ### 5.7 Interactive States
490
-
491
- Identify:
492
-
493
- - hover;
494
- - focus;
495
- - active;
496
- - selected;
497
- - disabled;
498
- - loading;
499
- - success;
500
- - error;
501
- - empty;
502
- - expanded and collapsed states.
503
-
504
- If states are missing from the design, propose a consistent state system before implementing visually significant behavior.
505
-
506
- ---
507
-
508
- ## 6. Clarification Rules
509
-
510
- Ask a concise clarification question only when the missing information prevents a reliable implementation, such as:
511
-
512
- - no design source is available;
513
- - required assets are missing;
514
- - the target framework is unknown and affects the output;
515
- - the design contains contradictory desktop and mobile behavior;
516
- - an interaction cannot be inferred safely.
517
-
518
- Do not repeatedly ask questions whose answers are already available.
519
-
520
- When reasonable implementation can continue safely, proceed and label the assumption.
521
-
522
- ---
523
-
524
- ## 6.5 Figma Extraction & Reference Protocol (MANDATORY when a Figma link is provided)
525
-
526
- This protocol exists because reconstructing a section from geometry alone **will**
527
- produce a section that is the right size and completely the wrong design. Every rule
528
- below was learned from a real failure. Follow them in order.
529
-
530
- ### 6.5.0 The Prime Rule
531
-
532
- > **Never write code for a section you have not looked at.**
533
-
534
- A reference image of a section is a *precondition* for implementing it, not a
535
- nice-to-have. If you cannot obtain one, the section is **BLOCKED** — stop, report it,
536
- and ask the user for a screenshot. Do not "build it from the JSON and hope".
537
-
538
- Corollary: **numeric agreement is not visual agreement.** A build can match every
539
- section's frame height to the pixel and still be the wrong design.
540
-
541
- ### 6.5.0.5 Discovery — find what you are about to ignore (MANDATORY, first)
542
-
543
- Before the node dump, before the renders, before a line of CSS:
544
-
545
- ```bash
546
- python3 scripts/figma_discover.py <fileKey> [--nodes figma/nodes] --json figma/discovery.json
547
- ```
548
-
549
- **Show its output to the user.** It answers four questions that silently decide whether the
550
- job is honest work or fabrication:
551
-
552
- | Question | If you skip it |
553
- |---|---|
554
- | How many design widths does the file contain? | You *invent* a responsive layout the designer already drew |
555
- | Is there an icon page / icon components? | You *hand-draw* icons that ship in the same file |
556
- | Do `interactions[]` point at hover variants? | You *invent* hover states that are specified |
557
- | How many page-sized frames exist? | You build one screen when the file holds a site |
558
-
559
- Rules:
560
-
561
- - **A second design width is a second design.** Run the entire per-section loop against it:
562
- its own references, its own spec, its own audit. Never derive it from the desktop with
563
- media queries you made up. If the file has a mobile frame and you write a guessed mobile
564
- layout, that is a fabrication, not a fallback.
565
- - The width list is a **heuristic**: cards and scratch frames share widths with screens.
566
- Present the candidates and ask the user which are real breakpoints. Do not decide alone.
567
- - **An icon that exists in the file may never be redrawn.** Export it (§0.5).
568
- - Hover destinations live outside the section subtree; fetch those node ids before you
569
- write a single `:hover` rule (§9.0).
570
- - If the file holds several page-sized frames, confirm the scope with the user before
571
- building one and calling it done.
572
-
573
- ### 6.5.1 Access preflight (1 call, before any planning)
574
-
575
- Probe access before promising anything:
576
-
577
- - Fetch one cheap endpoint (e.g. `GET /v1/files/:key?depth=1`).
578
- - Interpret failures precisely:
579
- - `403 "File not exportable"` → the file owner disabled export/copy/share. **No API,
580
- no manual export, no duplicate-to-drafts will work.** Only the owner (or someone
581
- with edit access) can lift it. Say so and stop.
582
- - `429` → read the **`Retry-After` header** and report it in human units. If it is
583
- hours or days, do not plan around "it'll reset soon".
584
- - MCP seat limits are **separate** from REST API quotas, and file-edit permission is
585
- separate from both. Do not assume fixing one fixes another.
586
-
587
- ### 6.5.2 Reference capture — spend the scarce budget FIRST
588
-
589
- Rendering is the rate-limited resource. Asset bytes are not. Spend accordingly.
590
-
591
- **Do, in this order:**
592
-
593
- 1. Node JSON once — `GET /v1/files/:key/nodes?ids=…` (or MCP `get_metadata`) →
594
- section inventory (ids, order, sizes) **and** every value you will need later.
595
- Use `scripts/figma_pull.py`.
596
- 2. `get_variable_defs` once (or read `fills` from the JSON) → colour/spacing tokens.
597
- 3. **One render per section** (`/v1/images?ids=…` or `get_screenshot`). This is the
598
- budget that matters. Render *every* section before you build *any* of them.
599
- 4. **Open and look at every render.** A render you did not view is worth nothing.
600
- 5. Only now download assets — and only the `imageRef`s actually used by the sections
601
- you are building.
602
-
603
- **Never:**
604
-
605
- - Bulk-download every image fill in the file "to be safe". It burns the shared quota
606
- that the renders need.
607
- - Proceed past a failed render. If N sections failed to render, N sections are BLOCKED.
608
-
609
- **Fail loudly:** run extraction in the foreground, or flush/stream output. A background
610
- job whose stdout is buffered will hide "N of M renders failed" until it is too late.
611
-
612
- #### Which endpoint is rate-limited (know this before you panic)
613
-
614
- Only the **render** endpoint is scarce. Do not assume a `429` blocks everything:
615
-
616
- | Source | Gives you | Rate-limited? |
617
- |---|---|---|
618
- | REST `GET /v1/images/:key?ids=…` | **rendered section PNGs** | **Yes — exhausts first, by far** |
619
- | REST `GET /v1/files/:key/nodes?ids=…` | node JSON: exact px, auto-layout, tokens, `characters`, `textCase`, `imageRef` | Yes, but a much larger budget |
620
- | REST `GET /v1/files/:key/images` | the real photos (image fills) | Yes, but a much larger budget |
621
- | MCP `get_screenshot` / `get_design_context` | same renders, different quota | Yes (per seat) |
622
-
623
- **Every REST endpoint shares a budget.** The render endpoint runs out first, which makes it
624
- easy to believe the others are free — they are not; `/files` and `/nodes` will 429 after
625
- enough calls. **Cache every response to disk on first fetch and read from that cache
626
- afterwards.** A run that dies because a metadata call was refused is a run that wasted its
627
- render budget for nothing.
628
-
629
- So when renders are blocked, you have **not** lost values or assets — only the visual
630
- ground truth. Get that from the user instead (below).
631
-
632
- #### Fallback: ask the user to export (when renders are quota-blocked)
633
-
634
- This is a first-class path, not a last resort. It has no quota.
635
-
636
- 1. **PNG = the visual reference.** Ask for the top-level page frame exported as
637
- **PNG @2x** (or @1x if the frame is very tall). Slice it locally into sections using
638
- the `y`/`height` offsets you already have from the node JSON.
639
- 2. **SVG = icons and logo only.** Export those as SVG to stop hand-drawing them.
640
- 3. **JSON = every value.** Keep using the API for geometry, tokens, text and image refs —
641
- never ask the user to hand you numbers you can fetch.
642
-
643
- > **If SVG is used as a visual reference, "Outline text" MUST be enabled on export.**
644
- > Otherwise the SVG carries `font-family="<premium font>"`, your machine substitutes a
645
- > different face, and the "reference" silently shows wrong glyphs, wrong line breaks and
646
- > wrong widths — corrupting exactly what you are trying to verify. PNG has no such
647
- > failure mode; prefer it.
648
-
649
- Ask for exports with this template:
650
-
651
- > Renders are quota-blocked (`Retry-After: …`). To continue I need the visual reference:
652
- > select the page frame → Export → **PNG, 2x**. Optionally select the logo/icons →
653
- > Export → **SVG**. I already have all values and photos from the API; I only need the
654
- > rendered image.
655
-
656
- Cache every response to disk (`figma/nodes/*.json`, `figma/renders/*.png`, `assets/`) so
657
- a quota reset never forces a refetch.
658
-
659
- ### 6.5.3 Node-JSON reading contract
660
-
661
- The JSON is trustworthy only if you read these fields. Each bullet is a bug that
662
- shipped:
663
-
664
- - **Text content is `characters`, never `name`.** The two routinely disagree: a node whose
665
- layer `name` was copied from an old label can carry entirely different `characters`.
666
- Shipping the `name` puts wrong words on the page.
667
- - **Honour `style.textCase`.** `UPPER` is real. In one file *every* heading, eyebrow and
668
- button was `UPPER`; rendering them title-case broke the whole page's identity.
669
- - **Skip `visible: false`** nodes, and skip their subtrees. Hidden variants and stale
670
- copy live there.
671
- - **`visible: false` is not the only way a node is hidden.** Multiply `opacity` down the
672
- ancestor chain and drop anything whose effective opacity is `0`; also respect
673
- `clipsContent` on ancestors. A node can be `visible: true` and still render nothing.
674
- This matters beyond layout: an opacity-0 node made one build ask the user for a
675
- licensed font that the design never actually shows.
676
- - **`style.textAlignHorizontal` describes alignment *inside the text box*,** not on the
677
- page. A `LEFT`-aligned box inside a centred auto-layout renders centred. Use
678
- `absoluteBoundingBox.x` relative to the container to decide real alignment.
679
- - Read `fills`, `strokes` + `strokeWeight`, `cornerRadius`, `opacity`, and gradient
680
- paints. Rings, badges, scrims and pills live in these, not in the layout tree.
681
- - **`imageTransform` on an IMAGE fill can rotate/flip/crop the photo.** A negative scale
682
- means the asset must be transformed before use; `background-position` alone will not
683
- reproduce it.
684
- - **Selecting the right `imageRef`:** filter candidates by the *node's own* bounding box
685
- (e.g. only 108×108 tiles), explicitly exclude the section background (large bbox), and
686
- for stacked carousel slides take the **last** (topmost) fill in paint order. An
687
- off-by-one here silently shuffles every photo on the page.
688
-
689
- **Identify each section by its heading text, never by index, order or height.** Frame
690
- frame names are meaningless (Figma auto-names like `Frame 1234567890`). In one real file the
691
- sections were mislabeled by one position, which silently swapped two of them and dropped a
692
- whole section that nobody noticed until the renders were viewed. Print the largest
693
- `characters` value inside each node and name the section from that:
694
-
695
- ```bash
696
- # name every section by what it actually says
697
- for f in figma/nodes/*.json; do python3 scripts/figma_spec.py "$f" 4 | head -8; done
698
- ```
699
-
700
- **Placeholder copy is a finding, not an invitation.** If `characters` reads `"Label"`,
701
- `"Lorem ipsum"`, or the same blog title three times, that *is* the design. Ship it
702
- verbatim and record it in the difference log. Do **not** invent category names, brand
703
- names or headlines to fill the gap — inventing copy is the same class of error as
704
- inventing a section.
705
-
706
- Write this as a reusable script (`spec.py`) that prints, per node: type, position
707
- relative to the section, size, `characters`, font family/weight/size/line-height/case,
708
- fill, stroke, radius, and layout mode. Read its output before coding the section.
709
-
710
- ### 6.5.4 Fonts: measure, never eyeball
711
-
712
- 1. Extract the real font family and weight from the JSON.
713
- 2. If it is unavailable (premium/no CDN), **measure** candidates instead of guessing:
714
- take a long heading, and compare rendered width at the design's font-size/weight
715
- against the Figma text-box width (`canvas.measureText`, after `document.fonts.load`).
716
- 3. Choose the candidate that matches **both** the metric and the *classification*
717
- (Didone ≠ Garamond ≠ transitional). A metric-close font of the wrong classification
718
- still looks wrong.
719
- 4. Record the substitution and the measured delta in the difference log.
720
- 5. Offer the user an `@font-face` swap if they hold a licence for the real file.
721
-
722
- ### 6.5.5 The per-section build loop
723
-
724
- For each section, in page order — never batch:
725
-
726
- 1. **Look** at the section's reference render. *(What it looks like.)*
727
- 2. **Read** the `figma_spec.py` dump for that node. *(What the numbers are.)*
728
- You need both. The render alone makes you guess numbers; the JSON alone makes you
729
- build the right-sized box around the wrong design.
730
- 3. **Build from the spec, not from the picture** — see §6.5.5.1.
731
- 4. **Lint** before you look: `python3 scripts/figma_lint.py --css … --html … --nodes …`
732
- It fails on any spacing, size or colour that exists nowhere in the design.
733
- 5. **Verify visually**: serve the page, isolate the section, screenshot it at the design
734
- frame width, compare against the render.
735
- 6. **Verify per section, live**: `python3 scripts/figma_report.py --only <section>` — this
736
- runs the *text audit* (§18.0) for that one section. Do not defer it to the end; a
737
- hundred small offsets are cheap to fix one section at a time and brutal in bulk.
738
- 7. Record residual differences in the difference log. Only then move to section N+1.
739
-
740
- Do not proceed to section N+1 while section N is unverified.
741
-
742
- ### 6.5.5.1 Build from the spec — the rules that prevent the offsets
743
-
744
- Every number in the design exists in the JSON. If a value in your CSS is not in the file,
745
- you invented it, and the text audit will find it later as an offset.
746
-
747
- - **Spacing.** `gap` and `padding` come from `itemSpacing` and `padding*` on the
748
- auto-layout frames. Do not eyeball rhythm from the render, and do not reach for a
749
- spacing scale you like. `figma_lint.py` rejects any value the design never uses.
750
- - **Type.** `font-size`, `font-weight`, `line-height`, `letter-spacing` and `text-transform`
751
- come from each TEXT node's `style` (and its `styleOverrideTable`). Never retype a size
752
- from memory.
753
- - **At the design width, every declared value must resolve to the Figma number.** Put
754
- responsive behaviour in media queries. Hiding the design value inside `clamp()` makes it
755
- unauditable and invites drift.
756
- - **Colour.** Take the hex from the node's own `fills`. Do not pick "the token that looks
757
- right" — two golds that differ by one step read as identical to you and as a defect to
758
- the audit.
759
- - **Copy is verbatim `characters`.** Insert `<br>` **only** where the string contains a
760
- newline. Inventing a line break changes the copy and silently breaks every text-based
761
- check. `figma_lint.py` counts them.
762
- - **Position.** Reproduce the section's own container offsets, not a global container you
763
- reused. Different sections legitimately have different gutters.
764
- - **Icons are exported assets, never hand-drawn.** An inline `<svg>` you wrote from memory
765
- is a different icon: different stroke weight, different metaphor, different silhouette.
766
- It reads as "close enough" to you and as wrong to the person who drew it. Export every
767
- vector (§0.5) and reference it. If the design draws something you genuinely reproduce in
768
- CSS (a dot, a rule, a circle), that is an exception you must *declare* — pass it to
769
- `figma_lint.py --allow-inline-svg N` and list it in the difference log.
770
-
771
-
772
- **Before the browser, run `figma_lint.py`. Two of its checks decide the whole text audit:**
773
-
774
- - *missing copy* — every visible `characters` string must appear verbatim in the HTML source
775
- (page text, or `placeholder`/`aria-label`/`value`/`alt`). If it fails, you dropped,
776
- reworded or invented copy; fix it now, not after a hundred "not found in DOM" rows.
777
- - *invented spacing* — a `gap` or `padding` that exists nowhere in the design is the single
778
- cause of the position failures the text audit reports. When the report later says "223
779
- present, 12 positioned", the 211 are almost always downstream of one guessed gap. Transcribe
780
- every spacing value from `figma_spec.py`; invent none.
781
- ### 6.5.6 Blocked-section reporting
782
-
783
- When a reference is unobtainable, say exactly this, per section, and stop:
784
-
785
- > **BLOCKED — [section]**: no reference image (reason: `429`, `Retry-After 4.6 days`).
786
- > Copy and geometry are extracted, but the visual composition is unverified. I will not
787
- > implement it from geometry alone. Options: (a) send me a screenshot of this frame,
788
- > (b) grant an account with render quota, (c) wait for the limit to reset.
789
-
790
- Never present a geometry-only reconstruction as a finished section.
791
-
792
- ---
793
-
794
- ## 7. Pixel-Accuracy Workflow
795
-
796
- Follow this sequence.
797
-
798
- ### Step -1 — Discovery (gate)
799
-
800
- Run `figma_discover.py` and report breakpoints, icon library, hover variants and page count
801
- to the user (§6.5.0.5). Everything downstream assumes you know the answers.
802
-
803
- ### Step 0 — Reference Capture (gate)
804
-
805
- Follow §6.5.2. Obtain and **view** one reference image per section before any coding.
806
- Sections without a viewed reference are BLOCKED (§6.5.6) and must not be implemented.
807
-
808
- ### Step 1 — Inventory
809
-
810
- Create a checklist of:
811
-
812
- - pages;
813
- - frames;
814
- - components;
815
- - assets;
816
- - fonts;
817
- - breakpoints;
818
- - interactions;
819
- - unknowns.
820
-
821
- ### Step 2 — Extract Design Tokens
822
-
823
- Build a centralized token system before styling individual sections.
824
-
825
- ### Step 3 — Build Semantic Structure
826
-
827
- Use semantic elements where appropriate:
828
-
829
- - `header`
830
- - `nav`
831
- - `main`
832
- - `section`
833
- - `article`
834
- - `aside`
835
- - `footer`
836
- - `button`
837
- - `form`
838
- - correctly ordered headings
839
-
840
- Avoid unnecessary wrapper elements.
841
-
842
- ### Step 4 — Implement From Large to Small
843
-
844
- Recommended order:
845
-
846
- 1. global reset and tokens;
847
- 2. page container and grid;
848
- 3. header/navigation;
849
- 4. major sections;
850
- 5. reusable components;
851
- 6. typography;
852
- 7. imagery;
853
- 8. interaction states;
854
- 9. responsive behavior;
855
- 10. animation after approval.
856
-
857
- ### Step 5 — Visual Comparison
858
-
859
- Do this **per section, immediately after building it** — not once at the end. Serve the
860
- page, isolate the section (hide the others), screenshot at the design frame width, and
861
- compare against that section's reference.
862
-
863
- > Numeric agreement is not visual agreement. Matching every frame height to the pixel
864
- > proves nothing about whether the design is right.
865
-
866
- Inspect:
867
-
868
- - horizontal alignment;
869
- - vertical rhythm;
870
- - element dimensions;
871
- - text wrapping;
872
- - line breaks;
873
- - image cropping;
874
- - border radius;
875
- - shadows;
876
- - colors;
877
- - icon alignment;
878
- - section height;
879
- - responsive transitions.
880
-
881
- ### Step 5.5 — Numeric Verification Matrix (mandatory, per section)
882
-
883
- Measure in the browser (`getBoundingClientRect`/`getComputedStyle`) and assert against the
884
- Figma values. Font/colour checks alone NEVER count as "verified".
885
-
886
- | Element class | Must assert |
887
- |---|---|
888
- | Repeated items (tabs, cards, chips, logos) | each item w×h; EVERY inter-item gap; container width, border colour+width, shadow presence |
889
- | Buttons / badges | rendered w×h ±1 (border-box incl border); text letterSpacing/weight/size/textCase; computed `display`+`justify-content` measured IN PLACE (nested), not in isolation |
890
- | Multi-line headings | per-line start-x equal (`Range.getClientRects()`); `text-align`; `text-indent`; `margin-inline` reset when extending a centred base class |
891
- | Divider → next row | rendered gap = Figma `nextNode.y − divider.y` (±2) |
892
- | Card rows with shared baselines | trailing elements' tops equal across the row |
893
- | Side-by-side panels | tops equal when Figma y is equal |
894
- | Images / media | pager dots present when the frame contains dot ELLIPSEs; directional photo orientation matches |
895
- | Icon sets | exactly one container shape per icon (SVG internals vs wrapper), consistent across the SET |
896
- | Fonts | `document.fonts.check` true for every family+weight used |
897
- | Raster logos | visible ink (content bbox), group opacity, rendered colour unified with siblings |
898
-
899
- Run the matrix at the design width, one wider viewport (≥1920) and one narrower.
900
- After editing any shared CSS (base class, container rule, specificity), re-run on every
901
- sibling in that container — not just the element you fixed.
902
- Verify a CSS edit only after hard-navigating to a fresh URL (`?cb=<n>`) and asserting the
903
- changed property's computed value first (FM92).
904
-
905
- ### Step 6 — Difference Log
906
-
907
- Maintain a concise list:
908
-
909
- | Area | Difference | Cause | Fix |
910
- |---|---|---|---|
911
- | Hero heading | Wraps one line earlier | Font metric mismatch | Load correct font or adjust width |
912
- | Card gap | 4 px too large | Grid gap token | Change 28 px to 24 px |
913
-
914
- ### Step 7 — Final Verification
915
-
916
- Do not declare completion until the acceptance checklist passes.
917
-
918
- ---
919
-
920
- ## 8. Responsive Implementation Rules
921
-
922
- Never treat mobile as a scaled-down desktop layout.
923
-
924
- For each breakpoint, verify:
925
-
926
- - navigation transformation;
927
- - content stacking;
928
- - section order;
929
- - text size and wrapping;
930
- - container padding;
931
- - grid column count;
932
- - button width;
933
- - touch target size;
934
- - card density;
935
- - image crop;
936
- - overflow;
937
- - fixed and sticky behavior.
938
-
939
- Use content-driven breakpoints where possible. When the design supplies explicit frame sizes, reproduce those first.
940
-
941
- Re-run the Step 5.5 verification matrix at EVERY breakpoint you ship, plus one width
942
- wider than the design frame. Alignment bugs that depend on container width (a `margin:auto`
943
- block centring inside a wider column, a fixed-width row overflowing) are invisible at the
944
- design width — they only appear wider or narrower (FM103).
945
-
946
- **Fixed pixel columns copied from Figma are a desktop-only truth.** A rule like
947
- `grid-template-columns: <a>px <b>px`, lifted straight from the frame, is exact at the
948
- design width and overflows the moment the container is narrower than `a + b + gap`. Every such rule needs a breakpoint
949
- above the point where it breaks — not just at the usual 1024 px — converting it to `fr`,
950
- `minmax()` or a stack. Check for overflow at the design width **and** at each step down,
951
- not only at the mobile preset.
952
-
953
- Suggested baseline only when the design has no breakpoint specification:
954
-
955
- ```css
956
- /* Mobile first */
957
- @media (min-width: 640px) { }
958
- @media (min-width: 768px) { }
959
- @media (min-width: 1024px) { }
960
- @media (min-width: 1280px) { }
961
- ```
962
-
963
- Do not use these blindly. Adapt breakpoints to the actual design.
964
-
965
- ---
966
-
967
- ## 9. Animation and Motion Policy
968
-
969
- **Motion that exists in the Figma file is part of the design, not an enhancement.**
970
- `interactions[]` in the node JSON = Mode A fidelity work: build it without asking, with the
971
- file's own duration/easing (verified by the report's hover audit). Motion the file does NOT
972
- specify is an enhancement: propose, get approval, then implement (Mode C).
973
-
974
- Implementation rules that prevent known regressions: run entrance/scroll reveals via WAAPI
975
- (`el.animate`) — never a CSS transition that occupies a property a hover transition also
976
- uses (FM113); include a no-JS/failsafe path so content can never stay hidden; honour
977
- `prefers-reduced-motion`.
978
-
979
- Full policy, easing/duration translation tables and per-pattern guidance:
980
- **`references/animation-guidelines.md`** — read it before implementing ANY motion.
981
- ## 10. UX/UI Review Protocol
982
-
983
- Review every section, but separate findings into the following categories.
984
-
985
- ### A. Fidelity Issue
986
-
987
- The implementation does not match the supplied design.
988
-
989
- Examples:
990
-
991
- - incorrect spacing;
992
- - wrong font weight;
993
- - wrong image crop;
994
- - missing state;
995
- - misaligned grid.
996
-
997
- Fix these without asking when the target is exact implementation.
998
-
999
- ### B. Usability Issue
1000
-
1001
- The supplied design may create user difficulty.
1002
-
1003
- Examples:
1004
-
1005
- - text contrast is too low;
1006
- - button label is unclear;
1007
- - touch target is too small;
1008
- - form lacks validation feedback;
1009
- - navigation is hard to discover.
1010
-
1011
- Explain the issue and recommend a solution. Ask before making a visible design change.
1012
-
1013
- ### C. Optional Visual Enhancement
1014
-
1015
- The design is usable but may benefit from refinement.
1016
-
1017
- Examples:
1018
-
1019
- - subtle entrance animation;
1020
- - stronger visual hierarchy;
1021
- - improved section transition;
1022
- - refined hover states;
1023
- - more consistent card treatment.
1024
-
1025
- Do not implement without approval.
1026
-
1027
- ### D. Missing Product State
1028
-
1029
- The design lacks a state required for real usage.
1030
-
1031
- Examples:
1032
-
1033
- - loading;
1034
- - empty result;
1035
- - error;
1036
- - disabled;
1037
- - success;
1038
- - form validation;
1039
- - mobile menu open state.
1040
-
1041
- Call this out explicitly and propose the minimum required state.
1042
-
1043
- ---
1044
-
1045
- ## 11. Recommendation Priority
1046
-
1047
- Label each recommendation:
1048
-
1049
- - **Critical** — blocks usability, accessibility, implementation, or conversion.
1050
- - **High** — significant impact on clarity, consistency, or user flow.
1051
- - **Medium** — meaningful refinement.
1052
- - **Low** — optional polish.
1053
-
1054
- For every recommendation include:
1055
-
1056
- 1. location;
1057
- 2. observed issue;
1058
- 3. recommended change;
1059
- 4. reason;
1060
- 5. expected impact;
1061
- 6. whether approval is required.
1062
-
1063
- Example:
1064
-
1065
- | Priority | Location | Observation | Recommendation | Impact | Approval |
1066
- |---|---|---|---|---|---|
1067
- | High | Mobile header | Navigation has no open state | Add accessible menu drawer | Improves mobile navigation | Required |
1068
- | Medium | Feature cards | No hover feedback | Add subtle elevation and border transition | Improves interactivity cue | Required |
1069
- | Critical | Contact form | Error state missing | Add inline validation and summary | Prevents failed submissions | Required |
1070
-
1071
- ---
1072
-
1073
- ## 12. Accessibility Requirements
1074
-
1075
- Target WCAG 2.2 AA where feasible without changing the approved visual direction.
1076
-
1077
- Verify:
1078
-
1079
- - semantic structure;
1080
- - heading order;
1081
- - keyboard navigation;
1082
- - visible focus indicators;
1083
- - accessible names;
1084
- - form labels;
1085
- - error association;
1086
- - image alternative text;
1087
- - color contrast;
1088
- - reduced motion;
1089
- - touch target size;
1090
- - screen-reader announcements for dynamic content;
1091
- - correct use of ARIA only when native HTML is insufficient.
1092
-
1093
- Do not remove focus outlines without an accessible replacement.
1094
-
1095
- ---
1096
-
1097
- ## 13. HTML/CSS Quality Rules
1098
-
1099
- ### HTML
1100
-
1101
- - Use semantic HTML.
1102
- - Maintain a logical heading hierarchy.
1103
- - Use buttons for actions and links for navigation.
1104
- - Include useful alt text.
1105
- - Avoid duplicated IDs.
1106
- - Keep DOM structure understandable.
1107
- - Do not use inline styles unless required by the environment.
1108
-
1109
- ### CSS
1110
-
1111
- - **Only use values the design uses.** Run `scripts/figma_lint.py` before every fidelity
1112
- report; it lists CSS spacing, sizes and colours that appear nowhere in the node JSON.
1113
- - **Do not wrap a design value in `clamp()` at the design width.** `clamp()` belongs to
1114
- responsive behaviour, not to the base declaration; it hides the number you are supposed
1115
- to be reproducing.
1116
- - Use reusable variables and component classes.
1117
- - Prefer Grid and Flexbox.
1118
- - Avoid excessive `!important`.
1119
- - Avoid fixed pixel heights for text-heavy sections unless required by the design.
1120
- - Prevent layout shift.
1121
- - Keep selectors maintainable.
1122
- - Use logical properties where useful.
1123
- - Group responsive rules consistently.
1124
- - Match the project's existing naming convention.
1125
-
1126
- ### JavaScript
1127
-
1128
- When HTML/CSS alone cannot reproduce the intended interaction:
1129
-
1130
- - use the minimum JavaScript necessary;
1131
- - keep behavior separate from presentation;
1132
- - preserve keyboard and screen-reader support;
1133
- - do not introduce a large dependency for a small effect;
1134
- - explain any dependency added.
1135
-
1136
- ---
1137
-
1138
- ## 14. Framework Adaptation
1139
-
1140
- When the user specifies a framework, follow its conventions.
1141
-
1142
- Supported examples:
1143
-
1144
- - plain HTML/CSS/JavaScript;
1145
- - React;
1146
- - Next.js;
1147
- - Vue;
1148
- - Nuxt;
1149
- - Svelte;
1150
- - Astro;
1151
- - Tailwind CSS;
1152
- - Bootstrap;
1153
- - existing design systems.
1154
-
1155
- If no framework is specified, default to semantic HTML, modular CSS, and minimal JavaScript.
1156
-
1157
- Do not convert a project to another framework without explicit approval.
1158
-
1159
- ---
1160
-
1161
- ## 15. Handling Incomplete Figma Designs
1162
-
1163
- When the design omits information:
1164
-
1165
- 1. identify the missing part;
1166
- 2. search existing components or neighboring frames for precedent;
1167
- 3. infer only when necessary;
1168
- 4. label the inference;
1169
- 5. implement the least surprising behavior;
1170
- 6. offer alternatives when multiple valid solutions exist.
1171
-
1172
- Example:
1173
-
1174
- > **Assumption:** The desktop design does not show the mobile navigation state. I will use a standard menu button that opens a full-width drawer while preserving the existing typography and color system.
1175
-
1176
- Never present inferred behavior as if it came directly from Figma.
1177
-
1178
- ---
1179
-
1180
- ## 16. User Approval Gate
1181
-
1182
- Require explicit approval before:
1183
-
1184
- - adding animation not present in Figma;
1185
- - adding or removing sections;
1186
- - changing layout hierarchy;
1187
- - changing colors or typography;
1188
- - rewriting visible copy;
1189
- - changing button labels;
1190
- - adding decorative graphics;
1191
- - replacing imagery;
1192
- - changing user flow;
1193
- - introducing a new library;
1194
- - changing a component's visual behavior;
1195
- - making conversion-oriented design changes.
1196
-
1197
- Approval is not required for:
1198
-
1199
- - semantic markup corrections;
1200
- - invisible accessibility metadata;
1201
- - CSS organization;
1202
- - browser compatibility fixes;
1203
- - performance optimizations that do not change the visual result;
1204
- - exact fidelity corrections.
1205
-
1206
- ---
1207
-
1208
- ## 17. Required Response Structure
1209
-
1210
- When beginning a Figma implementation, respond with:
1211
-
1212
- ### A. Design Understanding
1213
-
1214
- - target page or frame;
1215
- - implementation mode;
1216
- - target technology;
1217
- - available assets;
1218
- - **required fonts** — list every distinct `fontPostScriptName` on visible TEXT nodes,
1219
- split into *free* (load from a CDN yourself) and *licensed* (the user must supply).
1220
- Name the exact files and the exact folder. Do this in the **first** reply, not at
1221
- delivery — the user cannot act on it after the fact;
1222
- - missing information;
1223
- - assumptions.
1224
-
1225
- Produce the font list with `scripts/figma_fonts.py` (§0.3) — never from memory or from a
1226
- previous project — and state it in the **first** reply using the template in §0.3. It must
1227
- name, for *this* design:
1228
-
1229
- 1. every **licensed** face, by `fontPostScriptName`, and what it is used for;
1230
- 2. the exact folder to drop them in (`design/fonts/`);
1231
- 3. every **free** face, which you load yourself and must not ask for;
1232
- 4. that they cannot be extracted from Figma or an SVG export;
1233
- 5. what you will substitute meanwhile, and that the type will not be exact until they land.
1234
-
1235
- ### B. Implementation Plan
1236
-
1237
- - global tokens;
1238
- - component list;
1239
- - responsive strategy;
1240
- - interaction strategy;
1241
- - validation method.
1242
-
1243
- ### C. Optional Recommendations
1244
-
1245
- Only include recommendations supported by a concrete observation.
1246
-
1247
- ### D. Approval Questions
1248
-
1249
- Group optional changes into one concise approval request where practical.
1250
-
1251
- Example:
1252
-
1253
- > I can reproduce the Figma exactly first. I also identified two optional improvements:
1254
- > 1. subtle hero entrance animation;
1255
- > 2. stronger hover feedback for feature cards.
1256
- >
1257
- > Neither appears in the source design. Should I include both, only one, or keep the implementation strictly identical?
1258
-
1259
- ---
1260
-
1261
- ## 18. Final Delivery Structure
1262
-
1263
- ### 18.0 Ship a fidelity report — evidence, not assurances (required)
1264
-
1265
- Never close a build with a sentence like "it matches the design". Hand the user something
1266
- they can check without trusting you:
1267
-
1268
- ```bash
1269
- # 1. once per project: sign the icon set so identity (not just presence) is checked
1270
- python3 scripts/figma_icons.py --svg design/exports/page.svg --nodes figma/nodes --out figma/icons
1271
- # 2. every audit round:
1272
- python3 scripts/figma_report.py --page http://localhost:PORT/index.html \
1273
- --nodes figma/nodes --selectors selectors.json --icons-dir figma/icons
1274
- ```
1275
-
1276
- `selectors.json` maps each Figma section frame to ONE CSS selector:
1277
- `{"hero": ".hero", "collection": ".collection", ...}` — keys must match the node JSON
1278
- filenames. Without it sections are matched by DOM order, which mis-maps silently; the
1279
- report header lists any selector matching 0 or 2+ elements — fix those before reading rows.
1280
- Skipping `--icons-dir` downgrades the icon audit to presence-only (it will say so).
1281
- `--assets-map figma/assets-map.json` (`{imageRef: filename}`) enables image IDENTITY —
1282
- without it the report cannot say the RIGHT photo is on the node (on the reference build
1283
- this check caught two cards silently reusing another card's photo). Known noise: a file
1284
- with stacked overlapping variants stamps the template's imageRef over real positions —
1285
- identity rows saying "expects <first-photo>" at spots that already pass with their own
1286
- ref are variant ghosts (FM99), not defects.
1287
-
1288
- Version every stylesheet and script from the first commit (`styles.css?v=1`, `main.js?v=1`)
1289
- and bump on each edit — browsers heuristically cache unversioned assets and the report (and
1290
- you) will measure stale code (FM92).
1291
-
1292
- It writes `fidelity-report.html`, which for every section shows the Figma reference beside
1293
- the **live** page, with a cross-fade slider and a *difference* blend (a perfect match goes
1294
- black), and computes — in the browser, at load time:
1295
-
1296
- | Check | Why it exists |
1297
- |---|---|
1298
- | section height, Figma vs built | the obvious one |
1299
- | **content extents (left–right edge)** | heights match while a container is offset sideways; only this catches it |
1300
- | horizontal overflow | breaks silently below the design width |
1301
- | total page height | catches drift that per-section rounding hides |
1302
- | **every TEXT node: position, size, weight, colour** | the box can be perfect while the design inside it is wrong |
1303
- | **every icon: exported asset / hand-drawn / missing** | hand-drawn icons pass every geometric check and still look wrong |
1304
- | **every image: real photo / gradient placeholder / missing** | placeholders survive to production when nothing counts them |
1305
- | **every non-text box: fill colour, corner radius** | the text audit is blind to a panel with the wrong colour |
1306
- | **sections with no reference slice** | an unchecked section is how a whole section goes missing |
1307
- | **the section→selector mapping itself** | a selector matching 0 or 2 elements silently invalidates every row |
1308
- | **hover transition durations** vs `interactions[]` | the design states them; nothing else checks you shipped them |
1309
- | **stroke colour and drop shadows** on boxes | a rule the design draws as a stroke is easy to fake as an element |
1310
- | **image identity** (`--assets-map`) | "a photo is present" is not "the right photo is present" |
1311
- | **confirmed breakpoints not covered** (`--breakpoints`) | a second design silently never gets built |
1312
-
1313
- **Do not stop at the geometry checks.** They pass on a page that looks nothing like the
1314
- design. In one build every section matched on height *and* content extents while only
1315
- **10 of 224 text nodes** matched on position, size, weight and colour. The geometry row is
1316
- a smoke test, not a verdict.
1317
-
1318
- Three traps the text audit itself must avoid:
1319
-
1320
- - Compare the **ink box** of the text (`Range.getBoundingClientRect()`), not the element
1321
- box. A centred heading lives in a full-width block; comparing element rects reports a
1322
- huge false offset.
1323
- - Only compare `font-weight` **within the same typeface**. And a declared `@font-face`
1324
- whose file 404s still appears in `getComputedStyle().fontFamily` — ask
1325
- `document.fonts.check(...)` whether the face actually loaded before trusting it.
1326
- - `text not found in DOM` is a real finding, not noise: it means your copy or your markup
1327
- splits the string differently from the design.
1328
- - Group an icon at the **outermost pure-vector subtree**. Recurse further and every glyph
1329
- outline of a logo is counted as its own missing icon.
1330
- - An icon reported `missing` may be a vector the design draws as a shape and you
1331
- legitimately reproduce in CSS. That is a decision to record, not a row to ignore.
1332
-
1333
- #### Guard the guards
1334
-
1335
- An audit that can be fooled is worse than none, because it grants permission to stop
1336
- looking. Three holes are easy to leave open — close them:
1337
-
1338
- - **Custom properties.** A linter that skips declarations containing `var()` can be
1339
- defeated by moving the invented number into a token. Resolve `var()` before checking.
1340
- - **Section completeness.** A report that iterates over the references you happened to
1341
- produce cannot notice a section you never sliced. Enumerate the design's sections and
1342
- flag every one without a reference.
1343
- - **Selector mapping.** Require an explicit section→selector map, and fail if any selector
1344
- matches zero or several elements. Falling back to DOM order will one day compare the
1345
- wrong things and report green.
1346
-
1347
- And in the box audit, separate a **wrong fill/radius** (a defect) from **no 1:1 element**
1348
- (a structural difference — Figma frames do not map one-to-one onto DOM elements). Reporting
1349
- them together produces a number that means nothing.
1350
-
1351
- #### The audit runs in both directions
1352
-
1353
- Every check that walks **design → DOM** ("the design has an icon here; is it in the page?")
1354
- is blind to whatever you *added*. That is not a small gap. A stray element, a doubled
1355
- control, a leftover from a refactor — none of them exist in the design, so nothing looks for
1356
- them, and the report keeps saying the build matches.
1357
-
1358
- `figma_report.py` therefore also walks **DOM → design**: every graphic in a section that no
1359
- design node accounts for is listed as *not in the design*. Read that row. It is the only
1360
- place a mistake of commission can show up.
1361
-
1362
- The same asymmetry applies to identity. "An icon is present at this node" and "the icon the
1363
- design puts at this node is present" are different claims, and only the second one is worth
1364
- anything. `figma_icons.py` stamps each exported icon with `data-icon-shape` (a scale- and
1365
- translation-invariant outline profile) and `data-icon-paint`; the report compares those,
1366
- with a tolerance on the shape and exactly on the paint. Two traps it exists to avoid:
1367
-
1368
- - hashing the geometry — the same icon reused on the next card sits at sub-pixel-different
1369
- coordinates, so a hash reports every legitimate reuse as a mismatch;
1370
- - comparing geometry alone — a filled star and an outlined star are the *same* outline and
1371
- differ only in `fill`.
1372
-
1373
- And because a comparator that always answers "different" makes a perfect build look broken,
1374
- the report proves it can recognise a file as itself before it reports a single verdict.
1375
-
1376
- #### What this report still does *not* check
1377
-
1378
- State this to the user, every time. A green report is evidence about what was measured and
1379
- nothing else.
1380
-
1381
- - **the visual result of a hover state.** Durations are verified; what the variant *looks
1382
- like* is not. Fetch the variants (`figma_pull.py --hover <destIds>`), look at them, and
1383
- match your `:hover` by eye.
1384
- - opacity, gradients, z-order and overflow clipping
1385
- - focus and active states; keyboard behaviour
1386
- - accessibility and performance
1387
- - **regression** — there is no baseline diff; a fix that breaks another section shows up
1388
- only if you read the whole report again
1389
- - anything at viewports other than the design width, and any breakpoint you did not build
1390
-
1391
- Those need the difference-blend view, the reference frames, and your eyes.
1392
-
1393
- Rules:
1394
-
1395
- - The report's numbers are computed live from the page. **Do not paste a summary that the
1396
- report does not show**, and do not claim a section matches while its row says *differs*.
1397
- - **Verify the verifier.** Before trusting a comparison view, confirm the reference and the
1398
- live page are drawn at the same scale and offset; a mis-scaled overlay makes any build
1399
- look correct. Confirm a known-bad section actually reports as bad.
1400
- - When you change CSS and re-measure through an iframe, **bust the cache** — a stale
1401
- stylesheet will happily tell you the fix worked.
1402
- - Rows that legitimately differ (a decorative element you rendered as a shadow, a font
1403
- substitution) belong in the difference log with the reason — not hidden by loosening the
1404
- tolerance.
1405
- - A green report is *evidence about geometry*, not proof of visual fidelity. The
1406
- difference-blend view is what proves that; look at it, and say what you saw.
1407
-
1408
- At completion provide:
1409
-
1410
- ### 1. Completed Scope
1411
-
1412
- List pages, sections, and states implemented.
1413
-
1414
- ### 2. Files Changed
1415
-
1416
- List each created or modified file and its purpose.
1417
-
1418
- ### 3. Fidelity Notes
1419
-
1420
- Point at `fidelity-report.html` first, then state:
1421
-
1422
- - what matches the design;
1423
- - any unavoidable deviations;
1424
- - unavailable fonts or assets;
1425
- - assumptions used.
1426
-
1427
- ### 4. Responsive Coverage
1428
-
1429
- List tested viewport sizes and behavior.
1430
-
1431
- ### 5. Accessibility and Performance Notes
1432
-
1433
- State checks completed and remaining limitations.
1434
-
1435
- ### 6. Optional Next Improvements
1436
-
1437
- List only unimplemented, user-approved, or still-pending suggestions.
1438
-
1439
- Do not claim visual perfection without comparison evidence.
1440
-
1441
- ---
1442
-
1443
- ## 19. Acceptance Checklist
1444
-
1445
- Before marking the work complete, verify:
1446
-
1447
- ### Visual Fidelity
1448
-
1449
- - [ ] Container width matches reference
1450
- - [ ] Section spacing matches reference
1451
- - [ ] Grid and alignment match reference
1452
- - [ ] Typography matches reference
1453
- - [ ] Colors match reference
1454
- - [ ] Borders, radii, and shadows match reference
1455
- - [ ] Images use correct crop and aspect ratio
1456
- - [ ] Icons match size and alignment
1457
- - [ ] Text wraps similarly at reference viewport sizes
1458
-
1459
- ### Responsive Behavior
1460
-
1461
- - [ ] Mobile layout is intentionally designed
1462
- - [ ] Tablet behavior is valid
1463
- - [ ] Desktop behavior matches reference
1464
- - [ ] No horizontal overflow
1465
- - [ ] Navigation works at all breakpoints
1466
- - [ ] Touch targets are usable
1467
- - [ ] Images remain sharp and correctly cropped
1468
-
1469
- ### Interaction
1470
-
1471
- - [ ] `interactions[]` in the node JSON was read before any motion was written (§9.0)
1472
- - [ ] Figma's own durations/easings are used where they exist; springs are noted as approximations
1473
- - [ ] Inferred motion is labeled, and matches the mood the design already sets
1474
- - [ ] Entrance motion cannot hide content if the script fails (§9.0.2)
1475
- - [ ] Hover states work
1476
- - [ ] Focus states are visible
1477
- - [ ] Active and selected states work
1478
- - [ ] Keyboard operation works
1479
- - [ ] Form validation states exist when required
1480
- - [ ] Motion specified in Figma is implemented (no approval needed — it is the design)
1481
- - [ ] Motion *not* in Figma has user approval, or a standing instruction, and is labeled inferred
1482
- - [ ] Reduced-motion preference is respected
1483
-
1484
- ### Code Quality
1485
-
1486
- - [ ] Semantic HTML used
1487
- - [ ] CSS variables defined
1488
- - [ ] Reusable components extracted
1489
- - [ ] No unnecessary duplication
1490
- - [ ] No arbitrary hacks without explanation
1491
- - [ ] No unsupported dependencies added
1492
- - [ ] No console errors
1493
-
1494
- ### Reference Coverage
1495
-
1496
- - [ ] `fidelity-report.html` was generated, opened, and handed to the user
1497
- - [ ] The **text audit** was read, not just the geometry rows
1498
- - [ ] Every row in it is green, or every non-green row appears in the difference log
1499
- - [ ] No layout rule was weakened just to turn a metric green
1500
- - [ ] `figma_lint.py` passes: no invented spacing, size, colour, `<br>` or inline `<svg>`
1501
- - [ ] Icon audit is green, or every hand-drawn / missing icon is justified in the difference log
1502
- - [ ] Image audit is green: no gradient placeholders left
1503
- - [ ] The user was told what the report does **not** check
1504
- - [ ] Box audit reviewed: no wrong fills or radii among matched boxes
1505
- - [ ] No section is listed as "no reference slice"
1506
- - [ ] The section→selector map resolves to exactly one element per section
1507
- - [ ] Hover transition durations audited; hover *appearance* compared against the fetched variants
1508
- - [ ] `--assets-map` supplied so image identity, not just presence, is checked
1509
- - [ ] `--breakpoints` supplied; every confirmed width has its own report
1510
- - [ ] `figma_discover.py` was run **first** and its output shown to the user
1511
- - [ ] Every confirmed breakpoint frame was built and audited, not inferred
1512
- - [ ] No icon was hand-drawn that exists in the file's icon library
1513
- - [ ] Hover variants named by `interactions[]` were fetched and compared
1514
- - [ ] The scope (one screen vs a site) was confirmed against the file's page-sized frames
1515
- - [ ] Each section was audited with `--only <section>` as it was built, not just at the end
1516
- - [ ] The difference-blend overlay was actually looked at, not just the numbers
1517
- - [ ] Every implemented section has a reference image that was actually viewed
1518
- - [ ] No section was reconstructed from geometry/JSON alone
1519
- - [ ] Every section was screenshot-compared against its reference after being built
1520
- - [ ] Blocked sections are reported as blocked, not shipped as done
1521
- - [ ] Font substitutions are measured (§6.5.4), not guessed, and the delta is recorded
1522
- - [ ] Asset-to-node mapping was verified (no shuffled or off-by-one images)
1523
-
1524
- ### Final Honesty Check
1525
-
1526
- - [ ] Assumptions are labeled
1527
- - [ ] Inferences are labeled
1528
- - [ ] Missing assets are disclosed
1529
- - [ ] Unverified claims are not presented as fact
1530
- - [ ] “100% identical” is not claimed without measurable verification
1531
- - [ ] Section heights matching is never presented as evidence the design matches
1532
- - [ ] Quota/permission failures are reported with the real cause (`Retry-After`, “File not exportable”), not glossed over
1533
-
1534
- ---
1535
-
1536
- ## 20. Recommended Folder Structure
1537
-
1538
- A basic skill package may use:
1539
-
1540
- ```text
1541
- figma-to-html-pixel-perfect/
1542
- ├── SKILL.md # this file
1543
- ├── README.md # install + setup, for distribution
1544
- ├── references/
1545
- │ ├── visual-review-checklist.md
1546
- │ ├── accessibility-checklist.md
1547
- │ └── animation-guidelines.md
1548
- └── scripts/
1549
- ├── figma_discover.py # FIRST: breakpoints, icon library, hover, scope
1550
- ├── figma_icons.py # extract every icon offline from the page SVG
1551
- ├── figma_pull.py # preflight, node JSON, one render per section
1552
- ├── figma_spec.py # correct node-JSON reader (§6.5.3 contract)
1553
- ├── figma_fonts.py # which fonts render, free vs licensed (§0.3)
1554
- ├── figma_lint.py # build-stage guard: invented values, hand-drawn icons
1555
- └── figma_report.py # fidelity report: geometry + text + icon/image audits
1556
- ```
1557
-
1558
- `SKILL.md` is the only required file, but **use the scripts** — they encode §6.5.1–6.5.3.
1559
- Re-deriving them by hand is how the reading-contract bugs get reintroduced.
1560
-
1561
- ---
1562
-
1563
- ## 21. Core Principle
1564
-
1565
- The supplied Figma design is the visual source of truth.
1566
-
1567
- Implement first, verify second, recommend third, and change the design only after the user approves the change.
1568
-
1569
- ---
1570
-
1571
- ## 22. Known Failure Modes — references/failure-modes.md (MANDATORY reading points)
1572
-
1573
- 114 documented failure modes, every one observed in a real build. The full table lives in
1574
- `references/failure-modes.md` — do NOT skip it; read it at these three moments:
1575
-
1576
- 1. **Before building** any of: fonts/text (FM82-88, 96, 100), repeated-item controls —
1577
- tabs/cards/logo strips (FM87, 93-95, 101, 106), buttons (FM86, 91, 96, 108), icons/SVG
1578
- (FM104), images/carousels/backgrounds (FM98-99, 105, 107), section layout (FM84, 103,
1579
- 109), responsive (FM111).
1580
- 2. **Before verifying** — the verifier itself has failure modes: measurement viewport
1581
- (FM110), stale CSS/JS (FM34, 92), reveal/animation interference (FM68, 113), matcher
1582
- blind spots (FM112), API quotas (FM114).
1583
- 3. **When the user reports a defect** — find the matching FM class first; the fix pattern
1584
- and the verify assertion are already written there.
1585
-
1586
- Working rule for NEW defects: fix the build, fix the verify stage so it would have caught
1587
- it, and append a design-agnostic FM row to `references/failure-modes.md`.
47
+ | adds/changes anything visible | never | never (only proposes) | only what was approved |
48
+ | output | code + difference log | + prioritized audit | + what changed |
49
+
50
+ Static markup for carousels/accordions is A; wiring behaviour is C (explicit request).
51
+ Reproducing motion the design specifies is A Motion below). Mode is per-request.
52
+ When unsure: A.
53
+
54
+ ## Workflow — gates, in order
55
+
56
+ **0. Setup (once).** Token in `~/.figma_token`; check `design/` folder first; 403 "not
57
+ exportable" = owner must enable sharing, stop. `references/setup.md`
58
+
59
+ **1. Discover before any code.**
60
+ `python3 scripts/figma_discover.py <fileKey> --json figma/discovery.json`
61
+ Answers: how many design widths (= breakpoints to BUILD, not guess) · icon library? ·
62
+ hover variants? · multiple versions of the same screen? **Multiple versions: the user
63
+ picks; never pick for them.** Show the output to the user.
64
+
65
+ **2. Pull nodes, cache-first.** `python3 scripts/figma_pull.py …` every REST endpoint
66
+ shares one quota and can 429 for hours; pull once, then work only from `figma/nodes/`.
67
+ Renders are the scarcest; a user-supplied `design/exports/page.png` replaces them.
68
+
69
+ **3. Fonts — first reply to the user.** `python3 scripts/figma_fonts.py figma/nodes/*.json`
70
+ request the licensed files by exact PostScript name; declare `@font-face` now so they
71
+ hot-swap in later; until then measure a substitute and disclose (never claim type fidelity).
72
+
73
+ **4. Motion check before deciding "no animation":**
74
+ `grep -l '"interactions"' figma/nodes/*.json` anything found is Mode A work.
75
+ `references/animation-guidelines.md`
76
+
77
+ **5. Per-section build loop.** For each section, in document order:
78
+ a. `figma_spec.py` the section (NUMBER RULE step 1) — full spec table before any CSS.
79
+ b. Build from the spec. Hard rules that prevent whole classes of offsets:
80
+ - Top-anchor content; **never vertically centre inside a locked min-height** (FM109).
81
+ - Text: read `styleOverrideTable` for per-character font/size changes (§ref), never
82
+ only `style`. No invented `<br>` — lint counts them against design newlines.
83
+ - Icons: `figma_icons.py` output only, never hand-drawn; photos: real `imageRef`
84
+ bytes via `figma/assets-map.json`; logos never invented.
85
+ - Fixed px column values from the frame are desktop-only truths — they need a
86
+ breakpoint before the width where they overflow.
87
+ c. Verify: regen report (step 6) — section is done when its row says `match`.
88
+ full detail: `references/build-guide.md`
89
+
90
+ **6. Report loop.**
91
+ `python3 scripts/figma_report.py --page <url> --nodes figma/nodes --selectors selectors.json --icons-dir figma/icons --assets-map figma/assets-map.json --out fidelity.html`
92
+ - Poll `document.title === 'fidelity-READY'` never sleep a fixed wait.
93
+ - Bump `styles.css?v=N` after EVERY css edit or the browser measures stale CSS.
94
+ - Read every row; **batch all sections' fixes from one read into one edit.**
95
+ - The report waits for fonts + image decode and double-samples layout; if a count
96
+ still changes with no edit to that section, suspect the verifier/page, not the CSS.
97
+ - Fix section HEIGHT mismatches top-to-bottom before item spacing (deltas are
98
+ page-absolute; upstream height errors cascade into everything below). The report
99
+ renders a red `#heightDebt` banner whenever any section height is >6px off and a
100
+ `#guardWarnings` banner for duplicate-selector shadowing **every table read MUST
101
+ return both banners' text first**; while `#heightDebt` is non-empty, per-item deltas
102
+ are not to be acted on. A `⚠ LINEAR DRIFT` row means one repeated component's height
103
+ is wrong fix that unit, never the individual rows (FM128).
104
+ - Edit had zero effect? grep for duplicate selectors (auto-warned, FM123) and check
105
+ computed style in a fresh iframe (`contentWindow.getComputedStyle`) before retrying.
106
+ - **After any `position:relative;left/top` or `transform:translate` shift, re-check
107
+ `document.documentElement.scrollWidth === clientWidth` at every breakpoint before
108
+ moving on.** A shift-only fix leaves the element the right SIZE, just partly off
109
+ canvas — the text/box delta audit does not catch this; only an overflow sweep does,
110
+ and the visible symptom can appear on a completely unrelated section (FM130).
111
+ → `references/report-guide.md`
112
+
113
+ **7. Responsive = each confirmed breakpoint is a separate design.**
114
+ - Split the frame into `figma/nodes-<width>/`, then validate: total visible TEXT count
115
+ across split files MUST equal the frame's (a shortfall = a dropped subtree, FM119).
116
+ - Diff per-breakpoint `imageRef`s (frames swap photos) and component default states
117
+ (an accordion open on mobile, closed on desktop — reproduce via matchMedia, FM116).
118
+ - If the mobile DOM order differs from desktop, author the mobile structure — do NOT
119
+ bend desktop markup with `display:contents`+`order`+`:nth-of-type` (FM115).
120
+ - Run the full report loop per breakpoint, plus one width wider than the design frame.
121
+ - No mobile frame in the file → responsive behaviour is inferred: label it.
122
+
123
+ **8. Visual sweep — numeric green is necessary, never sufficient.** Twice: after the
124
+ first full build and before declaring done. Put the design render beside the settled
125
+ live build, section by section, and LOOK (a 40-line compare page; ~seconds per section).
126
+ Missing whole components hide in green reports (FM119-120).
127
+
128
+ **9. Deliver.** The fidelity report file + a difference log: every deviation, why, and
129
+ every placeholder/substitution. Do not claim a match the report does not show.
130
+ → `references/report-guide.md`, `references/visual-review-checklist.md`,
131
+ `references/accessibility-checklist.md`
132
+
133
+ ## Completion standard
134
+
135
+ - **Loop until green or every remaining row is documented, proven noise. Never ask
136
+ "shall I continue?" while non-green rows remain.** Stop only for a real external
137
+ blocker (quota, missing font/asset) then report the blocker, not a fake completion.
138
+ - **Convergence guard:** the failing-row count must trend down. If it rises or
139
+ oscillates 3 rounds without a new low, the structure is wrong, not the values —
140
+ revert to the best round, measure the CONTAINER (width/display/stretch), fix the
141
+ structure, only then resume value tuning.
142
+ - Speed comes from fewer rounds, never from skipping verification: extract-first
143
+ (≤2 rounds per section), batch fixes across sections, answer node-JSON questions
144
+ with python (never with a browser round-trip), dedupe CSS as you go.
145
+
146
+ ## Clarification
147
+
148
+ Ask only when the gap genuinely blocks a reliable build (which frame is current, missing
149
+ licensed font, contradictory sources). Otherwise: build, infer conservatively, and LABEL
150
+ every inference in the difference log.
151
+
152
+ ## Editing this skill
153
+
154
+ After ANY change to `scripts/` or this file: `python3 scripts/selftest.py` must pass
155
+ (exit 0). Every past failure mode with a testable guard is a fixture there — a red test
156
+ means a guard was broken or deleted, and the mistake it caught WILL recur silently.
157
+ When you add a new guard, add its fixture in the same edit.
158
+
159
+ ## Failure modes `references/failure-modes.md`
160
+
161
+ 126 documented ways this work goes wrong, each with its fix. **Mandatory reading points:**
162
+ before the first build (FM1-30 layout/type), before icons/images (FM60-99), before
163
+ responsive (FM103, 109-120), when a verify result looks wrong (FM110-126), when edits
164
+ stop having effects (FM123). When you hit ANY new defect: fix the build, fix the verify
165
+ stage so it would have caught it, and add the failure mode — in design-agnostic terms.