@eazo/sdk 0.13.0 → 0.14.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.
Files changed (43) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/internal/banner-ui/app-info.d.ts +78 -0
  4. package/dist/internal/banner-ui/app-info.d.ts.map +1 -0
  5. package/dist/internal/banner-ui/app-info.js +62 -0
  6. package/dist/internal/banner-ui/app-info.js.map +1 -0
  7. package/dist/internal/banner-ui/icons.d.ts +18 -4
  8. package/dist/internal/banner-ui/icons.d.ts.map +1 -1
  9. package/dist/internal/banner-ui/icons.js +57 -4
  10. package/dist/internal/banner-ui/icons.js.map +1 -1
  11. package/dist/internal/banner-ui/index.d.ts +16 -3
  12. package/dist/internal/banner-ui/index.d.ts.map +1 -1
  13. package/dist/internal/banner-ui/index.js +336 -41
  14. package/dist/internal/banner-ui/index.js.map +1 -1
  15. package/dist/internal/banner-ui/initial-info.d.ts +4 -0
  16. package/dist/internal/banner-ui/initial-info.d.ts.map +1 -0
  17. package/dist/internal/banner-ui/initial-info.js +22 -0
  18. package/dist/internal/banner-ui/initial-info.js.map +1 -0
  19. package/dist/internal/banner-ui/qr.d.ts +22 -0
  20. package/dist/internal/banner-ui/qr.d.ts.map +1 -0
  21. package/dist/internal/banner-ui/qr.js +95 -0
  22. package/dist/internal/banner-ui/qr.js.map +1 -0
  23. package/dist/internal/banner-ui/store-links.d.ts +38 -0
  24. package/dist/internal/banner-ui/store-links.d.ts.map +1 -1
  25. package/dist/internal/banner-ui/store-links.js +49 -0
  26. package/dist/internal/banner-ui/store-links.js.map +1 -1
  27. package/dist/internal/banner-ui/styles.d.ts +4 -2
  28. package/dist/internal/banner-ui/styles.d.ts.map +1 -1
  29. package/dist/internal/banner-ui/styles.js +578 -66
  30. package/dist/internal/banner-ui/styles.js.map +1 -1
  31. package/dist/react.d.ts +26 -1
  32. package/dist/react.d.ts.map +1 -1
  33. package/dist/react.js +15 -1
  34. package/dist/react.js.map +1 -1
  35. package/dist/react.server.d.ts +22 -0
  36. package/dist/react.server.d.ts.map +1 -0
  37. package/dist/react.server.js +118 -0
  38. package/dist/react.server.js.map +1 -0
  39. package/dist/server.d.ts +2 -0
  40. package/dist/server.d.ts.map +1 -1
  41. package/dist/server.js +13 -1
  42. package/dist/server.js.map +1 -1
  43. package/package.json +7 -2
@@ -1,92 +1,540 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.BANNER_UI_CSS = exports.BANNER_HEIGHT_MOBILE = exports.BANNER_HEIGHT_DESKTOP = void 0;
3
+ exports.BANNER_UI_CSS = exports.BOTTOM_HEIGHT_MOBILE = exports.BOTTOM_HEIGHT_DESKTOP = exports.BANNER_HEIGHT_MOBILE = exports.BANNER_HEIGHT_DESKTOP = void 0;
4
4
  exports.ensureBannerStylesInjected = ensureBannerStylesInjected;
5
5
  const STYLE_ID = "eazo-sdk-banner-ui";
6
6
  exports.BANNER_HEIGHT_DESKTOP = 52;
7
- exports.BANNER_HEIGHT_MOBILE = 64;
7
+ exports.BANNER_HEIGHT_MOBILE = 56;
8
+ /* Sized so the V5 / M5 bottom banner fits a 44px coral pill plus the
9
+ * design's 14 / 22-px breathing pad. Bumping these here also bumps the
10
+ * `<html>` padding-bottom the SDK reserves (see banner-ui/index.tsx),
11
+ * so the host page never tucks under the banner. */
12
+ exports.BOTTOM_HEIGHT_DESKTOP = 72;
13
+ exports.BOTTOM_HEIGHT_MOBILE = 78;
14
+ const TOKENS = `
15
+ --eazo-cream: #f1ebe0;
16
+ --eazo-paper: #faf6ee;
17
+ --eazo-ink: #11130f;
18
+ --eazo-ink-soft: rgba(17,19,15,0.62);
19
+ --eazo-ink-faint: rgba(17,19,15,0.32);
20
+ --eazo-hair: rgba(17,19,15,0.10);
21
+ --eazo-coral: #d4614a;
22
+ --eazo-coral-gradient: linear-gradient(180deg, #F47A42 0%, #EE5C2A 100%);
23
+ --eazo-glow: rgba(212,97,74,0.36);
24
+ --eazo-sans: "Inter", "Helvetica Neue", system-ui, sans-serif;
25
+ --eazo-serif: "Source Serif 4", "GT Sectra", "Tiempos", Georgia, serif;
26
+ --eazo-mono: "JetBrains Mono", "IBM Plex Mono", ui-monospace, Menlo, monospace;
27
+ `;
8
28
  exports.BANNER_UI_CSS = `
9
- .eazo-banner-root {
29
+ /* The whole handoff UI lives inside ONE fixed-positioned container that
30
+ * fills the viewport and flex-columns its three children: top banner +
31
+ * overlay (which holds the modal) + bottom banner. This replaces the
32
+ * earlier design where each piece was independently position:fixed
33
+ * with hand-tuned top:52px / bottom:60px insets — that scheme broke
34
+ * any time an ancestor of the SDK mount established a containing block
35
+ * (transform / filter / backdrop-filter / contain on <body>, a wrapper,
36
+ * etc.), at which point position:fixed becomes relative to that
37
+ * ancestor and the math goes wrong. Flex layout makes the overlay
38
+ * genuinely between the banners by structure, not by pixel math.
39
+ *
40
+ * The root is pointer-events:none so the user's page underneath stays
41
+ * interactive in transparent regions (there shouldn't be any when the
42
+ * overlay's modal is up, but it's the right default). Each visual child
43
+ * (banners + overlay dim) opts back in with pointer-events:auto. */
44
+ .eazo-handoff-root {
45
+ ${TOKENS}
10
46
  position: fixed;
11
- top: 0;
12
- left: 0;
13
- right: 0;
14
- z-index: 2147483550;
47
+ inset: 0;
48
+ z-index: 2147483540;
15
49
  display: flex;
16
- align-items: center;
17
- gap: 12px;
18
- height: ${exports.BANNER_HEIGHT_DESKTOP}px;
19
- padding: 0 14px 0 18px;
20
- background: #f1ebe0;
21
- color: #11130f;
22
- font-family: inherit;
50
+ flex-direction: column;
51
+ /* justify-content:space-between keeps the bottom banner pinned even
52
+ * when the user dismisses the modal (the overlay child unmounts) —
53
+ * without it the flex-column would collapse the bottom banner up to
54
+ * sit right under the top one. */
55
+ justify-content: space-between;
56
+ color: var(--eazo-ink);
57
+ font-family: var(--eazo-sans);
58
+ box-sizing: border-box;
59
+ pointer-events: none;
60
+ }
61
+ .eazo-handoff-root *, .eazo-handoff-root *::before, .eazo-handoff-root *::after {
23
62
  box-sizing: border-box;
24
- animation: eazo-banner-slide-down 220ms cubic-bezier(0.16, 1, 0.3, 1);
25
63
  }
26
64
 
27
- @keyframes eazo-banner-slide-down {
65
+ @keyframes eazo-handoff-slide-down {
28
66
  from { transform: translateY(-100%); opacity: 0; }
29
67
  to { transform: translateY(0); opacity: 1; }
30
68
  }
69
+ @keyframes eazo-handoff-slide-up {
70
+ from { transform: translateY(100%); opacity: 0; }
71
+ to { transform: translateY(0); opacity: 1; }
72
+ }
73
+ @keyframes eazo-handoff-orbit { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
74
+ @keyframes eazo-handoff-orbit-rev { from { transform: rotate(360deg); } to { transform: rotate(0deg); } }
75
+ @keyframes eazo-handoff-glow { 0%,100% { opacity: 0.7; } 50% { opacity: 1; } }
76
+ @keyframes eazo-handoff-fade-in { from { opacity: 0; } to { opacity: 1; } }
77
+ @keyframes eazo-handoff-pop-in {
78
+ from { opacity: 0; transform: translateY(12px) scale(0.97); }
79
+ to { opacity: 1; transform: translateY(0) scale(1); }
80
+ }
31
81
 
32
- .eazo-banner-brand {
33
- display: inline-flex;
82
+ /* ============ TOP BANNER ============
83
+ *
84
+ * Slim three-piece strip: brand mark, single-line copy, CTA. The
85
+ * underlying app's content sits below this. Non-dismissible.
86
+ */
87
+ .eazo-banner-root {
88
+ /* Flex child of .eazo-handoff-root — naturally pinned to the top of
89
+ * the viewport-filling container. No position:fixed needed. */
90
+ flex-shrink: 0;
91
+ display: flex;
34
92
  align-items: center;
93
+ gap: 12px;
94
+ height: ${exports.BANNER_HEIGHT_DESKTOP}px;
95
+ padding: 0 14px 0 18px;
96
+ background: var(--eazo-cream);
97
+ border-bottom: 1px solid var(--eazo-hair);
98
+ pointer-events: auto;
99
+ animation: eazo-handoff-slide-down 240ms cubic-bezier(0.16, 1, 0.3, 1);
100
+ }
101
+ .eazo-banner-brand {
102
+ display: inline-flex; align-items: center;
35
103
  flex-shrink: 0;
36
- color: #11130f;
104
+ color: var(--eazo-ink);
37
105
  }
38
-
39
106
  .eazo-banner-copy {
40
- flex: 1;
41
- min-width: 0;
42
- font-size: 14px;
43
- font-weight: 500;
44
- color: rgba(17, 19, 15, 0.62);
45
- overflow: hidden;
46
- text-overflow: ellipsis;
47
- white-space: nowrap;
107
+ flex: 1; min-width: 0;
108
+ font-size: 14px; font-weight: 500;
109
+ color: var(--eazo-ink-soft);
110
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
48
111
  }
49
-
50
112
  .eazo-banner-cta {
51
113
  flex-shrink: 0;
52
- display: inline-flex;
53
- align-items: center;
54
- height: 34px;
55
- padding: 0 14px;
56
- border-radius: 12px;
57
- background: #d4614a;
58
- color: #ffffff;
59
- font-size: 13px;
60
- font-weight: 600;
114
+ display: inline-flex; align-items: center; gap: 6px;
115
+ height: 30px; padding: 0 14px; border-radius: 10px;
116
+ background: var(--eazo-coral-gradient); color: #fff;
117
+ font-size: 12px; font-weight: 600; border: 0; cursor: pointer;
61
118
  text-decoration: none;
62
- cursor: pointer;
119
+ box-shadow: 0 10px 22px var(--eazo-glow);
63
120
  transition: filter 160ms ease, box-shadow 160ms ease;
64
121
  }
65
- .eazo-banner-cta:hover {
66
- filter: brightness(1.06);
67
- box-shadow: 0 8px 18px rgba(212, 97, 74, 0.36);
68
- }
122
+ .eazo-banner-cta:hover { filter: brightness(1.06); }
69
123
 
70
- .eazo-banner-close {
71
- flex-shrink: 0;
72
- width: 28px;
73
- height: 28px;
124
+ /* CTA wrapper anchors the hover/focus popover. position:relative is the
125
+ * coordinate origin for the absolutely-positioned popover below. */
126
+ .eazo-banner-cta-wrap {
127
+ position: relative;
74
128
  display: inline-flex;
129
+ flex-shrink: 0;
130
+ }
131
+
132
+ /* Hover popover holding the page-URL QR. Matches the v5-stagelight
133
+ * design (project/v5-stagelight.jsx:59-85). The CTA's right edge anchors
134
+ * the right edge of the popover so it never spills off the viewport on
135
+ * a banner where the CTA is hugged to the right padding. */
136
+ .eazo-banner-cta-popover {
137
+ position: absolute;
138
+ top: calc(100% + 10px);
139
+ right: 0;
140
+ z-index: 2147483560;
141
+ display: flex; flex-direction: column; align-items: center; gap: 8px;
142
+ min-width: 168px;
143
+ padding: 14px;
144
+ background: #fff;
145
+ border: 1px solid var(--eazo-hair);
146
+ border-radius: 14px;
147
+ box-shadow:
148
+ 0 24px 50px -20px rgba(17,19,15,0.22),
149
+ 0 0 0 1px rgba(17,19,15,0.03);
150
+ animation: eazo-handoff-fade-in 140ms ease-out;
151
+ }
152
+ /* Triangular tail pointing back up at the CTA. Rotated square so it
153
+ * inherits the card's border + background without an extra SVG. */
154
+ .eazo-banner-cta-popover-arrow {
155
+ position: absolute;
156
+ top: -7px; right: 24px;
157
+ width: 12px; height: 12px;
158
+ background: #fff;
159
+ border-top: 1px solid var(--eazo-hair);
160
+ border-left: 1px solid var(--eazo-hair);
161
+ transform: rotate(45deg);
162
+ }
163
+ .eazo-banner-cta-popover-qr {
164
+ padding: 4px;
165
+ background: #fff;
166
+ line-height: 0;
167
+ }
168
+ .eazo-banner-cta-popover-caption {
169
+ font-family: var(--eazo-mono);
170
+ font-size: 11px;
171
+ line-height: 1.4;
172
+ letter-spacing: 0.04em;
173
+ color: var(--eazo-ink-soft);
174
+ text-align: center;
175
+ }
176
+
177
+ /* ============ OVERLAY (backdrop + spotlight + modal) ============
178
+ *
179
+ * The flex-middle of .eazo-handoff-root. Takes whatever vertical space
180
+ * the top and bottom banners don't claim — i.e. it IS the inter-banner
181
+ * area by structure, not by pixel math. overflow:hidden clips any
182
+ * oversized modal at this seam; the modal's own max-height:100% plus
183
+ * the overlay's flex centering keeps it inside.
184
+ */
185
+ .eazo-handoff-overlay {
186
+ flex: 1;
187
+ min-height: 0; /* allow the flex item to shrink below content size */
188
+ position: relative;
189
+ display: flex;
75
190
  align-items: center;
76
191
  justify-content: center;
77
- border: 0;
192
+ overflow: hidden;
193
+ padding: 16px;
194
+ pointer-events: auto;
195
+ animation: eazo-handoff-fade-in 320ms ease-out;
196
+ }
197
+ .eazo-handoff-overlay-dim {
198
+ position: absolute; inset: 0;
199
+ background: rgba(241,235,224,0.78);
200
+ backdrop-filter: blur(3px);
201
+ -webkit-backdrop-filter: blur(3px);
202
+ }
203
+ .eazo-handoff-overlay-spot {
204
+ position: absolute; inset: 0;
205
+ background: radial-gradient(ellipse at 50% 50%, rgba(212,97,74,0.22) 0%, rgba(212,97,74,0.06) 30%, transparent 58%);
206
+ pointer-events: none;
207
+ }
208
+
209
+ .eazo-modal {
210
+ /* Natural flex centering by the overlay parent — no absolute
211
+ * positioning. This keeps the modal inside the overlay's banner-
212
+ * constrained box even when its content is tall, so it never bleeds
213
+ * into the top or bottom banner area. If the modal is taller than the
214
+ * overlay, the inner content scrolls. */
215
+ position: relative;
216
+ width: min(540px, 100%);
217
+ max-height: 100%;
218
+ overflow-y: auto;
219
+ padding: 32px 32px 28px;
220
+ background: rgba(255,255,255,0.92);
221
+ backdrop-filter: blur(20px);
222
+ -webkit-backdrop-filter: blur(20px);
223
+ border: 1px solid var(--eazo-hair);
224
+ border-radius: 24px;
225
+ color: var(--eazo-ink);
226
+ box-shadow:
227
+ 0 60px 100px -40px rgba(17,19,15,0.28),
228
+ inset 0 1px 0 rgba(255,255,255,0.7),
229
+ 0 0 60px var(--eazo-glow);
230
+ display: flex; flex-direction: column; align-items: center; gap: 18px;
231
+ animation: eazo-handoff-pop-in 360ms cubic-bezier(0.16, 1, 0.3, 1) both;
232
+ }
233
+ /* Close button — sits in the modal's top-right corner. The top + bottom
234
+ * Eazo banners stay visible after the user dismisses the modal; only
235
+ * this center "strong CTA" goes away (per-tab via sessionStorage). */
236
+ .eazo-modal-close {
237
+ position: absolute;
238
+ top: 12px; right: 12px;
239
+ width: 30px; height: 30px;
240
+ display: inline-flex; align-items: center; justify-content: center;
241
+ border: 0; padding: 0;
78
242
  border-radius: 999px;
79
- background: transparent;
80
- color: rgba(17, 19, 15, 0.52);
243
+ background: rgba(17,19,15,0.04);
244
+ color: var(--eazo-ink-soft);
81
245
  cursor: pointer;
82
- padding: 0;
83
- transition: color 120ms ease, background 120ms ease;
246
+ transition: background 140ms ease, color 140ms ease;
247
+ }
248
+ .eazo-modal-close:hover {
249
+ background: rgba(17,19,15,0.08);
250
+ color: var(--eazo-ink);
251
+ }
252
+ .eazo-modal-close:focus-visible {
253
+ outline: 2px solid var(--eazo-coral);
254
+ outline-offset: 2px;
255
+ }
256
+
257
+ /* ============ ORBITING CAPABILITIES + APP MONOLITH ============
258
+ *
259
+ * Geometry runs in a 280-unit coordinate space (matches the V5 design
260
+ * canvas). The rings SVG uses a viewBox so its content scales to whatever
261
+ * pixel size the .eazo-orbit container is in CSS (280 desktop, 220
262
+ * mobile). The capability nodes position via percentage left/top on
263
+ * the rotating track, then use negative margins to center on that point
264
+ * — margins do not fight the track rotate animation the way a
265
+ * transform: translate(-50%, -50%) would.
266
+ */
267
+ .eazo-orbit {
268
+ position: relative;
269
+ width: 280px; height: 280px;
270
+ display: grid; place-items: center;
271
+ }
272
+ .eazo-orbit-rings {
273
+ position: absolute; inset: 0;
274
+ width: 100%; height: 100%;
275
+ opacity: 0.95;
276
+ }
277
+ .eazo-orbit-track {
278
+ position: absolute; inset: 0; width: 100%; height: 100%;
279
+ animation: eazo-handoff-orbit 30s linear infinite;
280
+ }
281
+ .eazo-orbit-node {
282
+ position: absolute;
283
+ width: 36px; height: 36px;
284
+ margin: -18px 0 0 -18px;
285
+ border-radius: 10px;
286
+ background: #fff; border: 1px solid var(--eazo-hair);
287
+ display: grid; place-items: center;
288
+ box-shadow: 0 10px 22px -10px rgba(17,19,15,0.15);
289
+ animation: eazo-handoff-orbit-rev 30s linear infinite;
290
+ color: var(--eazo-coral);
84
291
  }
85
- .eazo-banner-close:hover {
86
- color: #11130f;
87
- background: rgba(17, 19, 15, 0.06);
292
+ .eazo-monolith {
293
+ width: 96px; height: 96px; border-radius: 22px;
294
+ /* Default fallback background visible behind emoji icons and the
295
+ * typographic initials fallback. URL icons render as a child <img>
296
+ * that covers this completely. Eazo coral gradient (same as primary
297
+ * CTAs) so the empty state reads as a clear Eazo-brand placeholder. */
298
+ background: var(--eazo-coral-gradient);
299
+ display: grid; place-items: center;
300
+ position: relative;
301
+ color: #ffffff;
302
+ font-family: var(--eazo-serif); font-weight: 500;
303
+ font-size: 42px; letter-spacing: -0.02em;
304
+ box-shadow:
305
+ 0 30px 60px -20px var(--eazo-glow),
306
+ inset 0 1px 0 rgba(255,255,255,0.30),
307
+ 0 0 0 1px rgba(255,255,255,0.14);
308
+ overflow: hidden;
309
+ }
310
+ .eazo-monolith img {
311
+ width: 100%; height: 100%; object-fit: cover; display: block;
312
+ }
313
+
314
+ .eazo-modal-eyebrow {
315
+ font-family: var(--eazo-mono); font-size: 10px;
316
+ letter-spacing: 0.18em; text-transform: uppercase;
317
+ color: var(--eazo-ink-faint);
318
+ text-align: center;
319
+ }
320
+ .eazo-modal-title {
321
+ margin: 0; font-family: var(--eazo-serif); font-weight: 500;
322
+ font-size: 32px; line-height: 1.15; letter-spacing: -0.02em;
323
+ text-align: center; max-width: 360px;
324
+ /* Clamp at 2 lines so an unusually long app name doesn't blow up the
325
+ * modal height. Ellipsis takes over for the overflow. */
326
+ display: -webkit-box;
327
+ -webkit-line-clamp: 2;
328
+ -webkit-box-orient: vertical;
329
+ overflow: hidden;
330
+ word-break: break-word;
331
+ }
332
+ .eazo-modal-sub {
333
+ margin: 0; font-size: 13px; line-height: 1.5;
334
+ color: var(--eazo-ink-soft);
335
+ text-align: center; max-width: 360px;
336
+ /* Same idea — long taglines clamp to 3 lines to keep the QR + CTA
337
+ * visible without scrolling. */
338
+ display: -webkit-box;
339
+ -webkit-line-clamp: 3;
340
+ -webkit-box-orient: vertical;
341
+ overflow: hidden;
342
+ word-break: break-word;
343
+ }
344
+
345
+ /* Skeleton blocks shown while public app info is in flight. The modal
346
+ * frame appears immediately so the user sees Eazo's commitment to the
347
+ * handoff; the name / tagline swap in once the fetch resolves. */
348
+ .eazo-skel {
349
+ display: inline-block;
350
+ vertical-align: middle;
351
+ background: linear-gradient(90deg,
352
+ rgba(17,19,15,0.05) 0%,
353
+ rgba(17,19,15,0.12) 50%,
354
+ rgba(17,19,15,0.05) 100%);
355
+ background-size: 200% 100%;
356
+ border-radius: 8px;
357
+ animation: eazo-skel-shimmer 1.4s linear infinite;
358
+ }
359
+ .eazo-skel-title { width: 60%; height: 36px; }
360
+ .eazo-skel-sub-1 { width: 80%; height: 13px; margin-top: 8px; }
361
+ .eazo-skel-sub-2 { width: 55%; height: 13px; margin-top: 6px; }
362
+ .eazo-skel-stat { width: 28px; height: 11px; border-radius: 4px; }
363
+ @keyframes eazo-skel-shimmer {
364
+ from { background-position: 200% 0; }
365
+ to { background-position: -200% 0; }
366
+ }
367
+
368
+ /* Monolith-tuned shimmer — sweeps a brighter band over the dark navy
369
+ * gradient. Used while public app info is still loading, and as the
370
+ * placeholder behind an <img> until it decodes. */
371
+ .eazo-monolith-skel {
372
+ position: absolute;
373
+ inset: 0;
374
+ background: linear-gradient(90deg,
375
+ rgba(255,255,255,0.00) 0%,
376
+ rgba(255,255,255,0.18) 50%,
377
+ rgba(255,255,255,0.00) 100%);
378
+ background-size: 200% 100%;
379
+ animation: eazo-skel-shimmer 1.4s linear infinite;
380
+ pointer-events: none;
381
+ }
382
+ .eazo-monolith-img {
383
+ width: 100%; height: 100%;
384
+ object-fit: cover; display: block;
385
+ opacity: 0;
386
+ transition: opacity 220ms ease-out;
88
387
  }
388
+ .eazo-monolith-img.is-loaded { opacity: 1; }
89
389
 
390
+ /* ============ QR + CTA ROW ============ */
391
+ .eazo-cta-row {
392
+ width: 100%; display: flex; gap: 12px; align-items: stretch; margin-top: 6px;
393
+ }
394
+ .eazo-qr-tile {
395
+ padding: 8px; border-radius: 10px;
396
+ background: #fff; border: 1px solid var(--eazo-hair);
397
+ display: grid; place-items: center;
398
+ }
399
+ .eazo-cta-body {
400
+ flex: 1; min-width: 0;
401
+ display: flex; flex-direction: column; justify-content: space-between; gap: 8px;
402
+ }
403
+ .eazo-cta-headline {
404
+ font-size: 12px; font-weight: 600;
405
+ }
406
+ .eazo-cta-fine {
407
+ font-size: 10px; color: var(--eazo-ink-faint); margin-top: 4px;
408
+ font-family: var(--eazo-mono); letter-spacing: 0.04em; line-height: 1.5;
409
+ }
410
+ .eazo-cta-primary {
411
+ display: inline-flex; align-items: center; justify-content: center; gap: 8px;
412
+ height: 40px; border-radius: 10px;
413
+ background: var(--eazo-coral-gradient); color: #fff;
414
+ font-size: 13px; font-weight: 600; border: 0; cursor: pointer;
415
+ text-decoration: none;
416
+ box-shadow: 0 14px 26px var(--eazo-glow);
417
+ transition: filter 160ms ease;
418
+ }
419
+ .eazo-cta-primary:hover { filter: brightness(1.06); }
420
+
421
+ /* ============ BOTTOM BANNER ============
422
+ *
423
+ * Per V5 / M5 design: two prominent stats on the left (heart + chat,
424
+ * each rendered as a tinted icon-tile with a stacked value-over-label
425
+ * column) separated by a thin hair-divider, and a coral "Remix" pill
426
+ * on the right that reuses the top-banner CTA handoff. A small
427
+ * "eazo.ai ↗" mark sits to the left of the pill on desktop only —
428
+ * on phone widths (≤480px) it drops out so the Remix pill keeps its
429
+ * thumb-zone weight.
430
+ */
431
+ .eazo-bottom-root {
432
+ /* Flex child of .eazo-handoff-root — naturally pinned to the bottom of
433
+ * the viewport-filling container. No position:fixed needed. */
434
+ flex-shrink: 0;
435
+ display: flex; align-items: center; justify-content: space-between;
436
+ gap: 16px;
437
+ height: ${exports.BOTTOM_HEIGHT_DESKTOP}px;
438
+ padding: 0 22px 0 26px;
439
+ background: #fff;
440
+ border-top: 1px solid var(--eazo-hair);
441
+ pointer-events: auto;
442
+ animation: eazo-handoff-slide-up 240ms cubic-bezier(0.16, 1, 0.3, 1);
443
+ }
444
+ .eazo-bottom-stats {
445
+ display: inline-flex; align-items: center; gap: 22px;
446
+ min-width: 0; color: var(--eazo-ink);
447
+ }
448
+ .eazo-bottom-stat {
449
+ display: inline-flex; align-items: center; gap: 9px;
450
+ font-family: var(--eazo-sans);
451
+ flex-shrink: 0;
452
+ }
453
+ /* Tinted square tile that frames each stat icon — coral-on-cream for
454
+ * filled glyphs (heart), neutral-on-cream for line glyphs (chat). */
455
+ .eazo-bottom-stat-icon {
456
+ display: inline-flex; align-items: center; justify-content: center;
457
+ width: 30px; height: 30px; border-radius: 8px;
458
+ background: rgba(212,97,74,0.10);
459
+ color: var(--eazo-coral);
460
+ flex-shrink: 0;
461
+ }
462
+ .eazo-bottom-stat-icon.is-line {
463
+ background: rgba(17,19,15,0.05);
464
+ color: var(--eazo-ink);
465
+ }
466
+ .eazo-bottom-stat-text {
467
+ display: inline-flex; flex-direction: column; line-height: 1.05;
468
+ }
469
+ .eazo-bottom-stat-value {
470
+ font-family: var(--eazo-sans);
471
+ font-size: 16px; font-weight: 600; letter-spacing: -0.01em;
472
+ }
473
+ .eazo-bottom-stat-label {
474
+ font-family: var(--eazo-sans);
475
+ font-size: 11px; font-weight: 500;
476
+ color: var(--eazo-ink-faint);
477
+ margin-top: 1px;
478
+ }
479
+ .eazo-bottom-stat-divider {
480
+ width: 1px; height: 28px;
481
+ background: var(--eazo-hair);
482
+ flex-shrink: 0;
483
+ }
484
+ .eazo-bottom-skel {
485
+ display: inline-block; vertical-align: middle;
486
+ width: 32px; height: 18px; border-radius: 4px;
487
+ background: linear-gradient(90deg,
488
+ rgba(17,19,15,0.05) 0%,
489
+ rgba(17,19,15,0.12) 50%,
490
+ rgba(17,19,15,0.05) 100%);
491
+ background-size: 200% 100%;
492
+ animation: eazo-skel-shimmer 1.4s linear infinite;
493
+ }
494
+
495
+ .eazo-bottom-actions {
496
+ display: inline-flex; align-items: center; gap: 14px;
497
+ flex-shrink: 0;
498
+ }
499
+ .eazo-bottom-site {
500
+ display: inline-flex; align-items: center; gap: 4px;
501
+ color: var(--eazo-ink-soft);
502
+ text-decoration: none;
503
+ font-family: var(--eazo-sans); font-size: 12px; font-weight: 500;
504
+ white-space: nowrap;
505
+ transition: color 140ms ease;
506
+ }
507
+ .eazo-bottom-site:hover { color: var(--eazo-ink); }
508
+ .eazo-bottom-site b { color: var(--eazo-ink); font-weight: 600; }
509
+
510
+ /* Primary CTA on the bottom banner. Renders as <a> so it picks up the
511
+ * same iOS-timeout fallback handler as the top-banner CTA via the
512
+ * shared bindCtaClick — keeps the Remix tap on the same install /
513
+ * deeplink path as the rest of the handoff UX. */
514
+ .eazo-bottom-remix {
515
+ display: inline-flex; align-items: center; justify-content: center; gap: 9px;
516
+ height: 44px; padding: 0 20px 0 18px;
517
+ border: 0; cursor: pointer;
518
+ border-radius: 999px;
519
+ background: var(--eazo-coral-gradient); color: #fff;
520
+ font-family: var(--eazo-sans);
521
+ font-size: 14px; font-weight: 600; letter-spacing: -0.005em;
522
+ white-space: nowrap;
523
+ box-shadow:
524
+ 0 12px 24px var(--eazo-glow),
525
+ inset 0 1px 0 rgba(255,255,255,0.18);
526
+ text-decoration: none;
527
+ transition: transform 140ms ease, box-shadow 140ms ease;
528
+ }
529
+ .eazo-bottom-remix:hover {
530
+ transform: translateY(-1px);
531
+ box-shadow:
532
+ 0 14px 28px var(--eazo-glow),
533
+ inset 0 1px 0 rgba(255,255,255,0.22);
534
+ }
535
+ .eazo-bottom-remix:active { transform: translateY(0); }
536
+
537
+ /* ============ MOBILE TWEAKS (≤480px) ============ */
90
538
  @media (max-width: 480px) {
91
539
  .eazo-banner-root {
92
540
  height: ${exports.BANNER_HEIGHT_MOBILE}px;
@@ -94,25 +542,89 @@ exports.BANNER_UI_CSS = `
94
542
  gap: 10px;
95
543
  }
96
544
  .eazo-banner-copy {
97
- font-size: 12px;
98
- line-height: 1.25;
99
- white-space: normal;
100
- display: -webkit-box;
101
- -webkit-line-clamp: 2;
102
- -webkit-box-orient: vertical;
545
+ font-size: 12px; line-height: 1.25; white-space: normal;
546
+ display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
547
+ }
548
+ .eazo-banner-cta { height: 28px; padding: 0 10px; font-size: 11px; border-radius: 8px; }
549
+ /* Hover doesn't resolve reliably on touch — the CTA still works as a
550
+ * plain link, no popover needed. Belt-and-suspenders to the JS check
551
+ * (the popover render is also gated on the 'open' state, which never
552
+ * flips without mouseenter / focus). */
553
+ .eazo-banner-cta-popover { display: none; }
554
+
555
+ .eazo-modal {
556
+ width: calc(100vw - 32px);
557
+ padding: 24px 20px 20px;
558
+ border-radius: 20px;
559
+ gap: 14px;
560
+ }
561
+ .eazo-orbit { width: 220px; height: 220px; }
562
+ .eazo-monolith {
563
+ width: 76px; height: 76px; border-radius: 18px;
564
+ font-size: 32px;
565
+ }
566
+ .eazo-orbit-node {
567
+ width: 28px; height: 28px; border-radius: 8px;
568
+ margin: -14px 0 0 -14px;
569
+ }
570
+ .eazo-modal-title { font-size: 26px; }
571
+ .eazo-modal-sub { font-size: 12px; }
572
+
573
+ /* Mobile: the user is already on a phone — no point showing them a QR
574
+ * to scan with their phone, and the "Scan to open" headline + fine
575
+ * print only made sense paired with the QR. Collapse to the primary
576
+ * CTA alone. */
577
+ .eazo-qr-tile { display: none; }
578
+ .eazo-cta-row { flex-direction: column; gap: 10px; }
579
+ .eazo-cta-primary { height: 44px; width: 100%; font-size: 14px; border-radius: 12px; }
580
+ .eazo-cta-headline { display: none; }
581
+ .eazo-cta-fine { display: none; }
582
+
583
+ .eazo-bottom-root {
584
+ height: ${exports.BOTTOM_HEIGHT_MOBILE}px;
585
+ padding: 0 16px 0 20px;
586
+ gap: 12px;
103
587
  }
104
- .eazo-banner-cta {
105
- height: 32px;
106
- padding: 0 12px;
107
- font-size: 12px;
588
+ /* Tighter cells per the M5 (390px) spec: smaller icon tile, smaller
589
+ * value, smaller divider. Labels stay — they're a key part of the
590
+ * visual rhythm in M5. */
591
+ .eazo-bottom-stats { gap: 12px; }
592
+ .eazo-bottom-stat { gap: 7px; }
593
+ .eazo-bottom-stat-icon { width: 26px; height: 26px; border-radius: 7px; }
594
+ .eazo-bottom-stat-value { font-size: 14px; }
595
+ .eazo-bottom-stat-label { font-size: 10px; }
596
+ .eazo-bottom-stat-divider { height: 24px; }
597
+ .eazo-bottom-skel { width: 28px; height: 15px; }
598
+ /* M5 drops the secondary eazo.ai mark on phone widths so the Remix
599
+ * pill keeps unambiguous thumb-zone weight. */
600
+ .eazo-bottom-site { display: none; }
601
+ .eazo-bottom-remix {
602
+ height: 44px; padding: 0 18px 0 16px;
603
+ gap: 8px; font-size: 13px;
604
+ box-shadow:
605
+ 0 10px 22px var(--eazo-glow),
606
+ inset 0 1px 0 rgba(255,255,255,0.18);
108
607
  }
608
+ /* Drop the trailing "this app" wording on phone widths — the icon
609
+ * plus the verb is already unambiguous and the pill stays compact. */
610
+ .eazo-bottom-remix-suffix { display: none; }
109
611
  }
110
612
  `;
111
613
  function ensureBannerStylesInjected() {
112
614
  if (typeof document === "undefined")
113
615
  return;
114
- if (document.getElementById(STYLE_ID))
616
+ // Always overwrite the textContent rather than early-return on
617
+ // existing tag presence. Next.js Fast Refresh re-imports this module
618
+ // with updated `BANNER_UI_CSS`, but the previously-injected <style>
619
+ // tag survives the React tree's hot reload — so an early-return left
620
+ // the page running stale CSS until a hard refresh. Overwriting is
621
+ // O(short string) and idempotent.
622
+ const existing = document.getElementById(STYLE_ID);
623
+ if (existing) {
624
+ if (existing.textContent !== exports.BANNER_UI_CSS)
625
+ existing.textContent = exports.BANNER_UI_CSS;
115
626
  return;
627
+ }
116
628
  const style = document.createElement("style");
117
629
  style.id = STYLE_ID;
118
630
  style.setAttribute("data-eazo-sdk", "banner-ui");