@nukipa/post-content 0.1.0 → 0.2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nukipa/post-content",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Framework-agnostic processor for Nukipa CMS post bodies — markdown + component markers + citations → HTML + interactive island descriptors.",
5
5
  "license": "MIT",
6
6
  "author": "Nukipa Labs",
@@ -21,9 +21,10 @@
21
21
  ".": {
22
22
  "types": "./src/index.d.ts",
23
23
  "import": "./src/index.js"
24
- }
24
+ },
25
+ "./styles.css": "./styles.css"
25
26
  },
26
- "files": ["src", "README.md"],
27
+ "files": ["src", "styles.css", "README.md"],
27
28
  "peerDependencies": {
28
29
  "marked": "^17.0.0"
29
30
  },
package/src/components.js CHANGED
@@ -358,22 +358,192 @@ function renderQuote(content) {
358
358
  };
359
359
  }
360
360
 
361
+ /**
362
+ * `referenced_blog_post` — an embeddable card pointing at another blog
363
+ * post. Same render output whether it's embedded inside a CMS blog
364
+ * post (via cms.post_components) or inside a newsletter issue (via
365
+ * the `{{post:UUID}}` shorthand the newsletters worker resolves).
366
+ *
367
+ * Content shape:
368
+ * { post_id, title?, url?, excerpt?, cover_url?, brand? }
369
+ *
370
+ * The host resolves `post_id` to the canonical metadata before render;
371
+ * we accept `title` / `url` / etc. on the content row so a pre-resolved
372
+ * snapshot can be passed in (newsletters does this — the worker has
373
+ * already fetched cms.posts when expanding markers, and passes the
374
+ * fields straight through).
375
+ *
376
+ * The HTML is email-safe (table + inline CSS) so the same renderer
377
+ * works for both web blog posts and email newsletters without
378
+ * branching.
379
+ */
380
+ /**
381
+ * `referenced_blog_post` renderer with two styles:
382
+ *
383
+ * - 'card' (default) — full-width card: hero cover on top, title,
384
+ * excerpt, prominent CTA button. The hero card.
385
+ * - 'compact' — row layout: small 96-px square cover on the left,
386
+ * title + truncated excerpt on the right,
387
+ * inline "Read more →" link (no CTA button).
388
+ * Half the vertical real estate of 'card', so
389
+ * it works as a list-style "see also" rail.
390
+ *
391
+ * Both shapes use the same brand colors and the same table-based
392
+ * email-safe markup; the only difference is the row layout. We render
393
+ * inline CSS only (Gmail strips <style> at the inbox).
394
+ */
395
+ function renderReferencedPost(content = {}) {
396
+ const style = content.style === 'compact' ? 'compact' : 'card';
397
+ return style === 'compact'
398
+ ? renderReferencedPostCompact(content)
399
+ : renderReferencedPostCard(content);
400
+ }
401
+
402
+ function commonRefPostInputs(content) {
403
+ const safe = (s) => String(s || '').replace(/[&<>"]/g, (c) => ({
404
+ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;'
405
+ }[c]));
406
+ const url = content.url || '';
407
+ const title = content.title || 'Untitled post';
408
+ const excerpt = content.excerpt || '';
409
+ const cover = content.cover_url || '';
410
+ const brand = content.brand || {};
411
+ return {
412
+ safe,
413
+ url, title, excerpt, cover,
414
+ primary: brand.primary_color || '#0054C9',
415
+ text: brand.text_color || '#001D21',
416
+ muted: '#5a5a5a',
417
+ card: '#ffffff',
418
+ border: '#e5e7eb'
419
+ };
420
+ }
421
+
422
+ function renderReferencedPostCard(content) {
423
+ const { safe, url, title, excerpt, cover, primary, text, muted, card, border } =
424
+ commonRefPostInputs(content);
425
+
426
+ const coverEl = cover
427
+ ? `<a href="${safe(url)}" target="_blank" style="text-decoration:none;display:block">
428
+ <img src="${safe(cover)}" alt="${safe(title)}" width="600"
429
+ style="display:block;width:100%;max-width:600px;height:auto;border:0;outline:none;text-decoration:none" />
430
+ </a>`
431
+ : `<a href="${safe(url)}" target="_blank" style="text-decoration:none;display:block">
432
+ <div style="width:100%;height:180px;background:${primary};
433
+ display:flex;align-items:center;justify-content:center;
434
+ padding:0 24px;color:#fff;font-weight:700;font-size:22px;
435
+ line-height:1.2;text-align:center;font-family:inherit">
436
+ ${safe(title)}
437
+ </div>
438
+ </a>`;
439
+
440
+ const html = `
441
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"
442
+ class="bp-component bp-referenced-post bp-referenced-post--card"
443
+ data-style="card"
444
+ style="border-collapse:collapse;margin:24px 0;background:${card};border:1px solid ${border};border-radius:10px;overflow:hidden">
445
+ <tr><td style="padding:0">${coverEl}</td></tr>
446
+ <tr><td style="padding:18px 22px 6px">
447
+ <a href="${safe(url)}" target="_blank"
448
+ style="font-size:19px;font-weight:700;line-height:1.25;color:${text};text-decoration:none">
449
+ ${safe(title)}
450
+ </a>
451
+ </td></tr>
452
+ ${excerpt ? `
453
+ <tr><td style="padding:6px 22px 14px;color:${muted};font-size:14px;line-height:1.5">
454
+ ${safe(excerpt)}
455
+ </td></tr>` : ''}
456
+ <tr><td style="padding:0 22px 22px">
457
+ <a href="${safe(url)}" target="_blank"
458
+ style="display:inline-block;padding:10px 16px;background:${primary};color:#ffffff;
459
+ font-weight:600;font-size:14px;text-decoration:none;border-radius:8px">
460
+ Read the article →
461
+ </a>
462
+ </td></tr>
463
+ </table>`.replace(/\s*\n\s*/g, ' ').trim();
464
+ return { html };
465
+ }
466
+
467
+ function renderReferencedPostCompact(content) {
468
+ const { safe, url, title, excerpt, cover, primary, text, muted, card, border } =
469
+ commonRefPostInputs(content);
470
+
471
+ // 96-px square cover keeps the row Outlook-safe (a width attribute on
472
+ // the <td> with fixed pixel values is the most reliable). When no
473
+ // cover exists, fall back to a brand-coloured tile with a tiny
474
+ // initial letter so the row still has visual presence.
475
+ const coverCell = cover
476
+ ? `<td width="96" style="width:96px;padding:0;vertical-align:top">
477
+ <a href="${safe(url)}" target="_blank" style="display:block">
478
+ <img src="${safe(cover)}" alt="${safe(title)}" width="96" height="96"
479
+ style="display:block;width:96px;height:96px;object-fit:cover;border:0" />
480
+ </a>
481
+ </td>`
482
+ : `<td width="96" style="width:96px;padding:0;vertical-align:top">
483
+ <a href="${safe(url)}" target="_blank" style="display:block">
484
+ <div style="width:96px;height:96px;background:${primary};
485
+ display:flex;align-items:center;justify-content:center;
486
+ color:#fff;font-weight:700;font-size:24px;font-family:inherit;
487
+ line-height:1;text-align:center">
488
+ ${safe(title.charAt(0).toUpperCase())}
489
+ </div>
490
+ </a>
491
+ </td>`;
492
+
493
+ // Excerpt is truncated to ~140 chars for the compact row. The card
494
+ // style keeps the full excerpt; this one trades fidelity for density.
495
+ const trimmedExcerpt = excerpt && excerpt.length > 140
496
+ ? excerpt.slice(0, 137).replace(/\s+\S*$/, '') + '…'
497
+ : excerpt;
498
+
499
+ const html = `
500
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"
501
+ class="bp-component bp-referenced-post bp-referenced-post--compact"
502
+ data-style="compact"
503
+ style="border-collapse:collapse;margin:14px 0;background:${card};border:1px solid ${border};border-radius:10px;overflow:hidden">
504
+ <tr>
505
+ ${coverCell}
506
+ <td style="padding:12px 14px;vertical-align:top">
507
+ <a href="${safe(url)}" target="_blank"
508
+ style="font-size:15px;font-weight:700;line-height:1.3;color:${text};text-decoration:none;display:block">
509
+ ${safe(title)}
510
+ </a>
511
+ ${trimmedExcerpt ? `
512
+ <p style="margin:4px 0 6px;color:${muted};font-size:13px;line-height:1.45">
513
+ ${safe(trimmedExcerpt)}
514
+ </p>` : ''}
515
+ <a href="${safe(url)}" target="_blank"
516
+ style="display:inline-block;color:${primary};font-weight:600;font-size:13px;text-decoration:none">
517
+ Read more →
518
+ </a>
519
+ </td>
520
+ </tr>
521
+ </table>`.replace(/\s*\n\s*/g, ' ').trim();
522
+ return { html };
523
+ }
524
+
361
525
  const RENDERERS = {
362
- cta: (c) => renderCta(c),
363
- form: (c) => renderForm(c),
364
- contact_form: (c) => renderContactForm(c),
365
- callout: (c) => renderCallout(c.content || {}),
366
- faq: (c) => renderFaq(c.content || {}),
367
- steps: (c) => renderSteps(c.content || {}),
368
- card: (c) => renderCard(c.content || {}),
369
- data_table: (c) => renderDataTable(c.content || {}),
370
- comparison: (c) => renderComparison(c.content || {}),
371
- process: (c) => renderProcess(c.content || {}),
372
- image: (c) => renderImage(c),
373
- image_carousel: (c) => renderImageCarousel(c.content || {}),
374
- chart: (c) => renderChart(c.content || {}),
375
- widget: (c, opts) => renderWidget(c, opts),
376
- quote: (c) => renderQuote(c.content || {})
526
+ cta: (c) => renderCta(c),
527
+ form: (c) => renderForm(c),
528
+ contact_form: (c) => renderContactForm(c),
529
+ callout: (c) => renderCallout(c.content || {}),
530
+ faq: (c) => renderFaq(c.content || {}),
531
+ steps: (c) => renderSteps(c.content || {}),
532
+ card: (c) => renderCard(c.content || {}),
533
+ data_table: (c) => renderDataTable(c.content || {}),
534
+ comparison: (c) => renderComparison(c.content || {}),
535
+ process: (c) => renderProcess(c.content || {}),
536
+ image: (c) => renderImage(c),
537
+ image_carousel: (c) => renderImageCarousel(c.content || {}),
538
+ chart: (c) => renderChart(c.content || {}),
539
+ widget: (c, opts) => renderWidget(c, opts),
540
+ quote: (c) => renderQuote(c.content || {}),
541
+ // Referenced blog-post card. Same renderer used by both the CMS
542
+ // post-renderer pipeline (when authors embed another post via the
543
+ // editor) and the newsletters send/preview pipeline (which converts
544
+ // `{{post:UUID}}` markers into a pre-resolved component-shaped object
545
+ // on the fly — see services/newsletters/src/lib/postEmbeds.js).
546
+ referenced_blog_post: (c) => renderReferencedPost(c.content || {})
377
547
  };
378
548
 
379
549
  /**
package/styles.css ADDED
@@ -0,0 +1,469 @@
1
+ /*
2
+ * Material Icons — pulled at the top so the @font-face rule wins the
3
+ * cascade before any of our `.material-icons` selectors below. The
4
+ * renderer emits glyph names verbatim (`help_outline`, `expand_more`,
5
+ * `arrow_forward`, `chevron_left`, `chevron_right`, plus whatever
6
+ * stage icons the post author picked); without this font those names
7
+ * render as literal text. To self-host (offline, privacy, or perf
8
+ * reasons): drop this @import, host the same font + matching CSS at
9
+ * the host, and the rest of this stylesheet keeps working unchanged.
10
+ */
11
+ @import url("https://fonts.googleapis.com/icon?family=Material+Icons");
12
+
13
+ /*
14
+ * Nukipa post-content default stylesheet.
15
+ *
16
+ * Ship-ready CSS for every component this package's renderPostBody()
17
+ * emits — callouts, FAQs, carousels, the inline CTA block, etc.
18
+ * Drop this into a Next.js layout (or any HTML host) and the
19
+ * rendered post is styled out of the box, no copying of ~800 lines
20
+ * of selectors.
21
+ *
22
+ * import '@nukipa/post-content/styles.css';
23
+ *
24
+ * THEME TOKENS — every color/spacing value falls back to a sensible
25
+ * default. To re-theme, set the matching CSS variable on `:root` (or
26
+ * any ancestor of the post body):
27
+ *
28
+ * :root {
29
+ * --color-primary: #0054C9;
30
+ * --color-primary-contrast:#FFFFFF;
31
+ * --color-bg: #FFFFFF;
32
+ * --color-text: #001D21;
33
+ * --color-text-muted: #51646A;
34
+ * --color-border: #E5DFDC;
35
+ * --color-surface: #F0EBEB;
36
+ * }
37
+ *
38
+ * Selectors target the classes the package emits directly — no host
39
+ * scope prefix (e.g. `.article-content` in apps/public). If you need
40
+ * to scope these rules to a single subtree, nest the @import in a
41
+ * cascade layer or wrap the body in a class and re-author the rules
42
+ * — this is the canonical stylesheet.
43
+ */
44
+
45
+ /* ───── Container + prose typography ──────────────────────────────── */
46
+ .prose-body {
47
+ max-width: 70ch;
48
+ line-height: 1.75;
49
+ font-size: 1.0625rem;
50
+ color: var(--color-text, #001D21);
51
+ }
52
+ .prose-body h1, .prose-body h2, .prose-body h3 {
53
+ font-weight: 600;
54
+ letter-spacing: -0.02em;
55
+ margin-top: 1.6em;
56
+ margin-bottom: 0.5em;
57
+ }
58
+ .prose-body h1 { font-size: 2rem; }
59
+ .prose-body h2 { font-size: 1.5rem; }
60
+ .prose-body h3 { font-size: 1.25rem; }
61
+ .prose-body p { margin: 0 0 1.1em; }
62
+ .prose-body a {
63
+ color: var(--color-primary, #0054C9);
64
+ text-decoration: underline;
65
+ text-underline-offset: 2px;
66
+ }
67
+ .prose-body code {
68
+ background: var(--color-surface, #F0EBEB);
69
+ padding: 0.1em 0.35em;
70
+ border-radius: 6px;
71
+ font-size: 0.9em;
72
+ font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace);
73
+ }
74
+ .prose-body pre {
75
+ background: var(--color-dark, #1F2732);
76
+ color: var(--color-on-dark, #EAF1F8);
77
+ padding: 1.1em;
78
+ border-radius: 16px;
79
+ overflow-x: auto;
80
+ }
81
+ .prose-body pre code { background: transparent; padding: 0; }
82
+ .prose-body img { border-radius: 16px; margin: 1.6em 0; max-width: 100%; height: auto; }
83
+ .prose-body blockquote {
84
+ border-left: 2px solid var(--color-primary, #0054C9);
85
+ padding-left: 1em;
86
+ color: var(--color-text-muted, #51646A);
87
+ margin: 1.2em 0;
88
+ }
89
+ .prose-body ul, .prose-body ol { padding-left: 1.4em; margin: 0 0 1.1em; }
90
+ .prose-body li { margin: 0.3em 0; }
91
+
92
+ /* ───── Component wrapper (every bp-* gets one) ────────────────────── */
93
+ .bp-component { margin: 1.5rem 0; }
94
+
95
+ /* ───── Callouts ───────────────────────────────────────────────────── */
96
+ .bp-callout {
97
+ padding: 0.75rem 1rem;
98
+ border-radius: 0 0.5rem 0.5rem 0;
99
+ border-left: 4px solid var(--color-border, #E5DFDC);
100
+ background: var(--color-surface, #F0EBEB);
101
+ }
102
+ .bp-callout__header {
103
+ font-weight: 600;
104
+ font-size: 0.8125rem;
105
+ text-transform: uppercase;
106
+ letter-spacing: 0.03em;
107
+ margin-bottom: 0.375rem;
108
+ }
109
+ .bp-callout__body p:last-child { margin-bottom: 0; }
110
+ .bp-callout--note { background: color-mix(in srgb, #3b82f6 8%, var(--color-bg, #FFFFFF)); border-left-color: #3b82f6; }
111
+ .bp-callout--note .bp-callout__header { color: #3b82f6; }
112
+ .bp-callout--tip { background: color-mix(in srgb, #10b981 8%, var(--color-bg, #FFFFFF)); border-left-color: #10b981; }
113
+ .bp-callout--tip .bp-callout__header { color: #10b981; }
114
+ .bp-callout--important { background: color-mix(in srgb, var(--color-primary, #0054C9) 8%, var(--color-bg, #FFFFFF)); border-left-color: var(--color-primary, #0054C9); }
115
+ .bp-callout--important .bp-callout__header { color: var(--color-primary, #0054C9); }
116
+ .bp-callout--warning { background: color-mix(in srgb, #f59e0b 8%, var(--color-bg, #FFFFFF)); border-left-color: #f59e0b; }
117
+ .bp-callout--warning .bp-callout__header { color: #f59e0b; }
118
+ .bp-callout--caution { background: color-mix(in srgb, #ef4444 8%, var(--color-bg, #FFFFFF)); border-left-color: #ef4444; }
119
+ .bp-callout--caution .bp-callout__header { color: #ef4444; }
120
+
121
+ /* ───── FAQ ────────────────────────────────────────────────────────── */
122
+ .bp-faq {
123
+ border: 1px solid var(--color-border, #E5DFDC);
124
+ border-radius: 0.75rem;
125
+ overflow: hidden;
126
+ }
127
+ .bp-faq-item { border-bottom: 1px solid var(--color-border, #E5DFDC); }
128
+ .bp-faq-item:last-child { border-bottom: none; }
129
+ .bp-faq-item summary {
130
+ padding: 0.875rem 1.25rem;
131
+ font-weight: 600;
132
+ font-size: 1rem;
133
+ cursor: pointer;
134
+ list-style: none;
135
+ display: flex;
136
+ align-items: center;
137
+ gap: 0.625rem;
138
+ color: var(--color-text, #001D21);
139
+ transition: background 0.15s;
140
+ }
141
+ .bp-faq-item summary:hover { background: var(--color-surface, #F0EBEB); }
142
+ .bp-faq-item summary::-webkit-details-marker { display: none; }
143
+ .bp-faq-icon { font-size: 1.25rem; color: var(--color-primary, #0054C9); flex-shrink: 0; }
144
+ .bp-faq-question { flex: 1; }
145
+ .bp-faq-chevron { font-size: 1.375rem; color: var(--color-text-muted, #51646A); transition: transform 0.2s; flex-shrink: 0; }
146
+ .bp-faq-item[open] .bp-faq-chevron { transform: rotate(180deg); }
147
+ .bp-faq-answer { padding: 0 1.25rem 1rem 3rem; color: var(--color-text-muted, #51646A); }
148
+ .bp-faq-answer p:last-child { margin-bottom: 0; }
149
+
150
+ /* ───── Steps — vertical timeline ──────────────────────────────────── */
151
+ .bp-steps { padding-left: 0; }
152
+ .bp-step { display: flex; gap: 1.25rem; }
153
+ .bp-step__marker {
154
+ display: flex; flex-direction: column; align-items: center; flex-shrink: 0;
155
+ }
156
+ .bp-step__num {
157
+ width: 2.25rem; height: 2.25rem; border-radius: 50%;
158
+ background: var(--color-primary, #0054C9);
159
+ color: var(--color-primary-contrast, #FFFFFF);
160
+ display: flex; align-items: center; justify-content: center;
161
+ font-weight: 700; font-size: 0.9rem; flex-shrink: 0;
162
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-primary, #0054C9) 15%, transparent);
163
+ }
164
+ .bp-step__line {
165
+ width: 2px; flex: 1; min-height: 1.5rem;
166
+ background: color-mix(in srgb, var(--color-primary, #0054C9) 25%, transparent);
167
+ }
168
+ .bp-step__content { padding-bottom: 2rem; flex: 1; }
169
+ .bp-step--last .bp-step__content { padding-bottom: 0; }
170
+ .bp-step__content strong { font-size: 1.0625rem; display: block; margin-bottom: 0.25rem; }
171
+ .bp-step__content p:last-child { margin-bottom: 0; }
172
+
173
+ /* ───── Card ───────────────────────────────────────────────────────── */
174
+ .bp-card {
175
+ border: 1px solid var(--color-border, #E5DFDC);
176
+ border-radius: 0.75rem;
177
+ overflow: hidden;
178
+ background: var(--color-surface, #F0EBEB);
179
+ box-shadow: 0 2px 12px rgba(0,0,0,0.1);
180
+ border-left: 4px solid var(--color-primary, #0054C9);
181
+ }
182
+ .bp-card__img { width: 100%; height: auto; display: block; }
183
+ .bp-card__body { padding: 1.25rem 1.5rem; }
184
+ .bp-card__title { font-weight: 700; font-size: 1.125rem; margin-bottom: 0.5rem; color: var(--color-primary, #0054C9); }
185
+ .bp-card__body p { color: var(--color-text-muted, #51646A); }
186
+ .bp-card__body p:last-child { margin-bottom: 0; }
187
+
188
+ /* ───── Chart (canvas placeholder, host hydrates) ──────────────────── */
189
+ .bp-chart { border: 1px solid var(--color-border, #E5DFDC); border-radius: 0.5rem; padding: 1rem; }
190
+ .bp-chart__title { font-weight: 600; margin-bottom: 0.5rem; }
191
+ .bp-chart__canvas { width: 100%; max-height: 400px; }
192
+
193
+ /* ───── Data Table ─────────────────────────────────────────────────── */
194
+ .bp-data-table {
195
+ border: 1px solid var(--color-border, #E5DFDC);
196
+ border-radius: 0.5rem;
197
+ overflow-x: auto;
198
+ -webkit-overflow-scrolling: touch;
199
+ }
200
+ .bp-data-table__title {
201
+ font-weight: 600; padding: 0.75rem 1rem;
202
+ background: var(--color-surface, #F0EBEB);
203
+ border-bottom: 1px solid var(--color-border, #E5DFDC);
204
+ }
205
+ .bp-data-table table { margin: 0; width: max-content; min-width: 100%; }
206
+ .bp-data-table th, .bp-data-table td { min-width: 100px; }
207
+
208
+ /* ───── Image Carousel ─────────────────────────────────────────────── */
209
+ .bp-carousel {
210
+ position: relative;
211
+ border: 1px solid var(--color-border, #E5DFDC);
212
+ border-radius: 0.75rem;
213
+ overflow: hidden;
214
+ }
215
+ .bp-carousel__track { display: flex; overflow: hidden; }
216
+ .bp-carousel__slide { min-width: 100%; display: none; }
217
+ .bp-carousel__slide.active { display: block; }
218
+ .bp-carousel__slide img { width: 100%; height: auto; display: block; }
219
+ .bp-carousel__caption {
220
+ padding: 0.625rem 1rem;
221
+ font-size: 0.875rem;
222
+ color: var(--color-text-muted, #51646A);
223
+ text-align: center;
224
+ background: var(--color-surface, #F0EBEB);
225
+ }
226
+ .bp-carousel__prev, .bp-carousel__next {
227
+ position: absolute; top: 50%; transform: translateY(-50%);
228
+ background: rgba(0,0,0,0.5); color: #fff; border: none; border-radius: 50%;
229
+ width: 2.5rem; height: 2.5rem; padding: 0; cursor: pointer;
230
+ display: flex; align-items: center; justify-content: center;
231
+ backdrop-filter: blur(4px); transition: background 0.15s;
232
+ }
233
+ .bp-carousel__prev .material-icons, .bp-carousel__next .material-icons { font-size: 1.5rem; }
234
+ .bp-carousel__prev:hover, .bp-carousel__next:hover { background: rgba(0,0,0,0.7); }
235
+ .bp-carousel__prev { left: 0.75rem; }
236
+ .bp-carousel__next { right: 0.75rem; }
237
+
238
+ /* ───── Inline image (single figure) ───────────────────────────────── */
239
+ .bp-image {
240
+ margin: 1.75rem 0;
241
+ display: flex; flex-direction: column; align-items: center; gap: 0.5rem;
242
+ }
243
+ .bp-image img {
244
+ max-width: 100%; height: auto; display: block;
245
+ border-radius: 0.75rem;
246
+ border: 1px solid var(--color-border, #E5DFDC);
247
+ }
248
+ .bp-image__caption {
249
+ font-size: 0.9375rem;
250
+ color: var(--color-text-muted, #51646A);
251
+ text-align: center;
252
+ line-height: 1.5;
253
+ }
254
+ .bp-image__caption p { margin: 0; }
255
+ .bp-image__credit {
256
+ font-size: 0.75rem;
257
+ color: var(--color-text-muted, #51646A);
258
+ opacity: 0.85;
259
+ }
260
+ .bp-image__credit a { color: inherit; text-decoration: underline; }
261
+ .bp-image-placeholder {
262
+ margin: 1.75rem 0; padding: 2rem 1rem;
263
+ border: 1px dashed var(--color-border, #E5DFDC);
264
+ border-radius: 0.75rem;
265
+ text-align: center;
266
+ color: var(--color-text-muted, #51646A);
267
+ font-style: italic;
268
+ }
269
+
270
+ /* ───── Process — auto-fit grid of stages ─────────────────────────────
271
+ The renderer interleaves <div class="bp-process__stage"> and
272
+ <div class="bp-process__connector"> in document order. Laying that
273
+ out as wrapping flex put orphan connector arrows at the start of
274
+ wrapped rows (the connector that "belongs" between stage 4 and
275
+ stage 5 ended up alone on row 2). The grid below tracks columns
276
+ by stage width — connectors are hidden because they only encode
277
+ left-to-right adjacency, which is meaningless once the grid wraps
278
+ into multiple rows. */
279
+ .bp-process {
280
+ display: grid;
281
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
282
+ align-items: start;
283
+ justify-items: center;
284
+ gap: 1rem 0.5rem;
285
+ }
286
+ .bp-process__stage {
287
+ display: flex; flex-direction: column; align-items: center; text-align: center;
288
+ min-width: 0; padding: 1rem 0.75rem;
289
+ }
290
+ .bp-process__icon {
291
+ width: 3rem; height: 3rem; border-radius: 50%;
292
+ background: var(--color-primary, #0054C9);
293
+ color: var(--color-primary-contrast, #FFFFFF);
294
+ display: flex; align-items: center; justify-content: center;
295
+ font-weight: 700; font-size: 1rem; flex-shrink: 0; margin-bottom: 0.75rem;
296
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-primary, #0054C9) 15%, transparent);
297
+ }
298
+ .bp-process__icon .material-icons { font-size: 1.375rem; }
299
+ .bp-process__num { font-weight: 700; font-size: 1rem; }
300
+ .bp-process__title { font-weight: 600; font-size: 1rem; margin-bottom: 0.25rem; }
301
+ .bp-process__desc { font-size: 0.875rem; color: var(--color-text-muted, #51646A); line-height: 1.5; }
302
+ .bp-process__connector { display: none; }
303
+
304
+ /* ───── Comparison ─────────────────────────────────────────────────── */
305
+ .bp-comparison {
306
+ border: 1px solid var(--color-border, #E5DFDC);
307
+ border-radius: 0.5rem;
308
+ overflow-x: auto;
309
+ -webkit-overflow-scrolling: touch;
310
+ }
311
+ .bp-comparison table {
312
+ margin: 0; width: max-content; min-width: 100%;
313
+ border-collapse: collapse;
314
+ }
315
+ .bp-comparison th, .bp-comparison td {
316
+ min-width: 100px;
317
+ padding: 0.625rem 0.875rem;
318
+ border-bottom: 1px solid var(--color-border, #E5DFDC);
319
+ }
320
+ .bp-comparison thead th {
321
+ background: var(--color-surface, #F0EBEB);
322
+ font-weight: 600;
323
+ text-align: left;
324
+ border-bottom-width: 2px;
325
+ }
326
+ .bp-comparison tbody tr:last-child th, .bp-comparison tbody tr:last-child td { border-bottom: none; }
327
+ .bp-comparison tbody tr:hover td, .bp-comparison tbody tr:hover th { background: color-mix(in srgb, var(--color-surface, #F0EBEB) 60%, transparent); }
328
+ .bp-comparison__cell { text-align: center; }
329
+
330
+ /* ───── Cite chip — superscript footnote link ──────────────────────── */
331
+ .bp-cite {
332
+ display: inline-block;
333
+ font-size: 0.7em;
334
+ vertical-align: super;
335
+ line-height: 1;
336
+ padding: 1px 5px;
337
+ margin: 0 1px;
338
+ border-radius: 999px;
339
+ background: color-mix(in srgb, var(--color-primary, #0054C9) 12%, transparent);
340
+ color: var(--color-primary, #0054C9);
341
+ font-weight: 600;
342
+ }
343
+ .bp-cite a { color: inherit; text-decoration: none; }
344
+ .bp-cite a:hover { text-decoration: underline; }
345
+ .bp-cite--orphan {
346
+ background: var(--color-surface, #F0EBEB);
347
+ color: var(--color-text-muted, #51646A);
348
+ }
349
+
350
+ /* ───── Inline widget iframe ───────────────────────────────────────── */
351
+ .inline-widget-container {
352
+ margin: 2rem 0;
353
+ border-radius: 0.75rem;
354
+ overflow: visible;
355
+ }
356
+ .inline-widget-container iframe {
357
+ width: 100%;
358
+ border: none;
359
+ min-height: 200px;
360
+ }
361
+
362
+ /* ───── Inline CTA ─────────────────────────────────────────────────── */
363
+ .inline-cta-block {
364
+ display: flex;
365
+ flex-direction: column;
366
+ align-items: center;
367
+ gap: 0.75rem;
368
+ padding: 2.5rem 2rem;
369
+ margin: 2.5rem 0;
370
+ background: var(--color-surface, #F0EBEB);
371
+ border: 1px solid var(--color-border, #E5DFDC);
372
+ border-radius: 1rem;
373
+ text-align: center;
374
+ }
375
+ .inline-cta-description {
376
+ color: var(--color-text-muted, #51646A);
377
+ font-size: 0.9375rem;
378
+ line-height: 1.5;
379
+ margin: 0;
380
+ }
381
+ .inline-cta-btn {
382
+ display: inline-block;
383
+ padding: 0.75rem 2rem;
384
+ border-radius: 0.5rem;
385
+ font-size: 1rem;
386
+ font-weight: 600;
387
+ text-decoration: none;
388
+ cursor: pointer;
389
+ transition: all 0.2s ease;
390
+ border: none;
391
+ font-family: inherit;
392
+ }
393
+ .inline-cta-btn.cta-primary {
394
+ background: var(--color-primary, #0054C9);
395
+ color: var(--color-primary-contrast, #FFFFFF);
396
+ }
397
+ .inline-cta-btn.cta-primary:hover {
398
+ opacity: 0.9;
399
+ transform: translateY(-1px);
400
+ }
401
+ .inline-cta-btn.cta-outline {
402
+ background: transparent;
403
+ border: 2px solid var(--color-primary, #0054C9);
404
+ color: var(--color-primary, #0054C9);
405
+ }
406
+ .inline-cta-btn.cta-outline:hover {
407
+ background: color-mix(in srgb, var(--color-primary, #0054C9) 10%, transparent);
408
+ }
409
+ .inline-cta-btn.cta-subtle {
410
+ background: transparent;
411
+ color: var(--color-primary, #0054C9);
412
+ text-decoration: underline;
413
+ padding: 0.5rem 0;
414
+ }
415
+ .inline-cta-btn.cta-subtle:hover { opacity: 0.8; }
416
+
417
+ /* ───── CTA Form ───────────────────────────────────────────────────── */
418
+ .inline-cta-form-block { padding: 2rem; }
419
+ .inline-cta-form-title {
420
+ font-size: 1.375rem;
421
+ font-weight: 700;
422
+ color: var(--color-text, #001D21);
423
+ margin: 0 0 1.25rem;
424
+ }
425
+ .inline-cta-form {
426
+ display: flex;
427
+ flex-direction: column;
428
+ gap: 0.75rem;
429
+ max-width: 480px;
430
+ width: 100%;
431
+ margin: 0 auto;
432
+ }
433
+ .inline-cta-input,
434
+ .inline-cta-textarea {
435
+ width: 100%;
436
+ padding: 0.75rem 1rem;
437
+ border: 1px solid var(--color-border, #E5DFDC);
438
+ border-radius: 0.5rem;
439
+ font-size: 0.9375rem;
440
+ font-family: inherit;
441
+ background: var(--color-bg, #FFFFFF);
442
+ color: var(--color-text, #001D21);
443
+ transition: border-color 0.2s ease;
444
+ box-sizing: border-box;
445
+ }
446
+ .inline-cta-input:focus,
447
+ .inline-cta-textarea:focus {
448
+ outline: none;
449
+ border-color: var(--color-primary, #0054C9);
450
+ }
451
+ .inline-cta-textarea {
452
+ resize: vertical;
453
+ min-height: 80px;
454
+ }
455
+ .inline-cta-submit { align-self: center; margin-top: 0.5rem; }
456
+ .inline-cta-success {
457
+ color: var(--color-primary, #0054C9);
458
+ font-weight: 600;
459
+ font-size: 1rem;
460
+ margin: 1rem 0;
461
+ }
462
+
463
+ /* ───── Mobile tweaks ──────────────────────────────────────────────── */
464
+ @media (max-width: 768px) {
465
+ .prose-body { font-size: 1rem; }
466
+ .prose-body h1 { font-size: 1.75rem; }
467
+ .prose-body h2 { font-size: 1.5rem; }
468
+ .prose-body h3 { font-size: 1.25rem; }
469
+ }