@certrev/cert-block 0.3.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/README.md +48 -1
  2. package/dist/builder/cert-chrome-data.d.ts +129 -0
  3. package/dist/builder/cert-chrome-data.d.ts.map +1 -0
  4. package/dist/builder/cert-chrome-data.js +59 -0
  5. package/dist/builder/cert-chrome-data.js.map +1 -0
  6. package/dist/builder/cert-review-card.d.ts +68 -0
  7. package/dist/builder/cert-review-card.d.ts.map +1 -0
  8. package/dist/builder/cert-review-card.js +129 -0
  9. package/dist/builder/cert-review-card.js.map +1 -0
  10. package/dist/builder/index.d.ts +2 -0
  11. package/dist/builder/index.d.ts.map +1 -1
  12. package/dist/builder/index.js +3 -0
  13. package/dist/builder/index.js.map +1 -1
  14. package/dist/components/preview-block.d.ts +43 -0
  15. package/dist/components/preview-block.d.ts.map +1 -0
  16. package/dist/components/preview-block.js +53 -0
  17. package/dist/components/preview-block.js.map +1 -0
  18. package/dist/components/render-cert-block.d.ts +50 -7
  19. package/dist/components/render-cert-block.d.ts.map +1 -1
  20. package/dist/components/render-cert-block.js +304 -80
  21. package/dist/components/render-cert-block.js.map +1 -1
  22. package/dist/components/render-def.d.ts +27 -0
  23. package/dist/components/render-def.d.ts.map +1 -1
  24. package/dist/components/render-def.js +38 -0
  25. package/dist/components/render-def.js.map +1 -1
  26. package/dist/index.d.ts +3 -2
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +3 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/modal/avatar-resize.d.ts +60 -0
  31. package/dist/modal/avatar-resize.d.ts.map +1 -0
  32. package/dist/modal/avatar-resize.js +91 -0
  33. package/dist/modal/avatar-resize.js.map +1 -0
  34. package/dist/modal/cert-modal-view.d.ts +74 -0
  35. package/dist/modal/cert-modal-view.d.ts.map +1 -0
  36. package/dist/modal/cert-modal-view.js +160 -0
  37. package/dist/modal/cert-modal-view.js.map +1 -0
  38. package/dist/modal/certrev-cert-modal.d.ts +72 -0
  39. package/dist/modal/certrev-cert-modal.d.ts.map +1 -0
  40. package/dist/modal/certrev-cert-modal.js +270 -0
  41. package/dist/modal/certrev-cert-modal.js.map +1 -0
  42. package/dist/modal/certrev-cert-modal.styles.d.ts +35 -0
  43. package/dist/modal/certrev-cert-modal.styles.d.ts.map +1 -0
  44. package/dist/modal/certrev-cert-modal.styles.js +488 -0
  45. package/dist/modal/certrev-cert-modal.styles.js.map +1 -0
  46. package/dist/modal/certrev-cert.entry.d.ts +19 -0
  47. package/dist/modal/certrev-cert.entry.d.ts.map +1 -0
  48. package/dist/modal/certrev-cert.entry.js +39 -0
  49. package/dist/modal/certrev-cert.entry.js.map +1 -0
  50. package/dist/modal/certrev-cert.js +557 -0
  51. package/dist/modal/display-strings.d.ts +22 -0
  52. package/dist/modal/display-strings.d.ts.map +1 -0
  53. package/dist/modal/display-strings.js +35 -0
  54. package/dist/modal/display-strings.js.map +1 -0
  55. package/dist/modal/escape.d.ts +13 -0
  56. package/dist/modal/escape.d.ts.map +1 -0
  57. package/dist/modal/escape.js +20 -0
  58. package/dist/modal/escape.js.map +1 -0
  59. package/dist/modal/font-face.d.ts +4 -0
  60. package/dist/modal/font-face.d.ts.map +1 -0
  61. package/dist/modal/font-face.js +137 -0
  62. package/dist/modal/font-face.js.map +1 -0
  63. package/dist/modal/ftc-disclosure.d.ts +12 -0
  64. package/dist/modal/ftc-disclosure.d.ts.map +1 -0
  65. package/dist/modal/ftc-disclosure.js +12 -0
  66. package/dist/modal/ftc-disclosure.js.map +1 -0
  67. package/dist/modal/ftc-guard.d.ts +50 -0
  68. package/dist/modal/ftc-guard.d.ts.map +1 -0
  69. package/dist/modal/ftc-guard.js +117 -0
  70. package/dist/modal/ftc-guard.js.map +1 -0
  71. package/dist/modal/index.d.ts +20 -0
  72. package/dist/modal/index.d.ts.map +1 -0
  73. package/dist/modal/index.js +25 -0
  74. package/dist/modal/index.js.map +1 -0
  75. package/dist/modal/interactions.d.ts +53 -0
  76. package/dist/modal/interactions.d.ts.map +1 -0
  77. package/dist/modal/interactions.js +147 -0
  78. package/dist/modal/interactions.js.map +1 -0
  79. package/dist/modal/logo-paths.d.ts +15 -0
  80. package/dist/modal/logo-paths.d.ts.map +1 -0
  81. package/dist/modal/logo-paths.js +24 -0
  82. package/dist/modal/logo-paths.js.map +1 -0
  83. package/dist/modal/placement.d.ts +95 -0
  84. package/dist/modal/placement.d.ts.map +1 -0
  85. package/dist/modal/placement.js +302 -0
  86. package/dist/modal/placement.js.map +1 -0
  87. package/dist/modal/revalidate.d.ts +48 -0
  88. package/dist/modal/revalidate.d.ts.map +1 -0
  89. package/dist/modal/revalidate.js +65 -0
  90. package/dist/modal/revalidate.js.map +1 -0
  91. package/dist/modal/verify-client.d.ts +24 -0
  92. package/dist/modal/verify-client.d.ts.map +1 -0
  93. package/dist/modal/verify-client.js +38 -0
  94. package/dist/modal/verify-client.js.map +1 -0
  95. package/package.json +14 -4
  96. package/src/__tests__/__snapshots__/render-cert-block.test.ts.snap +10 -6
  97. package/src/__tests__/preview-block.test.ts +84 -0
  98. package/src/__tests__/render-cert-block.test.ts +380 -5
  99. package/src/builder/__tests__/builder.test.tsx +71 -0
  100. package/src/builder/cert-chrome-data.ts +159 -0
  101. package/src/builder/cert-review-card.tsx +177 -0
  102. package/src/builder/index.tsx +18 -0
  103. package/src/components/preview-block.ts +74 -0
  104. package/src/components/render-cert-block.ts +387 -102
  105. package/src/components/render-def.ts +55 -0
  106. package/src/index.ts +9 -0
  107. package/src/modal/avatar-resize.ts +99 -0
  108. package/src/modal/cert-modal-view.test.ts +175 -0
  109. package/src/modal/cert-modal-view.ts +210 -0
  110. package/src/modal/certrev-cert-modal.styles.ts +494 -0
  111. package/src/modal/certrev-cert-modal.test.ts +125 -0
  112. package/src/modal/certrev-cert-modal.ts +273 -0
  113. package/src/modal/certrev-cert.entry.ts +44 -0
  114. package/src/modal/display-strings.ts +38 -0
  115. package/src/modal/escape.ts +20 -0
  116. package/src/modal/font-face.ts +138 -0
  117. package/src/modal/ftc-disclosure.ts +13 -0
  118. package/src/modal/ftc-guard.test.ts +153 -0
  119. package/src/modal/ftc-guard.ts +119 -0
  120. package/src/modal/index.ts +34 -0
  121. package/src/modal/interactions.test.ts +250 -0
  122. package/src/modal/interactions.ts +166 -0
  123. package/src/modal/logo-paths.ts +23 -0
  124. package/src/modal/placement.test.ts +398 -0
  125. package/src/modal/placement.ts +331 -0
  126. package/src/modal/revalidate.test.ts +111 -0
  127. package/src/modal/revalidate.ts +81 -0
  128. package/src/modal/verify-client.test.ts +164 -0
  129. package/src/modal/verify-client.ts +42 -0
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Pure envelope → modal-HTML mapping for `<certrev-cert-modal>` (POR-10102).
3
+ *
4
+ * DOM-free and side-effect-free so it unit-tests without a browser: given the
5
+ * signed envelope's `payload.content`, it returns the inner HTML of the certificate
6
+ * (3a) and reviewer (4b) `<dialog>`s. The element (`certrev-cert-modal.ts`) owns the
7
+ * shadow root, the `<dialog>` wrappers, and the open/close wiring; this module owns
8
+ * only the content.
9
+ *
10
+ * Invariants:
11
+ * - Every dynamic value is `escapeHtml`'d (the envelope is signed, but a reviewer
12
+ * name / memo is still attacker-adjacent content).
13
+ * - Only `http(s)` hrefs render (mirrors the Liquid `certrev-safe-url` guard).
14
+ * - Verified-only fields (POR-10091): the bio binds the envelope's verified
15
+ * `background`/`bio`; this module never reaches for a raw column.
16
+ * - Graceful degradation: a missing article title drops the "Certifies the article"
17
+ * block; a missing display cert id drops that meta column; no credentials → no
18
+ * chips / no creds line; no bio → no bio paragraph. Nothing renders an empty gap.
19
+ * - WS4a (display eviction) accept-on-read: an ABSENT `display` block resolves to the
20
+ * preset default (show-all) — every read below is `display?.x !== false` / `=== false`,
21
+ * so `undefined` never suppresses a field. The modal is the LOCKED navy trust chrome and
22
+ * is deliberately NOT themeable by a brand render-def; it reads visibility off the signed
23
+ * envelope only, so no render-def wiring belongs here.
24
+ */
25
+ import { escapeHtml } from './escape.js';
26
+ import { resizeAvatarUrl } from './avatar-resize.js';
27
+ import { CERT_SCOPE_LINE, formatCertDisplayDate } from './display-strings.js';
28
+ import { checkIcon, chevronMark, verifiedStamp } from './certrev-cert-modal.styles.js';
29
+ const HONORIFIC_RE = /^(dr|prof|professor|mr|mrs|ms|mx|rev|sir|dame)\.?\s+/i;
30
+ /** The bare given+family name: strip the leading honorific AND the trailing post-nominal
31
+ * (`, MD`), so "Dr. Erik Schraga, MD" → "Erik Schraga". */
32
+ function coreName(displayName) {
33
+ const withoutHonorific = displayName.replace(HONORIFIC_RE, '').trim();
34
+ const comma = withoutHonorific.indexOf(',');
35
+ return (comma === -1 ? withoutHonorific : withoutHonorific.slice(0, comma)).trim();
36
+ }
37
+ /** Two-letter initials — first + last of the core name (honorific + post-nominal stripped),
38
+ * so "Dr. Erik Schraga, MD" → "ES" (matches the Liquid derivation), not "EM". */
39
+ export function deriveInitials(name) {
40
+ const words = coreName(name).split(/\s+/).filter(Boolean);
41
+ if (words.length === 0)
42
+ return '';
43
+ if (words.length === 1)
44
+ return words[0].slice(0, 2).toUpperCase();
45
+ return (words[0][0] + words[words.length - 1][0]).toUpperCase();
46
+ }
47
+ /** The handwritten-signature form (the core name). */
48
+ export function signatureName(displayName) {
49
+ return coreName(displayName);
50
+ }
51
+ /** `https://certrev.com/verify/x` → `certrev.com/verify/x` (footer URL line). */
52
+ export function stripProtocol(url) {
53
+ return url.replace(/^https?:\/\//i, '').replace(/\/$/, '');
54
+ }
55
+ /** Return the url only if it is an absolute http(s) URL, else `#` (safe-url guard). */
56
+ export function safeHref(url) {
57
+ if (!url)
58
+ return '#';
59
+ return /^https?:\/\//i.test(url.trim()) ? url.trim() : '#';
60
+ }
61
+ /** A single credential's display label (cardLabel ?? fullName ?? abbreviation). */
62
+ function credLabel(c) {
63
+ return (c.cardLabel || c.fullName || c.abbreviation || '').trim();
64
+ }
65
+ /** The verified credential labels, in order, de-duped + non-empty. */
66
+ function credLabels(expert) {
67
+ const out = [];
68
+ for (const c of expert?.credentials ?? []) {
69
+ const label = credLabel(c);
70
+ if (label && !out.includes(label))
71
+ out.push(label);
72
+ }
73
+ return out;
74
+ }
75
+ /** The reviewer bio — verified background preferred, then verified bio. */
76
+ function bioText(expert, display) {
77
+ if (display?.showBio === false)
78
+ return '';
79
+ return (expert?.background || expert?.bio || '').trim();
80
+ }
81
+ /** An avatar cell — the cert-frozen photo (when shown) or the initials chip. */
82
+ function avatarHtml(expert, name, sizeClass, showPhoto) {
83
+ const photo = (expert?.photoUrl || '').trim();
84
+ if (showPhoto && /^https?:\/\//i.test(photo)) {
85
+ // Display px from the size class; request the @2x derivative for retina crispness (POR-10470).
86
+ // resizeAvatarUrl is host-guarded + enablement-gated: until Cloudflare Image Resizing is live it
87
+ // returns the raw canonical URL, so the modal never 404s an unresized host.
88
+ const displayPx = sizeClass.includes('--64') ? 64 : 56;
89
+ const src = resizeAvatarUrl(photo, displayPx === 64 ? 128 : 112);
90
+ return `<img class="crm-avatar ${sizeClass}" src="${escapeHtml(src)}" alt="" width="${displayPx}" height="${displayPx}" decoding="async">`;
91
+ }
92
+ return `<span class="crm-avatar ${sizeClass}">${escapeHtml(deriveInitials(name))}</span>`;
93
+ }
94
+ const CLOSE_BUTTON = `<button type="button" class="crm-close" data-certrev-modal-close aria-label="Close">&times;</button>`;
95
+ function headHtml(eyebrow) {
96
+ return `<div class="crm-head">${chevronMark(16, '#ffffff')}<span class="crm-head__eyebrow">${escapeHtml(eyebrow)}</span><span class="crm-head__wordmark">CertREV</span></div>`;
97
+ }
98
+ function certifiedChip(cls) {
99
+ return `<span class="${cls}">${checkIcon(8)}Certified</span>`;
100
+ }
101
+ /**
102
+ * The FTC disclosure footer (POR-10496), rendered on both dialogs. The material-connection cue
103
+ * (`expert.compensationCue`, a FINISHED string off the signed envelope) leads ONLY when the reviewer
104
+ * was compensated; a pro-bono reviewer (null/absent cue) drops the label but keeps the scope line —
105
+ * which makes no compensation claim, so it is true for volunteers too and renders unconditionally.
106
+ * The scope line is the verbatim FTC copy (`CERT_SCOPE_LINE`), never re-assembled here.
107
+ */
108
+ function disclosureFooter(expert) {
109
+ const cue = (expert?.compensationCue || '').trim();
110
+ const cueHtml = cue ? `<span class="crm-disclosure__cue">${escapeHtml(cue)}</span> ` : '';
111
+ return `<div class="crm-disclosure">${cueHtml}${escapeHtml(CERT_SCOPE_LINE)}</div>`;
112
+ }
113
+ /**
114
+ * The certificate modal (3a) inner HTML. Returns `null` when there is no reviewer
115
+ * name to display — a certificate card must never render anonymous (fail-closed,
116
+ * mirrors the Liquid `ename != ''` gate).
117
+ */
118
+ export function renderCertDialogInner(content) {
119
+ const expert = content.expert ?? null;
120
+ const name = (expert?.displayName || '').trim();
121
+ if (!name)
122
+ return null;
123
+ const showPhoto = content.display?.showExpertPhoto !== false;
124
+ const creds = credLabels(expert).join(' · ');
125
+ const issued = formatCertDisplayDate(content.certifiedAt) ?? '';
126
+ const certId = (content.displayCertId || '').trim();
127
+ const title = (content.articleTitle || '').trim();
128
+ const verifyUrl = (content.verifyUrl || '').trim();
129
+ const sig = signatureName(name);
130
+ const certifiesBlock = title
131
+ ? `<div class="crm-certifies"><div class="crm-certifies__eyebrow">Certifies the article</div><div class="crm-certifies__title">${escapeHtml(title)}</div></div>`
132
+ : '';
133
+ const certIdCol = certId
134
+ ? `<div class="crm-meta__col"><div class="crm-meta__label">Certificate ID</div><div class="crm-meta__value crm-meta__value--mono">${escapeHtml(certId)}</div></div>`
135
+ : '';
136
+ const urlLine = verifyUrl ? `<div class="crm-sig__url">${escapeHtml(stripProtocol(verifyUrl))}</div>` : '';
137
+ return `${CLOSE_BUTTON}${headHtml('Certificate of Expert Review')}<div class="crm-body--cert"><div class="crm-expert-row">${avatarHtml(expert, name, 'crm-avatar--56', showPhoto)}<div class="crm-expert-row__info"><div class="crm-expert-row__eyebrow">Reviewing Expert</div><div class="crm-expert-row__name">${escapeHtml(name)}</div>${creds ? `<div class="crm-expert-row__creds">${escapeHtml(creds)}</div>` : ''}</div></div>${certifiesBlock}<div class="crm-meta"><div class="crm-meta__col"><div class="crm-meta__label">Issued</div><div class="crm-meta__value">${escapeHtml(issued)}</div></div>${certIdCol}<div class="crm-meta__col"><div class="crm-meta__label">Status</div><div class="crm-meta__status">${certifiedChip('crm-chip')}</div></div></div><div class="crm-sig"><div><div class="crm-sig__name">${escapeHtml(sig)}</div><div class="crm-sig__rule"></div>${urlLine}</div>${verifiedStamp('cert')}</div>${disclosureFooter(expert)}<div class="crm-actions"><a class="crm-cta" href="${escapeHtml(safeHref(verifyUrl))}" target="_blank" rel="noopener noreferrer">View full certificate</a><button type="button" class="crm-ghost" data-certrev-modal-close>Close</button></div></div>`;
138
+ }
139
+ /**
140
+ * The reviewer/profile modal (4b) inner HTML. Returns `null` when there is no
141
+ * reviewer name (fail-closed). The affiliation subtitle is intentionally omitted
142
+ * (no verified data source — POR-10102 decision).
143
+ */
144
+ export function renderExpertDialogInner(content) {
145
+ const expert = content.expert ?? null;
146
+ const name = (expert?.displayName || '').trim();
147
+ if (!name)
148
+ return null;
149
+ const showPhoto = content.display?.showExpertPhoto !== false;
150
+ const chips = credLabels(expert);
151
+ const bio = bioText(expert, content.display);
152
+ const date = formatCertDisplayDate(content.certifiedAt);
153
+ const chipsHtml = chips.length
154
+ ? `<div class="crm-chips">${chips.map((c) => `<span class="crm-chips__chip">${escapeHtml(c)}</span>`).join('')}</div>`
155
+ : '';
156
+ const bioHtml = bio ? `<p class="crm-bio">${escapeHtml(bio)}</p>` : '';
157
+ const trustText = date ? `Independently reviewed · Certified ${escapeHtml(date)}` : 'Independently reviewed';
158
+ return `${CLOSE_BUTTON}${headHtml('Verified reviewer')}<div class="crm-body--expert"><div class="crm-id">${avatarHtml(expert, name, 'crm-avatar--64', showPhoto)}<div class="crm-id__info"><div class="crm-id__name">${escapeHtml(name)}</div></div></div>${chipsHtml}${bioHtml}<div class="crm-trust">${certifiedChip('crm-trust__chip')}<span class="crm-trust__text">${trustText}</span></div>${disclosureFooter(expert)}<div class="crm-actions"><a class="crm-cta" href="${escapeHtml(safeHref(expert?.profileUrl))}" target="_blank" rel="noopener noreferrer">View full profile on CertREV</a><button type="button" class="crm-ghost" data-certrev-modal-close>Close</button></div></div>`;
159
+ }
160
+ //# sourceMappingURL=cert-modal-view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cert-modal-view.js","sourceRoot":"","sources":["../../src/modal/cert-modal-view.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * `<certrev-cert-modal>` — the ONE Shadow-DOM element that renders both in-page cert
3
+ * dialogs (certificate 3a + reviewer 4b) across Shopify / WordPress / Builder
4
+ * (POR-10102). Replaces the three drifting per-platform modal implementations.
5
+ *
6
+ * Design:
7
+ * - One element, one OPEN shadow root, two `<dialog>`s (`#cert`, `#expert`). Public
8
+ * `open('cert'|'expert')` → `dialog.showModal()` (guarded). Data is public SSR
9
+ * content already, so an open shadow adds no exposure and eases debugging/tests.
10
+ * - CSS is adopted via a shared constructable stylesheet (falls back to an inline
11
+ * `<style>` on older engines). Shadow scoping means the brand theme can't bleed in
12
+ * and these rules can't leak out — the literal-hex/theme-armor constraints that
13
+ * bind the `body_html` modal do not apply here.
14
+ * - Envelope acquisition, in priority order: (1) a light-DOM
15
+ * `<script type="application/json" data-certrev-envelope>` child (Shopify + WP SSR,
16
+ * parsed lazily on first open — no network, crawl-safe); (2) a `data-envelope`
17
+ * attribute (stories/tests); (3) a Delivery-API fetch from
18
+ * `{data-delivery-api}/api/cert/v1/delivery/{data-platform}/{data-external-id}`
19
+ * (Builder/Hydrogen). The triggers (`data-certrev-modal-open`) stay in the light
20
+ * DOM (badge/memo) and are wired by `interactions.ts`.
21
+ * - Close plumbing (×, Close, backdrop, Esc) lives HERE, not in `interactions.ts`:
22
+ * a click inside a shadow root is retargeted to the host for a document listener,
23
+ * so the document-level delegate can't see the close controls.
24
+ * - Fonts inject at document level on first open (font faces don't scope into a
25
+ * shadow root); deferring to first open avoids loading them on every article view.
26
+ */
27
+ import { type CertDeliveryArtifact } from '@certrev/cert-contract';
28
+ export declare const CERT_MODAL_TAG = "certrev-cert-modal";
29
+ type ModalKind = 'cert' | 'expert';
30
+ export declare class CertRevCertModal extends HTMLElement {
31
+ private root;
32
+ private rendered;
33
+ private content;
34
+ private fetchInFlight;
35
+ constructor();
36
+ /**
37
+ * Open the given dialog. Returns `true` when this element will handle the open
38
+ * (so a link-shaped trigger should `preventDefault`), `false` when it cannot
39
+ * (no envelope + no fetch source) so the caller lets the `href` fall back.
40
+ */
41
+ open(kind: ModalKind): boolean;
42
+ /** Parse the synchronous envelope source (embedded JSON / attr) + render once. */
43
+ private ensureRendered;
44
+ private readSyncContent;
45
+ private hasFetchSource;
46
+ private fetchThenShow;
47
+ private renderDialogs;
48
+ private show;
49
+ /**
50
+ * The modal kill-switch (POR-10101): a fetched envelope is REVOKED/EXPIRED. Don't open;
51
+ * mark the host suppressed (mirrors the badge revalidation's `data-certrev-suppressed`) and
52
+ * hide the light-DOM triggers so a stale click can't re-open a pulled cert. Best-effort DOM.
53
+ */
54
+ private markSuppressed;
55
+ private onShadowClick;
56
+ }
57
+ /**
58
+ * The modal-side lifecycle gate (POR-10101 / POR-10684): a fetched Delivery-API artifact that is
59
+ * REVOKED or EXPIRED must NOT render "verified" content. Two revocation shapes suppress:
60
+ * • the SLIM `CertTombstone` the control plane serves for a revoked placement (POR-10481) —
61
+ * detected by the `kind: 'tombstone'` discriminator via `isTombstone`. It carries NO payload,
62
+ * so a `payload.lifecycle` read alone would MISS it (and `markSuppressed()` would never fire);
63
+ * • a legacy envelope with `lifecycle.revokedAt` set, or any envelope whose `expiresAt` passed.
64
+ * The Delivery API only serves CertREV-signed artifacts, so this shape read is a safe suppress
65
+ * signal for the display surface (the `<certrev-badge>` revalidation runs the full ed25519
66
+ * verdict via `verifyArtifact` — this is the modal's belt-and-suspenders).
67
+ */
68
+ export declare function isArtifactSuppressed(artifact: CertDeliveryArtifact | null | undefined): boolean;
69
+ /** Register the element once (idempotent — a double script-load must not throw). */
70
+ export declare function defineCertModal(): void;
71
+ export {};
72
+ //# sourceMappingURL=certrev-cert-modal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"certrev-cert-modal.d.ts","sourceRoot":"","sources":["../../src/modal/certrev-cert-modal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAE,KAAK,oBAAoB,EAAe,MAAM,wBAAwB,CAAA;AAK/E,eAAO,MAAM,cAAc,uBAAuB,CAAA;AAClD,KAAK,SAAS,GAAG,MAAM,GAAG,QAAQ,CAAA;AA+ClC,qBAAa,gBAAiB,SAAQ,WAAW;IAChD,OAAO,CAAC,IAAI,CAAY;IACxB,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,OAAO,CAAgC;IAC/C,OAAO,CAAC,aAAa,CAA6B;;IAiBlD;;;;OAIG;IACH,IAAI,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO;IAgB9B,kFAAkF;IAClF,OAAO,CAAC,cAAc;IAUtB,OAAO,CAAC,eAAe;IAQvB,OAAO,CAAC,cAAc;YAQR,aAAa;IA4C3B,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,IAAI;IAKZ;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,aAAa;CAcrB;AAWD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,oBAAoB,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAQ/F;AAED,oFAAoF;AACpF,wBAAgB,eAAe,IAAI,IAAI,CAGtC"}
@@ -0,0 +1,270 @@
1
+ /**
2
+ * `<certrev-cert-modal>` — the ONE Shadow-DOM element that renders both in-page cert
3
+ * dialogs (certificate 3a + reviewer 4b) across Shopify / WordPress / Builder
4
+ * (POR-10102). Replaces the three drifting per-platform modal implementations.
5
+ *
6
+ * Design:
7
+ * - One element, one OPEN shadow root, two `<dialog>`s (`#cert`, `#expert`). Public
8
+ * `open('cert'|'expert')` → `dialog.showModal()` (guarded). Data is public SSR
9
+ * content already, so an open shadow adds no exposure and eases debugging/tests.
10
+ * - CSS is adopted via a shared constructable stylesheet (falls back to an inline
11
+ * `<style>` on older engines). Shadow scoping means the brand theme can't bleed in
12
+ * and these rules can't leak out — the literal-hex/theme-armor constraints that
13
+ * bind the `body_html` modal do not apply here.
14
+ * - Envelope acquisition, in priority order: (1) a light-DOM
15
+ * `<script type="application/json" data-certrev-envelope>` child (Shopify + WP SSR,
16
+ * parsed lazily on first open — no network, crawl-safe); (2) a `data-envelope`
17
+ * attribute (stories/tests); (3) a Delivery-API fetch from
18
+ * `{data-delivery-api}/api/cert/v1/delivery/{data-platform}/{data-external-id}`
19
+ * (Builder/Hydrogen). The triggers (`data-certrev-modal-open`) stay in the light
20
+ * DOM (badge/memo) and are wired by `interactions.ts`.
21
+ * - Close plumbing (×, Close, backdrop, Esc) lives HERE, not in `interactions.ts`:
22
+ * a click inside a shadow root is retargeted to the host for a document listener,
23
+ * so the document-level delegate can't see the close controls.
24
+ * - Fonts inject at document level on first open (font faces don't scope into a
25
+ * shadow root); deferring to first open avoids loading them on every article view.
26
+ */
27
+ import { isTombstone } from '@certrev/cert-contract';
28
+ import { CERT_FONT_FACE_CSS, CERT_FONTS_ORIGIN } from './font-face.js';
29
+ import { renderCertDialogInner, renderExpertDialogInner } from './cert-modal-view.js';
30
+ import { CERT_MODAL_CSS } from './certrev-cert-modal.styles.js';
31
+ export const CERT_MODAL_TAG = 'certrev-cert-modal';
32
+ /** Shared constructable stylesheet (one per document lifetime). */
33
+ let sharedSheet = null;
34
+ function supportsConstructableSheets() {
35
+ return (typeof CSSStyleSheet !== 'undefined' &&
36
+ 'replaceSync' in CSSStyleSheet.prototype &&
37
+ 'adoptedStyleSheets' in Document.prototype);
38
+ }
39
+ function getSharedSheet() {
40
+ if (!supportsConstructableSheets())
41
+ return null;
42
+ if (!sharedSheet) {
43
+ sharedSheet = new CSSStyleSheet();
44
+ sharedSheet.replaceSync(CERT_MODAL_CSS);
45
+ }
46
+ return sharedSheet;
47
+ }
48
+ /**
49
+ * Inject the modal font families once, at document level, on first open — SELF-HOSTED from R2
50
+ * (POR-10657). Declares the shared CERT_FONT_FACE_CSS @font-face block inline instead of loading a
51
+ * render-blocking `fonts.googleapis.com` stylesheet (which also leaked the visitor's IP to Google).
52
+ * @font-face at document level resolves across the shadow boundary, so the shadow-DOM modal picks
53
+ * these up exactly as it did the old Google <link>. A crossorigin preconnect warms the R2 origin
54
+ * (an @font-face fetch is always a CORS request). Idempotent across instances via the marker attr.
55
+ */
56
+ let fontsInjected = false;
57
+ function injectFontsOnce(doc) {
58
+ if (fontsInjected)
59
+ return;
60
+ fontsInjected = true;
61
+ if (doc.querySelector('style[data-certrev-modal-fonts]'))
62
+ return;
63
+ if (!doc.querySelector('link[data-certrev-modal-preconnect]')) {
64
+ const pre = doc.createElement('link');
65
+ pre.rel = 'preconnect';
66
+ pre.href = CERT_FONTS_ORIGIN;
67
+ pre.crossOrigin = '';
68
+ pre.setAttribute('data-certrev-modal-preconnect', '');
69
+ doc.head.appendChild(pre);
70
+ }
71
+ const style = doc.createElement('style');
72
+ style.textContent = CERT_FONT_FACE_CSS;
73
+ style.setAttribute('data-certrev-modal-fonts', '');
74
+ doc.head.appendChild(style);
75
+ }
76
+ export class CertRevCertModal extends HTMLElement {
77
+ root;
78
+ rendered = false;
79
+ content = null;
80
+ fetchInFlight = null;
81
+ constructor() {
82
+ super();
83
+ this.root = this.attachShadow({ mode: 'open' });
84
+ const sheet = getSharedSheet();
85
+ if (sheet) {
86
+ this.root.adoptedStyleSheets = [sheet];
87
+ }
88
+ else {
89
+ const style = document.createElement('style');
90
+ style.textContent = CERT_MODAL_CSS;
91
+ this.root.appendChild(style);
92
+ }
93
+ // One delegated close listener inside the shadow root.
94
+ this.root.addEventListener('click', (e) => this.onShadowClick(e));
95
+ }
96
+ /**
97
+ * Open the given dialog. Returns `true` when this element will handle the open
98
+ * (so a link-shaped trigger should `preventDefault`), `false` when it cannot
99
+ * (no envelope + no fetch source) so the caller lets the `href` fall back.
100
+ */
101
+ open(kind) {
102
+ // A host page that already provides the five families (e.g. the design guide, which
103
+ // self-hosts them) suppresses the modal's font injection with `data-fonts="host"` — so
104
+ // embedding the component adds no font-src/style-src origins to that page's CSP.
105
+ if (this.getAttribute('data-fonts') !== 'host')
106
+ injectFontsOnce(document);
107
+ if (this.ensureRendered()) {
108
+ this.show(kind);
109
+ return true;
110
+ }
111
+ if (this.hasFetchSource()) {
112
+ void this.fetchThenShow(kind);
113
+ return true;
114
+ }
115
+ return false;
116
+ }
117
+ /** Parse the synchronous envelope source (embedded JSON / attr) + render once. */
118
+ ensureRendered() {
119
+ if (this.rendered)
120
+ return this.content != null;
121
+ const content = this.readSyncContent();
122
+ if (!content)
123
+ return false;
124
+ this.content = content;
125
+ this.renderDialogs(content);
126
+ this.rendered = true;
127
+ return true;
128
+ }
129
+ readSyncContent() {
130
+ const attr = this.getAttribute('data-envelope');
131
+ if (attr)
132
+ return safeParseContent(attr);
133
+ const script = this.querySelector('script[data-certrev-envelope]');
134
+ if (script?.textContent)
135
+ return safeParseContent(script.textContent);
136
+ return null;
137
+ }
138
+ hasFetchSource() {
139
+ return !!(this.getAttribute('data-delivery-api') &&
140
+ this.getAttribute('data-platform') &&
141
+ this.getAttribute('data-external-id'));
142
+ }
143
+ async fetchThenShow(kind) {
144
+ if (!this.fetchInFlight) {
145
+ const base = (this.getAttribute('data-delivery-api') || '').replace(/\/$/, '');
146
+ const platform = this.getAttribute('data-platform') || '';
147
+ const externalId = this.getAttribute('data-external-id') || '';
148
+ this.fetchInFlight = (async () => {
149
+ try {
150
+ const res = await fetch(`${base}/api/cert/v1/delivery/${encodeURIComponent(platform)}/${encodeURIComponent(externalId)}`, {
151
+ cache: 'no-store',
152
+ credentials: 'omit',
153
+ });
154
+ if (!res.ok)
155
+ return;
156
+ const artifact = (await res.json());
157
+ // A 200 may be a REVOCATION — either the SLIM `CertTombstone` the control plane now
158
+ // serves for a revoked placement (POR-10481, no payload) or a legacy envelope with
159
+ // `revokedAt` set — plus an EXPIRED envelope. The Delivery API serves those signed so
160
+ // the edge SUPPRESSES rather than fail-opening on a 404 (POR-10101). Never render
161
+ // "verified" content from a revoked/expired artifact.
162
+ if (isArtifactSuppressed(artifact)) {
163
+ this.markSuppressed();
164
+ return;
165
+ }
166
+ // Not suppressed → a live envelope (a tombstone always suppresses above); `isTombstone`
167
+ // narrows the union so the payload read is type-safe (POR-10684).
168
+ const content = isTombstone(artifact)
169
+ ? undefined
170
+ : artifact.payload?.content;
171
+ if (content) {
172
+ this.content = content;
173
+ this.renderDialogs(content);
174
+ this.rendered = true;
175
+ }
176
+ }
177
+ catch {
178
+ // fail-open: no modal, the trigger's href (if any) already prevented — a dead click at worst
179
+ }
180
+ })();
181
+ }
182
+ await this.fetchInFlight;
183
+ if (this.rendered)
184
+ this.show(kind);
185
+ }
186
+ renderDialogs(content) {
187
+ const cert = renderCertDialogInner(content);
188
+ const expert = renderExpertDialogInner(content);
189
+ const wrap = document.createElement('div');
190
+ wrap.innerHTML = `${cert ? `<dialog id="cert" class="crm-dialog crm-dialog--cert" aria-label="CertREV certificate">${cert}</dialog>` : ''}${expert ? `<dialog id="expert" class="crm-dialog crm-dialog--expert" aria-label="Verified reviewer profile">${expert}</dialog>` : ''}`;
191
+ // keep the adopted stylesheet / fallback <style>; append the dialogs
192
+ for (const node of Array.from(wrap.childNodes))
193
+ this.root.appendChild(node);
194
+ }
195
+ show(kind) {
196
+ const dlg = this.root.getElementById(kind);
197
+ if (dlg && typeof dlg.showModal === 'function' && !dlg.open)
198
+ dlg.showModal();
199
+ }
200
+ /**
201
+ * The modal kill-switch (POR-10101): a fetched envelope is REVOKED/EXPIRED. Don't open;
202
+ * mark the host suppressed (mirrors the badge revalidation's `data-certrev-suppressed`) and
203
+ * hide the light-DOM triggers so a stale click can't re-open a pulled cert. Best-effort DOM.
204
+ */
205
+ markSuppressed() {
206
+ this.setAttribute('data-certrev-suppressed', '');
207
+ for (const t of Array.from(document.querySelectorAll('[data-certrev-modal-open]'))) {
208
+ t.setAttribute('hidden', '');
209
+ t.setAttribute('aria-hidden', 'true');
210
+ }
211
+ }
212
+ onShadowClick(e) {
213
+ const target = e.target;
214
+ if (!target)
215
+ return;
216
+ // backdrop click (the click landed directly on the dialog element)
217
+ if (target instanceof HTMLDialogElement && target.classList.contains('crm-dialog')) {
218
+ target.close();
219
+ return;
220
+ }
221
+ const closer = target.closest('[data-certrev-modal-close]');
222
+ if (closer) {
223
+ e.preventDefault();
224
+ closer.closest('dialog')?.close();
225
+ }
226
+ }
227
+ }
228
+ function safeParseContent(json) {
229
+ try {
230
+ const parsed = JSON.parse(json);
231
+ return parsed && typeof parsed === 'object' ? parsed : null;
232
+ }
233
+ catch {
234
+ return null;
235
+ }
236
+ }
237
+ /**
238
+ * The modal-side lifecycle gate (POR-10101 / POR-10684): a fetched Delivery-API artifact that is
239
+ * REVOKED or EXPIRED must NOT render "verified" content. Two revocation shapes suppress:
240
+ * • the SLIM `CertTombstone` the control plane serves for a revoked placement (POR-10481) —
241
+ * detected by the `kind: 'tombstone'` discriminator via `isTombstone`. It carries NO payload,
242
+ * so a `payload.lifecycle` read alone would MISS it (and `markSuppressed()` would never fire);
243
+ * • a legacy envelope with `lifecycle.revokedAt` set, or any envelope whose `expiresAt` passed.
244
+ * The Delivery API only serves CertREV-signed artifacts, so this shape read is a safe suppress
245
+ * signal for the display surface (the `<certrev-badge>` revalidation runs the full ed25519
246
+ * verdict via `verifyArtifact` — this is the modal's belt-and-suspenders).
247
+ */
248
+ export function isArtifactSuppressed(artifact) {
249
+ if (!artifact)
250
+ return false;
251
+ if (isTombstone(artifact))
252
+ return true;
253
+ const lc = artifact.payload?.lifecycle;
254
+ if (!lc)
255
+ return false;
256
+ if (lc.revokedAt != null)
257
+ return true;
258
+ if (lc.expiresAt && Date.now() >= new Date(lc.expiresAt).getTime())
259
+ return true;
260
+ return false;
261
+ }
262
+ /** Register the element once (idempotent — a double script-load must not throw). */
263
+ export function defineCertModal() {
264
+ if (typeof customElements === 'undefined')
265
+ return;
266
+ if (!customElements.get(CERT_MODAL_TAG))
267
+ customElements.define(CERT_MODAL_TAG, CertRevCertModal);
268
+ }
269
+ defineCertModal();
270
+ //# sourceMappingURL=certrev-cert-modal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"certrev-cert-modal.js","sourceRoot":"","sources":["../../src/modal/certrev-cert-modal.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Scoped styles + inline SVGs for the `<certrev-cert-modal>` Shadow-DOM element
3
+ * (POR-10102). The single source of truth for the certificate (3a) + reviewer (4b)
4
+ * modal look, ported byte-faithfully from the Claude Design reference
5
+ * (`docs/claude/design/certificate-modals/`, footer variant 3a + reviewer 4b).
6
+ *
7
+ * Because the element renders inside a shadow root, this CSS is fully scoped: the
8
+ * brand storefront theme cannot bleed in, and these rules never leak out — so the
9
+ * old `certrev-cert.css` "theme-bleed armor" and the literal-hex-only constraint
10
+ * (which the `body_html` WordPress modal still needs) do NOT apply here. Tokens are
11
+ * plain custom properties on `:host`; fonts resolve from document-level `@font-face`
12
+ * that the element injects on first construct (font faces do not scope into a shadow
13
+ * root, so they must live at document level — see `certrev-cert-modal.ts`).
14
+ */
15
+ /** The four-chevron CertREV mark (viewBox 0 0 375 375), tinted via the passed fill. Path data is
16
+ * single-sourced from src/lib/design-guide/logo.ts (the SOT the codegen mirrors into every twin). */
17
+ export declare function chevronMark(size: number, fill: string): string;
18
+ /** The 8px lime-chip check glyph (viewBox 0 0 24 24). */
19
+ export declare function checkIcon(size?: number): string;
20
+ /**
21
+ * The rotated 74×74 verified stamp: two concentric navy rings + a circular
22
+ * `textPath` reading "CERTREV VERIFIED · CERTIFIED ·" + the chevron mark centered.
23
+ * `idSuffix` uniquifies the internal `<path>` id so multiple stamps on one page
24
+ * (or a story grid) don't collide on the shared `href`.
25
+ */
26
+ export declare function verifiedStamp(idSuffix?: string): string;
27
+ /**
28
+ * The five families the modal uses, self-hosted-or-Google (the element injects
29
+ * ONE document-level `<link>`/`@font-face` for these; font-display:swap). Kept as
30
+ * a constant so the loader + any story reference the identical set.
31
+ */
32
+ export declare const CERT_MODAL_FONT_FAMILIES: readonly ["DM Sans", "Playfair Display", "JetBrains Mono", "Plus Jakarta Sans", "Allura"];
33
+ /** The full scoped stylesheet, adopted via a constructable `CSSStyleSheet`. */
34
+ export declare const CERT_MODAL_CSS = "\n:host {\n\t--cr-navy: #0a1b3f;\n\t--cr-lime: #d4e157;\n\t--cr-white: #ffffff;\n\t--cr-navy-06: rgba(10, 27, 63, 0.06);\n\t--cr-navy-10: rgba(10, 27, 63, 0.1);\n\t--cr-navy-25: rgba(10, 27, 63, 0.25);\n\t--cr-navy-40: rgba(10, 27, 63, 0.4);\n\t--cr-navy-45: rgba(10, 27, 63, 0.45);\n\t--cr-navy-50: rgba(10, 27, 63, 0.5);\n\t--cr-navy-55: rgba(10, 27, 63, 0.55);\n\t--cr-navy-60: rgba(10, 27, 63, 0.6);\n\t--cr-navy-85: rgba(10, 27, 63, 0.85);\n\t--cr-white-60: rgba(255, 255, 255, 0.6);\n\t--cr-white-85: rgba(255, 255, 255, 0.85);\n\t--cr-cta-hover: #0c2149;\n\t--cr-ghost-hover: rgba(10, 27, 63, 0.12);\n\t--cr-shadow: 0 30px 60px -18px rgba(10, 27, 63, 0.5);\n\t--cr-backdrop: rgba(10, 27, 63, 0.45);\n\t--cr-sans: 'DM Sans', system-ui, -apple-system, sans-serif;\n\t--cr-serif: 'Playfair Display', Georgia, serif;\n\t--cr-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;\n\t--cr-wordmark: 'Plus Jakarta Sans', system-ui, sans-serif;\n\t--cr-script: 'Allura', cursive;\n}\n\n.crm-dialog {\n\tborder: none;\n\tborder-radius: 14px;\n\tpadding: 0;\n\twidth: 92%;\n\tmax-height: 92vh;\n\toverflow: auto;\n\tbox-shadow: var(--cr-shadow);\n\tbackground: var(--cr-white);\n\tcolor: var(--cr-navy);\n\tfont-family: var(--cr-sans);\n\t/* fixed, NOT relative (POR-10473): a :modal dialog (showModal) renders in the TOP LAYER; a\n\t * relative dialog anchors to its scrolled document position, so once the page is scrolled the\n\t * panel renders off-screen while the ::backdrop still covers the viewport (measured top -1972 at\n\t * scrollY 2537). fixed is viewport-anchored (the UA inset:0 + margin:auto centers it) and still\n\t * provides the containing block for the absolutely-positioned .crm-close (x stays in-corner). */\n\tposition: fixed;\n\t/* defensive resets against a hostile brand theme reaching the top-layer dialog */\n\tbox-sizing: border-box;\n\ttext-align: left;\n\tline-height: 1.4;\n}\n.crm-dialog *,\n.crm-dialog *::before,\n.crm-dialog *::after {\n\tbox-sizing: border-box;\n}\n.crm-dialog::backdrop {\n\tbackground: var(--cr-backdrop);\n}\n.crm-dialog--cert {\n\tmax-width: 484px;\n}\n.crm-dialog--expert {\n\tmax-width: 436px;\n}\n\n/* Close (\u00D7) */\n.crm-close {\n\tposition: absolute;\n\ttop: 12px;\n\tright: 15px;\n\tz-index: 2;\n\tbackground: none;\n\tborder: none;\n\tcursor: pointer;\n\tpadding: 2px;\n\tcolor: var(--cr-white-60);\n\tfont-size: 1.35rem;\n\tline-height: 1;\n\tfont-family: var(--cr-sans);\n\ttransition: color 0.15s;\n}\n.crm-close:hover {\n\tcolor: var(--cr-white);\n}\n\n/* Header bar */\n.crm-head {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 9px;\n\tpadding: 13px 22px;\n\tbackground: var(--cr-navy);\n\tcolor: var(--cr-white);\n}\n.crm-head__eyebrow {\n\tfont-family: var(--cr-mono);\n\tfont-size: 8.5px;\n\tfont-weight: 600;\n\tletter-spacing: 0.16em;\n\ttext-transform: uppercase;\n\tcolor: var(--cr-white-85);\n}\n.crm-head__wordmark {\n\tmargin-left: auto;\n\tmargin-right: 22px;\n\tfont-family: var(--cr-wordmark);\n\tfont-weight: 800;\n\tfont-size: 13px;\n\tletter-spacing: 0.03em;\n\tcolor: var(--cr-white);\n}\n\n/* Body */\n.crm-body--cert {\n\tpadding: 24px 28px 26px;\n}\n.crm-body--expert {\n\tpadding: 22px 26px 24px;\n}\n\n/* Avatar (shared, lime ring) */\n.crm-avatar {\n\tposition: relative;\n\tflex-shrink: 0;\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tborder-radius: 50%;\n\tbackground: var(--cr-navy);\n\tcolor: var(--cr-white);\n\tfont-weight: 700;\n\tobject-fit: cover;\n\tbox-shadow: 0 0 0 2px var(--cr-white), 0 0 0 3.5px var(--cr-lime);\n}\n.crm-avatar--56 {\n\twidth: 56px;\n\theight: 56px;\n\tfont-size: 17px;\n}\n.crm-avatar--64 {\n\twidth: 64px;\n\theight: 64px;\n\tfont-size: 1.35rem;\n\tletter-spacing: 0.02em;\n}\n\n/* Certificate \u2014 reviewing-expert row */\n.crm-expert-row {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 14px;\n}\n.crm-expert-row__info {\n\tflex: 1;\n\tmin-width: 0;\n}\n.crm-expert-row__eyebrow {\n\tfont-family: var(--cr-mono);\n\tfont-size: 8.5px;\n\tfont-weight: 600;\n\tletter-spacing: 0.14em;\n\ttext-transform: uppercase;\n\tcolor: var(--cr-navy-45);\n}\n.crm-expert-row__name {\n\tfont-size: 15px;\n\tfont-weight: 700;\n\tcolor: var(--cr-navy);\n\tmargin-top: 2px;\n}\n.crm-expert-row__creds {\n\tfont-size: 12px;\n\tcolor: var(--cr-navy-60);\n\tmargin-top: 1px;\n}\n\n/* Certificate \u2014 \"Certifies the article\" */\n.crm-certifies {\n\tmargin: 18px 0 0;\n\tpadding-top: 16px;\n\tborder-top: 1px solid var(--cr-navy-10);\n}\n.crm-certifies__eyebrow {\n\tfont-family: var(--cr-mono);\n\tfont-size: 9px;\n\tfont-weight: 600;\n\tletter-spacing: 0.16em;\n\ttext-transform: uppercase;\n\tcolor: var(--cr-navy-40);\n}\n.crm-certifies__title {\n\tfont-family: var(--cr-serif);\n\tfont-weight: 700;\n\tfont-size: 18px;\n\tline-height: 1.25;\n\tcolor: var(--cr-navy);\n\tmargin-top: 5px;\n\ttext-wrap: balance;\n}\n\n/* Certificate \u2014 meta row */\n.crm-meta {\n\tdisplay: flex;\n\tgap: 10px;\n\tmargin-top: 16px;\n}\n.crm-meta__col {\n\tflex: 1;\n}\n.crm-meta__label {\n\tfont-family: var(--cr-mono);\n\tfont-size: 8px;\n\tletter-spacing: 0.12em;\n\ttext-transform: uppercase;\n\tcolor: var(--cr-navy-40);\n}\n.crm-meta__value {\n\tfont-size: 12.5px;\n\tfont-weight: 600;\n\tcolor: var(--cr-navy);\n\tmargin-top: 3px;\n}\n.crm-meta__value--mono {\n\tfont-family: var(--cr-mono);\n\tfont-size: 12px;\n}\n.crm-meta__status {\n\tmargin-top: 3px;\n}\n\n/* Certified chip (meta status) */\n.crm-chip {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 3px;\n\tpadding: 2px 7px;\n\tborder-radius: 4px;\n\tbackground: var(--cr-lime);\n\tcolor: var(--cr-navy);\n\tfont-family: var(--cr-mono);\n\tfont-size: 8.5px;\n\tfont-weight: 700;\n\tletter-spacing: 0.06em;\n\ttext-transform: uppercase;\n}\n\n/* Certificate \u2014 signature + stamp footer */\n.crm-sig {\n\tdisplay: flex;\n\talign-items: flex-end;\n\tjustify-content: space-between;\n\tmargin-top: 20px;\n\tpadding-top: 16px;\n\tborder-top: 1px solid var(--cr-navy-10);\n\tgap: 12px;\n}\n.crm-sig__name {\n\tfont-family: var(--cr-script);\n\tfont-size: 30px;\n\tline-height: 0.9;\n\tcolor: var(--cr-navy);\n}\n.crm-sig__rule {\n\twidth: 146px;\n\tmax-width: 100%;\n\theight: 1px;\n\tbackground: var(--cr-navy-25);\n\tmargin: 4px 0 6px;\n}\n.crm-sig__url {\n\tfont-family: var(--cr-mono);\n\tfont-size: 8px;\n\tfont-weight: 500;\n\tletter-spacing: 0.12em;\n\ttext-transform: uppercase;\n\tcolor: var(--cr-navy-50);\n}\n.crm-stamp {\n\ttransform: rotate(-8deg);\n\tflex-shrink: 0;\n}\n.crm-stamp svg {\n\tdisplay: block;\n}\n\n/* Actions (shared) */\n.crm-actions {\n\tdisplay: flex;\n\tgap: 10px;\n\tmargin-top: 20px;\n}\n.crm-cta {\n\tflex: 1;\n\tdisplay: inline-block;\n\ttext-align: center;\n\tpadding: 11px 16px;\n\tbackground: var(--cr-navy);\n\tcolor: var(--cr-white);\n\ttext-decoration: none;\n\tborder-radius: 8px;\n\tfont-size: 13px;\n\tfont-weight: 600;\n\tfont-family: var(--cr-sans);\n\tborder: none;\n\tcursor: pointer;\n\ttransition: background 0.15s;\n}\n.crm-cta:hover {\n\tbackground: var(--cr-cta-hover);\n}\n.crm-ghost {\n\tpadding: 11px 18px;\n\tbackground: var(--cr-navy-06);\n\tcolor: var(--cr-navy);\n\tborder: none;\n\tborder-radius: 8px;\n\tfont-size: 13px;\n\tfont-weight: 600;\n\tfont-family: var(--cr-sans);\n\tcursor: pointer;\n\ttransition: background 0.15s;\n}\n.crm-ghost:hover {\n\tbackground: var(--cr-ghost-hover);\n}\n\n/* Reviewer \u2014 identity row */\n.crm-id {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 14px;\n}\n.crm-id__info {\n\tmin-width: 0;\n}\n.crm-id__name {\n\tfont-size: 16px;\n\tfont-weight: 700;\n\tline-height: 1.25;\n\tcolor: var(--cr-navy);\n}\n.crm-id__subtitle {\n\tfont-size: 12.5px;\n\tline-height: 1.4;\n\tcolor: var(--cr-navy-60);\n\tmargin-top: 2px;\n}\n\n/* Reviewer \u2014 credential chips */\n.crm-chips {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 7px;\n\tmargin-top: 16px;\n}\n.crm-chips__chip {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tpadding: 4px 10px;\n\tborder-radius: 999px;\n\tbackground: var(--cr-navy-06);\n\tcolor: var(--cr-navy);\n\tfont-size: 11px;\n\tfont-weight: 600;\n}\n\n/* Reviewer \u2014 bio */\n.crm-bio {\n\tmargin: 16px 0 0;\n\tfont-size: 0.9rem;\n\tline-height: 1.55;\n\tcolor: var(--cr-navy-85);\n}\n\n/* Reviewer \u2014 trust line (the disclosure footer below now owns the spacing to the actions) */\n.crm-trust {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 9px;\n\tflex-wrap: wrap;\n\tmargin: 16px 0 0;\n\tpadding-top: 16px;\n\tborder-top: 1px solid var(--cr-navy-10);\n}\n.crm-trust__chip {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 4px;\n\tpadding: 3px 8px;\n\tborder-radius: 4px;\n\tbackground: var(--cr-lime);\n\tcolor: var(--cr-navy);\n\tfont-family: var(--cr-mono);\n\tfont-size: 8.5px;\n\tfont-weight: 700;\n\tletter-spacing: 0.08em;\n\ttext-transform: uppercase;\n}\n.crm-trust__text {\n\tfont-family: var(--cr-mono);\n\tfont-size: 9px;\n\tletter-spacing: 0.05em;\n\tcolor: var(--cr-navy-50);\n}\n\n/* FTC disclosure footer (POR-10496) \u2014 material-connection cue (compensated only) + scope line\n * (always). Rendered on both the certificate + reviewer dialogs, immediately above the actions. */\n.crm-disclosure {\n\tmargin-top: 14px;\n\tpadding-top: 14px;\n\tborder-top: 1px solid var(--cr-navy-10);\n\tfont-family: var(--cr-sans);\n\tfont-size: 11.5px;\n\tline-height: 1.5;\n\tcolor: var(--cr-navy-50);\n}\n.crm-disclosure__cue {\n\tfont-family: var(--cr-mono);\n\tfont-size: 10px;\n\tfont-weight: 700;\n\tletter-spacing: 0.06em;\n\ttext-transform: uppercase;\n\tcolor: var(--cr-navy);\n}\n\n/* Narrow storefronts / phones */\n@media (max-width: 440px) {\n\t.crm-body--cert {\n\t\tpadding: 20px 18px 22px;\n\t}\n\t.crm-body--expert {\n\t\tpadding: 18px 18px 20px;\n\t}\n}\n";
35
+ //# sourceMappingURL=certrev-cert-modal.styles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"certrev-cert-modal.styles.d.ts","sourceRoot":"","sources":["../../src/modal/certrev-cert-modal.styles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH;qGACqG;AACrG,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAG9D;AAED,yDAAyD;AACzD,wBAAgB,SAAS,CAAC,IAAI,SAAI,GAAG,MAAM,CAE1C;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,QAAQ,SAAQ,GAAG,MAAM,CAItD;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,2FAM3B,CAAA;AAOV,+EAA+E;AAC/E,eAAO,MAAM,cAAc,ghTAib1B,CAAA"}