@nukipa/post-content 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.0",
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,432 @@
1
+ /*
2
+ * Nukipa post-content default stylesheet.
3
+ *
4
+ * Ship-ready CSS for every component this package's renderPostBody()
5
+ * emits — callouts, FAQs, carousels, the inline CTA block, etc.
6
+ * Drop this into a Next.js layout (or any HTML host) and the
7
+ * rendered post is styled out of the box, no copying of ~800 lines
8
+ * of selectors.
9
+ *
10
+ * import '@nukipa/post-content/styles.css';
11
+ *
12
+ * THEME TOKENS — every color/spacing value falls back to a sensible
13
+ * default. To re-theme, set the matching CSS variable on `:root` (or
14
+ * any ancestor of the post body):
15
+ *
16
+ * :root {
17
+ * --color-primary: #0054C9;
18
+ * --color-primary-contrast:#FFFFFF;
19
+ * --color-bg: #FFFFFF;
20
+ * --color-text: #001D21;
21
+ * --color-text-muted: #51646A;
22
+ * --color-border: #E5DFDC;
23
+ * --color-surface: #F0EBEB;
24
+ * }
25
+ *
26
+ * Selectors target the classes the package emits directly — no host
27
+ * scope prefix (e.g. `.article-content` in apps/public). If you need
28
+ * to scope these rules to a single subtree, nest the @import in a
29
+ * cascade layer or wrap the body in a class and re-author the rules
30
+ * — this is the canonical stylesheet.
31
+ */
32
+
33
+ /* ───── Container + prose typography ──────────────────────────────── */
34
+ .prose-body {
35
+ max-width: 70ch;
36
+ line-height: 1.75;
37
+ font-size: 1.0625rem;
38
+ color: var(--color-text, #001D21);
39
+ }
40
+ .prose-body h1, .prose-body h2, .prose-body h3 {
41
+ font-weight: 600;
42
+ letter-spacing: -0.02em;
43
+ margin-top: 1.6em;
44
+ margin-bottom: 0.5em;
45
+ }
46
+ .prose-body h1 { font-size: 2rem; }
47
+ .prose-body h2 { font-size: 1.5rem; }
48
+ .prose-body h3 { font-size: 1.25rem; }
49
+ .prose-body p { margin: 0 0 1.1em; }
50
+ .prose-body a {
51
+ color: var(--color-primary, #0054C9);
52
+ text-decoration: underline;
53
+ text-underline-offset: 2px;
54
+ }
55
+ .prose-body code {
56
+ background: var(--color-surface, #F0EBEB);
57
+ padding: 0.1em 0.35em;
58
+ border-radius: 6px;
59
+ font-size: 0.9em;
60
+ font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace);
61
+ }
62
+ .prose-body pre {
63
+ background: var(--color-dark, #1F2732);
64
+ color: var(--color-on-dark, #EAF1F8);
65
+ padding: 1.1em;
66
+ border-radius: 16px;
67
+ overflow-x: auto;
68
+ }
69
+ .prose-body pre code { background: transparent; padding: 0; }
70
+ .prose-body img { border-radius: 16px; margin: 1.6em 0; max-width: 100%; height: auto; }
71
+ .prose-body blockquote {
72
+ border-left: 2px solid var(--color-primary, #0054C9);
73
+ padding-left: 1em;
74
+ color: var(--color-text-muted, #51646A);
75
+ margin: 1.2em 0;
76
+ }
77
+ .prose-body ul, .prose-body ol { padding-left: 1.4em; margin: 0 0 1.1em; }
78
+ .prose-body li { margin: 0.3em 0; }
79
+
80
+ /* ───── Component wrapper (every bp-* gets one) ────────────────────── */
81
+ .bp-component { margin: 1.5rem 0; }
82
+
83
+ /* ───── Callouts ───────────────────────────────────────────────────── */
84
+ .bp-callout {
85
+ padding: 0.75rem 1rem;
86
+ border-radius: 0 0.5rem 0.5rem 0;
87
+ border-left: 4px solid var(--color-border, #E5DFDC);
88
+ background: var(--color-surface, #F0EBEB);
89
+ }
90
+ .bp-callout__header {
91
+ font-weight: 600;
92
+ font-size: 0.8125rem;
93
+ text-transform: uppercase;
94
+ letter-spacing: 0.03em;
95
+ margin-bottom: 0.375rem;
96
+ }
97
+ .bp-callout__body p:last-child { margin-bottom: 0; }
98
+ .bp-callout--note { background: color-mix(in srgb, #3b82f6 8%, var(--color-bg, #FFFFFF)); border-left-color: #3b82f6; }
99
+ .bp-callout--note .bp-callout__header { color: #3b82f6; }
100
+ .bp-callout--tip { background: color-mix(in srgb, #10b981 8%, var(--color-bg, #FFFFFF)); border-left-color: #10b981; }
101
+ .bp-callout--tip .bp-callout__header { color: #10b981; }
102
+ .bp-callout--important { background: color-mix(in srgb, var(--color-primary, #0054C9) 8%, var(--color-bg, #FFFFFF)); border-left-color: var(--color-primary, #0054C9); }
103
+ .bp-callout--important .bp-callout__header { color: var(--color-primary, #0054C9); }
104
+ .bp-callout--warning { background: color-mix(in srgb, #f59e0b 8%, var(--color-bg, #FFFFFF)); border-left-color: #f59e0b; }
105
+ .bp-callout--warning .bp-callout__header { color: #f59e0b; }
106
+ .bp-callout--caution { background: color-mix(in srgb, #ef4444 8%, var(--color-bg, #FFFFFF)); border-left-color: #ef4444; }
107
+ .bp-callout--caution .bp-callout__header { color: #ef4444; }
108
+
109
+ /* ───── FAQ ────────────────────────────────────────────────────────── */
110
+ .bp-faq {
111
+ border: 1px solid var(--color-border, #E5DFDC);
112
+ border-radius: 0.75rem;
113
+ overflow: hidden;
114
+ }
115
+ .bp-faq-item { border-bottom: 1px solid var(--color-border, #E5DFDC); }
116
+ .bp-faq-item:last-child { border-bottom: none; }
117
+ .bp-faq-item summary {
118
+ padding: 0.875rem 1.25rem;
119
+ font-weight: 600;
120
+ font-size: 1rem;
121
+ cursor: pointer;
122
+ list-style: none;
123
+ display: flex;
124
+ align-items: center;
125
+ gap: 0.625rem;
126
+ color: var(--color-text, #001D21);
127
+ transition: background 0.15s;
128
+ }
129
+ .bp-faq-item summary:hover { background: var(--color-surface, #F0EBEB); }
130
+ .bp-faq-item summary::-webkit-details-marker { display: none; }
131
+ .bp-faq-icon { font-size: 1.25rem; color: var(--color-primary, #0054C9); flex-shrink: 0; }
132
+ .bp-faq-question { flex: 1; }
133
+ .bp-faq-chevron { font-size: 1.375rem; color: var(--color-text-muted, #51646A); transition: transform 0.2s; flex-shrink: 0; }
134
+ .bp-faq-item[open] .bp-faq-chevron { transform: rotate(180deg); }
135
+ .bp-faq-answer { padding: 0 1.25rem 1rem 3rem; color: var(--color-text-muted, #51646A); }
136
+ .bp-faq-answer p:last-child { margin-bottom: 0; }
137
+
138
+ /* ───── Steps — vertical timeline ──────────────────────────────────── */
139
+ .bp-steps { padding-left: 0; }
140
+ .bp-step { display: flex; gap: 1.25rem; }
141
+ .bp-step__marker {
142
+ display: flex; flex-direction: column; align-items: center; flex-shrink: 0;
143
+ }
144
+ .bp-step__num {
145
+ width: 2.25rem; height: 2.25rem; border-radius: 50%;
146
+ background: var(--color-primary, #0054C9);
147
+ color: var(--color-primary-contrast, #FFFFFF);
148
+ display: flex; align-items: center; justify-content: center;
149
+ font-weight: 700; font-size: 0.9rem; flex-shrink: 0;
150
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-primary, #0054C9) 15%, transparent);
151
+ }
152
+ .bp-step__line {
153
+ width: 2px; flex: 1; min-height: 1.5rem;
154
+ background: color-mix(in srgb, var(--color-primary, #0054C9) 25%, transparent);
155
+ }
156
+ .bp-step__content { padding-bottom: 2rem; flex: 1; }
157
+ .bp-step--last .bp-step__content { padding-bottom: 0; }
158
+ .bp-step__content strong { font-size: 1.0625rem; display: block; margin-bottom: 0.25rem; }
159
+ .bp-step__content p:last-child { margin-bottom: 0; }
160
+
161
+ /* ───── Card ───────────────────────────────────────────────────────── */
162
+ .bp-card {
163
+ border: 1px solid var(--color-border, #E5DFDC);
164
+ border-radius: 0.75rem;
165
+ overflow: hidden;
166
+ background: var(--color-surface, #F0EBEB);
167
+ box-shadow: 0 2px 12px rgba(0,0,0,0.1);
168
+ border-left: 4px solid var(--color-primary, #0054C9);
169
+ }
170
+ .bp-card__img { width: 100%; height: auto; display: block; }
171
+ .bp-card__body { padding: 1.25rem 1.5rem; }
172
+ .bp-card__title { font-weight: 700; font-size: 1.125rem; margin-bottom: 0.5rem; color: var(--color-primary, #0054C9); }
173
+ .bp-card__body p { color: var(--color-text-muted, #51646A); }
174
+ .bp-card__body p:last-child { margin-bottom: 0; }
175
+
176
+ /* ───── Chart (canvas placeholder, host hydrates) ──────────────────── */
177
+ .bp-chart { border: 1px solid var(--color-border, #E5DFDC); border-radius: 0.5rem; padding: 1rem; }
178
+ .bp-chart__title { font-weight: 600; margin-bottom: 0.5rem; }
179
+ .bp-chart__canvas { width: 100%; max-height: 400px; }
180
+
181
+ /* ───── Data Table ─────────────────────────────────────────────────── */
182
+ .bp-data-table {
183
+ border: 1px solid var(--color-border, #E5DFDC);
184
+ border-radius: 0.5rem;
185
+ overflow-x: auto;
186
+ -webkit-overflow-scrolling: touch;
187
+ }
188
+ .bp-data-table__title {
189
+ font-weight: 600; padding: 0.75rem 1rem;
190
+ background: var(--color-surface, #F0EBEB);
191
+ border-bottom: 1px solid var(--color-border, #E5DFDC);
192
+ }
193
+ .bp-data-table table { margin: 0; width: max-content; min-width: 100%; }
194
+ .bp-data-table th, .bp-data-table td { min-width: 100px; }
195
+
196
+ /* ───── Image Carousel ─────────────────────────────────────────────── */
197
+ .bp-carousel {
198
+ position: relative;
199
+ border: 1px solid var(--color-border, #E5DFDC);
200
+ border-radius: 0.75rem;
201
+ overflow: hidden;
202
+ }
203
+ .bp-carousel__track { display: flex; overflow: hidden; }
204
+ .bp-carousel__slide { min-width: 100%; display: none; }
205
+ .bp-carousel__slide.active { display: block; }
206
+ .bp-carousel__slide img { width: 100%; height: auto; display: block; }
207
+ .bp-carousel__caption {
208
+ padding: 0.625rem 1rem;
209
+ font-size: 0.875rem;
210
+ color: var(--color-text-muted, #51646A);
211
+ text-align: center;
212
+ background: var(--color-surface, #F0EBEB);
213
+ }
214
+ .bp-carousel__prev, .bp-carousel__next {
215
+ position: absolute; top: 50%; transform: translateY(-50%);
216
+ background: rgba(0,0,0,0.5); color: #fff; border: none; border-radius: 50%;
217
+ width: 2.5rem; height: 2.5rem; padding: 0; cursor: pointer;
218
+ display: flex; align-items: center; justify-content: center;
219
+ backdrop-filter: blur(4px); transition: background 0.15s;
220
+ }
221
+ .bp-carousel__prev .material-icons, .bp-carousel__next .material-icons { font-size: 1.5rem; }
222
+ .bp-carousel__prev:hover, .bp-carousel__next:hover { background: rgba(0,0,0,0.7); }
223
+ .bp-carousel__prev { left: 0.75rem; }
224
+ .bp-carousel__next { right: 0.75rem; }
225
+
226
+ /* ───── Inline image (single figure) ───────────────────────────────── */
227
+ .bp-image {
228
+ margin: 1.75rem 0;
229
+ display: flex; flex-direction: column; align-items: center; gap: 0.5rem;
230
+ }
231
+ .bp-image img {
232
+ max-width: 100%; height: auto; display: block;
233
+ border-radius: 0.75rem;
234
+ border: 1px solid var(--color-border, #E5DFDC);
235
+ }
236
+ .bp-image__caption {
237
+ font-size: 0.9375rem;
238
+ color: var(--color-text-muted, #51646A);
239
+ text-align: center;
240
+ line-height: 1.5;
241
+ }
242
+ .bp-image__caption p { margin: 0; }
243
+ .bp-image__credit {
244
+ font-size: 0.75rem;
245
+ color: var(--color-text-muted, #51646A);
246
+ opacity: 0.85;
247
+ }
248
+ .bp-image__credit a { color: inherit; text-decoration: underline; }
249
+ .bp-image-placeholder {
250
+ margin: 1.75rem 0; padding: 2rem 1rem;
251
+ border: 1px dashed var(--color-border, #E5DFDC);
252
+ border-radius: 0.75rem;
253
+ text-align: center;
254
+ color: var(--color-text-muted, #51646A);
255
+ font-style: italic;
256
+ }
257
+
258
+ /* ───── Process — horizontal stages with connectors ───────────────── */
259
+ .bp-process { display: flex; align-items: flex-start; gap: 0; flex-wrap: wrap; justify-content: center; }
260
+ .bp-process__stage {
261
+ display: flex; flex-direction: column; align-items: center; text-align: center;
262
+ flex: 1; min-width: 0; padding: 1rem 0.75rem;
263
+ }
264
+ .bp-process__icon {
265
+ width: 3rem; height: 3rem; border-radius: 50%;
266
+ background: var(--color-primary, #0054C9);
267
+ color: var(--color-primary-contrast, #FFFFFF);
268
+ display: flex; align-items: center; justify-content: center;
269
+ font-weight: 700; font-size: 1rem; flex-shrink: 0; margin-bottom: 0.75rem;
270
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-primary, #0054C9) 15%, transparent);
271
+ }
272
+ .bp-process__icon .material-icons { font-size: 1.375rem; }
273
+ .bp-process__num { font-weight: 700; font-size: 1rem; }
274
+ .bp-process__title { font-weight: 600; font-size: 1rem; margin-bottom: 0.25rem; }
275
+ .bp-process__desc { font-size: 0.875rem; color: var(--color-text-muted, #51646A); line-height: 1.5; }
276
+ .bp-process__connector {
277
+ display: flex; align-items: center; padding-top: 1.25rem;
278
+ color: var(--color-text-muted, #51646A);
279
+ }
280
+ .bp-process__connector .material-icons { font-size: 1.5rem; }
281
+
282
+ /* ───── Comparison ─────────────────────────────────────────────────── */
283
+ .bp-comparison {
284
+ border: 1px solid var(--color-border, #E5DFDC);
285
+ border-radius: 0.5rem;
286
+ overflow-x: auto;
287
+ -webkit-overflow-scrolling: touch;
288
+ }
289
+ .bp-comparison table { margin: 0; width: max-content; min-width: 100%; }
290
+ .bp-comparison th, .bp-comparison td { min-width: 100px; }
291
+ .bp-comparison__cell { text-align: center; }
292
+
293
+ /* ───── Cite chip — superscript footnote link ──────────────────────── */
294
+ .bp-cite {
295
+ display: inline-block;
296
+ font-size: 0.7em;
297
+ vertical-align: super;
298
+ line-height: 1;
299
+ padding: 1px 5px;
300
+ margin: 0 1px;
301
+ border-radius: 999px;
302
+ background: color-mix(in srgb, var(--color-primary, #0054C9) 12%, transparent);
303
+ color: var(--color-primary, #0054C9);
304
+ font-weight: 600;
305
+ }
306
+ .bp-cite a { color: inherit; text-decoration: none; }
307
+ .bp-cite a:hover { text-decoration: underline; }
308
+ .bp-cite--orphan {
309
+ background: var(--color-surface, #F0EBEB);
310
+ color: var(--color-text-muted, #51646A);
311
+ }
312
+
313
+ /* ───── Inline widget iframe ───────────────────────────────────────── */
314
+ .inline-widget-container {
315
+ margin: 2rem 0;
316
+ border-radius: 0.75rem;
317
+ overflow: visible;
318
+ }
319
+ .inline-widget-container iframe {
320
+ width: 100%;
321
+ border: none;
322
+ min-height: 200px;
323
+ }
324
+
325
+ /* ───── Inline CTA ─────────────────────────────────────────────────── */
326
+ .inline-cta-block {
327
+ display: flex;
328
+ flex-direction: column;
329
+ align-items: center;
330
+ gap: 0.75rem;
331
+ padding: 2.5rem 2rem;
332
+ margin: 2.5rem 0;
333
+ background: var(--color-surface, #F0EBEB);
334
+ border: 1px solid var(--color-border, #E5DFDC);
335
+ border-radius: 1rem;
336
+ text-align: center;
337
+ }
338
+ .inline-cta-description {
339
+ color: var(--color-text-muted, #51646A);
340
+ font-size: 0.9375rem;
341
+ line-height: 1.5;
342
+ margin: 0;
343
+ }
344
+ .inline-cta-btn {
345
+ display: inline-block;
346
+ padding: 0.75rem 2rem;
347
+ border-radius: 0.5rem;
348
+ font-size: 1rem;
349
+ font-weight: 600;
350
+ text-decoration: none;
351
+ cursor: pointer;
352
+ transition: all 0.2s ease;
353
+ border: none;
354
+ font-family: inherit;
355
+ }
356
+ .inline-cta-btn.cta-primary {
357
+ background: var(--color-primary, #0054C9);
358
+ color: var(--color-primary-contrast, #FFFFFF);
359
+ }
360
+ .inline-cta-btn.cta-primary:hover {
361
+ opacity: 0.9;
362
+ transform: translateY(-1px);
363
+ }
364
+ .inline-cta-btn.cta-outline {
365
+ background: transparent;
366
+ border: 2px solid var(--color-primary, #0054C9);
367
+ color: var(--color-primary, #0054C9);
368
+ }
369
+ .inline-cta-btn.cta-outline:hover {
370
+ background: color-mix(in srgb, var(--color-primary, #0054C9) 10%, transparent);
371
+ }
372
+ .inline-cta-btn.cta-subtle {
373
+ background: transparent;
374
+ color: var(--color-primary, #0054C9);
375
+ text-decoration: underline;
376
+ padding: 0.5rem 0;
377
+ }
378
+ .inline-cta-btn.cta-subtle:hover { opacity: 0.8; }
379
+
380
+ /* ───── CTA Form ───────────────────────────────────────────────────── */
381
+ .inline-cta-form-block { padding: 2rem; }
382
+ .inline-cta-form-title {
383
+ font-size: 1.375rem;
384
+ font-weight: 700;
385
+ color: var(--color-text, #001D21);
386
+ margin: 0 0 1.25rem;
387
+ }
388
+ .inline-cta-form {
389
+ display: flex;
390
+ flex-direction: column;
391
+ gap: 0.75rem;
392
+ max-width: 480px;
393
+ width: 100%;
394
+ margin: 0 auto;
395
+ }
396
+ .inline-cta-input,
397
+ .inline-cta-textarea {
398
+ width: 100%;
399
+ padding: 0.75rem 1rem;
400
+ border: 1px solid var(--color-border, #E5DFDC);
401
+ border-radius: 0.5rem;
402
+ font-size: 0.9375rem;
403
+ font-family: inherit;
404
+ background: var(--color-bg, #FFFFFF);
405
+ color: var(--color-text, #001D21);
406
+ transition: border-color 0.2s ease;
407
+ box-sizing: border-box;
408
+ }
409
+ .inline-cta-input:focus,
410
+ .inline-cta-textarea:focus {
411
+ outline: none;
412
+ border-color: var(--color-primary, #0054C9);
413
+ }
414
+ .inline-cta-textarea {
415
+ resize: vertical;
416
+ min-height: 80px;
417
+ }
418
+ .inline-cta-submit { align-self: center; margin-top: 0.5rem; }
419
+ .inline-cta-success {
420
+ color: var(--color-primary, #0054C9);
421
+ font-weight: 600;
422
+ font-size: 1rem;
423
+ margin: 1rem 0;
424
+ }
425
+
426
+ /* ───── Mobile tweaks ──────────────────────────────────────────────── */
427
+ @media (max-width: 768px) {
428
+ .prose-body { font-size: 1rem; }
429
+ .prose-body h1 { font-size: 1.75rem; }
430
+ .prose-body h2 { font-size: 1.5rem; }
431
+ .prose-body h3 { font-size: 1.25rem; }
432
+ }