similarbuild 0.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.
Files changed (76) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/LICENSE +21 -0
  3. package/README.md +301 -0
  4. package/bin/install.js +256 -0
  5. package/lib/copy-templates.mjs +52 -0
  6. package/lib/install-deps.mjs +62 -0
  7. package/lib/prompt-config.mjs +83 -0
  8. package/lib/verify-env.mjs +19 -0
  9. package/package.json +63 -0
  10. package/scripts/sync-templates.mjs +71 -0
  11. package/templates/commands/build-page.md +490 -0
  12. package/templates/commands/build-site.md +548 -0
  13. package/templates/commands/clip-section.md +519 -0
  14. package/templates/memory/anti-patterns.md +212 -0
  15. package/templates/memory/design-knowledge.md +225 -0
  16. package/templates/memory/fixes.md +163 -0
  17. package/templates/memory/patterns.md +681 -0
  18. package/templates/presets/shopify-section.yaml +51 -0
  19. package/templates/presets/wp-elementor.yaml +49 -0
  20. package/templates/reports/fixtures/mock-run-1.json +115 -0
  21. package/templates/reports/fixtures/mock-run-2.json +72 -0
  22. package/templates/reports/report-renderer.mjs +218 -0
  23. package/templates/reports/report-template.html +571 -0
  24. package/templates/skills/sb-build-shopify/SKILL.md +104 -0
  25. package/templates/skills/sb-build-shopify/references/shopify-build-rules.md +563 -0
  26. package/templates/skills/sb-build-shopify/scripts/build-shopify.mjs +637 -0
  27. package/templates/skills/sb-build-shopify/scripts/tests/test-build-shopify.mjs +424 -0
  28. package/templates/skills/sb-build-wp/SKILL.md +83 -0
  29. package/templates/skills/sb-build-wp/references/wp-build-rules.md +376 -0
  30. package/templates/skills/sb-build-wp/scripts/build-wp.mjs +327 -0
  31. package/templates/skills/sb-build-wp/scripts/tests/test-build-wp.mjs +224 -0
  32. package/templates/skills/sb-compare-visual/SKILL.md +121 -0
  33. package/templates/skills/sb-compare-visual/scripts/compare-visual.mjs +387 -0
  34. package/templates/skills/sb-compare-visual/scripts/lib/compare-tokens.mjs +273 -0
  35. package/templates/skills/sb-compare-visual/scripts/tests/test-compare-tokens.mjs +350 -0
  36. package/templates/skills/sb-compare-visual/scripts/tests/test-compare-visual.mjs +626 -0
  37. package/templates/skills/sb-crawl-and-list/SKILL.md +99 -0
  38. package/templates/skills/sb-crawl-and-list/scripts/crawl-and-list.mjs +437 -0
  39. package/templates/skills/sb-crawl-and-list/scripts/lib/blocklist-filter.mjs +176 -0
  40. package/templates/skills/sb-crawl-and-list/scripts/lib/fallback-crawler.mjs +107 -0
  41. package/templates/skills/sb-crawl-and-list/scripts/lib/page-classifier.mjs +89 -0
  42. package/templates/skills/sb-crawl-and-list/scripts/lib/sitemap-parser.mjs +118 -0
  43. package/templates/skills/sb-crawl-and-list/scripts/tests/test-blocklist-filter.mjs +204 -0
  44. package/templates/skills/sb-crawl-and-list/scripts/tests/test-crawl-and-list.mjs +276 -0
  45. package/templates/skills/sb-crawl-and-list/scripts/tests/test-fallback-crawler.mjs +243 -0
  46. package/templates/skills/sb-crawl-and-list/scripts/tests/test-page-classifier.mjs +120 -0
  47. package/templates/skills/sb-crawl-and-list/scripts/tests/test-sitemap-parser.mjs +157 -0
  48. package/templates/skills/sb-extract-assets/SKILL.md +112 -0
  49. package/templates/skills/sb-extract-assets/scripts/extract-assets.mjs +484 -0
  50. package/templates/skills/sb-extract-assets/scripts/tests/test-extract-assets.mjs +112 -0
  51. package/templates/skills/sb-inspect-live/SKILL.md +105 -0
  52. package/templates/skills/sb-inspect-live/scripts/inspect-live.mjs +693 -0
  53. package/templates/skills/sb-inspect-live/scripts/tests/test-inspect-live.mjs +181 -0
  54. package/templates/skills/sb-review-checks/SKILL.md +113 -0
  55. package/templates/skills/sb-review-checks/references/review-rules.md +195 -0
  56. package/templates/skills/sb-review-checks/scripts/lib/anti-patterns.mjs +379 -0
  57. package/templates/skills/sb-review-checks/scripts/lib/cross-reference.mjs +115 -0
  58. package/templates/skills/sb-review-checks/scripts/lib/design-quality.mjs +541 -0
  59. package/templates/skills/sb-review-checks/scripts/review-checks.mjs +250 -0
  60. package/templates/skills/sb-review-checks/scripts/tests/test-anti-patterns.mjs +343 -0
  61. package/templates/skills/sb-review-checks/scripts/tests/test-cross-reference.mjs +170 -0
  62. package/templates/skills/sb-review-checks/scripts/tests/test-design-quality.mjs +493 -0
  63. package/templates/skills/sb-review-checks/scripts/tests/test-review-checks.mjs +267 -0
  64. package/templates/skills/sb-tweak/SKILL.md +130 -0
  65. package/templates/skills/sb-tweak/references/tweak-patterns.md +157 -0
  66. package/templates/skills/sb-tweak/scripts/lib/diff-summarizer.mjs +140 -0
  67. package/templates/skills/sb-tweak/scripts/lib/element-locator.mjs +507 -0
  68. package/templates/skills/sb-tweak/scripts/lib/intent-parser.mjs +324 -0
  69. package/templates/skills/sb-tweak/scripts/tests/test-diff-summarizer.mjs +248 -0
  70. package/templates/skills/sb-tweak/scripts/tests/test-element-locator.mjs +418 -0
  71. package/templates/skills/sb-tweak/scripts/tests/test-intent-parser.mjs +496 -0
  72. package/templates/skills/sb-tweak/scripts/tests/test-tweak.mjs +407 -0
  73. package/templates/skills/sb-tweak/scripts/tweak.mjs +656 -0
  74. package/templates/skills/sb-validate-render/SKILL.md +120 -0
  75. package/templates/skills/sb-validate-render/scripts/tests/test-validate-render.mjs +304 -0
  76. package/templates/skills/sb-validate-render/scripts/validate-render.mjs +645 -0
@@ -0,0 +1,376 @@
1
+ # WP/Elementor Build Rules
2
+
3
+ Everything `sb-build-wp` needs to compose a production-grade `.html` file standalone enough to paste into an Elementor HTML widget and survive the theme's CSS overrides. These rules are non-negotiable — they encode hard-earned lessons from real WP migrations where the theme reset silently mutated the output.
4
+
5
+ If the host project provides these files, prefer them — they reflect the user's most recent curated knowledge:
6
+
7
+ - `<plugin>/memory/patterns.md` (overrides patterns A-H below)
8
+ - `<plugin>/memory/anti-patterns.md` (overrides anti-patterns #1-9 below)
9
+ - `<plugin>/memory/design-knowledge.md` (subset is usually passed by the orchestrator instead)
10
+ - `<plugin>/presets/wp-elementor.yaml` (overrides the theme reset and known overrides below)
11
+
12
+ When they're absent (early in the framework's life), use everything below as the source of truth.
13
+
14
+ ## Composition contract
15
+
16
+ The output is a **single `.html` file** containing exactly:
17
+
18
+ 1. A `<style>` block scoped to one root class (`.{scope}`).
19
+ 2. Markup using that scope class on the outermost element.
20
+ 3. Optional `<script>` block for vanilla-JS interactivity (no frameworks).
21
+ 4. Optional `<link rel="preconnect">` + `<link rel="stylesheet">` for Google Fonts at the top of the markup or in `<head>`-like position (since Elementor strips real `<head>`, place fonts inline at the top of the body fragment).
22
+
23
+ No `<html>`, no `<head>`, no `<body>` wrappers — this is a fragment that Elementor renders inside its own DOM. The scope class IS the boundary.
24
+
25
+ ## Scope class naming
26
+
27
+ - Pick a kebab-case name from the section type detected in `inspection.sectionType` and a short content hint (e.g. `hero-mobile-shop`, `pdp-variant-card`, `faq-collapsible`).
28
+ - The scope class appears on the outermost element and prefixes EVERY selector in the `<style>` block.
29
+ - BEM-ish modifiers: `.{scope}__title`, `.{scope}__cta`, `.{scope}--variant-active`. Pick what reads cleanly — don't over-engineer.
30
+
31
+ ## Defensive specificity (anti-pattern #5b — non-negotiable)
32
+
33
+ EVERY selector that styles user-visible content (typography, color, layout, spacing, borders) MUST include the scope class as an ancestor — not just as the element's own class.
34
+
35
+ ```css
36
+ /* WRONG — single class, theme will win */
37
+ .hero__title { font-size: 32px; }
38
+
39
+ /* RIGHT — chained scope, beats theme specificity */
40
+ .hero .hero__title { font-size: 32px; }
41
+ ```
42
+
43
+ Even when the element already has the scope on itself, prefix with the scope as ancestor again. The double-occurrence raises specificity (0,2,0 vs 0,1,0) and protects against `.elementor-widget-container *` rules.
44
+
45
+ ## `!important` policy
46
+
47
+ The Elementor + active theme stack frequently injects `!important` on these properties via `.elementor *` blanket rules. Match them:
48
+
49
+ - `font-family` — almost always overridden by a theme typography block
50
+ - `font-size` — set on `h1..h6`, `p`, `body`
51
+ - `font-weight` — set on `h1..h6`, `body`
52
+ - `color` — set on links via `.elementor a`, on body via theme
53
+
54
+ Use `!important` ONLY on these four properties when they appear on critical text. Do NOT spray `!important` everywhere — overuse turns debugging into a nightmare. Specifically: do NOT `!important` margins, paddings, widths, heights, backgrounds, transforms, transitions.
55
+
56
+ ## Reset universal
57
+
58
+ First rule in the `<style>` block, always:
59
+
60
+ ```css
61
+ .{scope}, .{scope} * {
62
+ box-sizing: border-box;
63
+ margin: 0;
64
+ padding: 0;
65
+ }
66
+ ```
67
+
68
+ This neutralizes the most common theme bleed without touching properties Elementor needs (positioning, display, etc).
69
+
70
+ ## Full-bleed container (escape Elementor's max-width)
71
+
72
+ When the section needs to span the viewport edge-to-edge, Elementor's column wrapper caps it at the theme's container width. Escape with:
73
+
74
+ ```css
75
+ .{scope} {
76
+ width: 100vw;
77
+ margin-left: calc(50% - 50vw);
78
+ }
79
+ ```
80
+
81
+ This works regardless of how Elementor sized the widget. Do NOT use negative margins of fixed pixel values — they break on mobile breakpoints.
82
+
83
+ ## Mobile-first + desktop breakpoint
84
+
85
+ Base styles target mobile (390px viewport). Desktop overrides go in:
86
+
87
+ ```css
88
+ @media (min-width: 1000px) {
89
+ /* desktop-only overrides */
90
+ }
91
+ ```
92
+
93
+ 1000px (not 768px, not 1024px) — large enough that tablets get the mobile layout (which is usually safe), small enough that landscape tablets and small laptops get desktop. This is the threshold the Alpha Infuse migration validated.
94
+
95
+ ## Asset substitution
96
+
97
+ For every `<img>` you emit, the URL must be a value from `assetsMap.assets[originalUrl].localPath` — NEVER the original CDN URL, never an inline `data:` URI for raster, never a hand-drawn SVG approximation (anti-pattern #8).
98
+
99
+ For SVGs: WP blocks SVG uploads by default, so SVGs come back as inline markup in `assetsMap.assets[url].inline`. Inline that markup directly into the HTML (it's already sanitized by `sb-extract-assets`). Do NOT save it to a file path.
100
+
101
+ If a URL appears in `assetsMap.failed[]`, you have two choices:
102
+
103
+ 1. Drop the image entirely (preferred for decorative images).
104
+ 2. Use a `<div>` placeholder with a literal background-color from the inspection's color tokens (NEVER a stock-photo URL).
105
+
106
+ Never fabricate a substitute asset. If a hero image failed, surface that to the orchestrator via a comment in the output: `<!-- sb-build-wp: hero asset failed download (URL recorded in assets-map.json) -->`.
107
+
108
+ ## Hero rules (anti-pattern #1)
109
+
110
+ NEVER use `100vh` for hero height. Mobile browser chrome (URL bar collapse + safe areas) makes `100vh` taller than the visible viewport, causing the hero to clip below the fold. Use:
111
+
112
+ ```css
113
+ .{scope} .{scope}__hero {
114
+ aspect-ratio: 1 / 1.3; /* mobile: portrait-ish */
115
+ max-height: 700px;
116
+ }
117
+
118
+ @media (min-width: 1000px) {
119
+ .{scope} .{scope}__hero {
120
+ aspect-ratio: 16 / 9;
121
+ max-height: 720px;
122
+ }
123
+ }
124
+ ```
125
+
126
+ If the hero is a background-image: prefer `background-image` on the container with `background-size: cover` over an `<img>` with `height: 100%` (anti-pattern #2). The `<img>` approach breaks the moment Elementor injects `img { height: auto !important }`.
127
+
128
+ ## Google Fonts inlining
129
+
130
+ At the top of the body fragment (before the scoped section), emit:
131
+
132
+ ```html
133
+ <link rel="preconnect" href="https://fonts.googleapis.com">
134
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
135
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family={Family}:wght@{weights}&display=swap">
136
+ ```
137
+
138
+ `display=swap` is mandatory — without it, FOIT happens and Elementor's font-loading dance causes layout shift. Pull the family name and weights from `inspection.tokens.typography` (h1, h2, body, button — usually 2 weights cover all four).
139
+
140
+ ## Design knowledge — applied automatically
141
+
142
+ These come from the `designKnowledge` subset the orchestrator passed in. When that subset is absent or thin, use these defaults — they are baseline web standards, not opinions:
143
+
144
+ ### Accessibility
145
+
146
+ - Every `<img>` MUST have an `alt` attribute. Decorative images: `alt=""` (empty, intentional). Content images: descriptive alt derived from `inspection.imgUrls[].alt` if present, else from surrounding context.
147
+ - Every `<button>` MUST have `type="button"` (so it doesn't submit a parent form by accident — Elementor pages often have hidden forms).
148
+ - Buttons that are icon-only (no visible text) MUST have `aria-label="..."` describing the action.
149
+ - Heading hierarchy: a section that starts with H2 cannot have an H1 below it. Don't skip levels (H1 → H2 → H3, not H1 → H3). The orchestrator may tell you this is a sub-section that should start at H2 — respect that.
150
+ - Use `<nav>` for navigation regions, `<main>` is owned by the page (don't emit it from a section), `<dialog>` for modals (native, no library needed).
151
+
152
+ ### Performance
153
+
154
+ - The hero image (largest above-the-fold content image) MUST get `loading="eager"` AND a `<link rel="preload" as="image" href="{localPath}">` at the top of the fragment. Every other image gets `loading="lazy"`.
155
+ - `fetchpriority="high"` on the hero `<img>` is allowed but not required (Chromium-only at the time of writing).
156
+ - `<link rel="preconnect">` for Google Fonts goes before the stylesheet link, not after.
157
+
158
+ ### Web standards
159
+
160
+ - Responsive images: when the inspection has multiple sources (`imgUrls[].type === 'source-srcset'`), emit `srcset` + `sizes` on the `<img>`. The `sizes` value should be `"(min-width: 1000px) 50vw, 100vw"` for half-width desktop content, `"100vw"` for full-bleed.
161
+ - Use `<dialog>` for modals — close with `dialog.close()`, open with `dialog.showModal()`. No library, no custom backdrop.
162
+ - Use `<details>/<summary>` for FAQ collapsibles (Pattern F). Theme styling? Reset summary's default marker via `summary { list-style: none } summary::-webkit-details-marker { display: none }`.
163
+
164
+ ## fixHints (when present)
165
+
166
+ `fixHints` arrives as a JSON array from `sb-review-checks` after a failed validation cycle. Each hint has the shape:
167
+
168
+ ```json
169
+ { "area": "string", "issue": "string", "fix": "string", "severity": "high|medium|low" }
170
+ ```
171
+
172
+ When fixHints are present, you are NOT building from scratch — you are patching a previous build to address specific issues. Apply each fix surgically:
173
+
174
+ - Re-read your previous output (the orchestrator passes its path).
175
+ - For each fixHint, identify the exact selector or markup it references and apply the fix.
176
+ - Keep everything else identical — don't refactor neighboring code, don't rename classes, don't re-pick patterns. The reviewer's diff should be the only diff.
177
+ - High-severity fixes are mandatory. Medium are strongly recommended. Low are optional but apply them unless they conflict with high-severity ones.
178
+
179
+ ## Patterns (A-H fallback)
180
+
181
+ These are the canonical recipes. When `<plugin>/memory/patterns.md` exists, prefer it. Each pattern is a markup-and-CSS skeleton — adapt the class names and content to the specific section.
182
+
183
+ ### A. Sticky Header (announcement + nav, mobile drawer)
184
+
185
+ ```html
186
+ <header class="hdr">
187
+ <div class="hdr__bar">Free shipping over $50</div>
188
+ <nav class="hdr__nav" aria-label="Main">
189
+ <a href="/" class="hdr__logo"><img src="..." alt="Brand"></a>
190
+ <button class="hdr__menu-btn" type="button" aria-label="Open menu" aria-expanded="false">☰</button>
191
+ <ul class="hdr__menu" hidden>
192
+ <li><a href="/shop">Shop</a></li>
193
+ <li><a href="/about">About</a></li>
194
+ </ul>
195
+ </nav>
196
+ </header>
197
+ ```
198
+
199
+ Toggle `hidden` + `aria-expanded` on the button click via `<script>`. ~10 lines of vanilla JS.
200
+
201
+ ### B. Full-bleed Hero (aspect-ratio, background-image, escape Elementor)
202
+
203
+ Use `background-image` on the container, NOT an `<img>` with `object-fit: cover`. See Hero rules above.
204
+
205
+ ### C. Carousel mobile / Grid desktop (scroll-snap → grid)
206
+
207
+ ```css
208
+ .products { display: flex; overflow-x: auto; scroll-snap-type: x mandatory; gap: 16px; }
209
+ .products__item { flex: 0 0 80vw; scroll-snap-align: start; }
210
+ @media (min-width: 1000px) {
211
+ .products { display: grid; grid-template-columns: repeat(4, 1fr); overflow: visible; }
212
+ .products__item { flex: none; }
213
+ }
214
+ ```
215
+
216
+ ### D. Image-with-text overlap
217
+
218
+ When the source has an image overlapping a colored band, the band is a `::before` on the SECTION, not a sibling box. Inspect `inspection.pseudoElements` for the exact bounds (anti-pattern #6).
219
+
220
+ ```css
221
+ .story { position: relative; }
222
+ .story::before { content: ""; position: absolute; inset: 30% 0 0 0; background: #f4eee8; z-index: -1; }
223
+ .story__image { width: 100%; }
224
+ ```
225
+
226
+ ### E. Defensive button (chain class + appearance + !important)
227
+
228
+ ```css
229
+ .{scope} .{scope}__cta.{scope}__cta {
230
+ appearance: none;
231
+ background: #d8112a !important;
232
+ color: #fff !important;
233
+ font-family: inherit !important;
234
+ font-weight: 700 !important;
235
+ font-size: 16px !important;
236
+ border: 0;
237
+ padding: 14px 28px;
238
+ border-radius: 999px;
239
+ cursor: pointer;
240
+ }
241
+ ```
242
+
243
+ The double-class chain (`.{scope}__cta.{scope}__cta`) doubles specificity again — this is the only pattern where we double the modifier class itself, because button overrides are the most aggressive theme bleed.
244
+
245
+ ### F. FAQ collapsible (`<details>/<summary>`)
246
+
247
+ ```html
248
+ <details class="faq__item">
249
+ <summary class="faq__q">How do I return an order?</summary>
250
+ <div class="faq__a">Email us within 30 days...</div>
251
+ </details>
252
+ ```
253
+
254
+ ```css
255
+ .faq__item summary::-webkit-details-marker { display: none; }
256
+ .faq__item summary { list-style: none; cursor: pointer; }
257
+ ```
258
+
259
+ ### G. Trust banner with modal (vanilla JS ~30 lines)
260
+
261
+ Use `<dialog>`:
262
+
263
+ ```html
264
+ <button type="button" class="trust__more" data-open="trust-modal">Learn more</button>
265
+ <dialog id="trust-modal" class="trust__modal">
266
+ <button type="button" class="trust__close" data-close>×</button>
267
+ <h2>Our guarantee</h2>
268
+ <p>...</p>
269
+ </dialog>
270
+ ```
271
+
272
+ ```js
273
+ document.querySelectorAll('[data-open]').forEach(btn =>
274
+ btn.addEventListener('click', () => document.getElementById(btn.dataset.open).showModal())
275
+ );
276
+ document.querySelectorAll('[data-close]').forEach(btn =>
277
+ btn.addEventListener('click', () => btn.closest('dialog').close())
278
+ );
279
+ ```
280
+
281
+ ### H. Variant card e-commerce (radio + photo)
282
+
283
+ Real product photos from `assetsMap`, not SVG illustrations. Radios with hidden inputs + styled labels.
284
+
285
+ ```html
286
+ <fieldset class="variants">
287
+ <legend class="variants__title">Choose your size</legend>
288
+ <label class="variants__opt">
289
+ <input type="radio" name="size" value="30ml" checked>
290
+ <img src="{localPath}" alt="30ml bottle">
291
+ <span>30ml — $24</span>
292
+ </label>
293
+ </fieldset>
294
+ ```
295
+
296
+ ## Anti-patterns (1-9 fallback)
297
+
298
+ When `<plugin>/memory/anti-patterns.md` exists, prefer it. Otherwise these are the canon:
299
+
300
+ 1. **Hero `100vh` in Elementor** — clips on mobile. Use `aspect-ratio` + `max-height`.
301
+ 2. **`<img>` with `height: 100%` for full-bleed hero** — Elementor's `img { height: auto !important }` clobbers it. Use `background-image` on the container.
302
+ 3. **Guessing overlay opacity** — read the exact alpha from `inspection.tokens` or computed style. Never eyeball.
303
+ 4. **`.scope a { color: inherit }` clobbers button color** — buttons use `<button>`, not `<a>`. If a CTA must be an `<a>`, set its color literally, not via `inherit` or `currentColor`.
304
+ 5. **`<button>` decorative without defensive specificity** — chain the class + `!important` on the four font properties + background + color (Pattern E).
305
+ 6. **`!important` alone without ancestor prefix** — useless. The theme has both ancestor specificity AND `!important`. You need both too.
306
+ 7. **Modeling overlap-image as a sibling box** — it's a `::before` on the section. Check `inspection.pseudoElements`.
307
+ 8. **Showing output before validating in Playwright** — every build must pass through `sb-validate-render` before being shown. Aggressive theme reset injected, screenshot diff'd.
308
+ 9. **Replicating an image as inline SVG** — download the actual asset (it's already in `assetsMap`). Inline SVG is for icons that came from the source as SVG, not for raster impostors.
309
+
310
+ ## Theme reset (preset fallback — wp-elementor.yaml)
311
+
312
+ When `<plugin>/presets/wp-elementor.yaml` exists, prefer its `theme_reset` block. Otherwise these are the known overrides Elementor + Hello/Astra/most themes inject:
313
+
314
+ ```css
315
+ /* What the theme injects (informational — do NOT include in your output) */
316
+ .elementor, .elementor * { font-weight: 300 !important; font-size: 18px !important; font-family: 'Roboto', sans-serif !important; }
317
+ .elementor h1, .elementor h2, .elementor h3 { font-weight: 300 !important; font-size: 24px !important; }
318
+ .elementor img, img { max-width: 100% !important; height: auto !important; border-radius: 0 !important; }
319
+ .elementor button, button { background: transparent !important; border: 0 !important; padding: 0 !important; }
320
+ .elementor a { color: inherit !important; text-decoration: none !important; }
321
+ ```
322
+
323
+ This is what you are defending against. Every selector in your output that styles a property on this list MUST win on specificity (chained scope) and may need `!important` (font properties only).
324
+
325
+ ## Output skeleton
326
+
327
+ Lay out the file in this order — it's the order Elementor parses and the order CSP browsers like:
328
+
329
+ ```html
330
+ <link rel="preconnect" href="https://fonts.googleapis.com">
331
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
332
+ <link rel="preload" as="image" href="{hero localPath}">
333
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=...&display=swap">
334
+
335
+ <style>
336
+ /* 1. Reset */
337
+ .{scope}, .{scope} * { box-sizing: border-box; margin: 0; padding: 0; }
338
+
339
+ /* 2. Scope container (full-bleed if applicable) */
340
+ .{scope} { width: 100vw; margin-left: calc(50% - 50vw); }
341
+
342
+ /* 3. Element styles, mobile-first, with defensive specificity */
343
+ .{scope} .{scope}__hero { ... }
344
+ .{scope} .{scope}__title { font-size: 32px !important; font-weight: 700 !important; ... }
345
+
346
+ /* 4. Desktop overrides */
347
+ @media (min-width: 1000px) {
348
+ .{scope} .{scope}__hero { ... }
349
+ }
350
+ </style>
351
+
352
+ <section class="{scope}">
353
+ <!-- markup -->
354
+ </section>
355
+
356
+ <script>
357
+ /* optional vanilla JS */
358
+ </script>
359
+ ```
360
+
361
+ If there's no JS, omit the `<script>` block entirely. If no Google Font is needed (system stack only), omit the font links.
362
+
363
+ ## What "production-grade" means
364
+
365
+ The output must score high on Lighthouse the moment it lands — not after follow-up tweaks. Concretely:
366
+
367
+ - All images have alt + correct loading attr
368
+ - Hero has preload link
369
+ - Fonts have display=swap + preconnect
370
+ - No layout-shift bombs (aspect-ratio set on media)
371
+ - Buttons have type="button"
372
+ - No console errors from missing assets (every src is a real localPath)
373
+ - HTML validates (no unclosed tags, proper nesting)
374
+ - CSS is scoped, no global pollution
375
+
376
+ The structural validator (`scripts/build-wp.mjs validate`) checks the mechanical subset of these. The rest is on you — they're judgment calls.