@thg-agentic/hyper-personalisation-web 3.22.2 → 3.24.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.
@@ -0,0 +1,73 @@
1
+ /**
2
+ * A creative's RichText as DOM, through DOM APIs only: text nodes, `<b>`, `<i>` and `<a>` with an
3
+ * https href. Anything else in the tree is dropped, so no creative can carry markup or script.
4
+ *
5
+ * RichText = string | Array<string | { b: RichText } | { i: RichText } | { a: RichText, href }>
6
+ */
7
+
8
+ const MAX_DEPTH = 6;
9
+
10
+ /** The href as an https URL, or null. */
11
+ export function httpsHref(href) {
12
+ if (typeof href !== 'string') return null;
13
+ try {
14
+ const url = new URL(href);
15
+ return url.protocol === 'https:' ? url.href : null;
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+
21
+ /** An image or video asset's URL: https, or http on localhost for a local test page; else null. */
22
+ export function assetHref(href) {
23
+ if (typeof href !== 'string') return null;
24
+ try {
25
+ const url = new URL(href);
26
+ return url.protocol === 'https:' ||
27
+ (url.protocol === 'http:' && url.hostname === 'localhost')
28
+ ? url.href
29
+ : null;
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ /** The characters of a rich text, without its marks. */
36
+ export function plainText(text, depth = 0) {
37
+ if (typeof text === 'string') return text;
38
+ if (depth > MAX_DEPTH || !text || typeof text !== 'object') return '';
39
+ if (Array.isArray(text))
40
+ return text.map((t) => plainText(t, depth + 1)).join('');
41
+ return plainText(text.b ?? text.i ?? text.a, depth + 1);
42
+ }
43
+
44
+ function append(parent, node, doc, depth) {
45
+ if (typeof node === 'string') {
46
+ parent.append(doc.createTextNode(node));
47
+ return;
48
+ }
49
+ if (depth > MAX_DEPTH || node === null) return;
50
+ if (Array.isArray(node)) {
51
+ for (const child of node) append(parent, child, doc, depth + 1);
52
+ return;
53
+ }
54
+ if (typeof node !== 'object') return;
55
+ const tag = 'b' in node ? 'b' : 'i' in node ? 'i' : 'a' in node ? 'a' : null;
56
+ if (!tag) return;
57
+ const href = tag === 'a' ? httpsHref(node.href) : null;
58
+ // A link without an https target is its text alone.
59
+ const el = tag === 'a' && !href ? parent : doc.createElement(tag);
60
+ if (href) {
61
+ el.setAttribute('href', href);
62
+ el.setAttribute('rel', 'noopener');
63
+ }
64
+ append(el, node[tag], doc, depth + 1);
65
+ if (el !== parent) parent.append(el);
66
+ }
67
+
68
+ /** `text` as a fragment of text nodes, `<b>`, `<i>` and https `<a>`. */
69
+ export function richText(text, doc = document) {
70
+ const fragment = doc.createDocumentFragment();
71
+ append(fragment, text, doc, 0);
72
+ return fragment;
73
+ }
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Which forms may show on this page view, and why not: pure functions of the form, the page and the
3
+ * counters, so the eligibility trace (`hp.onsite.debug()`) says exactly what decided.
4
+ */
5
+
6
+ export const FORMATS = [
7
+ 'popup',
8
+ 'sheet',
9
+ 'bar',
10
+ 'teaser',
11
+ 'inline',
12
+ 'fullscreen',
13
+ ];
14
+ export const TRIGGERS = [
15
+ 'entry',
16
+ 'exit',
17
+ 'scroll',
18
+ 'time',
19
+ 'idle',
20
+ 'inline',
21
+ 'manual',
22
+ ];
23
+ /**
24
+ * The formats that cover the page: one of them per page view, the highest priority first. Once one
25
+ * has shown (or the visitor was held out of it) the page view's allowance is spent, so closing it
26
+ * never lets the next waiting form in.
27
+ */
28
+ export const OVERLAY = (format) => format !== 'inline';
29
+ /**
30
+ * The formats that interrupt the shopper, which the property's gap spaces out: not a bar or a
31
+ * teaser, which leave the page usable (Klaviyo's "multiple forms" setting counts the same).
32
+ */
33
+ export const INTERRUPTS = (format) =>
34
+ format === 'popup' || format === 'sheet' || format === 'fullscreen';
35
+
36
+ const isObject = (v) =>
37
+ v !== null && typeof v === 'object' && !Array.isArray(v);
38
+ const strings = (v) =>
39
+ Array.isArray(v) ? v.filter((s) => typeof s === 'string') : [];
40
+ const count = (v) => (Number.isInteger(v) && v >= 0 ? v : null);
41
+
42
+ /** A form from the settings as the runtime uses it, or null when it cannot be used. */
43
+ export function readForm(raw) {
44
+ if (!isObject(raw) || typeof raw.slot !== 'string' || raw.slot === '')
45
+ return null;
46
+ const trigger = isObject(raw.trigger) ? raw.trigger : {};
47
+ if (!FORMATS.includes(raw.format) || !TRIGGERS.includes(trigger.type))
48
+ return null;
49
+ const caps = isObject(raw.caps) ? raw.caps : {};
50
+ return {
51
+ ...raw,
52
+ trigger,
53
+ priority: Number.isFinite(raw.priority) ? raw.priority : 0,
54
+ pages: isObject(raw.pages) ? raw.pages : {},
55
+ devices: strings(raw.devices),
56
+ aliases: strings(raw.aliases),
57
+ audiences: strings(raw.audiences),
58
+ caps: {
59
+ per_session: count(caps.per_session),
60
+ lifetime: count(caps.lifetime),
61
+ after_close_days: Number.isFinite(caps.after_close_days)
62
+ ? caps.after_close_days
63
+ : null,
64
+ },
65
+ window: isObject(raw.window) ? raw.window : {},
66
+ questions: Array.isArray(raw.questions)
67
+ ? raw.questions.filter(isObject)
68
+ : [],
69
+ prizes: Array.isArray(raw.prizes) ? raw.prizes.filter(isObject) : [],
70
+ };
71
+ }
72
+
73
+ const STEP_ID = /^[a-z][a-z0-9_-]{0,31}$/;
74
+
75
+ /**
76
+ * The decision's payload as a creative this runtime can render, or null. Here rather than in the
77
+ * renderer, so a form with no creative never loads ./render.js.
78
+ */
79
+ export function readCreative(payload) {
80
+ if (!isObject(payload) || payload.v !== 1 || !Array.isArray(payload.steps))
81
+ return null;
82
+ const steps = payload.steps.filter(
83
+ (s) => isObject(s) && STEP_ID.test(s.id) && Array.isArray(s.blocks),
84
+ );
85
+ if (steps.length === 0) return null;
86
+ return {
87
+ format: FORMATS.includes(payload.format) ? payload.format : null,
88
+ theme: isObject(payload.theme) ? payload.theme : {},
89
+ steps,
90
+ };
91
+ }
92
+
93
+ /** `*` matches any run of characters, `/` included; everything else is literal, in any case. */
94
+ export function globMatch(glob, path) {
95
+ const body = String(glob)
96
+ .split('*')
97
+ .map((part) => part.replace(/[.+?^${}()|[\]\\]/g, '\\$&'))
98
+ .join('.*');
99
+ return new RegExp(`^${body}$`, 'i').test(path);
100
+ }
101
+
102
+ /** Included (all pages when the form names none) and not excluded: exclusion wins. */
103
+ export function pageAllowed(pages, path) {
104
+ const include = strings(pages?.include);
105
+ return (
106
+ (!include.length || include.some((g) => globMatch(g, path))) &&
107
+ !strings(pages?.exclude).some((g) => globMatch(g, path))
108
+ );
109
+ }
110
+
111
+ /**
112
+ * `mobile` for a touch-only device (no hover, coarse pointer: a touch laptop with a trackpad is a
113
+ * desktop) or a viewport narrower than 768px, otherwise `desktop`.
114
+ */
115
+ export function deviceOf(win) {
116
+ const touchOnly =
117
+ win.matchMedia?.('(hover: none) and (pointer: coarse)')?.matches === true;
118
+ return touchOnly || win.innerWidth < 768 ? 'mobile' : 'desktop';
119
+ }
120
+
121
+ const at = (iso) => (typeof iso === 'string' ? Date.parse(iso) : Number.NaN);
122
+
123
+ /**
124
+ * A form the site opens itself (`manual`: its own sign-up button) is the shopper asking for it, so
125
+ * how often it has shown does not hold it back: no caps, gap or page view allowance, as Klaviyo's
126
+ * custom-triggered forms ignore their frequency settings. Its pages, devices, dates and audiences
127
+ * still do.
128
+ */
129
+ export const ASKED = (form) => form.trigger.type === 'manual';
130
+
131
+ /**
132
+ * Every reason `form` may not show now, empty when it may. `env` is the page:
133
+ * `{ path, device, now, seen, subscribed, personalisation, preview, audiences }`, where `seen` is
134
+ * the slot's counters and `audiences` the session audiences the host gave, or null.
135
+ */
136
+ export function blockers(form, env) {
137
+ const reasons = [];
138
+ if (!pageAllowed(form.pages, env.path)) reasons.push('page not included');
139
+ if (form.devices.length && !form.devices.includes(env.device))
140
+ reasons.push(`not on ${env.device}`);
141
+ const starts = at(form.window.starts_at);
142
+ const ends = at(form.window.ends_at);
143
+ if (env.now < starts || env.now >= ends) reasons.push('outside its dates');
144
+ if (form.audiences.length) {
145
+ if (!env.personalisation)
146
+ reasons.push('audiences need personalisation consent');
147
+ else if (
148
+ env.audiences &&
149
+ !form.audiences.some((a) => env.audiences.includes(a))
150
+ )
151
+ reasons.push('not in its audiences');
152
+ }
153
+ // A trader previewing a form, or a shopper who asked for it, sees it whatever the counters say.
154
+ if (env.preview || env.asked || ASKED(form)) return reasons;
155
+ const { seen } = env;
156
+ const { caps } = form;
157
+ if (seen.submitted) reasons.push('already submitted');
158
+ if (form.exclude_subscribed === true && env.subscribed)
159
+ reasons.push('already subscribed');
160
+ if (caps.per_session !== null && seen.session >= caps.per_session)
161
+ reasons.push('per_session cap');
162
+ if (caps.lifetime !== null && seen.lifetime >= caps.lifetime)
163
+ reasons.push('lifetime cap');
164
+ if (
165
+ caps.after_close_days !== null &&
166
+ seen.closedAt &&
167
+ env.now - seen.closedAt < caps.after_close_days * 86_400_000
168
+ )
169
+ reasons.push('closed recently');
170
+ return reasons;
171
+ }
172
+
173
+ /**
174
+ * Forms whose triggers fired together, in the order to try them: highest priority first. A stable
175
+ * sort keeps the first to fire ahead on a tie.
176
+ */
177
+ export const byPriority = (a, b) => b.priority - a.priority;
178
+
179
+ /**
180
+ * The step after `step`: the button's own `next`, else the first `answers` rule the answers meet
181
+ * (`"question=value"`), else the step's default. Null when there is none: the form is done.
182
+ */
183
+ export function nextStep(step, answers, explicit) {
184
+ if (typeof explicit === 'string' && explicit) return explicit;
185
+ const rules = isObject(step?.next?.answers) ? step.next.answers : {};
186
+ for (const [rule, target] of Object.entries(rules)) {
187
+ const split = rule.indexOf('=');
188
+ const values = answers[rule.slice(0, split)];
189
+ if (split > 0 && values?.includes(rule.slice(split + 1))) return target;
190
+ }
191
+ return typeof step?.next?.default === 'string' ? step.next.default : null;
192
+ }
193
+
194
+ /** Calling codes for the markets a phone block names, so a national number can be sent in E.164. */
195
+ export const CALLING_CODES = {
196
+ AT: '43',
197
+ AU: '61',
198
+ BE: '32',
199
+ CA: '1',
200
+ CH: '41',
201
+ DE: '49',
202
+ DK: '45',
203
+ ES: '34',
204
+ FI: '358',
205
+ FR: '33',
206
+ GB: '44',
207
+ IE: '353',
208
+ IT: '39',
209
+ NL: '31',
210
+ NO: '47',
211
+ NZ: '64',
212
+ PL: '48',
213
+ PT: '351',
214
+ SE: '46',
215
+ US: '1',
216
+ };
217
+
218
+ /** A phone number as E.164 (`+353…`), reading a national one in `country`; null when it is not one. */
219
+ export function e164(raw, country) {
220
+ let n = String(raw ?? '').replace(/[\s.()-]/g, '');
221
+ if (n.startsWith('00')) n = `+${n.slice(2)}`;
222
+ const code = CALLING_CODES[String(country ?? '').toUpperCase()];
223
+ if (!n.startsWith('+') && code) n = `+${code}${n.replace(/^0/, '')}`;
224
+ return /^\+[1-9]\d{7,14}$/.test(n) ? n : null;
225
+ }
226
+
227
+ export const isEmail = (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
228
+
229
+ /**
230
+ * A `<select>` made with `make(tag, className, text)`: a blank first option reading `blank`, then
231
+ * each `[value, text]`, `chosen` selected. The phone field's country code, a choice's dropdown and a
232
+ * birthday's day and month.
233
+ */
234
+ export function select(make, label, blank, pairs, chosen) {
235
+ const s = make('select', 'i');
236
+ s.setAttribute('aria-label', label);
237
+ for (const [value, text] of [['', blank], ...pairs]) {
238
+ const o = make('option', '', text);
239
+ o.value = value;
240
+ o.selected = value === chosen;
241
+ s.append(o);
242
+ }
243
+ return s;
244
+ }
245
+
246
+ /** Where a wheel must come to rest, in degrees clockwise, for prize `index` to sit under the top pointer. */
247
+ export function restAngle(index, segments, turns = 5) {
248
+ const size = 360 / segments;
249
+ return turns * 360 + (360 - (index + 0.5) * size);
250
+ }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * The forms' only stylesheet, scoped to their shadow root. The creative's theme arrives as custom
3
+ * properties on the host (`--hp-accent`, `--hp-bg`, `--hp-text`, `--hp-radius`, `--hp-font`,
4
+ * `--hp-on-accent`, `--hp-ov`) and classes on the shell: the format, `p-<position>`, `b-<shape>`,
5
+ * `bo` (outline buttons), `uc` (capitals), `mob` (a small screen) and `pv` (a console preview).
6
+ * A button's label is large text (WCAG: 14pt bold), so `--hp-on-accent` needs only 3:1; chips and
7
+ * a teaser's tab use `--hp-on-accent-text`. Fields have a surface of their own (`--hp-field`). The card (`.w`)
8
+ * carries the step's template (`t-*`) and size (`z-*`); type sizes are `x-*`.
9
+ */
10
+
11
+ /**
12
+ * Theme fonts: system stacks that download nothing, after Modern Font Stacks (CC0), cut to the
13
+ * families that cover macOS/iOS, Windows, Android and Linux.
14
+ */
15
+ export const FONTS = {
16
+ system: 'system-ui,sans-serif',
17
+ 'neo-grotesque': 'Inter,Roboto,"Helvetica Neue",Arial,sans-serif',
18
+ humanist: 'Seravek,"Gill Sans Nova",Ubuntu,Calibri,sans-serif',
19
+ 'geometric-humanist': 'Avenir,Montserrat,Corbel,sans-serif',
20
+ 'rounded-sans':
21
+ 'ui-rounded,Quicksand,Comfortaa,"Arial Rounded MT Bold",sans-serif',
22
+ industrial: 'Bahnschrift,"DIN Alternate","Franklin Gothic Medium",sans-serif',
23
+ transitional: 'Charter,"Sitka Text",Cambria,serif',
24
+ 'old-style': '"Iowan Old Style","Palatino Linotype",P052,serif',
25
+ didone: 'Didot,"Bodoni MT","Noto Serif Display",serif',
26
+ 'slab-serif': 'Rockwell,"Roboto Slab","DejaVu Serif",serif',
27
+ };
28
+
29
+ const EASE = 'cubic-bezier(.16,1,.3,1)';
30
+
31
+ export const CSS = `
32
+ :host{all:initial}
33
+ :host([preview]){display:block;position:relative;width:100%;height:100%;overflow:hidden;contain:layout paint}
34
+ .s{--a:var(--hp-accent,#111);--b:var(--hp-bg,#fff);--t:var(--hp-text,#111);--r:var(--hp-radius,16px);--oa:var(--hp-on-accent,#fff);--ot:var(--hp-on-accent-text,var(--oa));--fl:var(--hp-field,color-mix(in srgb,var(--t) 5%,var(--b)));--ft:var(--hp-field-text,var(--t));--mu:color-mix(in srgb,var(--t) 74%,var(--b));--ln:color-mix(in srgb,var(--t) 18%,var(--b));--br:min(12px,var(--r));--ir:min(var(--r),8px);font:16px/1.5 var(--hp-font,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif);color:var(--t);-webkit-font-smoothing:antialiased;box-sizing:border-box;margin:0;padding:0;border:0;background:none;max-width:none;max-height:none;overflow:visible}
35
+ .s *,.s ::before,.s ::after{box-sizing:border-box}
36
+ dialog.s:not([open]){display:none}
37
+ dialog::backdrop{background:none}
38
+ [hidden]{display:none!important}
39
+ .b-pill{--br:999px}
40
+ .b-square{--br:0px;--ir:0px}
41
+ .popup,.fullscreen{position:fixed;inset:0;width:100%;height:100%;display:grid;place-items:center;padding:24px;z-index:2147483646}
42
+ .fullscreen{padding:0}
43
+ .mob.popup{padding:12px}
44
+ .ov{display:none;position:absolute;inset:0;background:var(--hp-ov,rgb(10 10 14/.6));animation:hp-fade .3s ease both}
45
+ .popup>.ov,.fullscreen>.ov{display:block}
46
+ .w{position:relative;display:flex;flex-direction:column;width:min(100%,480px);max-height:100%;min-height:0;overflow:auto;overscroll-behavior:contain;background:var(--b);color:var(--t);border-radius:var(--r);box-shadow:0 24px 48px -24px rgb(0 0 0/.35),0 0 0 1px rgb(0 0 0/.05);animation:hp-pop .44s ${EASE} both}
47
+ .z-sm{width:min(100%,380px)}
48
+ .z-lg{width:min(100%,880px)}
49
+ .z-full,.fullscreen .w{width:100%;height:100%;max-height:none;border-radius:0;box-shadow:none}
50
+ .m{position:relative;flex:none;overflow:hidden;background:color-mix(in srgb,var(--t) 7%,var(--b))}
51
+ .m img,.m video{display:block;width:100%;height:100%;object-fit:cover}
52
+ .t-stack .m{aspect-ratio:var(--ar,16/9);max-height:280px}
53
+ .t-split{flex-direction:row;min-height:min(500px,100%)}
54
+ .t-split.right{flex-direction:row-reverse}
55
+ .t-split .m{flex:0 0 46%}
56
+ .t-split .m>*{position:absolute;inset:0}
57
+ .mob .t-split,.sheet .t-split{flex-direction:column;min-height:0}
58
+ .mob .t-split .m,.sheet .t-split .m{flex:none;height:190px}
59
+ .t-overlay{justify-content:center;min-height:min(520px,100%)}
60
+ .mob .t-overlay{min-height:min(460px,100%)}
61
+ .t-overlay .m{position:absolute;inset:0}
62
+ .t-overlay .m::after{content:"";position:absolute;inset:0;background:linear-gradient(to top,var(--b),color-mix(in srgb,var(--b) 50%,transparent));opacity:var(--sc,.45)}
63
+ .t-overlay .f{position:relative;z-index:1;flex:none;width:min(100%,560px);margin-inline:auto}
64
+ .t-image-only .m{background:none}
65
+ .t-image-only .m img{height:auto;aspect-ratio:var(--ar,auto)}
66
+ .io{display:block;width:100%;padding:0;border:0;background:none;cursor:pointer;color:inherit}
67
+ .f{display:flex;flex-direction:column;gap:14px;padding:44px 40px 36px;flex:1 0 auto;min-width:0}
68
+ .t-split .f{flex:1 1 0;justify-content:center}
69
+ .lk{min-width:var(--lw);min-height:var(--lh)}
70
+ .lk>.f{justify-content:center}
71
+ .an{align-self:start;margin-top:var(--tp,0px);max-height:calc(100% - var(--tp,0px))}
72
+ .z-full .f,.fullscreen .f{justify-content:center;width:min(100%,560px);margin:auto}
73
+ .pd-none{padding:0}
74
+ .pd-sm{padding:24px}
75
+ .pd-lg{padding:60px 56px 52px}
76
+ .mob .pd-md{padding:34px 24px 24px}
77
+ .ta-center .f{text-align:center}
78
+ .a-left{text-align:left}.a-center{text-align:center}.a-right{text-align:right}
79
+ h2{margin:0;font-weight:700;letter-spacing:-.02em;text-wrap:balance}
80
+ h2:focus{outline:none}
81
+ .nm .f>h2:first-child,.t-split:not(.right) .f>h2:first-child{padding-inline-end:36px}
82
+ .nm.ta-center .f>h2:first-child,.nm .f>h2.a-center:first-child{padding-inline:36px}
83
+ p{margin:0;text-wrap:pretty}
84
+ .f>p{color:var(--mu)}
85
+ h2+p{margin-top:-4px}
86
+ .x-xs{font-size:12px;line-height:1.45}
87
+ .x-sm{font-size:14px;line-height:1.5}
88
+ .x-md{font-size:16px;line-height:1.55}
89
+ .x-lg{font-size:20px;line-height:1.35}
90
+ .x-xl{font-size:32px;line-height:1.1}
91
+ .x-display{font-size:54px;line-height:1;letter-spacing:-.035em;font-weight:800}
92
+ .mob .x-xl{font-size:27px}
93
+ .mob .x-display{font-size:40px}
94
+ a{color:inherit;text-underline-offset:3px}
95
+ .l{display:flex;flex-direction:column;gap:6px;font-size:13px;font-weight:600;letter-spacing:.01em;text-align:left}
96
+ .i{font:inherit;font-size:16px;font-weight:400;letter-spacing:0;width:100%;height:52px;padding:0 18px;border:1.5px solid var(--ln);border-radius:var(--ir);background:var(--fl);color:var(--ft);caret-color:var(--a);outline:none}
97
+ .ph{display:flex;gap:8px}.ph .cc{flex:0 0 auto;width:auto;padding:0 10px}
98
+ .i::placeholder{color:color-mix(in srgb,var(--ft) 72%,var(--fl))}
99
+ ::selection{background:color-mix(in srgb,var(--a) 30%,var(--b))}
100
+ .i:focus{border-color:var(--a);box-shadow:0 0 0 4px color-mix(in srgb,var(--a) 22%,transparent)}
101
+ .i[aria-invalid=true]{border-color:#d93025;box-shadow:0 0 0 4px rgb(217 48 37/.16)}
102
+ .btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;font:inherit;font-size:18.67px;font-weight:700;letter-spacing:.01em;line-height:1.2;min-height:52px;padding:14px 24px;border:1.5px solid var(--a);border-radius:var(--br);background:var(--a);color:var(--oa);cursor:pointer;text-decoration:none;-webkit-tap-highlight-color:transparent;transition:transform .15s,filter .2s}
103
+ .btn:hover{filter:brightness(1.08)}
104
+ .btn:active{transform:scale(.98)}
105
+ .bo .btn{background:transparent;color:var(--a)}
106
+ .btn[disabled]{opacity:.55;cursor:progress;filter:none;transform:none}
107
+ .btn.no{align-self:center;min-height:44px;padding:6px 10px;border:0;background:none;color:var(--mu);font-size:14px;font-weight:500;text-decoration:underline;text-decoration-color:color-mix(in srgb,var(--t) 30%,transparent);text-underline-offset:4px;filter:none}
108
+ .btn.no:hover{color:var(--t)}
109
+ :focus-visible,.ch input:focus-visible+span{outline:2px solid var(--a);outline-offset:3px}
110
+ .x{position:absolute;top:12px;right:12px;z-index:2;display:grid;place-items:center;width:44px;height:44px;padding:0;border:0;border-radius:50%;background:var(--b);box-shadow:0 0 0 1px var(--ln);color:var(--t);cursor:pointer}
111
+ .x::before,.x::after{content:"";grid-area:1/1;width:16px;height:2px;border-radius:2px;background:currentColor;transform:rotate(45deg)}
112
+ .x::after{transform:rotate(-45deg)}
113
+ .c{display:flex;gap:10px;align-items:flex-start;font-size:13px;line-height:1.45;color:var(--mu);text-align:left;cursor:pointer}
114
+ .c input{appearance:none;flex:none;display:grid;place-content:center;width:20px;height:20px;margin:0;border:1.5px solid color-mix(in srgb,var(--t) 50%,var(--b));border-radius:min(var(--r),5px);background:var(--b);cursor:pointer}
115
+ .c input:checked{background:var(--a);border-color:var(--a)}
116
+ .c input:checked::after{content:"";width:10px;height:5px;border:solid var(--ot);border-width:0 0 2px 2px;transform:translateY(-1px) rotate(-45deg)}
117
+ .sm{font-size:12px;line-height:1.5;color:var(--mu)}
118
+ fieldset{display:flex;flex-wrap:wrap;gap:8px;min-width:0;margin:0;padding:0;border:0}
119
+ legend{padding:0;margin-bottom:8px;font-size:13px;font-weight:600}
120
+ .ta-center fieldset{justify-content:center}
121
+ .ch{position:relative;display:inline-flex}
122
+ .ch input{position:absolute;inset:0;margin:0;opacity:0}
123
+ .ch span{display:inline-flex;align-items:center;min-height:44px;padding:10px 18px;border:1.5px solid var(--ln);border-radius:999px;font-size:15px}
124
+ .ch input:checked+span{background:var(--a);border-color:var(--a);color:var(--ot)}
125
+ .ls{flex-direction:column}.ls .c{font-size:15px;color:var(--t)}.ls [type=radio]{border-radius:50%}
126
+ .ph .u{align-self:center;color:var(--mu)}
127
+ .bd{flex-wrap:wrap}.bd .i{min-width:0;padding:0 12px}.bd [name=hp_d]{flex:0 0 5.5em}.bd [name=hp_m]{flex:1 1 8.5em}.bd [name=hp_y]{flex:1 1 8em}
128
+ .e.won{color:var(--t);font-size:18px;font-weight:700;text-align:center}
129
+ .e{color:#d93025;font-size:13px;font-weight:500}
130
+ .e:empty{display:none}
131
+ .k{display:flex;flex-direction:column;align-items:stretch;gap:10px}
132
+ .k:empty{display:none}
133
+ .k output{display:block;padding:16px;border:1.5px dashed var(--a);border-radius:min(var(--r),16px);background:color-mix(in srgb,var(--a) 8%,var(--b));font:700 22px/1.2 Menlo,monospace;letter-spacing:.12em;text-align:center}
134
+ .btn.cp,.btn.b2{background:transparent;border-color:var(--ln);color:var(--t)}
135
+ .pg{display:flex;flex-direction:column;gap:8px}
136
+ .pg p{font-weight:600}
137
+ .tr{height:6px;border-radius:99px;background:color-mix(in srgb,var(--t) 18%,var(--b));overflow:hidden}
138
+ .tr i{display:block;height:100%;border-radius:inherit;background:var(--a);transform:scaleX(0);transform-origin:left;transition:transform .6s ${EASE}}
139
+ .tm{font-size:40px;line-height:1;font-weight:800;letter-spacing:.02em;font-variant-numeric:tabular-nums;text-align:center}
140
+ .bi{display:block;width:100%;height:auto}
141
+ .bl{display:block;border-radius:var(--br)}
142
+ hr{width:100%;margin:4px 0;border:0;border-top:1px solid var(--ln)}
143
+ .wh{width:min(100%,320px);aspect-ratio:1;align-self:center}
144
+ .wh svg{display:block;width:100%;height:auto;filter:drop-shadow(0 16px 24px rgb(0 0 0/.2))}
145
+ .hd{display:none}
146
+ .uc h2{text-transform:uppercase;font-weight:800;letter-spacing:-.01em;line-height:1.02}
147
+ .uc .btn:not(.no),.uc .tb{text-transform:uppercase;letter-spacing:.06em}
148
+ .sheet,.bar,.teaser{position:fixed;inset:auto;z-index:2147483646;display:flex;flex-direction:column}
149
+ .sheet{right:24px;bottom:24px;width:min(400px,calc(100% - 32px));max-height:calc(100% - 48px)}
150
+ .sheet.p-bottom-left{right:auto;left:24px}
151
+ .sheet.p-bottom{right:auto;left:50%;translate:-50% 0}
152
+ .sheet .w,.bar .w{width:100%}
153
+ .mob.sheet{left:0;right:0;bottom:0;translate:none;width:100%;max-height:92%}
154
+ .mob.sheet .w{border-radius:var(--r) var(--r) 0 0;box-shadow:0 -16px 48px -16px rgb(0 0 0/.35);animation-name:hp-up}
155
+ .mob.sheet .hd{display:block;position:absolute;top:0;left:50%;translate:-50% 0;z-index:3;width:96px;height:28px;cursor:grab;touch-action:none}
156
+ .mob.sheet .hd::before{content:"";display:block;width:40px;height:5px;margin:10px auto 0;border-radius:3px;background:color-mix(in srgb,var(--t) 25%,var(--b))}
157
+ .bar{left:0;right:0;bottom:0;width:100%}
158
+ .bar.p-top{bottom:auto;top:0}
159
+ .bar .w{flex-direction:row;border-radius:0;box-shadow:0 -10px 30px -14px rgb(0 0 0/.3);animation-name:hp-up}
160
+ .bar.p-top .w{animation-name:hp-down}
161
+ .bar .m{display:none}
162
+ .bar .f{flex-direction:row;flex-wrap:wrap;align-items:center;justify-content:center;gap:10px 16px;width:min(100%,1200px);margin:auto;padding:14px 68px 14px 24px}
163
+ .bar h2{font-size:18px;padding:0!important}
164
+ .bar .f>p{font-size:15px}
165
+ .bar .l{flex:0 1 300px}
166
+ .bar .lt{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
167
+ .bar .i,.bar .btn{height:46px;min-height:46px}
168
+ .bar .btn{padding:10px 22px}
169
+ .bar .c,.bar .sm,.bar .e{flex-basis:100%;justify-content:center;text-align:center}
170
+ .bar .pg{flex:0 1 520px;text-align:center}
171
+ .bar .k{flex-direction:row;align-items:center}
172
+ .bar .k output{padding:9px 16px;font-size:16px;border-radius:999px}
173
+ .bar .k .btn{height:44px;min-height:44px;padding:6px 16px;font-size:14px}
174
+ .bar .x{top:50%;right:12px;translate:0 -50%}
175
+ .mob.bar .f{justify-content:flex-start;padding:14px 60px 16px 16px}
176
+ .mob.bar h2,.mob.bar .f>p{flex-basis:100%}
177
+ .mob.bar .l{flex:1 1 160px;min-width:160px}
178
+ .mob.bar .f>.btn:not(.no){flex:1 0 auto}
179
+ .mob.bar .x{top:6px;translate:none}
180
+ .teaser{left:20px;bottom:20px}
181
+ .teaser.p-bottom-right{left:auto;right:20px}
182
+ .teaser.p-left,.teaser.p-right{bottom:auto;top:50%;translate:0 -50%}
183
+ .teaser.p-left{left:0}
184
+ .teaser.p-right{left:auto;right:0}
185
+ .teaser .w{width:auto;overflow:visible;background:none;box-shadow:none;border-radius:0;animation-name:hp-fade}
186
+ .teaser .f{padding:0}
187
+ .teaser .m{overflow:visible}
188
+ .teaser .io img{width:auto;max-width:220px;max-height:220px;border-radius:var(--r);box-shadow:0 16px 40px -12px rgb(0 0 0/.45)}
189
+ .tb{display:inline-flex;align-items:center;gap:10px;padding:15px 24px;border:0;border-radius:var(--br);background:var(--a);color:var(--ot);font:inherit;font-size:15px;font-weight:650;letter-spacing:.01em;cursor:pointer;box-shadow:0 14px 34px -10px rgb(0 0 0/.45)}
190
+ .b-rounded .tb{border-radius:999px}
191
+ .p-left .tb,.p-right .tb{writing-mode:vertical-rl;padding:22px 13px;border-radius:var(--br) 0 0 var(--br)}
192
+ .p-left .tb{transform:rotate(180deg)}
193
+ .b-pill.teaser .tb{border-radius:999px}
194
+ .teaser .x{top:auto;right:0;bottom:100%;border:10px solid transparent;background:var(--t) padding-box;box-shadow:none;color:var(--b)}
195
+ .teaser .x::before,.teaser .x::after{width:10px}
196
+ .teaser.p-left .x,.teaser.p-right .x{bottom:auto;top:100%}
197
+ .teaser.p-left .x{right:auto;left:0}
198
+ .inline{position:static;display:block;height:100%}
199
+ .inline .w{width:100%;height:100%;box-shadow:none;border:1px solid var(--ln);animation:none}
200
+ .inline .t-stack .m{max-height:none}
201
+ .out .w{animation:hp-out .2s ease-in both}
202
+ .out .ov{animation:hp-fade .2s ease-in reverse both}
203
+ .f>*{animation:hp-rise .5s ${EASE}}
204
+ .pv .w,.pv .ov,.pv .f>*,.inline .f>*{animation:none}
205
+ @keyframes hp-pop{from{opacity:0;transform:translateY(24px) scale(.96)}}
206
+ @keyframes hp-fade{from{opacity:0}}
207
+ @keyframes hp-rise{from{opacity:0;transform:translateY(10px)}}
208
+ @keyframes hp-up{from{transform:translateY(100%)}}
209
+ @keyframes hp-down{from{transform:translateY(-100%)}}
210
+ @keyframes hp-out{to{opacity:0;transform:translateY(12px) scale(.98)}}
211
+ @media (prefers-reduced-motion:reduce){.s .w,.s .ov,.f>*{animation:hp-fade .15s ease both!important}.btn{transition:none}}
212
+ `;