open-brandkit 0.4.7

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 (61) hide show
  1. package/LICENSE +72 -0
  2. package/README.md +284 -0
  3. package/dist/adapters/next/brandkit-page.d.ts +15 -0
  4. package/dist/adapters/next/brandkit-page.d.ts.map +1 -0
  5. package/dist/adapters/next/brandkit-page.js +1085 -0
  6. package/dist/adapters/next/brandkit-page.js.map +1 -0
  7. package/dist/adapters/next/index.d.ts +3 -0
  8. package/dist/adapters/next/index.d.ts.map +1 -0
  9. package/dist/adapters/next/index.js +3 -0
  10. package/dist/adapters/next/index.js.map +1 -0
  11. package/dist/adapters/next/manifest.d.ts +33 -0
  12. package/dist/adapters/next/manifest.d.ts.map +1 -0
  13. package/dist/adapters/next/manifest.js +57 -0
  14. package/dist/adapters/next/manifest.js.map +1 -0
  15. package/dist/adapters/next/route-handlers.d.ts +102 -0
  16. package/dist/adapters/next/route-handlers.d.ts.map +1 -0
  17. package/dist/adapters/next/route-handlers.js +451 -0
  18. package/dist/adapters/next/route-handlers.js.map +1 -0
  19. package/dist/adapters/next/server.d.ts +2 -0
  20. package/dist/adapters/next/server.d.ts.map +1 -0
  21. package/dist/adapters/next/server.js +2 -0
  22. package/dist/adapters/next/server.js.map +1 -0
  23. package/dist/cli/index.d.ts +3 -0
  24. package/dist/cli/index.d.ts.map +1 -0
  25. package/dist/cli/index.js +1079 -0
  26. package/dist/cli/index.js.map +1 -0
  27. package/dist/core/assets.d.ts +34 -0
  28. package/dist/core/assets.d.ts.map +1 -0
  29. package/dist/core/assets.js +200 -0
  30. package/dist/core/assets.js.map +1 -0
  31. package/dist/core/banner-renderer.d.ts +21 -0
  32. package/dist/core/banner-renderer.d.ts.map +1 -0
  33. package/dist/core/banner-renderer.js +119 -0
  34. package/dist/core/banner-renderer.js.map +1 -0
  35. package/dist/core/build.d.ts +13 -0
  36. package/dist/core/build.d.ts.map +1 -0
  37. package/dist/core/build.js +345 -0
  38. package/dist/core/build.js.map +1 -0
  39. package/dist/core/colors.d.ts +12 -0
  40. package/dist/core/colors.d.ts.map +1 -0
  41. package/dist/core/colors.js +335 -0
  42. package/dist/core/colors.js.map +1 -0
  43. package/dist/core/config.d.ts +103 -0
  44. package/dist/core/config.d.ts.map +1 -0
  45. package/dist/core/config.js +119 -0
  46. package/dist/core/config.js.map +1 -0
  47. package/dist/core/index.d.ts +8 -0
  48. package/dist/core/index.d.ts.map +1 -0
  49. package/dist/core/index.js +8 -0
  50. package/dist/core/index.js.map +1 -0
  51. package/dist/core/static-page.d.ts +3 -0
  52. package/dist/core/static-page.d.ts.map +1 -0
  53. package/dist/core/static-page.js +1830 -0
  54. package/dist/core/static-page.js.map +1 -0
  55. package/dist/core/types.d.ts +163 -0
  56. package/dist/core/types.d.ts.map +1 -0
  57. package/dist/core/types.js +2 -0
  58. package/dist/core/types.js.map +1 -0
  59. package/docs/STYLE_CONTRACT.md +48 -0
  60. package/examples/acme-studio-color-system.md +99 -0
  61. package/package.json +89 -0
@@ -0,0 +1,1830 @@
1
+ function escapeHtml(value) {
2
+ return value
3
+ .replaceAll('&', '&')
4
+ .replaceAll('<', '&lt;')
5
+ .replaceAll('>', '&gt;')
6
+ .replaceAll('"', '&quot;')
7
+ .replaceAll("'", '&#39;');
8
+ }
9
+ function escapeJsonForHtml(value) {
10
+ return JSON.stringify(value).replaceAll('<', '\\u003c');
11
+ }
12
+ const deterministicIntro = 'Approved marks, avatar-ready presets, social profile assets, and the current color system.';
13
+ const pageStyles = `
14
+ :root {
15
+ color-scheme: light;
16
+ --ink: #020617;
17
+ --muted: #64748b;
18
+ --copy: #475569;
19
+ --line: #e2e8f0;
20
+ --neutral-line: #e5e5e5;
21
+ --neutral: #737373;
22
+ --surface: #ffffff;
23
+ --wash: #f8fafc;
24
+ --dark: #2b333f;
25
+ --dark-hover: #1d232b;
26
+ --favicon: #0d2249;
27
+ }
28
+ * { box-sizing: border-box; }
29
+ body {
30
+ margin: 0;
31
+ background: var(--wash);
32
+ color: var(--ink);
33
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
34
+ }
35
+ a { color: inherit; }
36
+ .wrap {
37
+ width: 100%;
38
+ max-width: 1280px;
39
+ margin: 0 auto;
40
+ padding-right: 16px;
41
+ padding-left: 16px;
42
+ }
43
+ .header {
44
+ border-bottom: 1px solid var(--line);
45
+ background: var(--surface);
46
+ }
47
+ .header-inner {
48
+ display: grid;
49
+ gap: 40px;
50
+ padding-top: 48px;
51
+ padding-bottom: 48px;
52
+ }
53
+ .brand-link,
54
+ .eyebrow {
55
+ margin: 0;
56
+ color: var(--neutral);
57
+ font-size: 12px;
58
+ font-weight: 700;
59
+ letter-spacing: 0.2em;
60
+ line-height: 1.3;
61
+ text-decoration: none;
62
+ text-transform: uppercase;
63
+ }
64
+ .brand-link:hover { color: #0a0a0a; }
65
+ h1 {
66
+ margin: 12px 0 0;
67
+ color: var(--ink);
68
+ font-size: 48px;
69
+ font-weight: 500;
70
+ letter-spacing: 0;
71
+ line-height: 1;
72
+ }
73
+ .copy {
74
+ max-width: 672px;
75
+ margin: 16px 0 0;
76
+ color: var(--copy);
77
+ font-size: 16px;
78
+ line-height: 1.75;
79
+ }
80
+ .nav,
81
+ .button-row,
82
+ .group-actions {
83
+ display: flex;
84
+ flex-wrap: wrap;
85
+ gap: 12px;
86
+ align-items: center;
87
+ }
88
+ .nav { margin-top: 32px; }
89
+ .nav-link {
90
+ display: inline-flex;
91
+ align-items: center;
92
+ gap: 6px;
93
+ color: #404040;
94
+ font-size: 14px;
95
+ font-weight: 500;
96
+ text-decoration: none;
97
+ text-underline-offset: 4px;
98
+ }
99
+ .nav-link:hover {
100
+ color: #0a0a0a;
101
+ text-decoration: underline;
102
+ }
103
+ .nav-arrow,
104
+ .icon {
105
+ display: inline-block;
106
+ flex: 0 0 auto;
107
+ stroke-width: 2;
108
+ }
109
+ .nav-arrow {
110
+ width: 14px;
111
+ height: 14px;
112
+ }
113
+ .icon {
114
+ width: 16px;
115
+ height: 16px;
116
+ }
117
+ .copy-icon {
118
+ width: 14px;
119
+ height: 14px;
120
+ }
121
+ .asset-count {
122
+ color: var(--muted);
123
+ font-size: 14px;
124
+ }
125
+ .hero-media {
126
+ display: flex;
127
+ align-items: center;
128
+ justify-content: flex-start;
129
+ }
130
+ .hero-asset {
131
+ display: flex;
132
+ aspect-ratio: 673 / 489;
133
+ width: 100%;
134
+ max-width: 320px;
135
+ align-items: center;
136
+ justify-content: center;
137
+ }
138
+ .hero-asset img,
139
+ .footer-logo img {
140
+ display: block;
141
+ max-width: 100%;
142
+ max-height: 100%;
143
+ object-fit: contain;
144
+ }
145
+ .section-muted {
146
+ border-bottom: 1px solid var(--line);
147
+ background: var(--wash);
148
+ }
149
+ .section {
150
+ background: var(--surface);
151
+ }
152
+ .section-banners {
153
+ border-top: 1px solid var(--line);
154
+ border-bottom: 1px solid var(--line);
155
+ background: var(--wash);
156
+ }
157
+ .section-inner {
158
+ padding-top: 56px;
159
+ padding-bottom: 56px;
160
+ }
161
+ .section-heading {
162
+ max-width: 672px;
163
+ }
164
+ .section-heading-row {
165
+ display: flex;
166
+ flex-direction: column;
167
+ gap: 16px;
168
+ }
169
+ .section-heading-actions {
170
+ flex: 0 0 auto;
171
+ }
172
+ .section-heading h2 {
173
+ margin: 12px 0 0;
174
+ color: var(--ink);
175
+ font-size: 36px;
176
+ font-weight: 500;
177
+ letter-spacing: 0;
178
+ line-height: 1.1;
179
+ }
180
+ .section-heading p:last-child {
181
+ margin: 16px 0 0;
182
+ color: var(--copy);
183
+ font-size: 16px;
184
+ line-height: 1.75;
185
+ }
186
+ .stack {
187
+ display: flex;
188
+ flex-direction: column;
189
+ gap: 48px;
190
+ margin-top: 40px;
191
+ }
192
+ .group,
193
+ .avatar,
194
+ .banner-group {
195
+ display: flex;
196
+ flex-direction: column;
197
+ gap: 20px;
198
+ }
199
+ .group-header {
200
+ display: flex;
201
+ flex-direction: column;
202
+ gap: 12px;
203
+ }
204
+ .group h3,
205
+ .avatar h3,
206
+ .color-group h3 {
207
+ margin: 0;
208
+ color: var(--ink);
209
+ font-size: 18px;
210
+ font-weight: 600;
211
+ line-height: 1.4;
212
+ }
213
+ .group p,
214
+ .avatar p,
215
+ .color-group p,
216
+ .banner-group p {
217
+ margin: 4px 0 0;
218
+ color: var(--copy);
219
+ font-size: 14px;
220
+ line-height: 1.5;
221
+ }
222
+ .asset-grid {
223
+ display: grid;
224
+ grid-template-columns: 1fr;
225
+ gap: 16px;
226
+ }
227
+ .card,
228
+ .color-card,
229
+ .banner-card {
230
+ overflow: hidden;
231
+ border: 1px solid var(--neutral-line);
232
+ border-radius: 8px;
233
+ background: var(--surface);
234
+ }
235
+ .card {
236
+ display: flex;
237
+ height: 100%;
238
+ flex-direction: column;
239
+ }
240
+ .checker {
241
+ background-color: #f7f7f7;
242
+ background-image: repeating-conic-gradient(#ececec 0% 25%, #f8f8f8 0% 50%);
243
+ background-position: 50%;
244
+ background-size: 20px 20px;
245
+ }
246
+ .asset-preview {
247
+ display: flex;
248
+ min-height: 220px;
249
+ align-items: center;
250
+ justify-content: center;
251
+ width: 100%;
252
+ border: 0;
253
+ border-bottom: 1px solid var(--neutral-line);
254
+ padding: 40px 24px;
255
+ cursor: zoom-in;
256
+ transition: opacity 160ms ease;
257
+ }
258
+ .asset-preview:hover { opacity: 0.9; }
259
+ .preview-dark {
260
+ background: var(--dark);
261
+ background-image: none;
262
+ }
263
+ .asset-preview img {
264
+ display: block;
265
+ width: auto;
266
+ max-width: 100%;
267
+ max-height: 112px;
268
+ object-fit: contain;
269
+ }
270
+ .card-body {
271
+ display: flex;
272
+ flex: 1;
273
+ flex-direction: column;
274
+ justify-content: space-between;
275
+ gap: 16px;
276
+ padding: 16px;
277
+ }
278
+ .card h4,
279
+ .color-card h4,
280
+ .banner-card h4 {
281
+ margin: 0;
282
+ color: #171717;
283
+ font-size: 14px;
284
+ font-weight: 600;
285
+ line-height: 1.4;
286
+ }
287
+ .button,
288
+ .copy-button {
289
+ display: inline-flex;
290
+ align-items: center;
291
+ justify-content: center;
292
+ gap: 4px;
293
+ border: 1px solid #d4d4d4;
294
+ border-radius: 6px;
295
+ background: var(--surface);
296
+ color: #262626;
297
+ cursor: pointer;
298
+ font: inherit;
299
+ font-size: 14px;
300
+ font-weight: 500;
301
+ line-height: 1.2;
302
+ text-decoration: none;
303
+ transition: background-color 160ms ease, border-color 160ms ease, color 160ms ease;
304
+ }
305
+ .button {
306
+ min-height: 36px;
307
+ padding: 8px 12px;
308
+ }
309
+ .button:hover {
310
+ border-color: #a3a3a3;
311
+ background: #fafafa;
312
+ }
313
+ .button-dark {
314
+ border-color: var(--dark);
315
+ background: var(--dark);
316
+ color: #ffffff;
317
+ }
318
+ .button-dark,
319
+ .button-dark * {
320
+ color: #ffffff;
321
+ }
322
+ .button-dark:hover {
323
+ border-color: var(--dark-hover);
324
+ background: var(--dark-hover);
325
+ color: #ffffff;
326
+ }
327
+ .button-favicon {
328
+ border-color: var(--favicon);
329
+ background: var(--favicon);
330
+ color: #ffffff;
331
+ }
332
+ .button-favicon:hover {
333
+ border-color: #1e293b;
334
+ background: #1e293b;
335
+ color: #ffffff;
336
+ }
337
+ .copy-button {
338
+ min-height: 30px;
339
+ border-color: var(--neutral-line);
340
+ padding: 6px 10px;
341
+ color: #404040;
342
+ font-size: 12px;
343
+ }
344
+ .avatar-grid {
345
+ display: grid;
346
+ gap: 20px;
347
+ }
348
+ .avatar-controls {
349
+ position: relative;
350
+ display: grid;
351
+ gap: 48px;
352
+ border: 1px solid var(--line);
353
+ border-radius: 6px;
354
+ background: var(--surface);
355
+ padding: 24px;
356
+ }
357
+ .reset-button {
358
+ position: absolute;
359
+ top: 12px;
360
+ left: 12px;
361
+ display: inline-flex;
362
+ width: 32px;
363
+ height: 32px;
364
+ align-items: center;
365
+ justify-content: center;
366
+ border: 0;
367
+ background: transparent;
368
+ color: var(--muted);
369
+ cursor: pointer;
370
+ }
371
+ .control-group {
372
+ display: flex;
373
+ width: max-content;
374
+ max-width: 100%;
375
+ flex-direction: column;
376
+ gap: 16px;
377
+ justify-self: center;
378
+ }
379
+ .control-title,
380
+ .color-section-title,
381
+ .banner-group h3 {
382
+ margin: 0;
383
+ color: var(--muted);
384
+ font-size: 12px;
385
+ font-weight: 700;
386
+ letter-spacing: 0.12em;
387
+ line-height: 1.3;
388
+ text-transform: uppercase;
389
+ }
390
+ .chip-row {
391
+ display: flex;
392
+ flex-wrap: wrap;
393
+ justify-content: center;
394
+ gap: 16px 12px;
395
+ }
396
+ .chip {
397
+ display: flex;
398
+ width: 64px;
399
+ flex-direction: column;
400
+ align-items: flex-start;
401
+ gap: 8px;
402
+ border: 0;
403
+ background: transparent;
404
+ cursor: pointer;
405
+ padding: 0;
406
+ text-align: center;
407
+ }
408
+ .chip-preview {
409
+ display: flex;
410
+ aspect-ratio: 1;
411
+ width: 64px;
412
+ align-items: center;
413
+ justify-content: center;
414
+ overflow: hidden;
415
+ border: 1px solid #cbd5e1;
416
+ border-radius: 6px;
417
+ background: var(--surface);
418
+ }
419
+ .chip.is-selected .chip-preview {
420
+ border-color: #2563eb;
421
+ box-shadow: 0 0 0 2px #2563eb, 0 0 0 4px #ffffff;
422
+ }
423
+ .chip-label {
424
+ width: 64px;
425
+ color: #334155;
426
+ font-size: 12px;
427
+ font-weight: 500;
428
+ line-height: 1.35;
429
+ }
430
+ .chip-preview img {
431
+ display: block;
432
+ max-width: 42px;
433
+ max-height: 42px;
434
+ object-fit: contain;
435
+ }
436
+ .shape-sample {
437
+ display: block;
438
+ width: 32px;
439
+ height: 32px;
440
+ background: var(--favicon);
441
+ }
442
+ .border-sample {
443
+ display: block;
444
+ width: 32px;
445
+ height: 32px;
446
+ border-radius: 4px;
447
+ background: var(--surface);
448
+ }
449
+ .avatar-divider { display: none; }
450
+ .avatar-preview {
451
+ display: flex;
452
+ min-height: 320px;
453
+ flex-direction: column;
454
+ align-items: center;
455
+ justify-content: center;
456
+ gap: 20px;
457
+ border: 1px solid var(--line);
458
+ border-radius: 6px;
459
+ background: var(--surface);
460
+ padding: 20px;
461
+ text-align: center;
462
+ box-shadow: 0 1px 2px rgba(15,23,42,0.06);
463
+ }
464
+ .avatar-surface {
465
+ display: flex;
466
+ aspect-ratio: 1;
467
+ width: 100%;
468
+ max-width: 256px;
469
+ align-items: center;
470
+ justify-content: center;
471
+ overflow: hidden;
472
+ }
473
+ .avatar-surface canvas {
474
+ display: block;
475
+ aspect-ratio: 1;
476
+ width: 100%;
477
+ height: auto;
478
+ }
479
+ .avatar-range {
480
+ display: flex;
481
+ width: 100%;
482
+ max-width: 256px;
483
+ flex-direction: column;
484
+ gap: 12px;
485
+ }
486
+ .avatar-range span {
487
+ color: var(--muted);
488
+ font-size: 12px;
489
+ font-weight: 700;
490
+ letter-spacing: 0.12em;
491
+ text-transform: uppercase;
492
+ }
493
+ .avatar-range input {
494
+ width: 100%;
495
+ cursor: pointer;
496
+ accent-color: #2563eb;
497
+ }
498
+ .custom-color-label {
499
+ display: flex;
500
+ width: 100%;
501
+ max-width: 220px;
502
+ flex-direction: column;
503
+ gap: 8px;
504
+ color: var(--muted);
505
+ font-size: 12px;
506
+ font-weight: 700;
507
+ letter-spacing: 0.12em;
508
+ text-transform: uppercase;
509
+ }
510
+ .custom-color-label[hidden] { display: none; }
511
+ .custom-color-row {
512
+ display: flex;
513
+ gap: 8px;
514
+ align-items: center;
515
+ }
516
+ .custom-color-row input[type="color"] {
517
+ width: 40px;
518
+ height: 40px;
519
+ border: 1px solid #cbd5e1;
520
+ border-radius: 6px;
521
+ background: #ffffff;
522
+ cursor: pointer;
523
+ padding: 3px;
524
+ }
525
+ .custom-color-row input[type="text"] {
526
+ min-width: 0;
527
+ flex: 1;
528
+ height: 40px;
529
+ border: 1px solid #cbd5e1;
530
+ border-radius: 6px;
531
+ background: #ffffff;
532
+ color: #0f172a;
533
+ font: inherit;
534
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
535
+ font-size: 13px;
536
+ letter-spacing: 0;
537
+ padding: 0 10px;
538
+ text-transform: none;
539
+ }
540
+ .status {
541
+ min-height: 20px;
542
+ margin: 0;
543
+ color: var(--muted);
544
+ font-size: 14px;
545
+ font-weight: 500;
546
+ line-height: 1.4;
547
+ }
548
+ .color-group { margin-top: 48px; }
549
+ .color-sections,
550
+ .color-section,
551
+ .color-rows {
552
+ display: flex;
553
+ flex-direction: column;
554
+ }
555
+ .color-sections { gap: 16px; margin-top: 20px; }
556
+ .color-section { gap: 12px; }
557
+ .color-rows { gap: 16px; }
558
+ .color-row {
559
+ display: grid;
560
+ grid-template-columns: 1fr;
561
+ gap: 16px;
562
+ }
563
+ .swatch {
564
+ height: 112px;
565
+ width: 100%;
566
+ border-bottom: 1px solid var(--neutral-line);
567
+ }
568
+ .color-body {
569
+ display: flex;
570
+ flex-direction: column;
571
+ gap: 16px;
572
+ padding: 16px;
573
+ }
574
+ .print-colors {
575
+ display: flex;
576
+ flex-direction: column;
577
+ gap: 32px;
578
+ margin-top: 48px;
579
+ }
580
+ .print-intro h3 {
581
+ margin: 0;
582
+ color: var(--ink);
583
+ font-size: 18px;
584
+ font-weight: 600;
585
+ line-height: 1.4;
586
+ }
587
+ .print-family h4 {
588
+ margin: 0;
589
+ color: var(--ink);
590
+ font-size: 16px;
591
+ font-weight: 600;
592
+ line-height: 1.4;
593
+ }
594
+ .print-intro p {
595
+ margin: 4px 0 0;
596
+ color: var(--copy);
597
+ font-size: 14px;
598
+ line-height: 1.5;
599
+ }
600
+ .print-family {
601
+ display: flex;
602
+ flex-direction: column;
603
+ gap: 20px;
604
+ }
605
+ .print-grid {
606
+ display: grid;
607
+ grid-template-columns: repeat(3, minmax(0, 1fr));
608
+ gap: 12px;
609
+ }
610
+ .print-card {
611
+ position: relative;
612
+ min-width: 0;
613
+ }
614
+ .print-card:hover,
615
+ .print-card:focus-within {
616
+ z-index: 50;
617
+ }
618
+ .print-chip {
619
+ overflow: hidden;
620
+ border: 1px solid var(--neutral-line);
621
+ border-radius: 6px;
622
+ background: var(--surface);
623
+ box-shadow: 0 1px 2px rgba(15,23,42,0.06);
624
+ }
625
+ .print-swatch {
626
+ display: block;
627
+ aspect-ratio: 1;
628
+ width: 100%;
629
+ border: 0;
630
+ border-bottom: 1px solid var(--neutral-line);
631
+ cursor: pointer;
632
+ }
633
+ .print-swatch:focus-visible {
634
+ outline: 2px solid #2563eb;
635
+ outline-offset: 2px;
636
+ }
637
+ .print-chip-body {
638
+ padding: 6px 10px 10px;
639
+ text-align: left;
640
+ }
641
+ .print-kicker {
642
+ margin: 0;
643
+ color: var(--muted);
644
+ font-weight: 600;
645
+ letter-spacing: 0.12em;
646
+ line-height: 1.3;
647
+ text-transform: uppercase;
648
+ }
649
+ .print-chip .print-kicker {
650
+ font-size: 10px;
651
+ }
652
+ .print-popover-chip .print-kicker {
653
+ font-size: 9px;
654
+ }
655
+ .print-value-group > .print-kicker {
656
+ font-size: 12px;
657
+ }
658
+ .print-pantone {
659
+ margin: 2px 0 0;
660
+ color: var(--ink);
661
+ font-size: 12px;
662
+ font-weight: 600;
663
+ line-height: 1.35;
664
+ }
665
+ .print-popover {
666
+ position: absolute;
667
+ top: 100%;
668
+ left: 0;
669
+ z-index: 40;
670
+ display: none;
671
+ width: max-content;
672
+ min-width: 256px;
673
+ max-width: calc(100vw - 32px);
674
+ padding-top: 8px;
675
+ }
676
+ .print-card:hover .print-popover,
677
+ .print-card:focus-within .print-popover {
678
+ display: block;
679
+ }
680
+ .print-popover-panel {
681
+ position: relative;
682
+ border: 1px solid var(--neutral-line);
683
+ border-radius: 8px;
684
+ background: var(--surface);
685
+ padding: 16px;
686
+ text-align: left;
687
+ box-shadow: 0 20px 40px rgba(15,23,42,0.18);
688
+ }
689
+ .print-popover-chip {
690
+ position: absolute;
691
+ top: 12px;
692
+ right: 12px;
693
+ border: 1px solid var(--neutral-line);
694
+ border-radius: 6px;
695
+ background: var(--surface);
696
+ padding: 6px 10px;
697
+ text-align: right;
698
+ box-shadow: 0 1px 2px rgba(15,23,42,0.08);
699
+ }
700
+ .print-values {
701
+ display: flex;
702
+ max-width: 180px;
703
+ flex-direction: column;
704
+ gap: 12px;
705
+ }
706
+ .print-value-group {
707
+ display: flex;
708
+ flex-direction: column;
709
+ gap: 8px;
710
+ }
711
+ .print-copy-row {
712
+ display: flex;
713
+ align-items: center;
714
+ gap: 8px;
715
+ }
716
+ .print-copy-icon {
717
+ width: 14px;
718
+ height: 14px;
719
+ flex: 0 0 auto;
720
+ color: #a3a3a3;
721
+ stroke-width: 2;
722
+ }
723
+ .print-component-row {
724
+ display: flex;
725
+ flex-wrap: wrap;
726
+ gap: 12px;
727
+ }
728
+ .print-copy-value,
729
+ .print-component-copy {
730
+ border: 0;
731
+ background: transparent;
732
+ color: #262626;
733
+ cursor: pointer;
734
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
735
+ font-size: 12px;
736
+ line-height: 20px;
737
+ padding: 0;
738
+ text-align: left;
739
+ transition: color 160ms ease;
740
+ }
741
+ .print-component-copy {
742
+ display: flex;
743
+ align-items: baseline;
744
+ gap: 6px;
745
+ }
746
+ .print-copy-value:hover,
747
+ .print-component-copy:hover {
748
+ color: #3a89c0;
749
+ text-decoration: underline;
750
+ }
751
+ .print-channel {
752
+ color: var(--muted);
753
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
754
+ font-size: 11px;
755
+ font-weight: 700;
756
+ }
757
+ .banner-stack {
758
+ display: flex;
759
+ flex-direction: column;
760
+ gap: 40px;
761
+ margin-top: 48px;
762
+ }
763
+ .banner-list {
764
+ display: flex;
765
+ flex-direction: column;
766
+ gap: 16px;
767
+ }
768
+ .banner-card {
769
+ max-width: 100%;
770
+ }
771
+ .banner-preview {
772
+ width: 100%;
773
+ overflow: hidden;
774
+ border-bottom: 1px solid var(--neutral-line);
775
+ background: #f1f5f9;
776
+ }
777
+ .banner-preview img {
778
+ display: block;
779
+ width: 100%;
780
+ height: 100%;
781
+ object-fit: cover;
782
+ }
783
+ .banner-card-body {
784
+ display: flex;
785
+ flex-direction: column;
786
+ gap: 16px;
787
+ padding: 16px;
788
+ }
789
+ .banner-card p {
790
+ margin: 4px 0 0;
791
+ color: var(--neutral);
792
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
793
+ font-size: 12px;
794
+ line-height: 1.5;
795
+ }
796
+ .footer {
797
+ border-top: 1px solid var(--line);
798
+ background: var(--surface);
799
+ }
800
+ .footer-inner {
801
+ display: flex;
802
+ flex-direction: column;
803
+ gap: 16px;
804
+ padding-top: 32px;
805
+ padding-bottom: 32px;
806
+ }
807
+ .footer-logo {
808
+ display: flex;
809
+ width: 160px;
810
+ height: 48px;
811
+ align-items: center;
812
+ }
813
+ .footer p {
814
+ margin: 0;
815
+ color: var(--muted);
816
+ font-size: 14px;
817
+ }
818
+ .lightbox[hidden] { display: none; }
819
+ .lightbox {
820
+ position: fixed;
821
+ inset: 0;
822
+ z-index: 120;
823
+ overflow-y: auto;
824
+ background: rgba(0,0,0,0.7);
825
+ padding: 16px;
826
+ }
827
+ .lightbox-align {
828
+ display: flex;
829
+ min-height: 100%;
830
+ align-items: center;
831
+ justify-content: center;
832
+ }
833
+ .lightbox-panel {
834
+ width: 100%;
835
+ max-width: 1024px;
836
+ overflow: hidden;
837
+ border-radius: 8px;
838
+ background: var(--surface);
839
+ box-shadow: 0 25px 50px -12px rgba(0,0,0,0.35);
840
+ }
841
+ .lightbox-preview {
842
+ display: flex;
843
+ min-height: 60vh;
844
+ align-items: center;
845
+ justify-content: center;
846
+ background: #f7f7f7;
847
+ padding: 40px 32px;
848
+ }
849
+ .lightbox-checker {
850
+ max-width: 100%;
851
+ overflow: auto;
852
+ border: 1px solid #d4d4d4;
853
+ border-radius: 6px;
854
+ padding: 16px;
855
+ box-shadow: 0 1px 2px rgba(0,0,0,0.08);
856
+ }
857
+ .lightbox-checker img {
858
+ display: block;
859
+ width: auto;
860
+ max-width: 100%;
861
+ max-height: 70vh;
862
+ height: auto;
863
+ }
864
+ .lightbox-body {
865
+ display: flex;
866
+ flex-direction: column;
867
+ gap: 16px;
868
+ border-top: 1px solid var(--neutral-line);
869
+ padding: 20px;
870
+ }
871
+ .lightbox-title {
872
+ margin: 0;
873
+ color: #171717;
874
+ font-size: 18px;
875
+ font-weight: 600;
876
+ }
877
+ .lightbox-file {
878
+ margin: 4px 0 0;
879
+ color: var(--neutral);
880
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
881
+ font-size: 12px;
882
+ }
883
+ .shape-square { border-radius: 0; }
884
+ .shape-rounded { border-radius: 22%; }
885
+ .shape-round { border-radius: 999px; }
886
+ @media (min-width: 640px) {
887
+ .wrap { padding-right: 24px; padding-left: 24px; }
888
+ .asset-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
889
+ .group-header {
890
+ flex-direction: row;
891
+ align-items: flex-end;
892
+ justify-content: space-between;
893
+ }
894
+ .section-heading-row {
895
+ flex-direction: row;
896
+ align-items: flex-end;
897
+ justify-content: space-between;
898
+ }
899
+ .color-row-2,
900
+ .color-row-3 {
901
+ grid-template-columns: repeat(2, minmax(0, 1fr));
902
+ }
903
+ .print-grid { grid-template-columns: repeat(5, minmax(0, 1fr)); }
904
+ .lightbox { padding: 32px; }
905
+ .lightbox-body {
906
+ flex-direction: row;
907
+ align-items: center;
908
+ justify-content: space-between;
909
+ }
910
+ }
911
+ @media (min-width: 768px) {
912
+ .avatar-controls {
913
+ grid-template-columns: minmax(0, 1fr) 1px minmax(0, 1fr);
914
+ }
915
+ .avatar-divider {
916
+ position: relative;
917
+ display: block;
918
+ align-self: stretch;
919
+ grid-column: 2;
920
+ grid-row: 1 / span 3;
921
+ }
922
+ .avatar-divider::before {
923
+ position: absolute;
924
+ top: -12px;
925
+ bottom: -12px;
926
+ left: 50%;
927
+ width: 1px;
928
+ content: "";
929
+ transform: translateX(-50%);
930
+ background: linear-gradient(to bottom, transparent, var(--line), transparent);
931
+ }
932
+ .avatar-left { grid-column: 1; }
933
+ .avatar-right { grid-column: 3; }
934
+ .avatar-row-1 { grid-row: 1; }
935
+ .avatar-row-2 { grid-row: 2; }
936
+ .avatar-row-3 { grid-row: 3; }
937
+ .footer-inner {
938
+ flex-direction: row;
939
+ align-items: center;
940
+ justify-content: space-between;
941
+ }
942
+ .footer p { text-align: right; }
943
+ }
944
+ @media (min-width: 1024px) {
945
+ .wrap { padding-right: 32px; padding-left: 32px; }
946
+ .header-inner {
947
+ grid-template-columns: minmax(0, 1fr) 360px;
948
+ align-items: center;
949
+ }
950
+ .hero-media { justify-content: flex-end; }
951
+ .asset-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
952
+ .avatar-grid {
953
+ grid-template-columns: minmax(0, 1fr) 384px;
954
+ align-items: stretch;
955
+ }
956
+ .color-row-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
957
+ .print-grid { grid-template-columns: repeat(7, minmax(0, 1fr)); }
958
+ }
959
+ @media (min-width: 1280px) {
960
+ .print-grid { grid-template-columns: repeat(10, minmax(0, 1fr)); }
961
+ }
962
+ `;
963
+ function lucideIcon(name, className = 'icon') {
964
+ const paths = {
965
+ 'align-center': '<path d="M21 5H3" /><path d="M17 12H7" /><path d="M19 19H5" />',
966
+ 'align-left': '<path d="M21 5H3" /><path d="M15 12H3" /><path d="M17 19H3" />',
967
+ 'align-right': '<path d="M21 5H3" /><path d="M21 12H9" /><path d="M21 19H7" />',
968
+ 'arrow-down': '<path d="M12 5v14" /><path d="m19 12-7 7-7-7" />',
969
+ copy: '<rect width="14" height="14" x="8" y="8" rx="2" ry="2" /><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />',
970
+ download: '<path d="M12 15V3" /><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><path d="m7 10 5 5 5-5" />',
971
+ 'rotate-ccw': '<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" /><path d="M3 3v5h5" />',
972
+ upload: '<path d="M12 3v12" /><path d="m17 8-5-5-5 5" /><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />',
973
+ };
974
+ return `<svg class="${className}" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${paths[name] ?? paths.download}</svg>`;
975
+ }
976
+ function renderDownloadButtons(downloads) {
977
+ return downloads
978
+ .map((download) => `
979
+ <a class="button button-dark" href="${escapeHtml(download.url)}" download="${escapeHtml(download.fileName)}">
980
+ ${lucideIcon('download')}
981
+ <span>${escapeHtml(download.format)}</span>
982
+ </a>
983
+ `)
984
+ .join('');
985
+ }
986
+ function renderAssetGroup(group, downloadUrl) {
987
+ return `
988
+ <section class="group">
989
+ <div class="group-header">
990
+ <div>
991
+ <h3>${escapeHtml(group.label)}</h3>
992
+ ${group.description ? `<p>${escapeHtml(group.description)}</p>` : ''}
993
+ </div>
994
+ <div class="group-actions">
995
+ <span class="asset-count">${group.items.length} ${group.items.length === 1 ? 'asset' : 'assets'} available</span>
996
+ <a class="button" href="${escapeHtml(downloadUrl)}" download>
997
+ ${lucideIcon('download')}
998
+ <span>Download all</span>
999
+ </a>
1000
+ </div>
1001
+ </div>
1002
+ <div class="asset-grid">
1003
+ ${group.items
1004
+ .map((asset) => `
1005
+ <article class="card">
1006
+ <button class="asset-preview checker" type="button" data-lightbox-id="${escapeHtml(asset.id)}">
1007
+ <img src="${escapeHtml(asset.previewUrl)}" alt="${escapeHtml(asset.title)}" loading="lazy" />
1008
+ </button>
1009
+ <div class="card-body">
1010
+ <h4>${escapeHtml(asset.title)}</h4>
1011
+ <div class="button-row">${renderDownloadButtons(asset.downloads)}</div>
1012
+ </div>
1013
+ </article>
1014
+ `)
1015
+ .join('')}
1016
+ </div>
1017
+ </section>
1018
+ `;
1019
+ }
1020
+ function findAvatarAssets(groups) {
1021
+ const iconGroup = groups.find((group) => /icon/i.test(group.key)) ??
1022
+ groups.find((group) => /icon/i.test(group.label));
1023
+ const iconGroupAssets = iconGroup?.items.filter((asset) => !isWordmarkAsset(asset)) ?? [];
1024
+ if (iconGroupAssets.length)
1025
+ return iconGroupAssets;
1026
+ return groups
1027
+ .flatMap((group) => group.items)
1028
+ .filter((asset) => isIconAsset(asset));
1029
+ }
1030
+ function findHeroAsset(groups) {
1031
+ return findAvatarAssets(groups)[0] ?? groups[0]?.items[0] ?? null;
1032
+ }
1033
+ function findFooterAsset(groups) {
1034
+ const logoGroup = groups.find((group) => /logo|lockup|wordmark/i.test(group.key)) ??
1035
+ groups.find((group) => /logo|lockup|wordmark/i.test(group.label));
1036
+ return logoGroup?.items[0] ?? groups[0]?.items[0] ?? null;
1037
+ }
1038
+ function tokenize(value) {
1039
+ return value
1040
+ .toLowerCase()
1041
+ .split(/[^a-z0-9]+/)
1042
+ .filter(Boolean);
1043
+ }
1044
+ function findPrimaryColor(colors) {
1045
+ return (colors.find((color) => /primary/i.test(color.name)) ??
1046
+ colors[0] ?? {
1047
+ hex: '#0d2249',
1048
+ name: 'Primary',
1049
+ });
1050
+ }
1051
+ function normalizeHexColor(value) {
1052
+ if (/^#[0-9a-f]{6}$/i.test(value.trim()))
1053
+ return value.trim();
1054
+ if (/^[0-9a-f]{6}$/i.test(value.trim()))
1055
+ return `#${value.trim()}`;
1056
+ return '#4784de';
1057
+ }
1058
+ function fixedBackgroundOptions(customHex) {
1059
+ return [
1060
+ { key: 'transparent', label: 'Transparent', color: '' },
1061
+ { key: 'black', label: 'Black', color: '#05070b' },
1062
+ { key: 'white', label: 'White', color: '#ffffff' },
1063
+ { key: 'custom', label: 'Custom', color: normalizeHexColor(customHex) },
1064
+ ];
1065
+ }
1066
+ function fixedBorderOptions(colors, customHex) {
1067
+ const primary = findPrimaryColor(colors);
1068
+ return [
1069
+ { key: 'primary', label: 'Primary', color: primary.hex },
1070
+ { key: 'black', label: 'Black', color: '#05070b' },
1071
+ { key: 'white', label: 'White', color: '#ffffff' },
1072
+ { key: 'custom', label: 'Custom', color: normalizeHexColor(customHex) },
1073
+ ];
1074
+ }
1075
+ function assetSearchText(asset) {
1076
+ return [
1077
+ asset.id,
1078
+ asset.title,
1079
+ asset.previewUrl,
1080
+ ...asset.downloads.map((download) => download.fileName),
1081
+ ]
1082
+ .join(' ')
1083
+ .toLowerCase();
1084
+ }
1085
+ function isWordmarkAsset(asset) {
1086
+ const compact = assetSearchText(asset).replace(/[^a-z0-9]+/g, '');
1087
+ return compact.includes('wordmark');
1088
+ }
1089
+ function isIconAsset(asset) {
1090
+ const text = assetSearchText(asset);
1091
+ const tokens = text.split(/[^a-z0-9]+/).filter(Boolean);
1092
+ const compact = tokens.join('');
1093
+ return (!isWordmarkAsset(asset) &&
1094
+ (tokens.some((token) => ['icon', 'icons', 'symbol', 'symbols', 'favicon'].includes(token)) ||
1095
+ compact.includes('brandmark')));
1096
+ }
1097
+ function inferAvatarIconOptions(assets, colors) {
1098
+ const primary = findPrimaryColor(colors);
1099
+ const candidates = [
1100
+ ...colors.map((color, index) => ({
1101
+ color: color.hex,
1102
+ key: `brand-${index}-${color.hex.toLowerCase()}`,
1103
+ label: color.name,
1104
+ tokens: tokenize(color.name),
1105
+ })),
1106
+ { color: '#ffffff', key: 'white', label: 'White', tokens: ['white'] },
1107
+ { color: '#05070b', key: 'black', label: 'Black', tokens: ['black'] },
1108
+ ];
1109
+ const options = [];
1110
+ const seen = new Set();
1111
+ for (const asset of assets) {
1112
+ const text = assetSearchText(asset);
1113
+ const candidate = candidates.find((option) => option.tokens.some((token) => token && text.includes(token))) ?? {
1114
+ color: primary.hex,
1115
+ key: `primary-${primary.hex.toLowerCase()}`,
1116
+ label: primary.name,
1117
+ tokens: ['primary'],
1118
+ };
1119
+ if (seen.has(candidate.key))
1120
+ continue;
1121
+ options.push({
1122
+ asset,
1123
+ color: candidate.color,
1124
+ key: `${candidate.key}:${asset.id}`,
1125
+ label: candidate.label,
1126
+ });
1127
+ seen.add(candidate.key);
1128
+ }
1129
+ return options;
1130
+ }
1131
+ function renderColorChip({ attribute, color, index, label, selected, value, }) {
1132
+ return `
1133
+ <button class="chip ${selected ? 'is-selected' : ''}" type="button" data-${attribute}="${escapeHtml(value)}" data-avatar-color="${escapeHtml(color)}">
1134
+ <span class="chip-preview ${color ? '' : 'checker'}" style="${color ? `background-color:${escapeHtml(color)}` : ''}"></span>
1135
+ <span class="chip-label">${escapeHtml(label || `Color ${index + 1}`)}</span>
1136
+ </button>
1137
+ `;
1138
+ }
1139
+ function renderAvatarGenerator(manifest) {
1140
+ const avatarAssets = findAvatarAssets(manifest.assetGroups);
1141
+ if (!avatarAssets.length)
1142
+ return '';
1143
+ const iconOptions = inferAvatarIconOptions(avatarAssets, manifest.brandColors);
1144
+ const backgrounds = fixedBackgroundOptions('#4784de');
1145
+ const borders = fixedBorderOptions(manifest.brandColors, '#4784de');
1146
+ return `
1147
+ <section class="avatar" id="avatar">
1148
+ <div>
1149
+ <h3>Avatar Generator</h3>
1150
+ <p>Make a profile-ready PNG from the approved icon.</p>
1151
+ </div>
1152
+ <div class="avatar-grid">
1153
+ <div class="avatar-controls">
1154
+ <button class="reset-button" type="button" id="avatar-reset" aria-label="Reset avatar generator">${lucideIcon('rotate-ccw')}</button>
1155
+ <div class="control-group avatar-left avatar-row-1">
1156
+ <p class="control-title">Icon</p>
1157
+ <div class="chip-row">
1158
+ ${iconOptions
1159
+ .map((option, index) => `
1160
+ <button class="chip ${index === 0 ? 'is-selected' : ''}" type="button" data-avatar-icon="${escapeHtml(option.asset.previewUrl)}">
1161
+ <span class="chip-preview" style="background-color:${escapeHtml(option.color)}"></span>
1162
+ <span class="chip-label">${escapeHtml(option.label)}</span>
1163
+ </button>
1164
+ `)
1165
+ .join('')}
1166
+ </div>
1167
+ </div>
1168
+ <div class="control-group avatar-left avatar-row-2">
1169
+ <p class="control-title">Background</p>
1170
+ <div class="chip-row">
1171
+ ${backgrounds
1172
+ .map((option, index) => renderColorChip({
1173
+ attribute: 'avatar-background',
1174
+ color: option.color,
1175
+ index,
1176
+ label: option.label,
1177
+ selected: index === 0,
1178
+ value: option.key,
1179
+ }))
1180
+ .join('')}
1181
+ </div>
1182
+ <label class="custom-color-label" id="background-custom-controls" hidden>
1183
+ Custom
1184
+ <span class="custom-color-row">
1185
+ <input type="color" id="background-custom-color" value="#4784de" aria-label="Custom background color" />
1186
+ <input type="text" id="background-custom-text" value="#4784de" aria-label="Custom background hex" />
1187
+ </span>
1188
+ </label>
1189
+ </div>
1190
+ <div class="control-group avatar-left avatar-row-3">
1191
+ <p class="control-title">Shape</p>
1192
+ <div class="chip-row">
1193
+ ${['square', 'round', 'rounded']
1194
+ .map((shape, index) => `
1195
+ <button class="chip ${index === 0 ? 'is-selected' : ''}" type="button" data-avatar-shape="${shape}">
1196
+ <span class="chip-preview"><span class="shape-sample shape-${shape}"></span></span>
1197
+ <span class="chip-label">${shape[0].toUpperCase()}${shape.slice(1)}</span>
1198
+ </button>
1199
+ `)
1200
+ .join('')}
1201
+ </div>
1202
+ </div>
1203
+ <div class="avatar-divider" aria-hidden="true"></div>
1204
+ <div class="control-group avatar-right avatar-row-1">
1205
+ <p class="control-title">Border color</p>
1206
+ <div class="chip-row">
1207
+ ${borders
1208
+ .map((option, index) => renderColorChip({
1209
+ attribute: 'avatar-border',
1210
+ color: option.color,
1211
+ index,
1212
+ label: option.label,
1213
+ selected: index === 0,
1214
+ value: option.key,
1215
+ }))
1216
+ .join('')}
1217
+ </div>
1218
+ <label class="custom-color-label" id="border-custom-controls" hidden>
1219
+ Custom
1220
+ <span class="custom-color-row">
1221
+ <input type="color" id="border-custom-color" value="#4784de" aria-label="Custom border color" />
1222
+ <input type="text" id="border-custom-text" value="#4784de" aria-label="Custom border hex" />
1223
+ </span>
1224
+ </label>
1225
+ </div>
1226
+ <div class="control-group avatar-right avatar-row-2">
1227
+ <p class="control-title">Border thickness</p>
1228
+ <div class="chip-row">
1229
+ ${[
1230
+ ['none', 'None', '1px #cbd5e1'],
1231
+ ['thin', 'Thin', '3px #0d2249'],
1232
+ ['medium', 'Medium', '5px #0d2249'],
1233
+ ['heavy', 'Heavy', '7px #0d2249'],
1234
+ ]
1235
+ .map(([value, label, shadow], index) => `
1236
+ <button class="chip ${index === 0 ? 'is-selected' : ''}" type="button" data-avatar-thickness="${value}">
1237
+ <span class="chip-preview"><span class="border-sample" style="box-shadow:inset 0 0 0 ${shadow}"></span></span>
1238
+ <span class="chip-label">${label}</span>
1239
+ </button>
1240
+ `)
1241
+ .join('')}
1242
+ </div>
1243
+ </div>
1244
+ <div class="control-group avatar-right avatar-row-3">
1245
+ <p class="control-title">Size</p>
1246
+ <div class="chip-row">
1247
+ ${[512, 1024]
1248
+ .map((size, index) => `
1249
+ <button class="chip ${index === 1 ? 'is-selected' : ''}" type="button" data-avatar-size="${size}">
1250
+ <span class="chip-preview">${size}</span>
1251
+ <span class="chip-label">${size}px</span>
1252
+ </button>
1253
+ `)
1254
+ .join('')}
1255
+ </div>
1256
+ </div>
1257
+ </div>
1258
+ <div class="avatar-preview">
1259
+ <span class="avatar-surface checker shape-square" id="avatar-surface">
1260
+ <canvas id="avatar-canvas" width="1024" height="1024" aria-label="Avatar preview"></canvas>
1261
+ </span>
1262
+ <label class="avatar-range">
1263
+ <span>Icon padding</span>
1264
+ <input type="range" min="0" max="34" value="18" id="avatar-padding" />
1265
+ </label>
1266
+ <button class="button button-favicon" type="button" id="download-avatar">${lucideIcon('download')}<span>Download PNG (1024px)</span></button>
1267
+ <button class="button" type="button" id="download-favicons">${lucideIcon('download')}<span>Download favicon PNGs</span></button>
1268
+ <p class="status" id="avatar-status"></p>
1269
+ </div>
1270
+ </div>
1271
+ </section>
1272
+ `;
1273
+ }
1274
+ function colorRows(manifest) {
1275
+ const colorMap = new Map(manifest.brandColors.map((color) => [color.name.toLowerCase(), color]));
1276
+ const configured = manifest.colorSections
1277
+ .map((section) => ({
1278
+ columns: section.columns ?? 3,
1279
+ label: getDisplayColorSectionLabel(section.label),
1280
+ rows: section.rows
1281
+ .map((row) => row
1282
+ .map((name) => colorMap.get(name.toLowerCase()))
1283
+ .filter((color) => Boolean(color)))
1284
+ .filter((row) => row.length > 0),
1285
+ }))
1286
+ .filter((section) => section.rows.length > 0);
1287
+ if (configured.length)
1288
+ return configured;
1289
+ const rows = [];
1290
+ for (let index = 0; index < manifest.brandColors.length; index += 3) {
1291
+ rows.push(manifest.brandColors.slice(index, index + 3));
1292
+ }
1293
+ return [{ columns: 3, label: 'Primary', rows }];
1294
+ }
1295
+ function getDisplayColorSectionLabel(label) {
1296
+ const trimmed = label.trim();
1297
+ if (/^brand colors?$/i.test(trimmed))
1298
+ return 'Primary';
1299
+ return trimmed.replace(/\s+colors?$/i, '');
1300
+ }
1301
+ function renderColors(manifest) {
1302
+ return colorRows(manifest)
1303
+ .map((section) => `
1304
+ <section class="color-section">
1305
+ <h4 class="color-section-title">${escapeHtml(section.label)}</h4>
1306
+ <div class="color-rows">
1307
+ ${section.rows
1308
+ .map((row, index) => `
1309
+ <div class="color-row color-row-${Math.min(section.columns, 3)}" data-row="${index}">
1310
+ ${row
1311
+ .map((color) => `
1312
+ <article class="color-card">
1313
+ <div class="swatch" style="background-color:${escapeHtml(color.hex)}"></div>
1314
+ <div class="color-body">
1315
+ <h4>${escapeHtml(color.name)}</h4>
1316
+ <div class="button-row">
1317
+ <button class="copy-button" type="button" data-copy="${escapeHtml(color.hex)}">${lucideIcon('copy', 'copy-icon')}<span>${escapeHtml(color.hex)}</span></button>
1318
+ </div>
1319
+ </div>
1320
+ </article>
1321
+ `)
1322
+ .join('')}
1323
+ </div>
1324
+ `)
1325
+ .join('')}
1326
+ </div>
1327
+ </section>
1328
+ `)
1329
+ .join('');
1330
+ }
1331
+ function renderPrintCopyButton(value) {
1332
+ return `<button class="print-copy-value" type="button" data-copy="${escapeHtml(value)}" data-copy-static="true">${escapeHtml(value)}</button>`;
1333
+ }
1334
+ function renderPrintValueGroup({ channels, color, label, }) {
1335
+ const sourceValues = label === 'RGB' ? color.rgb : color.cmyk;
1336
+ const values = channels
1337
+ .map((channel, index) => ({ channel, value: sourceValues[index] }))
1338
+ .filter((item) => item.value);
1339
+ if (!values.length)
1340
+ return '';
1341
+ return `
1342
+ <div class="print-value-group">
1343
+ <p class="print-kicker">${escapeHtml(label)}</p>
1344
+ <div class="print-copy-row">
1345
+ ${lucideIcon('copy', 'print-copy-icon')}
1346
+ <div class="print-component-row">
1347
+ ${values
1348
+ .map((item) => `
1349
+ <button class="print-component-copy" type="button" data-copy="${escapeHtml(item.value ?? '')}" data-copy-static="true">
1350
+ <span class="print-channel">${escapeHtml(item.channel)}</span>
1351
+ <span>${escapeHtml(item.value ?? '')}</span>
1352
+ </button>
1353
+ `)
1354
+ .join('')}
1355
+ </div>
1356
+ </div>
1357
+ </div>
1358
+ `;
1359
+ }
1360
+ function renderPrintColorCard(color) {
1361
+ return `
1362
+ <article class="print-card">
1363
+ <div class="print-chip">
1364
+ <button
1365
+ aria-label="${escapeHtml(color.pantone)} color values"
1366
+ class="print-swatch"
1367
+ style="background-color:${escapeHtml(color.hex)}"
1368
+ type="button"
1369
+ ></button>
1370
+ <div class="print-chip-body">
1371
+ <p class="print-kicker">Pantone</p>
1372
+ <p class="print-pantone">${escapeHtml(color.pantone)}</p>
1373
+ </div>
1374
+ </div>
1375
+ <div class="print-popover">
1376
+ <div class="print-popover-panel">
1377
+ <div class="print-popover-chip">
1378
+ <p class="print-kicker">Pantone</p>
1379
+ <p class="print-pantone">${escapeHtml(color.pantone)}</p>
1380
+ </div>
1381
+ <div class="print-values">
1382
+ <div class="print-value-group">
1383
+ <p class="print-kicker">Hex</p>
1384
+ <div class="print-copy-row">
1385
+ ${lucideIcon('copy', 'print-copy-icon')}
1386
+ ${renderPrintCopyButton(color.hex)}
1387
+ </div>
1388
+ </div>
1389
+ ${renderPrintValueGroup({ channels: ['R', 'G', 'B'], color, label: 'RGB' })}
1390
+ ${renderPrintValueGroup({ channels: ['C', 'M', 'Y', 'K'], color, label: 'CMYK' })}
1391
+ </div>
1392
+ </div>
1393
+ </div>
1394
+ </article>
1395
+ `;
1396
+ }
1397
+ function renderPrintColorGroups(groups) {
1398
+ if (!groups.length)
1399
+ return '';
1400
+ return `
1401
+ <section class="print-colors">
1402
+ <div class="print-intro">
1403
+ <h3>Print Colors</h3>
1404
+ <p>Pantone-aligned swatches for merch, packaging, and print production.</p>
1405
+ </div>
1406
+ ${groups
1407
+ .map((group) => `
1408
+ <section class="print-family">
1409
+ <h4>${escapeHtml(group.label)}</h4>
1410
+ <div class="print-grid">
1411
+ ${group.items.map(renderPrintColorCard).join('')}
1412
+ </div>
1413
+ </section>
1414
+ `)
1415
+ .join('')}
1416
+ </section>
1417
+ `;
1418
+ }
1419
+ function renderBanners(manifest) {
1420
+ if (!manifest.bannerGroups.length)
1421
+ return '';
1422
+ const bannerAssetCount = manifest.bannerGroups.reduce((total, group) => total + group.items.length, 0);
1423
+ return `
1424
+ <section id="banners" class="section-banners">
1425
+ <div class="wrap section-inner">
1426
+ <div class="section-heading-row">
1427
+ <div class="section-heading">
1428
+ <p class="eyebrow">Banners</p>
1429
+ <h2>Social profile assets</h2>
1430
+ <p>Ready-to-use PNG cover images sized for each platform.</p>
1431
+ </div>
1432
+ <div class="group-actions section-heading-actions">
1433
+ <span class="asset-count">${bannerAssetCount} ${bannerAssetCount === 1 ? 'asset' : 'assets'} available</span>
1434
+ ${manifest.downloads.bannerAssets
1435
+ ? `<a class="button" href="${escapeHtml(manifest.downloads.bannerAssets)}" download>${lucideIcon('download')}<span>Download all</span></a>`
1436
+ : ''}
1437
+ </div>
1438
+ </div>
1439
+ <div class="banner-stack">
1440
+ ${manifest.bannerGroups
1441
+ .map((group) => `
1442
+ <section class="banner-group">
1443
+ <div>
1444
+ <h3>${escapeHtml(group.label)}</h3>
1445
+ ${group.description ? `<p>${escapeHtml(group.description)}</p>` : ''}
1446
+ </div>
1447
+ <div class="banner-list">
1448
+ ${group.items
1449
+ .map((asset) => `
1450
+ <article class="banner-card" style="width:${Math.round(asset.width * 0.5)}px">
1451
+ <div class="banner-preview" style="aspect-ratio:${asset.width} / ${asset.height}">
1452
+ <img src="${escapeHtml(asset.previewUrl)}" alt="${escapeHtml(asset.title)}" loading="lazy" />
1453
+ </div>
1454
+ <div class="banner-card-body">
1455
+ <div>
1456
+ <h4>${escapeHtml(asset.title)}</h4>
1457
+ <p>${escapeHtml(asset.description)}</p>
1458
+ </div>
1459
+ <div class="button-row">${renderDownloadButtons(asset.downloads)}</div>
1460
+ </div>
1461
+ </article>
1462
+ `)
1463
+ .join('')}
1464
+ </div>
1465
+ </section>
1466
+ `)
1467
+ .join('')}
1468
+ </div>
1469
+ </div>
1470
+ </section>
1471
+ `;
1472
+ }
1473
+ function clientScript(manifest) {
1474
+ return `
1475
+ window.__OPEN_BRANDKIT__ = ${escapeJsonForHtml(manifest)};
1476
+
1477
+ const copyButtons = document.querySelectorAll('[data-copy]');
1478
+ copyButtons.forEach((button) => {
1479
+ button.addEventListener('click', async () => {
1480
+ const value = button.getAttribute('data-copy') || '';
1481
+ await navigator.clipboard.writeText(value);
1482
+ if (button.hasAttribute('data-copy-static')) return;
1483
+ const label = button.querySelector('span');
1484
+ const original = label?.textContent || value;
1485
+ if (label) label.textContent = 'Copied';
1486
+ window.setTimeout(() => {
1487
+ if (label) label.textContent = original;
1488
+ }, 1000);
1489
+ });
1490
+ });
1491
+
1492
+ const allAssets = window.__OPEN_BRANDKIT__.assetGroups.flatMap((group) => group.items);
1493
+ const lightbox = document.getElementById('lightbox');
1494
+ const lightboxImage = document.getElementById('lightbox-image');
1495
+ const lightboxTitle = document.getElementById('lightbox-title');
1496
+ const lightboxFile = document.getElementById('lightbox-file');
1497
+ const lightboxDownloads = document.getElementById('lightbox-downloads');
1498
+ const lightboxClose = document.getElementById('lightbox-close');
1499
+
1500
+ function preferredDownload(asset) {
1501
+ return asset.downloads.find((download) => download.format === 'PNG') ||
1502
+ asset.downloads.find((download) => download.format === 'SVG') ||
1503
+ asset.downloads[0];
1504
+ }
1505
+
1506
+ function closeLightbox() {
1507
+ if (lightbox) lightbox.hidden = true;
1508
+ }
1509
+
1510
+ document.querySelectorAll('[data-lightbox-id]').forEach((button) => {
1511
+ button.addEventListener('click', () => {
1512
+ const asset = allAssets.find((item) => item.id === button.getAttribute('data-lightbox-id'));
1513
+ if (!asset || !lightbox || !lightboxImage || !lightboxTitle || !lightboxFile || !lightboxDownloads) return;
1514
+ const download = preferredDownload(asset);
1515
+ lightboxImage.src = download?.url || asset.previewUrl;
1516
+ lightboxImage.alt = asset.title;
1517
+ lightboxTitle.textContent = asset.title;
1518
+ lightboxFile.textContent = download?.fileName || asset.downloads.map((item) => item.fileName).join(' / ');
1519
+ lightboxDownloads.innerHTML = '';
1520
+ asset.downloads.forEach((item) => {
1521
+ const link = document.createElement('a');
1522
+ link.className = 'button button-dark';
1523
+ link.href = item.url;
1524
+ link.download = item.fileName;
1525
+ link.innerHTML = '${lucideIcon('download')}<span>' + item.format + '</span>';
1526
+ lightboxDownloads.appendChild(link);
1527
+ });
1528
+ lightbox.hidden = false;
1529
+ });
1530
+ });
1531
+ lightboxClose?.addEventListener('click', closeLightbox);
1532
+ lightbox?.addEventListener('mousedown', (event) => {
1533
+ if (event.target === lightbox) closeLightbox();
1534
+ });
1535
+ window.addEventListener('keydown', (event) => {
1536
+ if (event.key === 'Escape') closeLightbox();
1537
+ });
1538
+
1539
+ const canvas = document.getElementById('avatar-canvas');
1540
+ const surface = document.getElementById('avatar-surface');
1541
+ const paddingInput = document.getElementById('avatar-padding');
1542
+ const downloadAvatar = document.getElementById('download-avatar');
1543
+ const downloadFavicons = document.getElementById('download-favicons');
1544
+ const avatarStatus = document.getElementById('avatar-status');
1545
+ const resetAvatar = document.getElementById('avatar-reset');
1546
+ const backgroundCustomControls = document.getElementById('background-custom-controls');
1547
+ const backgroundCustomColor = document.getElementById('background-custom-color');
1548
+ const backgroundCustomText = document.getElementById('background-custom-text');
1549
+ const borderCustomControls = document.getElementById('border-custom-controls');
1550
+ const borderCustomColor = document.getElementById('border-custom-color');
1551
+ const borderCustomText = document.getElementById('border-custom-text');
1552
+ const thicknessRatios = { none: 0, thin: 0.16, medium: 0.32, heavy: 0.48 };
1553
+
1554
+ function selectedValue(selector, fallback) {
1555
+ return document.querySelector(selector + '.is-selected')?.getAttribute(selector.replace('[data-', 'data-').replace(']', '')) || fallback;
1556
+ }
1557
+
1558
+ function selectedDataset(selector, name, fallback) {
1559
+ return document.querySelector(selector + '.is-selected')?.getAttribute(name) || fallback;
1560
+ }
1561
+
1562
+ function normalizeHex(value) {
1563
+ const trimmed = String(value || '').trim();
1564
+ if (/^#[0-9a-f]{6}$/i.test(trimmed)) return trimmed;
1565
+ if (/^[0-9a-f]{6}$/i.test(trimmed)) return '#' + trimmed;
1566
+ return '#4784de';
1567
+ }
1568
+
1569
+ function syncCustomControls() {
1570
+ const backgroundCustomSelected = selectedValue('[data-avatar-background]', '') === 'custom';
1571
+ const borderCustomSelected = selectedValue('[data-avatar-border]', '') === 'custom';
1572
+ if (backgroundCustomControls) backgroundCustomControls.hidden = !backgroundCustomSelected;
1573
+ if (borderCustomControls) borderCustomControls.hidden = !borderCustomSelected;
1574
+ const backgroundChip = document.querySelector('[data-avatar-background="custom"]');
1575
+ const borderChip = document.querySelector('[data-avatar-border="custom"]');
1576
+ const backgroundHex = normalizeHex(backgroundCustomText?.value || backgroundCustomColor?.value);
1577
+ const borderHex = normalizeHex(borderCustomText?.value || borderCustomColor?.value);
1578
+ backgroundChip?.setAttribute('data-avatar-color', backgroundHex);
1579
+ borderChip?.setAttribute('data-avatar-color', borderHex);
1580
+ if (backgroundCustomColor && backgroundCustomColor.value !== backgroundHex) backgroundCustomColor.value = backgroundHex;
1581
+ if (borderCustomColor && borderCustomColor.value !== borderHex) borderCustomColor.value = borderHex;
1582
+ }
1583
+
1584
+ function addShapePath(context, size, shape, inset = 0) {
1585
+ const edge = size - inset * 2;
1586
+ const radius = Math.min(edge / 2, Math.max(0, size * 0.22 - inset));
1587
+ context.beginPath();
1588
+ if (shape === 'round') {
1589
+ context.arc(size / 2, size / 2, edge / 2, 0, Math.PI * 2);
1590
+ context.closePath();
1591
+ return;
1592
+ }
1593
+ if (shape === 'rounded' && typeof context.roundRect === 'function') {
1594
+ context.roundRect(inset, inset, edge, edge, radius);
1595
+ context.closePath();
1596
+ return;
1597
+ }
1598
+ context.rect(inset, inset, edge, edge);
1599
+ context.closePath();
1600
+ }
1601
+
1602
+ function loadImage(src) {
1603
+ return new Promise((resolve, reject) => {
1604
+ const image = new Image();
1605
+ image.onload = () => resolve(image);
1606
+ image.onerror = reject;
1607
+ image.src = src;
1608
+ });
1609
+ }
1610
+
1611
+ async function drawAvatar(targetCanvas, size) {
1612
+ if (!targetCanvas) return;
1613
+ const context = targetCanvas.getContext('2d');
1614
+ const icon = selectedValue('[data-avatar-icon]', '');
1615
+ const background = selectedDataset('[data-avatar-background]', 'data-avatar-color', '');
1616
+ const border = selectedDataset('[data-avatar-border]', 'data-avatar-color', '#05070b');
1617
+ const shape = selectedValue('[data-avatar-shape]', 'square');
1618
+ const thickness = selectedValue('[data-avatar-thickness]', 'none');
1619
+ const padding = Number(paddingInput?.value || 18);
1620
+ const borderThickness = Math.round((size * (96 / 512) * (thicknessRatios[thickness] || 0)) / 4) * 4;
1621
+ const image = await loadImage(icon);
1622
+ const maxIconWidth = size * ((100 - padding * 2) / 100);
1623
+ const ratio = Math.min(maxIconWidth / image.width, maxIconWidth / image.height);
1624
+ const imageWidth = image.width * ratio;
1625
+ const imageHeight = image.height * ratio;
1626
+ targetCanvas.width = size;
1627
+ targetCanvas.height = size;
1628
+ context.clearRect(0, 0, size, size);
1629
+ if (borderThickness > 0) {
1630
+ context.save();
1631
+ addShapePath(context, size, shape);
1632
+ addShapePath(context, size, shape, borderThickness);
1633
+ context.fillStyle = border || '#05070b';
1634
+ context.fill('evenodd');
1635
+ context.restore();
1636
+ }
1637
+ if (background) {
1638
+ context.save();
1639
+ addShapePath(context, size, shape, borderThickness);
1640
+ context.clip();
1641
+ context.fillStyle = background;
1642
+ context.fill();
1643
+ context.restore();
1644
+ }
1645
+ context.drawImage(image, (size - imageWidth) / 2, (size - imageHeight) / 2, imageWidth, imageHeight);
1646
+ if (surface) {
1647
+ surface.className = 'avatar-surface shape-' + shape + ((!background || shape !== 'square') ? ' checker' : '');
1648
+ }
1649
+ if (downloadAvatar) downloadAvatar.querySelector('span:last-child').textContent = 'Download PNG (' + selectedValue('[data-avatar-size]', '1024') + 'px)';
1650
+ }
1651
+
1652
+ function selectChip(button, selector) {
1653
+ document.querySelectorAll(selector).forEach((chip) => chip.classList.remove('is-selected'));
1654
+ button.classList.add('is-selected');
1655
+ syncCustomControls();
1656
+ void drawAvatar(canvas, 1024);
1657
+ }
1658
+
1659
+ ['[data-avatar-icon]', '[data-avatar-background]', '[data-avatar-shape]', '[data-avatar-border]', '[data-avatar-thickness]', '[data-avatar-size]'].forEach((selector) => {
1660
+ document.querySelectorAll(selector).forEach((button) => {
1661
+ button.addEventListener('click', () => selectChip(button, selector));
1662
+ });
1663
+ });
1664
+ [backgroundCustomColor, backgroundCustomText].forEach((input) => {
1665
+ input?.addEventListener('input', () => {
1666
+ const next = normalizeHex(input.value);
1667
+ if (backgroundCustomColor) backgroundCustomColor.value = next;
1668
+ if (backgroundCustomText) backgroundCustomText.value = next;
1669
+ syncCustomControls();
1670
+ void drawAvatar(canvas, 1024);
1671
+ });
1672
+ });
1673
+ [borderCustomColor, borderCustomText].forEach((input) => {
1674
+ input?.addEventListener('input', () => {
1675
+ const next = normalizeHex(input.value);
1676
+ if (borderCustomColor) borderCustomColor.value = next;
1677
+ if (borderCustomText) borderCustomText.value = next;
1678
+ syncCustomControls();
1679
+ void drawAvatar(canvas, 1024);
1680
+ });
1681
+ });
1682
+ paddingInput?.addEventListener('input', () => void drawAvatar(canvas, 1024));
1683
+ resetAvatar?.addEventListener('click', () => {
1684
+ ['[data-avatar-icon]', '[data-avatar-background]', '[data-avatar-shape]', '[data-avatar-border]', '[data-avatar-thickness]'].forEach((selector) => {
1685
+ const chips = document.querySelectorAll(selector);
1686
+ chips.forEach((chip) => chip.classList.remove('is-selected'));
1687
+ chips[0]?.classList.add('is-selected');
1688
+ });
1689
+ const sizes = document.querySelectorAll('[data-avatar-size]');
1690
+ sizes.forEach((chip) => chip.classList.remove('is-selected'));
1691
+ sizes[1]?.classList.add('is-selected');
1692
+ if (paddingInput) paddingInput.value = '18';
1693
+ if (avatarStatus) avatarStatus.textContent = '';
1694
+ syncCustomControls();
1695
+ void drawAvatar(canvas, 1024);
1696
+ });
1697
+
1698
+ async function downloadCanvasPng(fileName, size) {
1699
+ const exportCanvas = document.createElement('canvas');
1700
+ await drawAvatar(exportCanvas, size);
1701
+ const blob = await new Promise((resolve) => exportCanvas.toBlob(resolve, 'image/png'));
1702
+ const url = URL.createObjectURL(blob);
1703
+ const link = document.createElement('a');
1704
+ link.href = url;
1705
+ link.download = fileName;
1706
+ document.body.appendChild(link);
1707
+ link.click();
1708
+ link.remove();
1709
+ window.setTimeout(() => URL.revokeObjectURL(url), 1000);
1710
+ }
1711
+
1712
+ downloadAvatar?.addEventListener('click', () => {
1713
+ const size = Number(selectedValue('[data-avatar-size]', '1024'));
1714
+ void downloadCanvasPng('brand-avatar-' + size + 'px.png', size);
1715
+ });
1716
+ downloadFavicons?.addEventListener('click', async () => {
1717
+ for (const size of [16, 32, 180, 192, 512]) {
1718
+ await downloadCanvasPng(size === 180 ? 'apple-touch-icon.png' : 'favicon-' + size + 'x' + size + '.png', size);
1719
+ }
1720
+ if (avatarStatus) avatarStatus.textContent = 'Downloaded favicon PNGs.';
1721
+ });
1722
+ syncCustomControls();
1723
+ void drawAvatar(canvas, 1024);
1724
+ `;
1725
+ }
1726
+ export function generateStaticBrandKitPage(manifest) {
1727
+ const heroAsset = findHeroAsset(manifest.assetGroups);
1728
+ const footerAsset = findFooterAsset(manifest.assetGroups);
1729
+ const assetCount = manifest.assetGroups.reduce((total, group) => total + group.items.length, 0);
1730
+ const bannerAssetCount = manifest.bannerGroups.reduce((total, group) => total + group.items.length, 0);
1731
+ const totalAssetCount = assetCount + bannerAssetCount;
1732
+ const brandLabel = manifest.brand.shortName ?? manifest.brand.name;
1733
+ return `<!doctype html>
1734
+ <html lang="en">
1735
+ <head>
1736
+ <meta charset="utf-8" />
1737
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1738
+ <title>${escapeHtml(manifest.brand.name)} Brand Kit</title>
1739
+ <meta name="description" content="${escapeHtml(manifest.brand.description ?? deterministicIntro)}" />
1740
+ <style>${pageStyles}</style>
1741
+ </head>
1742
+ <body>
1743
+ <header class="header">
1744
+ <div class="wrap header-inner">
1745
+ <div>
1746
+ <a class="brand-link" href="${escapeHtml(manifest.brand.homeUrl ?? '/')}">${escapeHtml(brandLabel)}</a>
1747
+ <h1>Brand Kit</h1>
1748
+ <p class="copy">${escapeHtml(deterministicIntro)}</p>
1749
+ <nav class="nav" aria-label="Brand Kit sections">
1750
+ <a class="nav-link" href="#logos">${lucideIcon('arrow-down', 'nav-arrow')}<span>Logos</span></a>
1751
+ <a class="nav-link" href="#colors">${lucideIcon('arrow-down', 'nav-arrow')}<span>Colors</span></a>
1752
+ ${manifest.bannerGroups.length ? `<a class="nav-link" href="#banners">${lucideIcon('arrow-down', 'nav-arrow')}<span>Banners</span></a>` : ''}
1753
+ <span class="asset-count">${totalAssetCount} ${totalAssetCount === 1 ? 'asset' : 'assets'} available</span>
1754
+ ${manifest.downloads.allAssets ? `<a class="button" href="${escapeHtml(manifest.downloads.allAssets)}" download>${lucideIcon('download')}<span>Download all</span></a>` : ''}
1755
+ </nav>
1756
+ </div>
1757
+ ${heroAsset
1758
+ ? `<div class="hero-media"><div class="hero-asset"><img src="${escapeHtml(heroAsset.previewUrl)}" alt="${escapeHtml(heroAsset.title)}" /></div></div>`
1759
+ : ''}
1760
+ </div>
1761
+ </header>
1762
+ <main>
1763
+ <section id="logos" class="section-muted">
1764
+ <div class="wrap section-inner">
1765
+ <div class="section-heading">
1766
+ <p class="eyebrow">Logos</p>
1767
+ <h2>Approved marks</h2>
1768
+ </div>
1769
+ <div class="stack">
1770
+ ${manifest.assetGroups
1771
+ .map((group) => renderAssetGroup(group, manifest.downloads.assetGroups?.[group.key] ??
1772
+ `./downloads/${group.key}.zip`))
1773
+ .join('')}
1774
+ ${renderAvatarGenerator(manifest)}
1775
+ </div>
1776
+ </div>
1777
+ </section>
1778
+ <section id="colors" class="section">
1779
+ <div class="wrap section-inner">
1780
+ <div class="section-heading">
1781
+ <p class="eyebrow">Colors</p>
1782
+ <h2>Color system</h2>
1783
+ </div>
1784
+ <div class="color-group">
1785
+ <div>
1786
+ <h3>Brand Colors</h3>
1787
+ <p>Core digital colors for product and web work.</p>
1788
+ </div>
1789
+ <div class="color-sections">${renderColors(manifest)}</div>
1790
+ ${renderPrintColorGroups(manifest.printColorGroups ?? [])}
1791
+ </div>
1792
+ </div>
1793
+ </section>
1794
+ ${renderBanners(manifest)}
1795
+ </main>
1796
+ <footer class="footer">
1797
+ <div class="wrap footer-inner">
1798
+ ${footerAsset
1799
+ ? `<div class="footer-logo"><img src="${escapeHtml(footerAsset.previewUrl)}" alt="${escapeHtml(footerAsset.title)}" /></div>`
1800
+ : ''}
1801
+ <p>&copy; ${new Date().getFullYear()} ${escapeHtml(manifest.brand.name)}. All rights reserved.</p>
1802
+ </div>
1803
+ </footer>
1804
+ <div class="lightbox" id="lightbox" role="dialog" aria-modal="true" hidden>
1805
+ <div class="lightbox-align">
1806
+ <div class="lightbox-panel">
1807
+ <div class="lightbox-preview">
1808
+ <div class="lightbox-checker checker">
1809
+ <img id="lightbox-image" alt="" />
1810
+ </div>
1811
+ </div>
1812
+ <div class="lightbox-body">
1813
+ <div>
1814
+ <h2 class="lightbox-title" id="lightbox-title"></h2>
1815
+ <p class="lightbox-file" id="lightbox-file"></p>
1816
+ </div>
1817
+ <div class="button-row">
1818
+ <button class="button" type="button" id="lightbox-close">Close</button>
1819
+ <span class="button-row" id="lightbox-downloads"></span>
1820
+ </div>
1821
+ </div>
1822
+ </div>
1823
+ </div>
1824
+ </div>
1825
+ <script>${clientScript(manifest)}</script>
1826
+ </body>
1827
+ </html>
1828
+ `;
1829
+ }
1830
+ //# sourceMappingURL=static-page.js.map