@invisibleloop/pulse 0.12.3 → 0.14.1

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 (166) hide show
  1. package/.claude/commands/verify.md +9 -5
  2. package/.claude/pulse-checklist.md +21 -1
  3. package/.github/workflows/publish.yml +1 -0
  4. package/.pulse-verified +1 -0
  5. package/CLAUDE.md +56 -15
  6. package/docs/.claude/commands/verify.md +4 -0
  7. package/docs/.claude/pulse-checklist.md +20 -2
  8. package/docs/public/.pulse-ui-version +1 -1
  9. package/docs/public/andypixel.css +237 -0
  10. package/docs/public/bookworms.css +422 -0
  11. package/docs/public/coutumes.css +584 -0
  12. package/docs/public/images/bookworms/hero-books.jpg +0 -0
  13. package/docs/public/images/bookworms/how-we-read-now.jpg +0 -0
  14. package/docs/public/images/bookworms/lantern-keepers.jpg +0 -0
  15. package/docs/public/images/bookworms/provincial-bookseller.jpg +0 -0
  16. package/docs/public/images/journey/178.jpg +0 -0
  17. package/docs/public/images/journey/29.jpg +0 -0
  18. package/docs/public/images/retrowheels/hero.jpg +0 -0
  19. package/docs/public/images/shop/devbook-pro.jpg +0 -0
  20. package/docs/public/images/suzukibento/1015.jpg +0 -0
  21. package/docs/public/images/suzukibento/1025.jpg +0 -0
  22. package/docs/public/images/suzukibento/1043.jpg +0 -0
  23. package/docs/public/images/suzukibento/1059.jpg +0 -0
  24. package/docs/public/images/suzukibento/1062.jpg +0 -0
  25. package/docs/public/images/suzukibento/1084.jpg +0 -0
  26. package/docs/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
  27. package/docs/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
  28. package/docs/public/images/swish/1485518882345-15568b007407.jpg +0 -0
  29. package/docs/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
  30. package/docs/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
  31. package/docs/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
  32. package/docs/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
  33. package/docs/public/images/swish/1551803091-e20673f15770.jpg +0 -0
  34. package/docs/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
  35. package/docs/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
  36. package/docs/public/journey.css +528 -0
  37. package/docs/public/pulse-ui.css +1036 -22
  38. package/docs/public/retrowheels.css +402 -0
  39. package/docs/public/shop.css +235 -0
  40. package/docs/public/sparkys.css +349 -0
  41. package/docs/public/suzukibento.css +517 -0
  42. package/docs/public/swish.css +394 -0
  43. package/docs/server.js +19 -4
  44. package/docs/src/lib/nav.js +14 -3
  45. package/docs/src/pages/components/decorate.js +74 -0
  46. package/docs/src/pages/components/gallery.js +102 -0
  47. package/docs/src/pages/components/hero.js +1 -0
  48. package/docs/src/pages/components/marquee.js +76 -0
  49. package/docs/src/pages/components/photo-card.js +74 -0
  50. package/docs/src/pages/components/section.js +40 -1
  51. package/docs/src/pages/meta.js +26 -0
  52. package/docs/src/pages/templates/andypixel.js +260 -0
  53. package/docs/src/pages/templates/bookworms.js +227 -0
  54. package/docs/src/pages/templates/coutumes.js +226 -0
  55. package/docs/src/pages/templates/journey.js +186 -0
  56. package/docs/src/pages/templates/retrowheels.js +342 -0
  57. package/docs/src/pages/templates/shop.js +257 -0
  58. package/docs/src/pages/templates/sparkys.js +249 -0
  59. package/docs/src/pages/templates/suzukibento.js +200 -0
  60. package/docs/src/pages/templates/swish.js +313 -0
  61. package/docs/src/pages/templates.js +313 -2
  62. package/docs/src/pages/testing.js +2 -1
  63. package/examples/andypixel.css +237 -0
  64. package/examples/andypixel.js +258 -0
  65. package/examples/bookworms.css +422 -0
  66. package/examples/bookworms.js +225 -0
  67. package/examples/contact.js +1 -0
  68. package/examples/counter.js +1 -0
  69. package/examples/dev.server.js +17 -0
  70. package/examples/journey.css +528 -0
  71. package/examples/journey.js +186 -0
  72. package/examples/pricing.js +1 -0
  73. package/examples/quiz.js +1 -0
  74. package/examples/retrowheels.css +402 -0
  75. package/examples/retrowheels.js +340 -0
  76. package/examples/shared.js +6 -0
  77. package/examples/suzukibento.css +513 -0
  78. package/examples/suzukibento.js +200 -0
  79. package/examples/swish.css +394 -0
  80. package/examples/swish.js +311 -0
  81. package/examples/todos.js +1 -0
  82. package/package.json +2 -3
  83. package/public/.pulse-ui-version +1 -1
  84. package/public/images/bookworms/hero-books.jpg +0 -0
  85. package/public/images/bookworms/how-we-read-now.jpg +0 -0
  86. package/public/images/bookworms/lantern-keepers.jpg +0 -0
  87. package/public/images/bookworms/provincial-bookseller.jpg +0 -0
  88. package/public/images/journey/178.jpg +0 -0
  89. package/public/images/journey/29.jpg +0 -0
  90. package/public/images/retrowheels/hero.jpg +0 -0
  91. package/public/images/shop/devbook-pro.jpg +0 -0
  92. package/public/images/suzukibento/1015.jpg +0 -0
  93. package/public/images/suzukibento/1025.jpg +0 -0
  94. package/public/images/suzukibento/1043.jpg +0 -0
  95. package/public/images/suzukibento/1059.jpg +0 -0
  96. package/public/images/suzukibento/1062.jpg +0 -0
  97. package/public/images/suzukibento/1084.jpg +0 -0
  98. package/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
  99. package/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
  100. package/public/images/swish/1485518882345-15568b007407.jpg +0 -0
  101. package/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
  102. package/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
  103. package/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
  104. package/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
  105. package/public/images/swish/1551803091-e20673f15770.jpg +0 -0
  106. package/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
  107. package/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
  108. package/public/pulse-ui.css +1036 -22
  109. package/scripts/build.js +72 -18
  110. package/src/agent/checklist.md +20 -2
  111. package/src/agent/commands/verify.md +4 -0
  112. package/src/agent/guide-components.md +145 -7
  113. package/src/agent/guide-design-gallery.md +288 -0
  114. package/src/agent/guide-design-references.md +172 -0
  115. package/src/agent/guide-explore.md +313 -0
  116. package/src/agent/guide-styles.md +197 -11
  117. package/src/agent/guide-templates.md +220 -10
  118. package/src/agent/guide.md +9 -4
  119. package/src/agent/workflow.md +208 -58
  120. package/src/cli/dev.js +39 -7
  121. package/src/cli/fmt.js +113 -0
  122. package/src/cli/index.js +63 -30
  123. package/src/cli/scaffold.js +2 -2
  124. package/src/html.js +6 -1
  125. package/src/mcp/server.js +2415 -101
  126. package/src/mcp/validate-worker.js +34 -5
  127. package/src/runtime/ssr.js +4 -1
  128. package/src/server/index.js +22 -2
  129. package/src/spec/schema.js +5 -0
  130. package/src/testing/html.js +130 -4
  131. package/src/testing/testing.test.js +58 -0
  132. package/src/ui/banner.js +24 -9
  133. package/src/ui/brand-statement.js +58 -0
  134. package/src/ui/button.js +12 -1
  135. package/src/ui/card.js +20 -1
  136. package/src/ui/colophon.js +29 -0
  137. package/src/ui/contact-row.js +41 -0
  138. package/src/ui/cta.js +18 -4
  139. package/src/ui/decorate.js +55 -0
  140. package/src/ui/display-heading.js +44 -0
  141. package/src/ui/drop-cap.js +29 -0
  142. package/src/ui/editorial-hero.js +63 -0
  143. package/src/ui/entry-list.js +36 -0
  144. package/src/ui/feature.js +20 -3
  145. package/src/ui/footer.js +78 -9
  146. package/src/ui/gallery.js +56 -0
  147. package/src/ui/hero.js +68 -18
  148. package/src/ui/image-pair.js +72 -0
  149. package/src/ui/index.js +20 -0
  150. package/src/ui/input.js +1 -0
  151. package/src/ui/marquee.js +48 -0
  152. package/src/ui/nav.js +19 -9
  153. package/src/ui/photo-card.js +44 -0
  154. package/src/ui/product-card.js +65 -0
  155. package/src/ui/pullquote.js +14 -3
  156. package/src/ui/search.js +1 -0
  157. package/src/ui/section-label.js +38 -0
  158. package/src/ui/section.js +1 -1
  159. package/src/ui/service-card.js +47 -0
  160. package/src/ui/stat-grid.js +40 -0
  161. package/src/ui/stat.js +16 -2
  162. package/src/ui/testimonial.js +17 -6
  163. package/src/ui/textarea.js +1 -0
  164. package/src/ui/top-strip.js +47 -0
  165. package/src/ui/trust-list.js +46 -0
  166. package/src/ui/ui.test.js +35 -0
@@ -0,0 +1,513 @@
1
+ /*
2
+ * suzukibento.css — Susuki Bento artist portfolio example
3
+ *
4
+ * Brutalist gallery palette: near-black ground, bone-white text, vermilion accent.
5
+ * Dark by default — palette set as bare token overrides on :root so they
6
+ * cascade into --ui-bg, --ui-text, --ui-accent etc. via pulse-ui.css.
7
+ * Zero border-radius throughout (--radius: 0px).
8
+ * Typography: Inter → system-ui (no external font requests).
9
+ *
10
+ * Token responsibility:
11
+ * :root — dark palette + zero-radius + font stack
12
+ * Component styles — layout only, var() references throughout
13
+ */
14
+
15
+ /* ==========================================================================
16
+ 1. Token overrides (hex values live here)
17
+ ========================================================================== */
18
+
19
+ /*
20
+ * Override the bare cascade tokens that --ui-* variables reference.
21
+ * See pulse-ui.css :root: --ui-bg: var(--bg, ...) etc.
22
+ * Setting --bg, --surface, --text, --accent etc. on :root is enough to
23
+ * retheme the entire UI without touching --ui-* directly.
24
+ */
25
+ :root {
26
+ --bg: #0a0a0a;
27
+ --surface: #131313;
28
+ --surface-2: #1c1c1c;
29
+ --border: #2a2a2a;
30
+ --text: #f5f3ef;
31
+ --muted: #8a8780;
32
+ --accent: #ff4a2a; /* vermilion — 4.5:1 on #0a0a0a */
33
+ --accent-hover: #ff6b4f;
34
+ --accent-dim: rgba(255, 74, 42, 0.14);
35
+ --accent-text: #0a0a0a;
36
+
37
+ /* Zero-radius brutalist design language */
38
+ --radius: 0px;
39
+ --radius-sm: 0px;
40
+
41
+ /* Font stack — system Inter, no HTTP request */
42
+ --font: 'Inter', system-ui, sans-serif;
43
+ --font-display: 'Inter', system-ui, sans-serif;
44
+ }
45
+
46
+ /* ==========================================================================
47
+ 2. Component styles (var() only — no hex values below this line)
48
+ ========================================================================== */
49
+
50
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
51
+
52
+ body {
53
+ font-family: var(--ui-font, system-ui, sans-serif);
54
+ line-height: 1.5;
55
+ -webkit-font-smoothing: antialiased;
56
+ }
57
+
58
+ /* --------------------------------------------------------------------------
59
+ Skip link
60
+ -------------------------------------------------------------------------- */
61
+ .skip-link {
62
+ position: absolute;
63
+ left: -9999px;
64
+ top: 0;
65
+ background: var(--ui-accent);
66
+ color: var(--ui-accent-text);
67
+ padding: var(--ui-space-2) var(--ui-space-4);
68
+ z-index: 100;
69
+ }
70
+
71
+ .skip-link:focus { left: var(--ui-space-2); }
72
+
73
+ /* --------------------------------------------------------------------------
74
+ Sticky top nav (hand-rolled — no nav() component)
75
+ -------------------------------------------------------------------------- */
76
+ .sb-nav {
77
+ position: sticky;
78
+ top: 0;
79
+ z-index: 50;
80
+ display: flex;
81
+ justify-content: space-between;
82
+ align-items: center;
83
+ padding: var(--ui-space-5) var(--ui-space-8);
84
+ background: var(--ui-bg);
85
+ border-bottom: 1px solid var(--ui-border);
86
+ font-size: var(--ui-text-sm);
87
+ text-transform: uppercase;
88
+ letter-spacing: 0.08em;
89
+ }
90
+
91
+ .sb-nav__brand {
92
+ font-weight: 700;
93
+ color: var(--ui-text);
94
+ text-decoration: none;
95
+ display: inline-flex;
96
+ align-items: center;
97
+ gap: 0.3em;
98
+ }
99
+
100
+ .sb-nav__dot {
101
+ display: inline-block;
102
+ width: 0.45em;
103
+ height: 0.45em;
104
+ border-radius: 50%;
105
+ background: var(--ui-accent);
106
+ flex-shrink: 0;
107
+ }
108
+
109
+ .sb-nav__links {
110
+ display: flex;
111
+ gap: var(--ui-space-6);
112
+ list-style: none;
113
+ }
114
+
115
+ .sb-nav__links a {
116
+ color: var(--ui-muted);
117
+ text-decoration: none;
118
+ }
119
+
120
+ .sb-nav__links a:hover,
121
+ .sb-nav__links a:focus { color: var(--ui-accent); }
122
+
123
+ /* Burger — hidden on desktop */
124
+ .sb-nav__toggle-input { display: none; }
125
+ .sb-nav__burger { display: none; }
126
+
127
+ @media (max-width: 600px) {
128
+ .sb-nav {
129
+ flex-wrap: wrap;
130
+ padding: var(--ui-space-4) var(--ui-space-5);
131
+ }
132
+
133
+ .sb-nav__burger {
134
+ display: flex;
135
+ flex-direction: column;
136
+ justify-content: space-between;
137
+ width: 22px;
138
+ height: 16px;
139
+ cursor: pointer;
140
+ }
141
+
142
+ .sb-nav__burger span {
143
+ display: block;
144
+ height: 2px;
145
+ background: var(--ui-text);
146
+ transition: transform 0.2s, opacity 0.2s;
147
+ }
148
+
149
+ .sb-nav__toggle-input:checked ~ .sb-nav__burger span:nth-child(1) {
150
+ transform: translateY(7px) rotate(45deg);
151
+ }
152
+ .sb-nav__toggle-input:checked ~ .sb-nav__burger span:nth-child(2) {
153
+ opacity: 0;
154
+ }
155
+ .sb-nav__toggle-input:checked ~ .sb-nav__burger span:nth-child(3) {
156
+ transform: translateY(-7px) rotate(-45deg);
157
+ }
158
+
159
+ .sb-nav nav {
160
+ display: none;
161
+ position: absolute;
162
+ top: 100%;
163
+ left: 0;
164
+ right: 0;
165
+ background: var(--ui-bg);
166
+ border-bottom: 1px solid var(--ui-border);
167
+ z-index: 49;
168
+ }
169
+
170
+ .sb-nav__toggle-input:checked ~ nav {
171
+ display: block;
172
+ }
173
+
174
+ .sb-nav__links {
175
+ flex-direction: column;
176
+ gap: 0;
177
+ padding: var(--ui-space-4) var(--ui-space-5);
178
+ }
179
+
180
+ .sb-nav__links li { border-bottom: 1px solid var(--ui-border); }
181
+
182
+ .sb-nav__links a {
183
+ display: block;
184
+ padding: var(--ui-space-3) 0;
185
+ color: var(--ui-text);
186
+ }
187
+ }
188
+
189
+ /* --------------------------------------------------------------------------
190
+ Hero
191
+ -------------------------------------------------------------------------- */
192
+ .sb-hero {
193
+ padding: var(--ui-space-24) var(--ui-space-8) var(--ui-space-16);
194
+ border-bottom: 1px solid var(--ui-border);
195
+ }
196
+
197
+ .sb-hero__eyebrow {
198
+ display: inline-block;
199
+ font-size: var(--ui-text-sm);
200
+ text-transform: uppercase;
201
+ letter-spacing: 0.18em;
202
+ color: var(--ui-accent);
203
+ margin-bottom: var(--ui-space-6);
204
+ }
205
+
206
+ .sb-hero__title {
207
+ font-size: clamp(3.5rem, 12vw, 11rem);
208
+ font-weight: 800;
209
+ line-height: 0.9;
210
+ letter-spacing: -0.05em;
211
+ color: var(--ui-text);
212
+ }
213
+
214
+ .sb-hero__title em {
215
+ font-style: normal;
216
+ color: var(--ui-accent);
217
+ }
218
+
219
+ .sb-hero__meta {
220
+ margin-top: var(--ui-space-10);
221
+ display: flex;
222
+ flex-wrap: wrap;
223
+ gap: var(--ui-space-8);
224
+ }
225
+
226
+ .sb-hero__meta-label {
227
+ display: block;
228
+ font-size: var(--ui-text-sm);
229
+ text-transform: uppercase;
230
+ letter-spacing: 0.12em;
231
+ color: var(--ui-muted);
232
+ }
233
+
234
+ .sb-hero__meta strong {
235
+ display: block;
236
+ color: var(--ui-text);
237
+ font-weight: 600;
238
+ margin-top: var(--ui-space-1);
239
+ letter-spacing: 0.04em;
240
+ font-size: var(--ui-text-sm);
241
+ text-transform: uppercase;
242
+ }
243
+
244
+ /* --------------------------------------------------------------------------
245
+ Section scaffolding
246
+ -------------------------------------------------------------------------- */
247
+ .sb-section {
248
+ padding: var(--ui-space-20) var(--ui-space-8);
249
+ border-bottom: 1px solid var(--ui-border);
250
+ }
251
+
252
+ .sb-section__head {
253
+ display: flex;
254
+ justify-content: space-between;
255
+ align-items: baseline;
256
+ margin-bottom: var(--ui-space-12);
257
+ gap: var(--ui-space-6);
258
+ flex-wrap: wrap;
259
+ }
260
+
261
+ .sb-section__label {
262
+ font-size: var(--ui-text-sm);
263
+ text-transform: uppercase;
264
+ letter-spacing: 0.18em;
265
+ color: var(--ui-accent);
266
+ }
267
+
268
+ .sb-section__title {
269
+ font-size: clamp(1.75rem, 4vw, 2.5rem);
270
+ font-weight: 700;
271
+ letter-spacing: -0.02em;
272
+ color: var(--ui-text);
273
+ }
274
+
275
+ /* --------------------------------------------------------------------------
276
+ Works grid (12-col asymmetric layout)
277
+ -------------------------------------------------------------------------- */
278
+ .sb-works {
279
+ display: grid;
280
+ grid-template-columns: repeat(12, 1fr);
281
+ grid-auto-rows: 120px;
282
+ gap: var(--ui-space-4);
283
+ }
284
+
285
+ .sb-work {
286
+ position: relative;
287
+ overflow: hidden;
288
+ background: var(--ui-surface);
289
+ }
290
+
291
+ .sb-work img {
292
+ width: 100%;
293
+ height: 100%;
294
+ object-fit: cover;
295
+ display: block;
296
+ filter: grayscale(15%) contrast(1.05);
297
+ transition: filter 0.4s ease, transform 0.6s ease;
298
+ }
299
+
300
+ .sb-work:hover img {
301
+ filter: none;
302
+ transform: scale(1.02);
303
+ }
304
+
305
+ .sb-work__caption {
306
+ position: absolute;
307
+ left: var(--ui-space-3);
308
+ bottom: var(--ui-space-3);
309
+ background: var(--ui-bg);
310
+ color: var(--ui-text);
311
+ padding: var(--ui-space-1) var(--ui-space-3);
312
+ font-size: var(--ui-text-xs);
313
+ text-transform: uppercase;
314
+ letter-spacing: 0.1em;
315
+ }
316
+
317
+ .sb-work__caption span {
318
+ color: var(--ui-muted);
319
+ margin-left: var(--ui-space-2);
320
+ }
321
+
322
+ /* Explicit positions — gap-free brickwork layout */
323
+ .sb-work--a { grid-column: 1 / 8; grid-row: 1 / 5; }
324
+ .sb-work--b { grid-column: 8 / 13; grid-row: 1 / 4; }
325
+ .sb-work--c { grid-column: 8 / 13; grid-row: 4 / 8; }
326
+ .sb-work--d { grid-column: 1 / 5; grid-row: 5 / 8; }
327
+ .sb-work--e { grid-column: 5 / 8; grid-row: 5 / 8; }
328
+ .sb-work--f { grid-column: 1 / 13; grid-row: 8 / 12; }
329
+
330
+ @media (max-width: 720px) {
331
+ .sb-works { grid-auto-rows: 80px; }
332
+ .sb-work--a, .sb-work--b, .sb-work--c,
333
+ .sb-work--d, .sb-work--e, .sb-work--f {
334
+ grid-column: span 12;
335
+ grid-row: span 3;
336
+ }
337
+ }
338
+
339
+ /* --------------------------------------------------------------------------
340
+ Artist statement
341
+ -------------------------------------------------------------------------- */
342
+ .sb-statement {
343
+ max-width: 56rem;
344
+ display: grid;
345
+ grid-template-columns: 1fr 2fr;
346
+ gap: var(--ui-space-12);
347
+ }
348
+
349
+ .sb-statement p {
350
+ font-size: var(--ui-text-xl);
351
+ line-height: 1.6;
352
+ color: var(--ui-text);
353
+ margin-bottom: var(--ui-space-5);
354
+ }
355
+
356
+ .sb-statement p + p {
357
+ color: var(--ui-muted);
358
+ font-size: var(--ui-text-lg);
359
+ }
360
+
361
+ .sb-statement__bio {
362
+ font-size: var(--ui-text-sm);
363
+ color: var(--ui-muted);
364
+ line-height: 1.7;
365
+ }
366
+
367
+ .sb-statement__bio dt {
368
+ text-transform: uppercase;
369
+ letter-spacing: 0.12em;
370
+ color: var(--ui-accent);
371
+ font-size: var(--ui-text-xs);
372
+ margin-top: var(--ui-space-4);
373
+ }
374
+
375
+ .sb-statement__bio dd { color: var(--ui-text); }
376
+
377
+ @media (max-width: 720px) {
378
+ .sb-statement { grid-template-columns: 1fr; }
379
+ }
380
+
381
+ /* --------------------------------------------------------------------------
382
+ Exhibition table
383
+ -------------------------------------------------------------------------- */
384
+ .sb-exhibitions {
385
+ list-style: none;
386
+ border-top: 1px solid var(--ui-border);
387
+ }
388
+
389
+ .sb-exhibition {
390
+ display: grid;
391
+ grid-template-columns: 80px 1fr 1fr 120px;
392
+ gap: var(--ui-space-6);
393
+ padding: var(--ui-space-5) 0;
394
+ border-bottom: 1px solid var(--ui-border);
395
+ align-items: baseline;
396
+ }
397
+
398
+ .sb-exhibition__year {
399
+ font-weight: 700;
400
+ color: var(--ui-accent);
401
+ font-variant-numeric: tabular-nums;
402
+ }
403
+
404
+ .sb-exhibition__title { color: var(--ui-text); font-weight: 500; }
405
+ .sb-exhibition__venue { color: var(--ui-muted); }
406
+
407
+ .sb-exhibition__tag {
408
+ font-size: var(--ui-text-xs);
409
+ text-transform: uppercase;
410
+ letter-spacing: 0.1em;
411
+ color: var(--ui-muted);
412
+ text-align: right;
413
+ }
414
+
415
+ .sb-exhibition__tag--solo { color: var(--ui-accent); }
416
+
417
+ @media (max-width: 720px) {
418
+ .sb-exhibition { grid-template-columns: 60px 1fr; }
419
+ .sb-exhibition__venue,
420
+ .sb-exhibition__tag { grid-column: 2; text-align: left; }
421
+ }
422
+
423
+ /* --------------------------------------------------------------------------
424
+ Press list with dot separator
425
+ -------------------------------------------------------------------------- */
426
+ .sb-press {
427
+ display: flex;
428
+ flex-wrap: wrap;
429
+ gap: var(--ui-space-8);
430
+ font-size: var(--ui-text-lg);
431
+ font-weight: 500;
432
+ color: var(--ui-muted);
433
+ }
434
+
435
+ .sb-press span { white-space: nowrap; }
436
+
437
+ .sb-press span + span::before {
438
+ content: "\00b7";
439
+ margin-right: var(--ui-space-8);
440
+ color: var(--ui-accent);
441
+ }
442
+
443
+ @media (max-width: 600px) {
444
+ .sb-press {
445
+ flex-direction: column;
446
+ gap: var(--ui-space-3);
447
+ font-size: var(--ui-text-base);
448
+ }
449
+
450
+ .sb-press span + span::before { display: none; }
451
+ }
452
+
453
+ /* --------------------------------------------------------------------------
454
+ Contact
455
+ -------------------------------------------------------------------------- */
456
+ .sb-contact {
457
+ display: grid;
458
+ grid-template-columns: 1fr 1fr;
459
+ gap: var(--ui-space-12);
460
+ align-items: start;
461
+ }
462
+
463
+ .sb-contact h3 {
464
+ font-size: var(--ui-text-sm);
465
+ text-transform: uppercase;
466
+ letter-spacing: 0.18em;
467
+ color: var(--ui-accent);
468
+ margin-bottom: var(--ui-space-4);
469
+ }
470
+
471
+ .sb-contact p,
472
+ .sb-contact a {
473
+ font-size: var(--ui-text-lg);
474
+ color: var(--ui-text);
475
+ line-height: 1.6;
476
+ text-decoration: none;
477
+ display: block;
478
+ }
479
+
480
+ .sb-contact a:hover { color: var(--ui-accent); }
481
+
482
+ @media (max-width: 720px) {
483
+ .sb-contact { grid-template-columns: 1fr; gap: var(--ui-space-8); }
484
+ }
485
+
486
+ /* --------------------------------------------------------------------------
487
+ Footer
488
+ -------------------------------------------------------------------------- */
489
+ .sb-foot {
490
+ display: flex;
491
+ justify-content: space-between;
492
+ padding: var(--ui-space-6) var(--ui-space-8);
493
+ font-size: var(--ui-text-xs);
494
+ text-transform: uppercase;
495
+ letter-spacing: 0.12em;
496
+ color: var(--ui-muted);
497
+ border-top: 1px solid var(--ui-border);
498
+ }
499
+
500
+ @media (max-width: 600px) {
501
+ .sb-foot {
502
+ flex-direction: column;
503
+ gap: var(--ui-space-2);
504
+ padding: var(--ui-space-5);
505
+ }
506
+ }
507
+
508
+ .sb-foot__link {
509
+ color: var(--ui-muted);
510
+ text-decoration: none;
511
+ }
512
+
513
+ .sb-foot__link:hover { color: var(--ui-text); }
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Pulse — Susuki Bento artist portfolio example
3
+ *
4
+ * Demonstrates:
5
+ * - Dark-by-default brutalist gallery palette (near-black, bone, vermilion)
6
+ * - Zero-radius design language (--radius: 0px overrides Pulse defaults)
7
+ * - Asymmetric CSS grid works gallery (12-col with mixed span sizes)
8
+ * - Pure vanilla HTML throughout — no Pulse UI components used.
9
+ * Shows that Pulse views are just template literals; you can write
10
+ * raw HTML for fully custom layouts without any component overhead.
11
+ * - Custom sticky nav and simple footer hand-rolled in the view
12
+ * - HTML-escaped data with a utility function (safe pattern for user data)
13
+ * - Hover effect with CSS filter: grayscale + scale on artwork images
14
+ * - Exhibition table using CSS grid (year / title / venue / tag columns)
15
+ * - Press list with CSS ::before dot separator
16
+ * - Two-column contact grid
17
+ * - System font stack only (Inter → system-ui fallback) — no HTTP requests
18
+ * - picsum.photos for artwork images (placeholder seeded by id)
19
+ *
20
+ * Run: node examples/dev.server.js → http://localhost:3001/suzukibento
21
+ */
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Static data
25
+ // ---------------------------------------------------------------------------
26
+
27
+ const works = [
28
+ { id: 1043, cls: 'sb-work--a', title: 'Erosion (Shibuya)', year: '2025', medium: 'Oil, ash, dust on canvas — 220×180cm' },
29
+ { id: 1025, cls: 'sb-work--b', title: 'Fragments No. 14', year: '2024', medium: 'Mixed media — 140×140cm' },
30
+ { id: 1015, cls: 'sb-work--c', title: 'Displaced', year: '2024', medium: 'Acrylic, collage — 160×120cm' },
31
+ { id: 1059, cls: 'sb-work--d', title: 'Residue', year: '2023', medium: 'Oil on linen — 100×100cm' },
32
+ { id: 1062, cls: 'sb-work--e', title: 'Mother Tongue', year: '2023', medium: 'Oil, gold leaf — 180×140cm' },
33
+ { id: 1084, cls: 'sb-work--f', title: 'Untitled (Berlin VI)', year: '2025', medium: 'Concrete, pigment — 300×200cm' },
34
+ ]
35
+
36
+ const exhibitions = [
37
+ { year: '2026', title: 'Soft Architectures', venue: 'Kunsthalle Basel, CH', tag: 'Solo', solo: true },
38
+ { year: '2025', title: 'The Weight of Surface', venue: 'KW Institute, Berlin', tag: 'Solo', solo: true },
39
+ { year: '2025', title: 'Asia Now', venue: 'Monnaie de Paris', tag: 'Group', solo: false },
40
+ { year: '2024', title: 'Memory as Material', venue: 'Hauser & Wirth, Hong Kong', tag: 'Solo', solo: true },
41
+ { year: '2024', title: 'Berlin Biennale 13', venue: 'Hamburger Bahnhof', tag: 'Group', solo: false },
42
+ { year: '2023', title: 'Quiet Geographies', venue: 'Mori Art Museum, Tokyo', tag: 'Solo', solo: true },
43
+ { year: '2022', title: 'New Painting / New Asia', venue: 'Tate St Ives', tag: 'Group', solo: false },
44
+ ]
45
+
46
+ const press = [
47
+ 'Frieze', 'Artforum', 'ArtReview', 'S\u00fcddeutsche Zeitung',
48
+ 'The Japan Times', 'Mousse', 'e-flux',
49
+ ]
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Helpers
53
+ // ---------------------------------------------------------------------------
54
+
55
+ const esc = (s) => String(s).replace(/[&<>"']/g, (c) => (
56
+ { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]
57
+ ))
58
+
59
+ const work = (w) => `
60
+ <figure class="sb-work ${w.cls}">
61
+ <img src="/images/suzukibento/${w.id}.jpg" alt="${esc(w.title)}, ${esc(w.year)}" loading="lazy" width="1200" height="900">
62
+ <figcaption class="sb-work__caption">${esc(w.title)} <span>${esc(w.year)}</span></figcaption>
63
+ </figure>
64
+ `
65
+
66
+ const exhibition = (e) => `
67
+ <li class="sb-exhibition">
68
+ <span class="sb-exhibition__year">${esc(e.year)}</span>
69
+ <span class="sb-exhibition__title">${esc(e.title)}</span>
70
+ <span class="sb-exhibition__venue">${esc(e.venue)}</span>
71
+ <span class="sb-exhibition__tag${e.solo ? ' sb-exhibition__tag--solo' : ''}">${esc(e.tag)}</span>
72
+ </li>
73
+ `
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Spec
77
+ // ---------------------------------------------------------------------------
78
+
79
+ export default {
80
+ route: '/suzukibento',
81
+
82
+ meta: {
83
+ title: 'Susuki Bento \u2014 Painter',
84
+ description: 'Tokyo-born, Berlin-based mixed-media artist exploring memory, displacement, and urban erosion through large-scale painting.',
85
+ theme: 'dark',
86
+ styles: ['/pulse-ui.css', '/examples/suzukibento.css'],
87
+ },
88
+
89
+ view: () => `
90
+ <a href="#main-content" class="skip-link">Skip to main content</a>
91
+
92
+ <header class="sb-nav">
93
+ <a href="/suzukibento" class="sb-nav__brand" aria-label="Susuki Bento — home">Susuki Bento<span class="sb-nav__dot" aria-hidden="true"></span></a>
94
+ <input type="checkbox" id="sb-menu-toggle" class="sb-nav__toggle-input" aria-hidden="true">
95
+ <label for="sb-menu-toggle" class="sb-nav__burger" aria-label="Toggle menu">
96
+ <span></span><span></span><span></span>
97
+ </label>
98
+ <nav aria-label="Primary">
99
+ <ul class="sb-nav__links">
100
+ <li><a href="#works">Works</a></li>
101
+ <li><a href="#about">About</a></li>
102
+ <li><a href="#exhibitions">Exhibitions</a></li>
103
+ <li><a href="#contact">Contact</a></li>
104
+ </ul>
105
+ </nav>
106
+ </header>
107
+
108
+ <main id="main-content">
109
+ <section class="sb-hero">
110
+ <span class="sb-hero__eyebrow">ペインタ \u2014 b. Tokyo, 1984 \u00b7 lives Berlin</span>
111
+ <h1 class="sb-hero__title">Susuki<br>Bento<em>.</em></h1>
112
+ <div class="sb-hero__meta">
113
+ <div>
114
+ <span class="sb-hero__meta-label">Now showing</span>
115
+ <strong>The Weight of Surface \u2014 KW Institute, Berlin</strong>
116
+ </div>
117
+ <div>
118
+ <span class="sb-hero__meta-label">Closing</span>
119
+ <strong>14 September 2026</strong>
120
+ </div>
121
+ <div>
122
+ <span class="sb-hero__meta-label">Represented by</span>
123
+ <strong>Hauser &amp; Wirth</strong>
124
+ </div>
125
+ </div>
126
+ </section>
127
+
128
+ <section id="works" class="sb-section">
129
+ <div class="sb-section__head">
130
+ <span class="sb-section__label">01 \u2014 Selected works</span>
131
+ <h2 class="sb-section__title">Recent paintings, 2023\u20132025</h2>
132
+ </div>
133
+ <div class="sb-works">
134
+ ${works.map(work).join('')}
135
+ </div>
136
+ </section>
137
+
138
+ <section id="about" class="sb-section">
139
+ <div class="sb-section__head">
140
+ <span class="sb-section__label">02 \u2014 Statement</span>
141
+ <h2 class="sb-section__title">On surface, memory, dust</h2>
142
+ </div>
143
+ <div class="sb-statement">
144
+ <dl class="sb-statement__bio">
145
+ <dt>Born</dt><dd>Tokyo, 1984</dd>
146
+ <dt>Lives</dt><dd>Berlin-Wedding</dd>
147
+ <dt>Education</dt><dd>Tokyo University of the Arts, MFA<br>St\u00e4delschule, Frankfurt</dd>
148
+ <dt>Studio</dt><dd>Soldiner Stra\u00dfe 31, 13359 Berlin</dd>
149
+ </dl>
150
+ <div>
151
+ <p>The work begins with surface. I scrape, layer, bury and uncover until the paint behaves like architecture &mdash; weight, weather, decay. Each canvas is a record of what a place could not hold onto.</p>
152
+ <p>For two decades I have moved between Tokyo and Berlin, two cities defined by the things they have erased. My paintings collect this erasure: dust from demolition sites, charcoal from cleared lots, the colour of a wall before it was repainted. They are quiet objects made from loud absences.</p>
153
+ </div>
154
+ </div>
155
+ </section>
156
+
157
+ <section id="exhibitions" class="sb-section">
158
+ <div class="sb-section__head">
159
+ <span class="sb-section__label">03 \u2014 Exhibition history</span>
160
+ <h2 class="sb-section__title">Selected solo &amp; group, 2022\u20132026</h2>
161
+ </div>
162
+ <ul class="sb-exhibitions">
163
+ ${exhibitions.map(exhibition).join('')}
164
+ </ul>
165
+ </section>
166
+
167
+ <section class="sb-section">
168
+ <div class="sb-section__head">
169
+ <span class="sb-section__label">04 \u2014 Press</span>
170
+ <h2 class="sb-section__title">In print</h2>
171
+ </div>
172
+ <p class="sb-press">${press.map((p) => `<span>${esc(p)}</span>`).join('')}</p>
173
+ </section>
174
+
175
+ <section id="contact" class="sb-section">
176
+ <div class="sb-section__head">
177
+ <span class="sb-section__label">05 \u2014 Contact</span>
178
+ <h2 class="sb-section__title">Studio &amp; representation</h2>
179
+ </div>
180
+ <div class="sb-contact">
181
+ <div>
182
+ <h3>Studio enquiries</h3>
183
+ <a href="mailto:studio@susukibento.art">studio@susukibento.art</a>
184
+ <p>Soldiner Stra\u00dfe 31<br>13359 Berlin</p>
185
+ </div>
186
+ <div>
187
+ <h3>Gallery representation</h3>
188
+ <a href="mailto:bento@hauserwirth.com">bento@hauserwirth.com</a>
189
+ <p>Hauser &amp; Wirth<br>Z\u00fcrich \u00b7 London \u00b7 Hong Kong</p>
190
+ </div>
191
+ </div>
192
+ </section>
193
+ </main>
194
+
195
+ <footer class="sb-foot">
196
+ <span>&copy; Susuki Bento Studio 2026</span>
197
+ <span>Site by <a href="https://invisibleloop.com" class="sb-foot__link" target="_blank" rel="noopener">invisibleloop</a></span>
198
+ </footer>
199
+ `,
200
+ }