@invisibleloop/pulse 0.12.3 → 0.14.2

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 (181) hide show
  1. package/.claude/commands/build-page.md +5 -1
  2. package/.claude/commands/verify.md +31 -17
  3. package/.claude/pulse-checklist.md +21 -1
  4. package/.github/workflows/publish.yml +1 -0
  5. package/.pulse-verified +1 -0
  6. package/CLAUDE.md +56 -15
  7. package/docs/.claude/commands/verify.md +4 -0
  8. package/docs/.claude/pulse-checklist.md +20 -2
  9. package/docs/public/.pulse-ui-version +1 -1
  10. package/docs/public/andypixel.css +237 -0
  11. package/docs/public/bookworms.css +422 -0
  12. package/docs/public/coutumes.css +584 -0
  13. package/docs/public/images/bookworms/hero-books.jpg +0 -0
  14. package/docs/public/images/bookworms/how-we-read-now.jpg +0 -0
  15. package/docs/public/images/bookworms/lantern-keepers.jpg +0 -0
  16. package/docs/public/images/bookworms/provincial-bookseller.jpg +0 -0
  17. package/docs/public/images/journey/178.jpg +0 -0
  18. package/docs/public/images/journey/29.jpg +0 -0
  19. package/docs/public/images/retrowheels/hero.jpg +0 -0
  20. package/docs/public/images/shop/devbook-pro.jpg +0 -0
  21. package/docs/public/images/suzukibento/1015.jpg +0 -0
  22. package/docs/public/images/suzukibento/1025.jpg +0 -0
  23. package/docs/public/images/suzukibento/1043.jpg +0 -0
  24. package/docs/public/images/suzukibento/1059.jpg +0 -0
  25. package/docs/public/images/suzukibento/1062.jpg +0 -0
  26. package/docs/public/images/suzukibento/1084.jpg +0 -0
  27. package/docs/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
  28. package/docs/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
  29. package/docs/public/images/swish/1485518882345-15568b007407.jpg +0 -0
  30. package/docs/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
  31. package/docs/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
  32. package/docs/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
  33. package/docs/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
  34. package/docs/public/images/swish/1551803091-e20673f15770.jpg +0 -0
  35. package/docs/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
  36. package/docs/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
  37. package/docs/public/journey.css +528 -0
  38. package/docs/public/pulse-ui.css +1036 -22
  39. package/docs/public/retrowheels.css +402 -0
  40. package/docs/public/shop.css +235 -0
  41. package/docs/public/sparkys.css +349 -0
  42. package/docs/public/suzukibento.css +517 -0
  43. package/docs/public/swish.css +394 -0
  44. package/docs/server.js +19 -4
  45. package/docs/src/lib/nav.js +14 -3
  46. package/docs/src/pages/components/decorate.js +74 -0
  47. package/docs/src/pages/components/gallery.js +102 -0
  48. package/docs/src/pages/components/hero.js +1 -0
  49. package/docs/src/pages/components/marquee.js +76 -0
  50. package/docs/src/pages/components/photo-card.js +74 -0
  51. package/docs/src/pages/components/section.js +40 -1
  52. package/docs/src/pages/meta.js +26 -0
  53. package/docs/src/pages/templates/andypixel.js +260 -0
  54. package/docs/src/pages/templates/bookworms.js +227 -0
  55. package/docs/src/pages/templates/coutumes.js +226 -0
  56. package/docs/src/pages/templates/journey.js +186 -0
  57. package/docs/src/pages/templates/retrowheels.js +342 -0
  58. package/docs/src/pages/templates/shop.js +257 -0
  59. package/docs/src/pages/templates/sparkys.js +249 -0
  60. package/docs/src/pages/templates/suzukibento.js +200 -0
  61. package/docs/src/pages/templates/swish.js +313 -0
  62. package/docs/src/pages/templates.js +313 -2
  63. package/docs/src/pages/testing.js +2 -1
  64. package/examples/.claude/commands/pulse-build.md +7 -0
  65. package/examples/.claude/commands/pulse-dev.md +7 -0
  66. package/examples/.claude/commands/pulse-start.md +9 -0
  67. package/examples/.claude/commands/pulse-stop.md +5 -0
  68. package/examples/.claude/commands/verify.md +84 -0
  69. package/examples/.claude/coverage-check.js +127 -0
  70. package/examples/.claude/pulse-checklist.md +177 -0
  71. package/examples/andypixel.css +237 -0
  72. package/examples/andypixel.js +258 -0
  73. package/examples/bookworms.css +422 -0
  74. package/examples/bookworms.js +225 -0
  75. package/examples/contact.js +1 -0
  76. package/examples/counter.js +1 -0
  77. package/examples/dev.server.js +17 -0
  78. package/examples/journey.css +528 -0
  79. package/examples/journey.js +186 -0
  80. package/examples/pricing.js +1 -0
  81. package/examples/public/.pulse-ui-version +1 -0
  82. package/examples/public/pulse-ui.css +4031 -0
  83. package/examples/public/pulse-ui.js +257 -0
  84. package/examples/quiz.js +1 -0
  85. package/examples/retrowheels.css +402 -0
  86. package/examples/retrowheels.js +340 -0
  87. package/examples/shared.js +6 -0
  88. package/examples/suzukibento.css +513 -0
  89. package/examples/suzukibento.js +200 -0
  90. package/examples/swish.css +394 -0
  91. package/examples/swish.js +311 -0
  92. package/examples/todos.js +1 -0
  93. package/package.json +2 -3
  94. package/public/.pulse-ui-version +1 -1
  95. package/public/images/bookworms/hero-books.jpg +0 -0
  96. package/public/images/bookworms/how-we-read-now.jpg +0 -0
  97. package/public/images/bookworms/lantern-keepers.jpg +0 -0
  98. package/public/images/bookworms/provincial-bookseller.jpg +0 -0
  99. package/public/images/journey/178.jpg +0 -0
  100. package/public/images/journey/29.jpg +0 -0
  101. package/public/images/retrowheels/hero.jpg +0 -0
  102. package/public/images/shop/devbook-pro.jpg +0 -0
  103. package/public/images/suzukibento/1015.jpg +0 -0
  104. package/public/images/suzukibento/1025.jpg +0 -0
  105. package/public/images/suzukibento/1043.jpg +0 -0
  106. package/public/images/suzukibento/1059.jpg +0 -0
  107. package/public/images/suzukibento/1062.jpg +0 -0
  108. package/public/images/suzukibento/1084.jpg +0 -0
  109. package/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
  110. package/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
  111. package/public/images/swish/1485518882345-15568b007407.jpg +0 -0
  112. package/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
  113. package/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
  114. package/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
  115. package/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
  116. package/public/images/swish/1551803091-e20673f15770.jpg +0 -0
  117. package/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
  118. package/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
  119. package/public/pulse-ui.css +1080 -22
  120. package/scripts/build.js +72 -18
  121. package/src/agent/checklist.md +22 -2
  122. package/src/agent/commands/verify.md +4 -0
  123. package/src/agent/guide-components.md +146 -8
  124. package/src/agent/guide-design-gallery.md +288 -0
  125. package/src/agent/guide-design-references.md +172 -0
  126. package/src/agent/guide-explore.md +313 -0
  127. package/src/agent/guide-styles.md +212 -11
  128. package/src/agent/guide-templates.md +223 -10
  129. package/src/agent/guide.md +9 -4
  130. package/src/agent/identity.md +2 -1
  131. package/src/agent/skills/verify/SKILL.md +64 -28
  132. package/src/agent/workflow.md +247 -61
  133. package/src/cli/dev.js +39 -14
  134. package/src/cli/fmt.js +113 -0
  135. package/src/cli/index.js +60 -33
  136. package/src/cli/logger.js +145 -0
  137. package/src/cli/scaffold.js +57 -3
  138. package/src/html.js +6 -1
  139. package/src/mcp/server.js +2561 -99
  140. package/src/mcp/validate-worker.js +49 -5
  141. package/src/runtime/ssr.js +4 -1
  142. package/src/server/index.js +29 -5
  143. package/src/spec/schema.js +21 -0
  144. package/src/spec/schema.test.js +28 -0
  145. package/src/testing/html.js +130 -4
  146. package/src/testing/testing.test.js +58 -0
  147. package/src/ui/banner.js +24 -9
  148. package/src/ui/brand-statement.js +58 -0
  149. package/src/ui/button.js +13 -2
  150. package/src/ui/card.js +20 -1
  151. package/src/ui/colophon.js +29 -0
  152. package/src/ui/contact-row.js +41 -0
  153. package/src/ui/cta.js +18 -4
  154. package/src/ui/decorate.js +55 -0
  155. package/src/ui/display-heading.js +44 -0
  156. package/src/ui/drop-cap.js +29 -0
  157. package/src/ui/editorial-hero.js +63 -0
  158. package/src/ui/entry-list.js +36 -0
  159. package/src/ui/feature.js +20 -3
  160. package/src/ui/footer.js +78 -9
  161. package/src/ui/gallery.js +56 -0
  162. package/src/ui/hero.js +68 -18
  163. package/src/ui/image-pair.js +72 -0
  164. package/src/ui/index.js +20 -0
  165. package/src/ui/input.js +1 -0
  166. package/src/ui/marquee.js +48 -0
  167. package/src/ui/nav.js +19 -9
  168. package/src/ui/photo-card.js +44 -0
  169. package/src/ui/product-card.js +65 -0
  170. package/src/ui/pullquote.js +14 -3
  171. package/src/ui/search.js +1 -0
  172. package/src/ui/section-label.js +38 -0
  173. package/src/ui/section.js +1 -1
  174. package/src/ui/service-card.js +47 -0
  175. package/src/ui/stat-grid.js +40 -0
  176. package/src/ui/stat.js +16 -2
  177. package/src/ui/testimonial.js +17 -6
  178. package/src/ui/textarea.js +1 -0
  179. package/src/ui/top-strip.js +47 -0
  180. package/src/ui/trust-list.js +46 -0
  181. package/src/ui/ui.test.js +35 -0
@@ -0,0 +1,313 @@
1
+ # Pulse Explore Guide — Blank-Canvas Layout Thinking
2
+
3
+ This guide is for when you want a site that feels **distinct**, not just correct. It teaches structure-first thinking: start with layout zones and emotional intent, not with which template to adapt.
4
+
5
+ > Read this when `pulse_sketch` suggests an unusual layout, when the user says "make it feel different", or when no template in `pulse://guide/templates` is a good fit.
6
+
7
+ ---
8
+
9
+ ## The Problem with Defaulting to Components
10
+
11
+ Every Pulse component is well-built. That's also their risk: they pull strongly toward a recognisable vocabulary — centred hero, three-column features, testimonial cards. A dog groomer and a hedge fund built with default components will look like cousins.
12
+
13
+ **The escape hatch is always available.** The `view` function is raw HTML. You can use:
14
+ - Zero components — just utility classes and inline HTML
15
+ - A mix of raw layout with components for specific elements (nav, footer, forms)
16
+ - Component structure with overridden CSS via app.css class rules
17
+
18
+ > **Note on inline styles:** The raw HTML patterns in this guide use `style=` attributes for clarity. In your own specs, move those rules into `app.css` as named classes — the `pulse_review` checklist will flag inline styles as a smell, and they can't be overridden cleanly. The pattern is: sketch with inline styles → extract to `app.css` classes → done.
19
+
20
+ ---
21
+
22
+ ## Start with Intent, Not Components
23
+
24
+ Before opening `pulse://guide/components`, answer three questions:
25
+
26
+ ### 1. What is the emotional job?
27
+ Not "what does the page contain" but "how should the user feel 30 seconds after landing?"
28
+
29
+ | Feeling | Layout signal |
30
+ |---|---|
31
+ | Trust / authority | Wide margins, generous whitespace, restrained type, no decoration |
32
+ | Energy / urgency | Full-bleed images, bold type, tight spacing, strong diagonals |
33
+ | Warmth / handmade | Irregular layout, slight tilts, texture, serif body text |
34
+ | Precision / technical | Monospace accents, grid-aligned everything, data tables, no fluff |
35
+ | Playful / surprising | Breaking the grid, overlapping elements, unexpected colour pops |
36
+ | Editorial / serious | Long reading width, pullquotes, photo captions, no eyebrow badges |
37
+
38
+ ### 2. What is the dominant structural gesture?
39
+ One of these should describe the entire page:
40
+
41
+ - **Full-bleed** — images or colour fills edge-to-edge, type floats over them
42
+ - **Offset split** — persistent asymmetry: content always 60/40 or 70/30, never centered
43
+ - **Vertical rhythm** — everything stacked, each section separated by whitespace not dividers
44
+ - **Dense grid** — information-rich, small gutters, horizontal scannable
45
+ - **Long-form** — single narrow column, essay-like, designed for reading not scanning
46
+ - **Kinetic** — scrolling triggers layout shifts (parallax, sticky elements, revealed content)
47
+
48
+ ### 3. What should it NOT look like?
49
+ Name something to avoid. "Not another SaaS dark card page." "Not a startup landing page." "Not like it was made with a template."
50
+
51
+ This negative constraint is often more useful than the positive one.
52
+
53
+ ---
54
+
55
+ ## Layout Zones Approach
56
+
57
+ Design the page as zones before deciding what fills them:
58
+
59
+ ```
60
+ ┌─────────────────────────────────────────┐
61
+ │ IDENTITY ZONE │ ← Name, logo, hook. First impression.
62
+ │ (full-bleed image? type only? split?) │
63
+ ├─────────────────────────────────────────┤
64
+ │ PROOF ZONE │ ← Why believe it? Numbers, names, photos.
65
+ │ (stats? logos? photos? quote?) │
66
+ ├─────────────────────────────────────────┤
67
+ │ DETAIL ZONE │ ← What exactly do you offer?
68
+ │ (features? services? work? prices?) │
69
+ ├─────────────────────────────────────────┤
70
+ │ TRUST ZONE │ ← Social proof, credentials, story.
71
+ │ (testimonials? about? process?) │
72
+ ├─────────────────────────────────────────┤
73
+ │ ACTION ZONE │ ← The ask. Form, CTA, booking, purchase.
74
+ └─────────────────────────────────────────┘
75
+ ```
76
+
77
+ The *order* and *proportion* of these zones is a design decision. A personal trainer might lead with PROOF (transformation photos) before IDENTITY. A law firm might bury the ACTION ZONE beneath extensive TRUST ZONE content.
78
+
79
+ **Reordering zones is one of the cheapest ways to feel distinctive.**
80
+
81
+ ---
82
+
83
+ ## Raw HTML Patterns (Zero Components)
84
+
85
+ These are layouts that deliberately use no Pulse components except `nav` and `footer`. They demonstrate what's possible.
86
+
87
+ ### Full-Bleed Typography Hero
88
+ ```js
89
+ const typographicHero = `
90
+ <section style="
91
+ min-height: 100svh;
92
+ display: grid;
93
+ place-items: center;
94
+ background: var(--ui-bg);
95
+ padding: var(--ui-space-8);
96
+ ">
97
+ <div style="max-width: 900px; text-align: center;">
98
+ <p style="
99
+ font-size: clamp(0.75rem, 1.5vw, 0.875rem);
100
+ letter-spacing: 0.15em;
101
+ text-transform: uppercase;
102
+ color: var(--ui-muted);
103
+ margin-bottom: var(--ui-space-6);
104
+ ">Est. 2024 · London</p>
105
+ <h1 style="
106
+ font-size: clamp(3rem, 10vw, 8rem);
107
+ font-weight: 800;
108
+ line-height: 0.9;
109
+ letter-spacing: -0.04em;
110
+ color: var(--ui-text);
111
+ ">The Work<br>Speaks for<br>Itself.</h1>
112
+ <a href="#work" style="
113
+ display: inline-block;
114
+ margin-top: var(--ui-space-10);
115
+ padding: var(--ui-space-4) var(--ui-space-8);
116
+ border: 1px solid var(--ui-border);
117
+ color: var(--ui-text);
118
+ text-decoration: none;
119
+ font-size: 0.875rem;
120
+ letter-spacing: 0.05em;
121
+ ">View Work ↓</a>
122
+ </div>
123
+ </section>
124
+ `
125
+ ```
126
+
127
+ ### Asymmetric Split (Persistent Offset)
128
+ ```js
129
+ const asymmetricHero = `
130
+ <section style="
131
+ min-height: 100svh;
132
+ display: grid;
133
+ grid-template-columns: 1fr 1.6fr;
134
+ ">
135
+ <div style="
136
+ background: var(--ui-bg-alt);
137
+ padding: var(--ui-space-16) var(--ui-space-8);
138
+ display: flex;
139
+ flex-direction: column;
140
+ justify-content: flex-end;
141
+ ">
142
+ <p style="color:var(--ui-muted);font-size:0.75rem;letter-spacing:0.1em;text-transform:uppercase;margin-bottom:var(--ui-space-4)">Dog grooming · Maplewood</p>
143
+ <h1 style="font-size:clamp(2rem,4vw,3.5rem);font-weight:700;line-height:1.1;letter-spacing:-0.02em;color:var(--ui-text)">Your dog,<br>beautifully<br>groomed.</h1>
144
+ <a href="#book" style="display:inline-flex;align-items:center;gap:0.5rem;margin-top:var(--ui-space-8);color:var(--ui-text);text-decoration:none;font-weight:500">
145
+ Book now <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
146
+ </a>
147
+ </div>
148
+ <div style="overflow:hidden;">
149
+ <img src="${imageUrl}" alt="${imageAlt}" style="width:100%;height:100%;object-fit:cover;">
150
+ </div>
151
+ </section>
152
+ `
153
+ ```
154
+
155
+ ### Stacked Long-Form (Essay / Blog)
156
+ ```js
157
+ const essayLayout = `
158
+ <article style="
159
+ max-width: 680px;
160
+ margin: 0 auto;
161
+ padding: var(--ui-space-16) var(--ui-space-6);
162
+ ">
163
+ <header style="margin-bottom: var(--ui-space-12);">
164
+ <p style="font-size:0.75rem;letter-spacing:0.1em;text-transform:uppercase;color:var(--ui-muted);margin-bottom:var(--ui-space-3)">
165
+ ${category} · ${date} · ${readTime}
166
+ </p>
167
+ <h1 style="font-size:clamp(2rem,4vw,3rem);font-weight:700;line-height:1.15;letter-spacing:-0.02em;margin-bottom:var(--ui-space-4)">
168
+ ${title}
169
+ </h1>
170
+ <p style="font-size:1.125rem;color:var(--ui-muted);line-height:1.6;">${deck}</p>
171
+ <div style="display:flex;align-items:center;gap:1rem;margin-top:var(--ui-space-6);padding-top:var(--ui-space-6);border-top:1px solid var(--ui-border);">
172
+ <img src="${authorAvatar}" alt="${authorName}" style="width:40px;height:40px;border-radius:50%;object-fit:cover;">
173
+ <div>
174
+ <p style="font-weight:500;font-size:0.875rem;">${authorName}</p>
175
+ <p style="color:var(--ui-muted);font-size:0.75rem;">${authorRole}</p>
176
+ </div>
177
+ </div>
178
+ </header>
179
+ <img src="${heroImage}" alt="" style="width:100%;aspect-ratio:16/9;object-fit:cover;margin-bottom:var(--ui-space-10);">
180
+ <div style="font-size:1.0625rem;line-height:1.75;color:var(--ui-text);">
181
+ ${bodyHtml}
182
+ </div>
183
+ </article>
184
+ `
185
+ ```
186
+
187
+ ### Dense Grid (Portfolio / Gallery)
188
+ ```js
189
+ const denseGrid = `
190
+ <section style="
191
+ padding: var(--ui-space-4);
192
+ display: grid;
193
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
194
+ gap: var(--ui-space-3);
195
+ ">
196
+ ${items.map(item => `
197
+ <a href="${item.href}" style="
198
+ display: block;
199
+ overflow: hidden;
200
+ position: relative;
201
+ aspect-ratio: 4/3;
202
+ background: var(--ui-bg-alt);
203
+ ">
204
+ <img src="${item.img}" alt="${item.title}" style="width:100%;height:100%;object-fit:cover;transition:transform 0.4s ease;">
205
+ <div style="
206
+ position:absolute;inset:0;
207
+ background:linear-gradient(to top, rgba(0,0,0,0.6) 0%, transparent 50%);
208
+ display:flex;align-items:flex-end;padding:var(--ui-space-4);
209
+ opacity:0;transition:opacity 0.3s ease;
210
+ " class="grid-overlay">
211
+ <p style="color:#fff;font-weight:500;font-size:0.875rem;">${item.title}</p>
212
+ </div>
213
+ </a>
214
+ `).join('')}
215
+ </section>
216
+ `
217
+ ```
218
+
219
+ ---
220
+
221
+ ## Mixing Raw Layout with Pulse Components
222
+
223
+ You don't have to choose all-or-nothing. Common hybrid pattern:
224
+
225
+ ```js
226
+ view: () => `
227
+ ${uiNav({ ... })} ← Use nav — it handles mobile burger, sticky, a11y
228
+
229
+ <section style="...raw layout..."> ← Raw hero with your own structure
230
+ ...
231
+ </section>
232
+
233
+ ${section({ ← Use section for padding + background variant
234
+ variant: 'alt',
235
+ content: grid({ ... }) ← Use grid for the responsive column system
236
+ })}
237
+
238
+ ${cta({ ... })} ← Use cta — saves 10 lines and is a11y correct
239
+
240
+ ${footer({ ... })} ← Use footer — handles all the footer layout
241
+ `
242
+ ```
243
+
244
+ **Rule of thumb:** Use Pulse components for global chrome (nav, footer), repeating patterns (cards, testimonials, features), and interactive elements (buttons, forms, modals). Use raw HTML for hero sections, custom layouts, and any zone where the default shape doesn't serve the design.
245
+
246
+ ---
247
+
248
+ ## CSS Custom Properties Available Everywhere
249
+
250
+ Even in raw HTML, you have the full design token system:
251
+
252
+ ```css
253
+ /* Colours */
254
+ var(--ui-bg) /* page background */
255
+ var(--ui-bg-alt) /* slightly offset background (cards, alternating sections) */
256
+ var(--ui-surface) /* elevated surface (modals, dropdowns) */
257
+ var(--ui-text) /* primary text */
258
+ var(--ui-muted) /* secondary / caption text */
259
+ var(--ui-border) /* divider / outline color */
260
+ var(--ui-accent) /* brand accent (buttons, links, highlights) */
261
+
262
+ /* Spacing */
263
+ var(--ui-space-1) /* 0.25rem */
264
+ var(--ui-space-2) /* 0.5rem */
265
+ var(--ui-space-3) /* 0.75rem */
266
+ var(--ui-space-4) /* 1rem */
267
+ var(--ui-space-6) /* 1.5rem */
268
+ var(--ui-space-8) /* 2rem */
269
+ var(--ui-space-10) /* 2.5rem */
270
+ var(--ui-space-12) /* 3rem */
271
+ var(--ui-space-16) /* 4rem */
272
+
273
+ /* Typography */
274
+ var(--ui-font) /* body font stack */
275
+ var(--ui-radius) /* base border-radius (affected by vibe) */
276
+ var(--ui-radius-lg) /* larger radius */
277
+
278
+ /* Shadows */
279
+ var(--ui-shadow-sm) /* subtle card shadow */
280
+ var(--ui-shadow) /* standard shadow */
281
+ var(--ui-shadow-lg) /* large shadow */
282
+ ```
283
+
284
+ Use `clamp()` for fluid type: `font-size: clamp(2rem, 5vw, 4rem)` — scales smoothly across breakpoints.
285
+
286
+ ---
287
+
288
+ ## The 3-Layout Rule
289
+
290
+ Before writing any code, sketch 3 structurally different options for the hero / above-the-fold zone. Even one sentence each:
291
+
292
+ > Option A: Full-bleed photo, title in bottom-left corner, text reversed out of image
293
+ > Option B: 60/40 split — headline left, product photo right, earthy cream background
294
+ > Option C: Typography-only, massive headline, minimal, no image — lets the copy breathe
295
+
296
+ Then choose the one that matches the emotional intent. If you can't decide, pick the most unusual one.
297
+
298
+ **The `pulse_sketch` tool automates this** — call it with a brief description and it returns 3 structured layout directions ready to choose from.
299
+
300
+ ---
301
+
302
+ ## Anti-Pattern Checklist
303
+
304
+ These are the signs a design has defaulted to template mode rather than intent:
305
+
306
+ - [ ] Hero is centred with eyebrow badge + large title + subtitle + two buttons
307
+ - [ ] Three-column feature grid with icon + heading + body
308
+ - [ ] Testimonials are all in cards with quote + author name
309
+ - [ ] Every section alternates white/light-grey backgrounds
310
+ - [ ] The nav has a logo on the left, links in the middle, CTA on the right
311
+ - [ ] The footer has the same logo, same links, and a copyright line
312
+
313
+ None of these are wrong. But if they're all there together, the page will look like every other Pulse page. Consider swapping at least 2 of the 6 for something more distinctive.
@@ -16,8 +16,37 @@ Non-interactive components (nav, hero, button, card, etc.) only need '/pulse-ui.
16
16
 
17
17
  pulse-ui.css exposes CSS custom properties for every token. app.css MUST use these tokens — never hardcode colour hex values.
18
18
 
19
- Override tokens in :root inside app.css to retheme all components at once:
19
+ ### How theming works — two-layer token system
20
+
21
+ Pulse uses a **two-layer token system**:
22
+
23
+ 1. **Input tokens** (unprefixed: `--bg`, `--text`, `--accent`, etc.) — you define these in `:root` or `[data-theme="light"]` to set your palette
24
+ 2. **Output tokens** (prefixed: `--ui-bg`, `--ui-text`, `--ui-accent`, etc.) — pulse-ui components read these; you reference them in app.css
25
+
26
+ pulse-ui.css sets `--ui-*` tokens to `var(--your-input-token, fallback)`. When you override `--accent`, components automatically pick it up via `--ui-accent`.
27
+
28
+ **Which token names to use where:**
29
+
30
+ | File | Define input tokens | Reference output tokens |
31
+ |---|---|---|
32
+ | `public/theme.css` | ✓ `--accent: #e25;` | — |
33
+ | `app.css` | — | ✓ `color: var(--ui-accent);` |
34
+ | Pulse components | — | ✓ (already done internally) |
35
+
36
+ **Common mistake:** defining `--color-accent` or `--brand-primary` expecting components to use them. Components only read the canonical unprefixed names: `--accent`, `--text`, `--bg`, `--surface`, `--border`, `--muted`, `--radius`. Define those, and everything updates.
37
+
38
+ ### Dark by default
39
+
40
+ **The library is dark by default.** `pulse-ui.css` applies `background-color: var(--ui-bg)` and `color: var(--ui-text)` to `html, body` automatically using the dark palette defined in `:root` — you do NOT need to add these to app.css.
41
+
42
+ For a **light theme**, set `meta.theme: 'light'` in the spec — this adds `data-theme="light"` to the `<body>` and activates the built-in light token set (accessible contrast for badges, alerts, and all semantic colours).
43
+
44
+ ### Overriding tokens in your theme
45
+
46
+ Override input tokens to retheme all components at once. The pattern is the same for both dark and light themes — set the unprefixed input token in the appropriate selector:
47
+
20
48
  ```css
49
+ /* Dark theme overrides — in :root */
21
50
  :root {
22
51
  --bg: #0d0d10; /* page background */
23
52
  --surface: #111116; /* card / panel background */
@@ -29,8 +58,17 @@ Override tokens in :root inside app.css to retheme all components at once:
29
58
  --accent-hover: #b5aaff;
30
59
  --radius: 8px;
31
60
  }
61
+
62
+ /* Light theme overrides — in [data-theme="light"] */
63
+ [data-theme="light"] {
64
+ --bg: #ffffff;
65
+ --surface: #f5f5fa;
66
+ --accent: #e25; /* overrides the light default #6b5ce7 */
67
+ }
32
68
  ```
33
69
 
70
+ pulse-ui.css maps input → output via `var()` in **both** the dark (`:root`) and light (`[data-theme="light"]`) blocks, so the same two-layer pattern works in both themes.
71
+
34
72
  Then use the computed --ui-* tokens everywhere in app.css:
35
73
  ```css
36
74
  /* pulse-ui.css already sets background-color and color on html/body — do not repeat them */
@@ -40,28 +78,64 @@ a { color: var(--ui-accent); }
40
78
  code { background: var(--ui-surface-2); color: var(--ui-accent); border: 1px solid var(--ui-border); }
41
79
  ```
42
80
 
43
- **The library is dark by default.** `pulse-ui.css` applies `background-color: var(--ui-bg)` and `color: var(--ui-text)` to `html, body` automatically — you do NOT need to add these to app.css.
81
+ > **Light theme escape hatch:** if you need to override a specific output token that has no input equivalent, you can set `--ui-accent` directly inside `[data-theme="light"]` — but prefer input tokens wherever possible.
82
+
83
+ ```js
84
+ meta: {
85
+ theme: 'light',
86
+ styles: ['/pulse-ui.css', '/app.css'],
87
+ }
88
+ ```
89
+
90
+ **Colour tokens:** `--ui-bg`, `--ui-surface`, `--ui-surface-2`, `--ui-border`, `--ui-text`, `--ui-muted`, `--ui-accent`, `--ui-accent-hover`, `--ui-accent-dim`, `--ui-accent-text`, `--ui-green`, `--ui-red`, `--ui-yellow`, `--ui-blue`, `--ui-radius`, `--ui-radius-sm`, `--ui-font`, `--ui-font-display`, `--ui-mono`. Never hardcode hex values — override the tokens.
91
+
92
+ > **⚠ `--ui-muted` contrast on warm/light palettes** — The default `--ui-muted` value (`#60607a` in light theme) achieves 4.5:1 on the default white `--ui-bg`. When you set a warm or coloured background (`--bg: #f5e6d3` etc.) the luminance difference shrinks and `--ui-muted` can drop below the WCAG AA threshold of 4.5:1. **Always override `--muted` when changing `--bg`**, and run `pulse_check_contrast` immediately after writing your `theme.css`. A common failure mode: stat labels, nav links, and feature descriptions all use `--ui-muted` and will fail contrast silently if this token is not rechecked against your actual background colour.
44
93
 
45
- To apply a **light theme**, set `meta.theme: 'light'` in the spec — this adds `data-theme="light"` to the `<body>` and activates the built-in light token set (accessible contrast for badges, alerts, and all semantic colours). Do NOT manually copy token values into `:root`.
94
+ > **⚠ `section--dark` in light theme** — `section({ variant: 'dark' })` uses `--ui-surface-2` as its background, which in the default light theme resolves to `#f0f0f8` — a light grey, not a dark background. The framework now auto-corrects this via a `[data-theme="light"] .ui-section--dark` override (navy `#1a1a2e`), and ghost buttons inside it get automatic light-text treatment. If you use a custom `--surface-2` override in your theme, set `--ui-dark-bg` and `--ui-dark-text` to pin the dark section to specific values:
95
+ > ```css
96
+ > [data-theme="light"] { --ui-dark-bg: #1a1a2e; --ui-dark-text: #e8e0d0; }
97
+ > ```
46
98
 
47
- **Overriding tokens on a light-theme page:** `pulse-ui.css` defines light theme values under `[data-theme="light"]`, which has higher specificity than `:root`. Overrides written only to `:root` will be silently beaten. Always target `[data-theme="light"]` when overriding tokens for a light-theme page:
99
+ > **Ghost buttons on dark backgrounds** — use `variant: 'ghost-light'` instead of `variant: 'ghost'` whenever the button sits on a dark background (custom hero div, `section--dark`, navy sections). `ghost-light` renders white text with a subtle white border, maintaining 4.5:1 contrast regardless of the surrounding theme. `ghost` uses `--ui-muted` text which fails on dark backgrounds in light theme.
48
100
 
101
+ **Display font token:** `--ui-font-display` is used for hero titles and section headings. By default it inherits `--ui-font`. Override it separately to get a display/heading face that differs from body text:
49
102
  ```css
50
- /* WRONG — loses to pulse-ui.css */
51
- :root { --ui-accent: #e25; }
103
+ /* app.css */
104
+ :root { --font-display: 'Playfair Display', Georgia, serif; }
105
+ ```
52
106
 
53
- /* CORRECT */
54
- [data-theme="light"] { --ui-accent: #e25; }
107
+ **Letter-spacing token:** `--ui-letter-spacing-display` (default `-0.025em`) controls heading tightness. Override in `:root` to match your typeface:
108
+ ```css
109
+ :root { --letter-spacing-display: -0.04em; } /* tighter for heavy condensed fonts */
55
110
  ```
56
111
 
112
+ ## Visual personality — meta.vibe
113
+
114
+ `meta.vibe` sets `data-vibe` on `<body>` and adjusts geometry + type tokens as a preset. It doesn't touch colour — pair it with a `theme.css` for full personality.
115
+
57
116
  ```js
58
117
  meta: {
59
- theme: 'light',
60
- styles: ['/pulse-ui.css', '/app.css'],
118
+ vibe: 'warm', // warm | editorial | playful | minimal | bold
119
+ theme: 'light',
120
+ styles: ['/pulse-ui.css', '/theme.css', '/app.css'],
61
121
  }
62
122
  ```
63
123
 
64
- **Colour tokens:** `--ui-bg`, `--ui-surface`, `--ui-surface-2`, `--ui-border`, `--ui-text`, `--ui-muted`, `--ui-accent`, `--ui-accent-hover`, `--ui-accent-dim`, `--ui-accent-text`, `--ui-green`, `--ui-red`, `--ui-yellow`, `--ui-blue`, `--ui-radius`, `--ui-radius-sm`, `--ui-font`, `--ui-mono`. Never hardcode hex values — override the tokens.
124
+ | Vibe | Effect | Pairs well with |
125
+ |---|---|---|
126
+ | `warm` | radius 14px, softer letter-spacing | rounded photo cards, `section: paper`, warm palette |
127
+ | `editorial` | radius 0, serif display font, tight letter-spacing | `hero layout: overlap`, `section: diagonal`, `pullquote` |
128
+ | `playful` | radius 22px, neutral letter-spacing | `gallery` with rounded images, `marquee`, bright accent |
129
+ | `minimal` | radius 2px, open letter-spacing, no shadows | left-aligned hero, `section: spotlight`, monochrome |
130
+ | `bold` | radius 6px, very tight letter-spacing | `section: dark`, large `stat` components, gradient hero |
131
+
132
+ Vibes affect `--ui-radius`, `--ui-font-display`, and `--ui-letter-spacing-display`. All component shapes, borders, and heading appearance change automatically.
133
+
134
+ **Custom override:** Set vibe first, then fine-tune with CSS variable overrides in `public/theme.css`:
135
+ ```css
136
+ /* Fine-tune the warm vibe */
137
+ [data-vibe="warm"] { --ui-radius: 18px; }
138
+ ```
65
139
 
66
140
  **Spacing tokens** (`--ui-space-N`): `--ui-space-1` (4px), `--ui-space-2` (8px), `--ui-space-3` (12px), `--ui-space-4` (16px), `--ui-space-5` (20px), `--ui-space-6` (24px), `--ui-space-8` (32px), `--ui-space-10` (40px), `--ui-space-12` (48px), `--ui-space-16` (64px), `--ui-space-20` (80px), `--ui-space-24` (96px). Use these for `padding`, `margin`, and `gap`.
67
141
 
@@ -119,6 +193,49 @@ Then in app.css:
119
193
 
120
194
  If you cannot modify `createServer` (e.g. the project uses auto-discovery), use a self-hosted font in `public/fonts/` with `@font-face` in `app.css` — no CSP changes needed.
121
195
 
196
+ ### External images (img-src)
197
+
198
+ The default CSP does **not** restrict `img-src` — it inherits `default-src 'self'`, which means images served from the same origin load fine. But if you load images from an external host (e.g. `https://images.unsplash.com`, `https://picsum.photos`, a CDN), you must add the host to `img-src`.
199
+
200
+ **Do this at build time, before writing any image tags.** If you know you'll use external images, add the CSP entry to `pulse.config.js` first — discovering the block at Lighthouse time costs an extra round-trip.
201
+
202
+ ```js
203
+ // pulse.config.js
204
+ export default {
205
+ csp: {
206
+ 'img-src': ['https://picsum.photos', 'https://fastly.picsum.photos'],
207
+ },
208
+ }
209
+ ```
210
+
211
+ Or via `createServer`:
212
+
213
+ ```js
214
+ createServer(specs, {
215
+ csp: {
216
+ 'img-src': ['https://images.unsplash.com', 'https://picsum.photos'],
217
+ },
218
+ })
219
+ ```
220
+
221
+ **Common hosts to add:**
222
+ | Source | `img-src` entry |
223
+ |---|---|
224
+ | picsum.photos | `https://picsum.photos https://fastly.picsum.photos` |
225
+ | Unsplash | `https://images.unsplash.com` |
226
+ | Cloudinary | `https://res.cloudinary.com` |
227
+ | Imgix | your subdomain, e.g. `https://mysite.imgix.net` |
228
+
229
+ > **picsum CDN note:** `picsum.photos` redirects image requests through `fastly.picsum.photos`. You must whitelist **both** domains — whitelisting only `https://picsum.photos` will still block the actual image bytes and the browser error will point to the `fastly.picsum.photos` URL, which is confusing to debug.
230
+
231
+ > **⚠ Unsplash and third-party cookies — Lighthouse Best Practices failure:** `images.unsplash.com` sets tracking cookies (`azk`, `azk-ss`) on every image request. These are classified as third-party cookies and cause Lighthouse Best Practices to drop to ~77, failing the 100 pass bar. **For production builds, download images locally instead of referencing Unsplash URLs directly.** Use `curl` to save images to `public/images/`:
232
+ > ```bash
233
+ > curl -sL "https://images.unsplash.com/photo-ID?w=900&q=85" -o public/images/hero.jpg
234
+ > ```
235
+ > Then reference `/images/hero.jpg` in your spec. This also improves performance (no external DNS lookup) and avoids rate limiting. `picsum.photos` does **not** set tracking cookies and is safe to use via CDN for prototypes.
236
+
237
+ Without this, external images will be blocked in production and Lighthouse will flag a Best Practices failure. Use the same pattern for other external resource types (`media-src` for video, `connect-src` for fetch/XHR to external APIs).
238
+
122
239
  For multiple weights or italic variants, separate them with a semicolon in the URL:
123
240
  ```
124
241
  ?family=Inter:ital,wght@0,400;0,700;1,400&display=swap
@@ -171,6 +288,33 @@ For multi-brand sites, keep `@font-face` declarations (or the font service URL)
171
288
 
172
289
  ## CSS rules — where to put styles and when to use utilities
173
290
 
291
+ ### The hex colour rule — know this upfront
292
+
293
+ There are three places you write colour values. The rules are different for each:
294
+
295
+ | Where | Hex/raw values | `var(--ui-*)` tokens | Inline `style=""` |
296
+ |---|---|---|---|
297
+ | `public/theme.css` or `public/themes/*.css` | **✓ allowed** — this is where palette values live | ✓ allowed | — |
298
+ | `app.css` or any `.css` file in `public/` | **✗ blocked by lint** | **✓ required** | — |
299
+ | JS spec files (the `view` function) | **✓ allowed** (e.g. `color: '#fff'` on component props) | ✓ allowed | **✗ avoid** |
300
+
301
+ The rule in plain English: **CSS files reference tokens; theme files define them.** JS spec files are exempt because component props like `hero({ background: '#1a1a2e' })` are intentional design overrides, not CSS authoring.
302
+
303
+ **Common mistake:** Writing `--ui-accent: #e25;` directly in `app.css`. This trips the lint. Move it to `public/theme.css` (dark theme in `:root`) or `public/themes/brand.css` (light theme in `[data-theme="light"]`).
304
+
305
+ ```css
306
+ /* public/theme.css — hex values live here ✓ */
307
+ [data-theme="light"] { --ui-accent: #e25; }
308
+
309
+ /* app.css — var() only ✓ */
310
+ .hero { color: var(--ui-accent); }
311
+
312
+ /* app.css — this will be blocked ✗ */
313
+ .hero { color: #e25; }
314
+ ```
315
+
316
+ Load order in `meta.styles` matters: `pulse-ui.css` → `theme.css` → `app.css`. Theme tokens must exist before `app.css` references them.
317
+
174
318
  RULE: Never use inline style attributes (style="...") in HTML. Always use classes.
175
319
 
176
320
  RULE: For spacing, typography, layout, and colour, always prefer pulse-ui utility classes first. Only add to app.css if you need something the utilities cannot provide (e.g. a unique component style, a keyframe animation, a custom grid).
@@ -218,3 +362,60 @@ Example — a centred hero block using only utilities, no custom CSS:
218
362
  ```
219
363
 
220
364
  When you DO need to write CSS, add it to public/app.css — never inline.
365
+
366
+
367
+ ## Placeholder images for prototypes
368
+
369
+ **Use `picsum.photos` with numeric IDs for anything Lighthouse-audited.** Unsplash direct IDs rotate and 404 without warning. Picsum seeds are convenient for development but can return `ERR_CONNECTION_CLOSED` under Lighthouse's burst-load pattern — use numeric IDs for pages you'll audit:
370
+
371
+ ```html
372
+ <!-- Best for Lighthouse: numeric IDs — stable under burst load -->
373
+ <img src="https://picsum.photos/id/10/1200/600" alt="..." width="1200" height="600">
374
+ <img src="https://picsum.photos/id/64/80/80" alt="..." width="80" height="80">
375
+
376
+ <!-- OK for dev / visual work only: seeds can fail under Lighthouse burst -->
377
+ <img src="https://picsum.photos/seed/hero/1200/600" alt="..." width="1200" height="600">
378
+ ```
379
+
380
+ The numeric ID is sequential (1–1000+). Browse options at `https://picsum.photos/images`. Use numeric IDs whenever you're running Lighthouse — seeds are fine for screenshots and dev iteration.
381
+
382
+ **Avoid:** `https://images.unsplash.com/photo-LONGID?...` — these are unstable for prototypes. If you use Unsplash, add `https://images.unsplash.com` to `csp.img-src` in `pulse.config.js`, and expect some IDs to rot.
383
+
384
+ > **Unsplash photo IDs must be the full hash.** The format is `photo-` followed by an 11-character alphanumeric hash, e.g. `photo-1506905925346-21bda4d32df4`. A truncated or partial ID (e.g. `photo-1506905925346`) returns a 404 silently — the image is missing with no obvious error. Always copy the full ID from the Unsplash URL.
385
+
386
+
387
+ ## CSS gotchas
388
+
389
+ ### calc() with + and − requires whitespace
390
+
391
+ The CSS specification **requires** whitespace around `+` and `−` inside `calc()` when either operand is a value that starts with a sign (including CSS custom properties, since their resolved value may start with `−`):
392
+
393
+ ```css
394
+ /* ✓ CORRECT — spaces around + */
395
+ padding-top: calc(64px + var(--ui-space-16));
396
+
397
+ /* ✗ WRONG — will silently fail in browsers, padding becomes 0 */
398
+ padding-top: calc(64px+var(--ui-space-16));
399
+ ```
400
+
401
+ The Pulse production CSS minifier preserves these spaces. Never manually collapse `calc()` expressions.
402
+
403
+ ### Padding shorthand vs. longhand ordering
404
+
405
+ CSS processes declarations top-to-bottom. A shorthand that appears **after** a longhand will silently override it:
406
+
407
+ ```css
408
+ /* ✗ WRONG — padding-top: 0 because shorthand resets it */
409
+ .block {
410
+ padding-top: var(--ui-space-8); /* set */
411
+ padding: 0 var(--ui-space-6); /* shorthand resets padding-top to 0 */
412
+ }
413
+
414
+ /* ✓ CORRECT — shorthand first, longhand overrides */
415
+ .block {
416
+ padding: 0 var(--ui-space-6);
417
+ padding-top: var(--ui-space-8);
418
+ }
419
+ ```
420
+
421
+ This produces no browser error and is invisible at a glance — the expected spacing just doesn't appear. Always write shorthand before longhand in the same declaration block.