shopic 0.0.2 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,6 +3,68 @@
3
3
  Custom code bundle for the Shopic Webflow site. TypeScript, bundled with esbuild,
4
4
  published to npm and served to Webflow over the jsDelivr CDN.
5
5
 
6
+ This repository also contains the checked-in agent context for the Webflow work.
7
+ The npm package boundary remains unchanged: only `dist/` is published.
8
+
9
+ ## Repository layout
10
+
11
+ | Path | Contents |
12
+ | ------------------------------------ | -------------------------------------------------------------------------------------------- |
13
+ | `src/` | TypeScript source for the site bundle. |
14
+ | `tests/` | Playwright specs for the published site; node unit tests for `scripts/` in `tests/unit/`. |
15
+ | `scripts/` | Mechanical checks: class index, style payload lint, motion preview. See `scripts/README.md`. |
16
+ | `.agents/skills/` | Canonical project skill tree shared by Codex and Claude Code. |
17
+ | `.agents/agents/` | Canonical subagent definitions; the `tools:` list is the write lock. |
18
+ | `.claude/skills/`, `.claude/agents/` | Relative symlink views for Claude Code. |
19
+ | `AGENTS.md` | Rules that hold every session. Task-specific rules live in the skills. |
20
+ | `FLOWKIT.md` | FlowKit v2 naming and class-construction reference. |
21
+
22
+ ## Starting from a fresh clone
23
+
24
+ Start Codex or Claude Code from this repository root. Both tools then see the
25
+ same project skills; `.agents/skills/` is the source of truth and
26
+ `.claude/skills/` contains only relative links into it.
27
+
28
+ Read `AGENTS.md` first. It is deliberately short — it holds only what is true
29
+ every session, and points at the skill that holds the rest.
30
+
31
+ ### Which skill
32
+
33
+ Four skills are offered to the agent; the other thirteen are not, on purpose.
34
+
35
+ | Working on | Load |
36
+ | ------------------------------------ | ----------------------- |
37
+ | a section, start to finish | `webflow-section-build` |
38
+ | naming a class, units, variables | `flowkit-naming` |
39
+ | anything in `src/anim/`, motion bugs | `shopic-animation` |
40
+ | anything else on the Webflow site | `webflow-toolbox` |
41
+
42
+ `webflow-toolbox` is a catalogue, not a procedure. The thirteen vendored Webflow
43
+ skills — CMS, audits, link checking, publishing, custom code, comments — are
44
+ deliberately left out of `.claude/skills/` so they never load themselves and
45
+ never cost context unasked. `webflow-section-build` names the ones it needs at
46
+ the step that needs them; `webflow-toolbox` is how you find one by hand. Ask it
47
+ "which skill handles X" and it answers with a file path to read.
48
+
49
+ `node scripts/check-agent-refs.js` enforces that: a skill nothing names is
50
+ flagged as unreachable, and a skill linked into `.claude/skills/` that is not one
51
+ of the four is flagged as costing context every session.
52
+
53
+ ### What runs without asking
54
+
55
+ `.claude/settings.json` is committed and deliberately narrow: local read-only
56
+ commands and the repo's own scripts. Never `Bash(*)`, and no Webflow tool is on
57
+ the allow list, so every Webflow write still prompts.
58
+
59
+ That is the only granularity available. The MCP bundles reads and writes into one
60
+ tool — `data_style_tool` carries `get_styles` alongside `create_style` — so a
61
+ tool-level allow would auto-approve writes with the reads. Leaving Webflow off the
62
+ list entirely is what keeps the prompt.
63
+
64
+ Figma URLs and node ids belong in the ignored `DEV-LINKS.local.md` and must not
65
+ be committed. The staging URL is committed — it is stable and every agent needs
66
+ it. Local plans and design records belong in the ignored `docs/superpowers/`.
67
+
6
68
  ## How the code reaches the site
7
69
 
8
70
  ```
@@ -12,59 +74,36 @@ src/*.ts → esbuild → dist/*.js → npm publish → cdn.jsdelivr.net/
12
74
  npm stores the versions; jsDelivr is the CDN that serves them. The published package
13
75
  is public even though this repository is not — that is the price of a free CDN.
14
76
 
15
- ## Local development
77
+ ## Local checks
16
78
 
17
79
  ```bash
18
80
  pnpm install
19
- pnpm dev
81
+ pnpm check
82
+ pnpm lint
83
+ pnpm build
84
+ node scripts/check-agent-refs.js # agent and skill definitions still resolve
85
+ node scripts/check-runtime-classes.js # needs a class index; see scripts/README.md
20
86
  ```
21
87
 
22
- `pnpm dev` serves the bundle at `http://localhost:3000` with live reload.
88
+ The repository has no local Webflow page. `pnpm dev` only watches and serves the
89
+ bare bundle files; it is not a site preview. Use Playwright against staging for
90
+ rendered behavior.
23
91
 
24
- Put this in **Site Settings Custom Code Footer** once, and leave it there. It
25
- loads the CDN build for everyone, and the local build only for a browser you have
26
- flipped into dev mode:
92
+ The site loads the bundle from jsDelivr. One line in
93
+ **Site Settings Custom Code Footer**, and that is the whole integration:
27
94
 
28
95
  ```html
29
- <script>
30
- (() => {
31
- const PROD = 'https://cdn.jsdelivr.net/npm/shopic@0.0.2/dist/index.js';
32
- const DEV = 'https://localhost:3000/index.js';
33
-
34
- const load = (src, fallback) => {
35
- const s = document.createElement('script');
36
- s.src = src;
37
- if (fallback) s.onerror = fallback;
38
- document.head.appendChild(s);
39
- };
40
-
41
- if (localStorage.getItem('shopic-dev') === 'on') load(DEV, () => load(PROD));
42
- else load(PROD);
43
- })();
44
- </script>
45
- ```
46
-
47
- Flip it on in the console on the staging domain, once:
48
-
49
- ```js
50
- localStorage.setItem('shopic-dev', 'on'); // off: localStorage.removeItem('shopic-dev')
96
+ <script src="https://cdn.jsdelivr.net/npm/shopic@<version>/dist/index.js"></script>
51
97
  ```
52
98
 
53
- Without that gate every visitor's browser would probe your localhost. With it,
54
- only yours does, and it falls back to the CDN the moment `pnpm dev` is not running.
99
+ GSAP is **not** loaded there. It comes from Webflow's native Site Settings
100
+ toggle (3.15.0 with SplitText); a second copy would register ScrollTrigger twice.
55
101
 
56
- **The dev server must speak HTTPS.** Measured 2026-09-01 on the staging domain:
57
- Chrome blocks `http://localhost:3000/index.js` as mixed content when the page
58
- itself is HTTPS. The script tag appears in the DOM, the request never completes,
59
- and nothing logs — it looks like the bundle loaded and silently did nothing.
60
- `http://localhost` is a *potentially trustworthy origin*, which is a different
61
- rule from mixed-content subresource blocking; only the first one exempts it.
102
+ ## Preview
62
103
 
63
- Serve it over TLS instead (`mkcert localhost` once, then point esbuild's `serve`
64
- at the key and cert), and use `https://localhost:3000/index.js` in the loader.
65
-
66
- The dev URL has no `/dist` in it, because esbuild serves that directory as the
67
- root (`src/pages/home.ts` → `/pages/home.js`).
104
+ Run the local checks above. Rendered QA happens on the published Webflow staging
105
+ site with Playwright; release the bundle to npm and pin that version in the
106
+ Webflow footer first.
68
107
 
69
108
  ## Releasing
70
109
 
@@ -72,15 +111,18 @@ Every release is run by hand from this folder. There is no CI — the GitHub
72
111
  repository is history and backup, nothing publishes from it.
73
112
 
74
113
  ```bash
75
- pnpm changeset # describe the change, pick patch/minor/major
76
- pnpm changeset version # bumps the version, writes CHANGELOG.md
77
- git commit -am "release: 0.0.1"
114
+ pnpm changeset version # apply the prepared changeset
115
+ git diff -- package.json CHANGELOG.md
116
+ git add -A && git commit -m "release: <version>" && git push
78
117
 
79
118
  npm publish --access public # browser opens, confirm with the passkey
80
- git tag v0.0.1
119
+ git tag v<version>
81
120
  git push --follow-tags
82
121
  ```
83
122
 
123
+ Create one changeset per approved batch of code changes. Webflow-only style
124
+ edits do not need a new npm release.
125
+
84
126
  **Publish with `npm`, not `pnpm changeset publish`.** Changesets detects the pnpm
85
127
  lockfile and hands the publish to `pnpm publish`, and its only 2FA path is a typed
86
128
  six-digit code (`Enter one-time password:`). This account uses a passkey, where no
@@ -140,30 +182,43 @@ console instead of throwing.
140
182
 
141
183
  ## Layout
142
184
 
143
- | Path | Contents |
144
- | --- | --- |
145
- | `src/lib` | Shared helpers — GSAP access, environment checks |
146
- | `src/anim` | `data-anim` registry and one file per animation |
147
- | `src/pages` | Per-page entry points |
148
- | `tests` | Playwright measurement specs (Chromium, one viewport) |
185
+ | Path | Contents |
186
+ | ----------- | ----------------------------------------------------- |
187
+ | `src/lib` | Shared helpers — GSAP access, environment checks |
188
+ | `src/anim` | `data-anim` registry and one file per animation |
189
+ | `src/pages` | Per-page entry points |
190
+ | `tests` | Playwright measurement specs (Chromium, one viewport) |
149
191
 
150
192
  ## Commands
151
193
 
152
- | Command | Does |
153
- | --- | --- |
154
- | `pnpm dev` | Watch, rebuild, serve on :3000 with live reload |
155
- | `pnpm build` | Minified production build into `dist` |
156
- | `pnpm check` | TypeScript, no emit |
157
- | `pnpm lint` | ESLint + Prettier, read-only |
158
- | `pnpm lint:fix` | ESLint with `--fix` |
159
- | `pnpm format` | Prettier, writes |
160
- | `pnpm test` | Playwright measurement specs against `STAGING_URL` |
161
- | `pnpm test:ui` | The same specs in Playwright's UI runner |
162
- | `pnpm changeset` | Record a pending change and its version bump |
163
- | `pnpm changeset version` | Apply pending changesets, write `CHANGELOG.md` |
164
- | `npm publish` | Upload to npm (passkey in the browser) — see Releasing |
194
+ | Command | Does |
195
+ | ------------------------ | -------------------------------------------------------- |
196
+ | `pnpm dev` | Watch and serve bare bundle files; not a Webflow preview |
197
+ | `pnpm build` | Minified production build into `dist` |
198
+ | `pnpm check` | TypeScript, no emit |
199
+ | `pnpm lint` | ESLint + Prettier, read-only |
200
+ | `pnpm lint:fix` | ESLint with `--fix` |
201
+ | `pnpm format` | Prettier, writes |
202
+ | `pnpm test` | Playwright measurement specs against `STAGING_URL` |
203
+ | `pnpm test:ui` | The same specs in Playwright's UI runner |
204
+ | `pnpm changeset` | Record a pending change and its version bump |
205
+ | `pnpm changeset version` | Apply pending changesets, write `CHANGELOG.md` |
206
+ | `npm publish` | Upload to npm (passkey in the browser) — see Releasing |
165
207
 
166
208
  `prepublishOnly` chains `check`, `lint` and `build`; it runs on publish, not by hand.
167
209
 
168
- The Playwright specs measure the published site, not the local dev server — set
169
- `STAGING_URL` or they fall back to `https://shopic.webflow.io`.
210
+ The Playwright specs measure the published site. They default to
211
+ `https://shopic-stage.webflow.io`; set `STAGING_URL` to point elsewhere, and
212
+ `VERIFY_PATHS` to check more than `/`.
213
+
214
+ ### Scripts
215
+
216
+ Run by hand or from a build step; `scripts/README.md` says what each one proves.
217
+
218
+ | Command | Does |
219
+ | -------------------------------------------------------------- | --------------------------------------------------------------------- |
220
+ | `node scripts/class-index.js build \| append \| check \| stat` | The class vocabulary, and the duplicate-class guard |
221
+ | `node scripts/lint-style-payload.js <payload>` | Rejects CSS shorthands, px, hidden start state before the API does |
222
+ | `node scripts/check-runtime-classes.js` | Every class the bundle adds at runtime exists as a real Webflow style |
223
+ | `node scripts/check-agent-refs.js` | Agent and skill definitions still point at things that exist |
224
+ | `node scripts/preview-anim.js --seek <t> --measure <sel>` | The motion, on the real page, before the publish gate |
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";(()=>{var l=new Map,p=(t,r)=>{if(l.has(t))throw new Error(`[shopic] Animation "${t}" is registered twice.`);l.set(t,r)},m=(t,r=document)=>{let s=r.querySelectorAll("[data-anim]");for(let e of s){let i=e.dataset.anim;if(!i||e.dataset.animState==="ready")continue;let o=l.get(i);if(!o){console.warn(`[shopic] No animation registered for data-anim="${i}".`,e);continue}e.dataset.animState="ready",o(e,t)}};var g="split_line",w="split_word",f="split_word-inner",E=t=>{let r=document.createDocumentFragment(),s=[],e=document.createElement("span");e.className=g,r.appendChild(e);let i=(n,c)=>{if(!n)return;let a=document.createElement("span");a.className=w;let d=document.createElement("span");d.className=c?`${f} ${c}`:f,d.textContent=n,a.appendChild(d),e.appendChild(a),s.push(d)},o=(n,c)=>{for(let a of n.split(/(\s+)/))a&&(/^\s+$/.test(a)?e.appendChild(document.createTextNode(" ")):i(a,c))};for(let n of Array.from(t.childNodes))n.nodeType===Node.TEXT_NODE?o(n.textContent??""):n instanceof HTMLBRElement?(e=document.createElement("span"),e.className=g,r.appendChild(e)):n instanceof HTMLElement&&o(n.textContent??"",n.className||void 0);return t.textContent="",t.appendChild(r),s},u={yPercent:0,duration:.9,ease:"power4.out",stagger:.035};p("headline",(t,{gsap:r,ScrollTrigger:s,reducedMotion:e})=>{if(e){t.style.visibility="visible";return}let i=E(t);if(r.set(i,{yPercent:110}),t.style.visibility="visible",t.dataset.animTrigger==="load"||!s){r.to(i,{...u,delay:.3});return}r.to(i,{...u,scrollTrigger:{trigger:t,start:"top 85%"}})});var h=26;p("marquee",(t,{gsap:r,reducedMotion:s})=>{if(s)return;let e=Number(t.dataset.animSets??4),i=Number(t.dataset.animDuration??h);if(!Number.isFinite(e)||e<2)return;let o=()=>t.scrollWidth/e,n=r.to(t,{x:()=>-o(),duration:i,ease:"none",repeat:-1,modifiers:{x:a=>`${Number.parseFloat(a)%o()}px`}}),c=t.parentElement;c&&(c.addEventListener("pointerenter",()=>n.pause()),c.addEventListener("pointerleave",()=>n.resume()))});var y="is-visible";p("reveal",(t,{reducedMotion:r})=>{let s=Number(t.dataset.animDelay??0);if(s&&(t.style.transitionDelay=`${s}ms`),r||!("IntersectionObserver"in window)){t.classList.add(y);return}let e=new IntersectionObserver(i=>{for(let o of i)o.isIntersecting&&(o.target.classList.add(y),e.unobserve(o.target))},{threshold:.15});e.observe(t)});var T=()=>{let{gsap:t,ScrollTrigger:r}=window;if(!t)throw new Error("[shopic] GSAP is not on the page. Enable it in Webflow \u2192 Site Settings \u2192 Custom Code \u2192 GSAP.");return r&&t.registerPlugin(r),{gsap:t,ScrollTrigger:r}},S=()=>window.matchMedia("(prefers-reduced-motion: reduce)").matches;window.Webflow||(window.Webflow=[]);window.Webflow.push(()=>{let{gsap:t,ScrollTrigger:r}=T();m({gsap:t,ScrollTrigger:r,reducedMotion:S()})});})();
1
+ "use strict";(()=>{var W=new Map,y=(e,o)=>{if(W.has(e))throw new Error(`[shopic] Animation "${e}" is registered twice.`);W.set(e,o)},B=(e,o=document)=>{let a=o.querySelectorAll("[data-anim]");for(let t of a){let r=t.dataset.anim;if(!r||t.dataset.animState==="ready")continue;let l=W.get(r);if(!l){console.warn(`[shopic] No animation registered for data-anim="${r}".`,t);continue}t.dataset.animState="ready",l(t,e)}};var pe=2,me=150,ge=4,ye=1,he=5e3,ve=.75,Ee="power3.out",be=[0,.25,.5,.6,.7,.8,.9,1],Y="shopic:slide-enter";y("carousel",(e,{gsap:o,reducedMotion:a})=>{let t=Array.from(e.querySelectorAll(":scope > [data-slide]"));if(t.length<2)return;let r=e.closest("section")??e.parentElement??document.body,l=Array.from(r.querySelectorAll("[data-tab]")),d=a?0:ve,i=t.length,s=new Map(t.map((n,f)=>[n,f])),c=new Map;t.forEach((n,f)=>{let g=n.dataset.slide;g&&!c.has(g)&&c.set(g,f)});let u=[],A=()=>{let n=e.getBoundingClientRect().left,f=e.scrollLeft,g=parseFloat(getComputedStyle(e).paddingLeft)||0,E=e.scrollWidth-e.clientWidth;u=t.map(R=>Math.min(R.getBoundingClientRect().left-n+f-g,E))},I=n=>{for(let f of l){let g=f.dataset.tab===n;f.classList.toggle("is-active",g),g?f.setAttribute("aria-current","true"):f.removeAttribute("aria-current")}},O=-1,b=null,h=!1,T=!1,C,p,m=null,v=0,x=0,S=0,w=0,M=!1,_=()=>m!==null,H=()=>b!==null,L=()=>{clearTimeout(C),C=void 0},V=()=>!a&&!document.hidden&&T&&!_()&&!h&&!r.contains(document.activeElement),P=()=>{L(),V()&&(C=setTimeout(()=>{C=void 0,V()&&$((O+1)%i)},he))},K=n=>{let f=t[n],g=f.dataset.slide;if(g&&I(g),n!==O){O=n;for(let E of t)E.classList.toggle("is-current",E===f);f.dispatchEvent(new CustomEvent(Y))}},X=n=>{b=null,K(n),P()},z=()=>{clearTimeout(p),o.killTweensOf(e),b=null};function $(n){if(z(),b=n,d===0){e.scrollLeft=u[n],X(n);return}o.to(e,{scrollLeft:u[n],duration:d,ease:Ee,overwrite:"auto",onComplete:()=>X(n),onInterrupt:()=>{b=null}})}let Z=()=>{let n=e.scrollLeft,f=0;return u.forEach((g,E)=>{Math.abs(g-n)<Math.abs(u[f]-n)&&(f=E)}),f},D=()=>{let n=Z();Math.abs(u[n]-e.scrollLeft)<ye?X(n):$(n)},de=()=>{let n=v>0?u[v]-u[v-1]:1/0,f=v<i-1?u[v+1]-u[v]:1/0,g=Math.min(Math.abs(n),Math.abs(f));if(Math.abs(w)<g*.25){D();return}$((v+Math.sign(w)+i)%i)};for(let n of l)n.addEventListener("click",f=>{f.preventDefault();let g=n.dataset.tab??"",E=c.get(g);E!==void 0&&(L(),I(g),$(E))});let j=new Array(i).fill(0),fe=new IntersectionObserver(n=>{for(let E of n){let R=s.get(E.target);R!==void 0&&(j[R]=E.isIntersecting?E.intersectionRatio:0)}let f=-1,g=0;j.forEach((E,R)=>{E<=g||(g=E,f=R)}),f!==-1&&(b!==null&&f!==b||K(f))},{root:e,threshold:be});for(let n of t)fe.observe(n);A(),e.style.scrollSnapType="none",e.style.touchAction="pan-y",e.addEventListener("dragstart",n=>n.preventDefault());let[ue]=u;e.scrollLeft=ue;let J=new ResizeObserver(A);J.observe(e);for(let n of t)J.observe(n);"onscrollend"in window?e.addEventListener("scrollend",()=>{!H()&&!_()&&D()}):e.addEventListener("scroll",()=>{H()||_()||(clearTimeout(p),p=setTimeout(D,me))}),e.addEventListener("pointerdown",n=>{L(),z(),m=n.pointerId,M=!1,x=n.clientX,S=e.scrollLeft,w=0,v=Z()}),e.addEventListener("pointermove",n=>{if(n.pointerId!==m)return;let f=n.clientX-x;if(!M){if(Math.abs(f)<ge)return;M=!0,e.classList.add("is-dragging"),e.setPointerCapture(n.pointerId)}w=-f*pe,e.scrollLeft=S+w,n.pointerType==="mouse"&&n.preventDefault()});let Q=n=>{n.pointerId===m&&(m=null,e.hasPointerCapture(n.pointerId)&&e.releasePointerCapture(n.pointerId),M?(e.classList.remove("is-dragging"),de()):D())};window.addEventListener("pointerup",Q),window.addEventListener("pointercancel",Q),e.addEventListener("click",n=>{M&&(M=!1,n.preventDefault(),n.stopPropagation())},!0),r.addEventListener("pointerenter",n=>{n.pointerType==="mouse"&&(h=!0,L())}),r.addEventListener("pointerleave",n=>{n.pointerType==="mouse"&&(h=!1,P())}),r.addEventListener("focusin",L),r.addEventListener("focusout",()=>queueMicrotask(P)),document.addEventListener("visibilitychange",()=>{document.hidden?L():P()}),new IntersectionObserver(n=>{T=n.some(f=>f.isIntersecting),T?P():L()},{threshold:.2}).observe(e)});var k="!<>-_\\/[]{}=+*^?#$%01",q=new WeakMap,N=(e,{duration:o=1050,stagger:a=.75,text:t,reducedMotion:r=!1,relaunchOnHover:l=!1}={})=>{if(r){t!==void 0&&(e.textContent=t);return}if(t!==void 0)e.dataset.scrambleText=t;else if(!e.dataset.scrambleText){let h=e.textContent;if(!h||!h.trim())return;e.dataset.scrambleText=h}let d=e.dataset.scrambleText;if(!d||!d.trim())return;let i=Array.from(d),s=i.length,c=Array.from(e.children),u;c.length===s&&c.every(h=>h.tagName==="SPAN")?u=c:(u=i.map(h=>{let T=document.createElement("span");return T.textContent=h,T}),e.textContent="",e.append(...u));let A=Symbol("scramble");q.set(e,A);let I=i.map((h,T)=>s<=1?0:T/(s-1)*o*a),O=performance.now(),b=h=>{if(q.get(e)!==A)return;let T=h-O,C=!0;i.forEach((p,m)=>{if(p===" "){u[m].textContent=" ";return}T>=I[m]?u[m].textContent=p:(C=!1,u[m].textContent=k[Math.random()*k.length|0])}),C?q.delete(e):requestAnimationFrame(b)};requestAnimationFrame(b),l&&Te(e,{duration:o,stagger:a})},Te=(e,o)=>{e.dataset.scrambleHoverBound||(e.dataset.scrambleHoverBound="true",e.addEventListener("pointerenter",()=>{q.has(e)||N(e,o)}))};var Le=.5,Se="power2.out",Ae=".status-pill, .evidence_label, .evidence_sku",xe={duration:630,stagger:.6,relaunchOnHover:!0};y("evidence",(e,{gsap:o,reducedMotion:a})=>{let t=Array.from(e.children);if(!t.length)return;if(a){for(let s of t)s.style.height="auto",s.style.opacity="1";return}let r=()=>{o.killTweensOf(t),o.set(t,{height:0,opacity:0,overflow:"hidden"});let s=o.timeline();for(let c of t)s.call(()=>{for(let u of c.querySelectorAll(Ae))N(u,xe)}),s.to(c,{height:"auto",opacity:1,duration:Le,ease:Se,onComplete:()=>o.set(c,{height:"auto"})})},l=!1,d=!1,i=()=>{if(!l||d)return;let s=e.closest("[data-slide]");s&&!s.classList.contains("is-current")||(d=!0,r())};if("IntersectionObserver"in window){let s=new IntersectionObserver(c=>{for(let u of c)u.isIntersecting&&(l=!0,i(),d&&s.disconnect())},{threshold:.2});s.observe(e)}else l=!0,i();e.closest("[data-slide]")?.addEventListener(Y,()=>{d=!1,i()})});var ee=8;y("focus-drift",(e,{reducedMotion:o})=>{let a=e.querySelector(":scope > .camera_focus");!a||o||(e.addEventListener("pointermove",t=>{if(t.pointerType!=="mouse")return;let r=e.getBoundingClientRect(),l=(t.clientX-r.left)/r.width-.5,d=(t.clientY-r.top)/r.height-.5,i=(l*ee*2).toFixed(1),s=(d*ee*2).toFixed(1);a.style.transform=`translate(${i}px, ${s}px)`}),e.addEventListener("pointerleave",()=>{a.style.transform=""}))});var te="split_line",we="split_word",ne="split_word-inner",Me=e=>{let o=document.createDocumentFragment(),a=[],t=document.createElement("span");t.className=te,o.appendChild(t);let r=(d,i)=>{if(!d)return;let s=document.createElement("span");s.className=we;let c=document.createElement("span");c.className=i?`${ne} ${i}`:ne,c.textContent=d,s.appendChild(c),t.appendChild(s),a.push(c)},l=(d,i)=>{for(let s of d.split(/(\s+)/))s&&(/^\s+$/.test(s)?t.appendChild(document.createTextNode(" ")):r(s,i))};for(let d of Array.from(e.childNodes))d.nodeType===Node.TEXT_NODE?l(d.textContent??""):d instanceof HTMLBRElement?(t=document.createElement("span"),t.className=te,o.appendChild(t)):d instanceof HTMLElement&&l(d.textContent??"",d.className||void 0);return e.textContent="",e.appendChild(o),a},oe={yPercent:0,duration:.9,ease:"power4.out",stagger:.035};y("headline",(e,{gsap:o,ScrollTrigger:a,reducedMotion:t})=>{if(t){e.style.visibility="visible";return}let r=Me(e);if(o.set(r,{yPercent:110}),e.style.visibility="visible",e.dataset.animTrigger==="load"||!a){o.to(r,{...oe,delay:.3});return}o.to(r,{...oe,scrollTrigger:{trigger:e,start:"top 85%"}})});var Ie=26;y("marquee",(e,{gsap:o,reducedMotion:a})=>{if(a)return;let t=Number(e.dataset.animSets??4),r=Number(e.dataset.animDuration??Ie);if(!Number.isFinite(t)||t<2)return;let l=()=>e.scrollWidth/t,d=o.to(e,{x:()=>-l(),duration:r,ease:"none",repeat:-1,modifiers:{x:s=>`${Number.parseFloat(s)%l()}px`}}),i=e.parentElement;i&&(i.addEventListener("pointerenter",()=>d.pause()),i.addEventListener("pointerleave",()=>d.resume()))});var re=1.15,se=.18,F=8,Ce=.55,G=(e,o,a)=>Math.min(a,Math.max(o,e));y("photo-hover",(e,{reducedMotion:o})=>{let a=e.querySelector(":scope > img");if(!a)return;let t=e.querySelector(":scope > .camera_focus"),r=t?.querySelector(":scope > img:first-child")??null,l=t?.querySelector(":scope > img:nth-child(2)")??null;t&&(!r||!l)&&console.warn("[shopic] photo-hover expects two image layers inside the reticle.",t);let d=Array.from(e.querySelectorAll(".detect_box")),i=0,s=0,c=0,u=0,A=0,I=!1,O=p=>{let m=e.getBoundingClientRect(),v=e.offsetWidth,x=e.offsetHeight,S=m.width/v||1,w=(p.clientX-m.left)/S,M=(p.clientY-m.top)/S;return{x:w,y:M,width:v,height:x}},b=()=>{if(i=0,!!r){if(s-=s*se,c-=c*se,r.style.transform=`translate(${s.toFixed(2)}px, ${c.toFixed(2)}px)`,Math.abs(s)>.1||Math.abs(c)>.1){i=requestAnimationFrame(b);return}r.style.transform=""}},h=p=>{if(p.pointerType!=="mouse")return;let{x:m,y:v,width:x,height:S}=O(p),w=re-1,M=-(m/x-.5)*x*w,_=-(v/S-.5)*S*w;if(a.style.transform=`translate(${M.toFixed(2)}px, ${_.toFixed(2)}px) scale(${re})`,!t||I)return;let H=G(m,0,x),L=G(v,0,S);if(t.style.left=`${H.toFixed(2)}px`,t.style.top=`${L.toFixed(2)}px`,o){u=H,A=L;return}s-=H-u,c-=L-A,u=H,A=L,i||(i=requestAnimationFrame(b))},T=()=>{a.style.transform="",t&&(I=!1,t.style.left="",t.style.top="",t.style.width="",t.style.height="",t.style.transitionProperty="",l&&(l.style.transform=""),r&&(r.style.opacity="",r.style.transform=""),s=0,c=0,cancelAnimationFrame(i),i=0)},C=p=>{if(p.pointerType!=="mouse"||!t)return;t.style.transitionProperty="opacity, width, height";let{x:m,y:v,width:x,height:S}=O(p);u=G(m,0,x),A=G(v,0,S),s=0,c=0};e.addEventListener("pointerenter",C),e.addEventListener("pointermove",h),e.addEventListener("pointerleave",T);for(let p of d)p.addEventListener("pointerenter",m=>{m.pointerType!=="mouse"||!t||(I=!0,t.style.transitionProperty="",t.style.left=`${p.offsetLeft-F}px`,t.style.top=`${p.offsetTop-F}px`,t.style.width=`${p.offsetWidth+F*2}px`,t.style.height=`${p.offsetHeight+F*2}px`,r&&(r.style.opacity="0"),l&&(l.style.transform=`scale(${Ce})`))}),p.addEventListener("pointerleave",()=>{I=!1,r&&(r.style.opacity=""),l&&(l.style.transform="")})});var ie="is-visible";y("reveal",(e,{reducedMotion:o})=>{let a=Number(e.dataset.animDelay??0);if(a&&(e.style.transitionDelay=`${a}ms`),o||!("IntersectionObserver"in window)){e.classList.add(ie);return}let t=new IntersectionObserver(r=>{for(let l of r)l.isIntersecting&&(l.target.classList.add(ie),t.unobserve(l.target))},{threshold:.15});t.observe(e)});y("scramble",(e,{reducedMotion:o})=>{let a={duration:Number(e.dataset.animDuration??1050),stagger:Number(e.dataset.animStagger??.75),reducedMotion:o};if(o||!("IntersectionObserver"in window)){N(e,a);return}let t=new IntersectionObserver(r=>{for(let l of r)l.isIntersecting&&(N(e,{...a,relaunchOnHover:!0}),t.unobserve(l.target))},{threshold:.2});t.observe(e)});var U=e=>String(e).padStart(2,"0");y("timer",e=>{let o=()=>{let a=new Date;e.textContent=`${U(a.getHours())}:${U(a.getMinutes())}:${U(a.getSeconds())}`};o(),setInterval(o,1e3)});var ae="is-active",Oe="(max-width: 991px)";y("versus-tabs",e=>{let o=Array.from(e.querySelectorAll('[role="tab"]')),a=e.nextElementSibling,t=a?Array.from(a.querySelectorAll(":scope > *")):[];if(o.length===0||o.length!==t.length){console.warn(`[shopic] versus-tabs expects one tab per panel; found ${o.length} tabs and ${t.length} panels.`,e);return}let r=window.matchMedia(Oe),l=i=>{o.forEach((s,c)=>{s.classList.toggle(ae,c===i),s.setAttribute("aria-selected",String(c===i))}),t.forEach((s,c)=>{s.hidden=r.matches&&c!==i})},d=()=>Math.max(0,o.indexOf(e.querySelector(`.${ae}`)));o.forEach((i,s)=>{i.addEventListener("click",()=>l(s)),i.addEventListener("keydown",c=>{(c.key==="Enter"||c.key===" ")&&(c.preventDefault(),l(s))}),i.hasAttribute("tabindex")||(i.tabIndex=0)}),r.addEventListener("change",()=>l(d())),l(d())});var le=()=>{let{gsap:e,ScrollTrigger:o}=window;if(!e)throw new Error("[shopic] GSAP is not on the page. Enable it in Webflow \u2192 Site Settings \u2192 Custom Code \u2192 GSAP.");return o&&e.registerPlugin(o),{gsap:e,ScrollTrigger:o}},ce=()=>window.matchMedia("(prefers-reduced-motion: reduce)").matches;window.Webflow||(window.Webflow=[]);window.Webflow.push(()=>{let{gsap:e,ScrollTrigger:o}=le();B({gsap:e,ScrollTrigger:o,reducedMotion:ce()})});})();
package/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../bin/live-reload.js", "../src/anim/registry.ts", "../src/anim/headline.ts", "../src/anim/marquee.ts", "../src/anim/reveal.ts", "../src/lib/gsap.ts", "../src/index.ts"],
4
- "sourcesContent": ["new EventSource(`${SERVE_ORIGIN}/esbuild`).addEventListener('change', () => location.reload());\n", "import type { Gsap, ScrollTriggerType } from '$lib/gsap';\n\nexport type AnimationContext = {\n gsap: Gsap;\n ScrollTrigger?: ScrollTriggerType;\n reducedMotion: boolean;\n};\n\nexport type AnimationInit = (element: HTMLElement, context: AnimationContext) => void;\n\nconst registry = new Map<string, AnimationInit>();\n\n/**\n * Registers an animation under the name used in `data-anim`.\n * The client edits content in the Designer; the attribute is the only contract\n * between their markup and this bundle.\n */\nexport const registerAnimation = (name: string, init: AnimationInit): void => {\n if (registry.has(name)) {\n throw new Error(`[shopic] Animation \"${name}\" is registered twice.`);\n }\n\n registry.set(name, init);\n};\n\n/**\n * Runs every registered animation against the elements that ask for it.\n * Unknown names warn rather than throw \u2014 a stray attribute in the Designer\n * must not take the whole bundle down.\n *\n * Initialisation is once per element, and the marker lives on the element\n * rather than in module scope so that a *second copy* of this bundle sees it\n * too. That is not hypothetical: a stale inlined copy alongside the CDN one\n * re-split an already-split headline, and because the splitter carries a\n * parent's className onto the words it produces, every word inherited\n * `split_line` (display: block) and stacked one per line.\n */\nexport const runAnimations = (context: AnimationContext, root: ParentNode = document): void => {\n const elements = root.querySelectorAll<HTMLElement>('[data-anim]');\n\n for (const element of elements) {\n const name = element.dataset.anim;\n if (!name) continue;\n if (element.dataset.animState === 'ready') continue;\n\n const init = registry.get(name);\n\n if (!init) {\n // eslint-disable-next-line no-console\n console.warn(`[shopic] No animation registered for data-anim=\"${name}\".`, element);\n continue;\n }\n\n element.dataset.animState = 'ready';\n init(element, context);\n }\n};\n", "import { registerAnimation } from './registry';\n\nconst LINE = 'split_line';\nconst WORD = 'split_word';\nconst INNER = 'split_word-inner';\n\n/**\n * Wraps every word in an overflow-hidden span so it can slide up into view.\n * `<br>` starts a new line; existing child spans (accent colours) survive as\n * single words and keep their class. The class names are real Webflow styles \u2014\n * a class that only ever exists at runtime gets stripped from the published CSS.\n */\nconst splitHeadline = (element: HTMLElement): HTMLElement[] => {\n const fragment = document.createDocumentFragment();\n const words: HTMLElement[] = [];\n let line = document.createElement('span');\n line.className = LINE;\n fragment.appendChild(line);\n\n const addWord = (text: string, className?: string): void => {\n if (!text) return;\n\n const outer = document.createElement('span');\n outer.className = WORD;\n\n const inner = document.createElement('span');\n inner.className = className ? `${INNER} ${className}` : INNER;\n inner.textContent = text;\n\n outer.appendChild(inner);\n line.appendChild(outer);\n words.push(inner);\n };\n\n const addChunks = (text: string, className?: string): void => {\n for (const chunk of text.split(/(\\s+)/)) {\n if (!chunk) continue;\n if (/^\\s+$/.test(chunk)) line.appendChild(document.createTextNode(' '));\n else addWord(chunk, className);\n }\n };\n\n for (const node of Array.from(element.childNodes)) {\n if (node.nodeType === Node.TEXT_NODE) {\n addChunks(node.textContent ?? '');\n } else if (node instanceof HTMLBRElement) {\n line = document.createElement('span');\n line.className = LINE;\n fragment.appendChild(line);\n } else if (node instanceof HTMLElement) {\n addChunks(node.textContent ?? '', node.className || undefined);\n }\n }\n\n element.textContent = '';\n element.appendChild(fragment);\n\n return words;\n};\n\nconst TWEEN = { yPercent: 0, duration: 0.9, ease: 'power4.out', stagger: 0.035 } as const;\n\nregisterAnimation('headline', (element, { gsap, ScrollTrigger, reducedMotion }) => {\n if (reducedMotion) {\n element.style.visibility = 'visible';\n return;\n }\n\n const words = splitHeadline(element);\n gsap.set(words, { yPercent: 110 });\n element.style.visibility = 'visible';\n\n // The hero headline is above the fold, so it plays on load; everything else\n // waits for the scroll to reach it.\n if (element.dataset.animTrigger === 'load' || !ScrollTrigger) {\n gsap.to(words, { ...TWEEN, delay: 0.3 });\n return;\n }\n\n gsap.to(words, { ...TWEEN, scrollTrigger: { trigger: element, start: 'top 85%' } });\n});\n", "import { registerAnimation } from './registry';\n\nconst DEFAULT_DURATION = 26;\n\n/**\n * Infinite logo strip. The track holds the same set of logos repeated N times,\n * so translating by exactly one set's width lands on an identical frame \u2014 no\n * measuring loop, no drift. N comes from the DOM, not a constant, so adding a\n * logo in the Designer cannot desync the loop.\n */\nregisterAnimation('marquee', (element, { gsap, reducedMotion }) => {\n if (reducedMotion) return;\n\n const sets = Number(element.dataset.animSets ?? 4);\n const duration = Number(element.dataset.animDuration ?? DEFAULT_DURATION);\n if (!Number.isFinite(sets) || sets < 2) return;\n\n const distance = () => element.scrollWidth / sets;\n\n const tween = gsap.to(element, {\n x: () => -distance(),\n duration,\n ease: 'none',\n repeat: -1,\n modifiers: {\n // wrap inside one set so the tween never accumulates a huge offset\n x: (value: string) => `${Number.parseFloat(value) % distance()}px`,\n },\n });\n\n const parent = element.parentElement;\n if (!parent) return;\n\n parent.addEventListener('pointerenter', () => tween.pause());\n parent.addEventListener('pointerleave', () => tween.resume());\n});\n", "import { registerAnimation } from './registry';\n\nconst VISIBLE = 'is-visible';\n\n/**\n * Fade-and-rise on entry. The motion itself is a CSS transition on\n * `.hero_media`; this only decides when to add the state class, so the\n * client can restyle the easing in the Designer without touching the bundle.\n */\nregisterAnimation('reveal', (element, { reducedMotion }) => {\n const delay = Number(element.dataset.animDelay ?? 0);\n if (delay) element.style.transitionDelay = `${delay}ms`;\n\n if (reducedMotion || !('IntersectionObserver' in window)) {\n element.classList.add(VISIBLE);\n return;\n }\n\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (!entry.isIntersecting) continue;\n entry.target.classList.add(VISIBLE);\n observer.unobserve(entry.target);\n }\n },\n { threshold: 0.15 }\n );\n\n observer.observe(element);\n});\n", "import type { gsap as GsapStatic } from 'gsap';\nimport type { ScrollTrigger as ScrollTriggerStatic } from 'gsap/ScrollTrigger';\n\ndeclare global {\n interface Window {\n gsap?: typeof GsapStatic;\n ScrollTrigger?: typeof ScrollTriggerStatic;\n }\n}\n\nexport type Gsap = typeof GsapStatic;\nexport type ScrollTriggerType = typeof ScrollTriggerStatic;\n\n/**\n * GSAP ships from Webflow's native Site Settings toggle, not from this bundle.\n * A site transfer wipes Site Settings, so the toggle can silently disappear and\n * take every animation with it. Fail loudly here instead.\n */\nexport const requireGsap = (): { gsap: Gsap; ScrollTrigger?: ScrollTriggerType } => {\n const { gsap, ScrollTrigger } = window;\n\n if (!gsap) {\n throw new Error(\n '[shopic] GSAP is not on the page. Enable it in Webflow \u2192 Site Settings \u2192 Custom Code \u2192 GSAP.'\n );\n }\n\n if (ScrollTrigger) gsap.registerPlugin(ScrollTrigger);\n\n return { gsap, ScrollTrigger };\n};\n\n/**\n * Whether the visitor asked the OS to cut down on motion.\n */\nexport const prefersReducedMotion = (): boolean =>\n window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n", "import { runAnimations } from '$anim/index';\nimport { prefersReducedMotion, requireGsap } from '$lib/gsap';\n\nwindow.Webflow ||= [];\nwindow.Webflow.push(() => {\n const { gsap, ScrollTrigger } = requireGsap();\n\n runAnimations({ gsap, ScrollTrigger, reducedMotion: prefersReducedMotion() });\n});\n"],
5
- "mappings": "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",
3
+ "sources": ["../bin/live-reload.js", "../src/anim/carousel-state.ts", "../src/anim/registry.ts", "../src/anim/carousel.ts", "../src/lib/scramble.ts", "../src/anim/evidence.ts", "../src/anim/headline.ts", "../src/anim/marquee.ts", "../src/anim/reveal.ts", "../src/anim/scramble.ts", "../src/lib/gsap.ts", "../src/index.ts"],
4
+ "sourcesContent": ["new EventSource(`${SERVE_ORIGIN}/esbuild`).addEventListener('change', () => location.reload());\n", "/**\n * Wrap an index into `0..count - 1`.\n *\n * The carousel addresses slides by a *virtual* index that runs from `-1` (the\n * clone of the last slide, sitting before the first) to `count` (the clone of\n * the first, sitting after the last). This maps any such index back onto the\n * real slide it stands for.\n */\nexport const wrapIndex = (index: number, count: number): number =>\n ((index % count) + count) % count;\n", "import type { Gsap, ScrollTriggerType } from '$lib/gsap';\n\nexport type AnimationContext = {\n gsap: Gsap;\n ScrollTrigger?: ScrollTriggerType;\n reducedMotion: boolean;\n};\n\nexport type AnimationInit = (element: HTMLElement, context: AnimationContext) => void;\n\nconst registry = new Map<string, AnimationInit>();\n\n/**\n * Registers an animation under the name used in `data-anim`.\n * The client edits content in the Designer; the attribute is the only contract\n * between their markup and this bundle.\n */\nexport const registerAnimation = (name: string, init: AnimationInit): void => {\n if (registry.has(name)) {\n throw new Error(`[shopic] Animation \"${name}\" is registered twice.`);\n }\n\n registry.set(name, init);\n};\n\n/**\n * Runs every registered animation against the elements that ask for it.\n * Unknown names warn rather than throw \u2014 a stray attribute in the Designer\n * must not take the whole bundle down.\n *\n * Initialisation is once per element, and the marker lives on the element\n * rather than in module scope so that a *second copy* of this bundle sees it\n * too. That is not hypothetical: a stale inlined copy alongside the CDN one\n * re-split an already-split headline, and because the splitter carries a\n * parent's className onto the words it produces, every word inherited\n * `split_line` (display: block) and stacked one per line.\n */\nexport const runAnimations = (context: AnimationContext, root: ParentNode = document): void => {\n const elements = root.querySelectorAll<HTMLElement>('[data-anim]');\n\n for (const element of elements) {\n const name = element.dataset.anim;\n if (!name) continue;\n if (element.dataset.animState === 'ready') continue;\n\n const init = registry.get(name);\n\n if (!init) {\n // eslint-disable-next-line no-console\n console.warn(`[shopic] No animation registered for data-anim=\"${name}\".`, element);\n continue;\n }\n\n element.dataset.animState = 'ready';\n init(element, context);\n }\n};\n", "import { wrapIndex } from './carousel-state';\nimport { registerAnimation } from './registry';\n\n/**\n * Horizontal carousel driven by an external tab strip.\n *\n * Markup contract \u2014 all attributes, no class coupling, so the client can move\n * things around in the Designer:\n *\n * [data-anim=\"carousel\"] the carousel track\n * > [data-slide=\"<name>\"] one slide per name\n * [data-tab=\"<name>\"] a tab anywhere in the track's section\n *\n * The runtime keeps the existing clone-based infinite loop, but takes control\n * of the horizontal motion with GSAP. Native scroll-snap is left as the\n * no-JavaScript fallback and disabled once this controller is ready, so wheel,\n * touch, drag, tab navigation, and autoplay all finish through the same easing\n * and never fight a second browser animation.\n */\n\nconst DRAG_MULTIPLIER = 2;\nconst SCROLL_SETTLE_MS = 150;\n/** How far a pointer must travel before the gesture counts as a drag, not a click. */\nconst DRAG_THRESHOLD_PX = 4;\n/** Sub-pixel scroll differences are noise, not a slide the user wants to reach. */\nconst SETTLE_EPSILON_PX = 1;\nconst AUTOPLAY_DELAY_MS = 5000;\nconst MOTION_DURATION = 0.75;\nconst MOTION_EASE = 'power3.out';\nconst RATIO_THRESHOLDS = [0, 0.25, 0.5, 0.6, 0.7, 0.8, 0.9, 1];\n\n/** Fired on a slide the moment it becomes the current one. */\nexport const SLIDE_ENTER = 'shopic:slide-enter';\n\nregisterAnimation('carousel', (track, { gsap, reducedMotion }) => {\n const realSlides = Array.from(track.querySelectorAll<HTMLElement>(':scope > [data-slide]'));\n if (realSlides.length < 2) return;\n\n const scope = track.closest<HTMLElement>('section') ?? track.parentElement ?? document.body;\n const tabs = Array.from(scope.querySelectorAll<HTMLElement>('[data-tab]'));\n const motionDuration = reducedMotion ? 0 : MOTION_DURATION;\n\n const startClone = realSlides[realSlides.length - 1].cloneNode(true) as HTMLElement;\n const endClone = realSlides[0].cloneNode(true) as HTMLElement;\n\n for (const clone of [startClone, endClone]) {\n clone.dataset.slideClone = 'true';\n clone.setAttribute('aria-hidden', 'true');\n // Clones must never be initialised by runAnimations: they were produced\n // after their originals were already wired.\n for (const nested of clone.querySelectorAll<HTMLElement>('[data-anim]')) {\n nested.dataset.animState = 'ready';\n }\n }\n\n track.insertBefore(startClone, realSlides[0]);\n track.append(endClone);\n\n // [startClone, ...realSlides, endClone] \u2014 so a slide's index here is its real\n // index shifted by one, and the two clones sit at the virtual -1 and length.\n const slides = Array.from(track.querySelectorAll<HTMLElement>(':scope > [data-slide]'));\n const slideIndices = new Map(slides.map((slide, index) => [slide, index]));\n\n const realIndicesByName = new Map<string, number>();\n realSlides.forEach((slide, index) => {\n const name = slide.dataset.slide;\n if (name && !realIndicesByName.has(name)) realIndicesByName.set(name, index);\n });\n\n // Scroll position of every slide, measured once. Reading these live meant a\n // forced layout per slide on every settle, drag end and tween end; they only\n // actually change when the track is resized.\n let positions: number[] = [];\n\n const measure = (): void => {\n const trackLeft = track.getBoundingClientRect().left;\n const scroll = track.scrollLeft;\n const gutter = parseFloat(getComputedStyle(track).paddingLeft) || 0;\n positions = slides.map(\n (slide) => slide.getBoundingClientRect().left - trackLeft + scroll - gutter\n );\n };\n\n const positionOf = (slide: HTMLElement): number => positions[slideIndices.get(slide) ?? 0];\n\n const setActiveTab = (name: string): void => {\n for (const tab of tabs) {\n const active = tab.dataset.tab === name;\n tab.classList.toggle('is-active', active);\n // aria-selected is only valid on tab/option/row roles, and the markup\n // contract above does not promise one. aria-current is global.\n if (active) tab.setAttribute('aria-current', 'true');\n else tab.removeAttribute('aria-current');\n }\n };\n\n let currentIndex = 0;\n let current: HTMLElement | null = null;\n let hovering = false;\n let sectionInViewport = false;\n let autoplayTimer: ReturnType<typeof setTimeout> | undefined;\n let settleTimer: ReturnType<typeof setTimeout> | undefined;\n let motionInFlight = false;\n let activePointerId: number | null = null;\n\n const dragging = (): boolean => activePointerId !== null;\n\n const clearAutoplay = (): void => {\n if (autoplayTimer !== undefined) {\n clearTimeout(autoplayTimer);\n autoplayTimer = undefined;\n }\n };\n\n const canAutoplay = (): boolean =>\n !reducedMotion &&\n !document.hidden &&\n sectionInViewport &&\n !dragging() &&\n !hovering &&\n !scope.contains(document.activeElement);\n\n const scheduleAutoplay = (): void => {\n clearAutoplay();\n if (!canAutoplay()) return;\n\n autoplayTimer = setTimeout(() => {\n autoplayTimer = undefined;\n if (!canAutoplay()) return;\n\n const nextIndex = wrapIndex(currentIndex + 1, realSlides.length);\n // Wrapping to 0 means we are on the last slide: go through the end clone\n // so the last-to-first transition keeps moving forward. It is normalized\n // to the first real slide when the tween completes.\n animateToSlide(nextIndex === 0 ? endClone : realSlides[nextIndex]);\n }, AUTOPLAY_DELAY_MS);\n };\n\n for (const tab of tabs) {\n tab.addEventListener('click', (event) => {\n event.preventDefault();\n const name = tab.dataset.tab;\n const index = name === undefined ? undefined : realIndicesByName.get(name);\n if (index === undefined || name === undefined) return;\n\n currentIndex = index;\n setActiveTab(name);\n clearAutoplay();\n animateToSlide(realSlides[index]);\n });\n }\n\n // A callback batch only carries slides whose ratio crossed a threshold, not\n // every observed slide. Keep a running ratio per slide so the active tab is\n // always based on the actual most-visible slide.\n const ratios = new Map<HTMLElement, number>(slides.map((slide) => [slide, 0]));\n\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n ratios.set(entry.target as HTMLElement, entry.isIntersecting ? entry.intersectionRatio : 0);\n }\n\n let mostVisible: HTMLElement | null = null;\n let best = 0;\n for (const slide of slides) {\n const ratio = ratios.get(slide) ?? 0;\n if (ratio > best) {\n best = ratio;\n mostVisible = slide;\n }\n }\n // Eight thresholds across nine slides means this fires many times per\n // tween; the tab and class writes below only matter when the winner\n // actually changes.\n if (!mostVisible || mostVisible === current) return;\n\n current = mostVisible;\n const name = mostVisible.dataset.slide;\n if (name) {\n setActiveTab(name);\n const realIndex = realIndicesByName.get(name);\n if (realIndex !== undefined) currentIndex = realIndex;\n }\n for (const slide of slides) slide.classList.toggle('is-current', slide === mostVisible);\n\n mostVisible.dispatchEvent(new CustomEvent(SLIDE_ENTER));\n },\n { root: track, threshold: RATIO_THRESHOLDS }\n );\n\n for (const slide of slides) observer.observe(slide);\n\n const nearestSlide = (): HTMLElement => {\n const scroll = track.scrollLeft;\n let closest = slides[0];\n let closestDistance = Infinity;\n for (const [index, slide] of slides.entries()) {\n const distance = Math.abs(positions[index] - scroll);\n if (distance < closestDistance) {\n closestDistance = distance;\n closest = slide;\n }\n }\n return closest;\n };\n\n const finishMotion = (target: HTMLElement): void => {\n // A clone is a stand-in for a real slide. Once the tween lands on one, jump\n // the scroll to its original \u2014 snap is disabled while this controller is\n // active, so nothing competes with the correction. motionInFlight stays\n // true across the jump so the scroll event it emits cannot schedule a\n // second tween.\n if (target.dataset.slideClone) {\n const virtualIndex = (slideIndices.get(target) ?? 0) - 1;\n track.scrollLeft = positionOf(realSlides[wrapIndex(virtualIndex, realSlides.length)]);\n }\n\n motionInFlight = false;\n scheduleAutoplay();\n };\n\n function animateToSlide(slide: HTMLElement): void {\n clearTimeout(settleTimer);\n gsap.killTweensOf(track);\n motionInFlight = true;\n\n if (motionDuration === 0) {\n track.scrollLeft = positionOf(slide);\n finishMotion(slide);\n return;\n }\n\n gsap.to(track, {\n scrollLeft: positionOf(slide),\n duration: motionDuration,\n ease: MOTION_EASE,\n overwrite: 'auto',\n onComplete: () => finishMotion(slide),\n onInterrupt: () => {\n motionInFlight = false;\n },\n });\n }\n\n const settleToNearest = (): void => {\n const target = nearestSlide();\n // Already parked on it. This is the common case after a clone correction:\n // the jump emits a scroll event of its own, which would otherwise arm a\n // second tween to the slide we are already on.\n if (Math.abs(positionOf(target) - track.scrollLeft) < SETTLE_EPSILON_PX) {\n scheduleAutoplay();\n return;\n }\n\n animateToSlide(target);\n };\n\n // Land on the first real slide before anyone sees it. This touches only the\n // horizontal track and cannot move the page vertically like scrollIntoView.\n measure();\n track.style.scrollSnapType = 'none';\n track.style.touchAction = 'pan-y';\n track.scrollLeft = positionOf(realSlides[0]);\n\n // Slide widths follow the track's, so one observer over the track and its\n // slides catches every layout that invalidates the measured positions.\n const resizeObserver = new ResizeObserver(() => measure());\n resizeObserver.observe(track);\n for (const slide of slides) resizeObserver.observe(slide);\n\n // Wheel, keyboard, and any native scroll source still get a single GSAP\n // settle tween. Pointer gestures use the same settle path below.\n //\n // 'scrollend' fires only once scrolling \u2014 momentum included \u2014 has truly\n // finished. The debounced fallback can fire while momentum is still coasting,\n // and snap is off, so nothing else stops the browser's inertia: the settle\n // tween would then be animating scrollLeft against a live fling.\n if ('onscrollend' in window) {\n track.addEventListener('scrollend', () => {\n if (motionInFlight || dragging()) return;\n settleToNearest();\n });\n } else {\n track.addEventListener('scroll', () => {\n if (motionInFlight || dragging()) return;\n clearTimeout(settleTimer);\n settleTimer = setTimeout(settleToNearest, SCROLL_SETTLE_MS);\n });\n }\n\n let dragStartX = 0;\n let dragStartScroll = 0;\n let dragged = false;\n let suppressClick = false;\n\n // A press is not a drag until it has travelled DRAG_THRESHOLD_PX. Until then\n // the pointer is left completely alone \u2014 no capture, no preventDefault \u2014 so\n // links, buttons and text selection inside a slide behave normally.\n track.addEventListener('pointerdown', (event) => {\n clearAutoplay();\n clearTimeout(settleTimer);\n gsap.killTweensOf(track);\n motionInFlight = false;\n activePointerId = event.pointerId;\n dragged = false;\n suppressClick = false;\n dragStartX = event.clientX;\n dragStartScroll = track.scrollLeft;\n });\n\n track.addEventListener('pointermove', (event) => {\n if (event.pointerId !== activePointerId) return;\n\n const travelled = event.clientX - dragStartX;\n\n if (!dragged) {\n if (Math.abs(travelled) < DRAG_THRESHOLD_PX) return;\n dragged = true;\n track.classList.add('is-dragging');\n // Capture only now, so move/up/cancel keep reaching the track once the\n // pointer wanders outside it. Nothing is bound to window.\n track.setPointerCapture(event.pointerId);\n }\n\n track.scrollLeft = dragStartScroll - travelled * DRAG_MULTIPLIER;\n if (event.pointerType === 'mouse') event.preventDefault();\n });\n\n const stopPointerInteraction = (event: PointerEvent): void => {\n if (event.pointerId !== activePointerId) return;\n\n activePointerId = null;\n if (track.hasPointerCapture(event.pointerId)) track.releasePointerCapture(event.pointerId);\n\n if (dragged) {\n dragged = false;\n suppressClick = true;\n track.classList.remove('is-dragging');\n }\n\n // The press killed any tween in flight, so settle back onto the nearest\n // slide either way. On a plain click nothing has moved, so this only\n // restarts the autoplay clock. The settle tween restores it through\n // finishMotion.\n settleToNearest();\n };\n\n track.addEventListener('pointerup', stopPointerInteraction);\n track.addEventListener('pointercancel', stopPointerInteraction);\n\n // The click that ends a drag would otherwise activate whatever link sits\n // under the finger. Swallowing it here keeps the flag from leaking into the\n // next gesture: pointerdown clears it either way.\n track.addEventListener(\n 'click',\n (event) => {\n if (!suppressClick) return;\n suppressClick = false;\n event.preventDefault();\n event.stopPropagation();\n },\n true\n );\n\n // Bound to the section, not the track: the tab strip sits outside the track\n // and hovering it is still hovering the carousel.\n scope.addEventListener('pointerenter', (event) => {\n if (event.pointerType !== 'mouse') return;\n hovering = true;\n clearAutoplay();\n });\n\n scope.addEventListener('pointerleave', (event) => {\n if (event.pointerType !== 'mouse') return;\n hovering = false;\n scheduleAutoplay();\n });\n\n scope.addEventListener('focusin', clearAutoplay);\n scope.addEventListener('focusout', () => {\n // canAutoplay reads document.activeElement, which is still the old element\n // during focusout itself.\n queueMicrotask(scheduleAutoplay);\n });\n\n document.addEventListener('visibilitychange', () => {\n if (document.hidden) clearAutoplay();\n else scheduleAutoplay();\n });\n\n const visibilityObserver = new IntersectionObserver(\n (entries) => {\n sectionInViewport = entries.some((entry) => entry.isIntersecting);\n if (sectionInViewport) scheduleAutoplay();\n else clearAutoplay();\n },\n { threshold: 0.2 }\n );\n visibilityObserver.observe(track);\n});\n", "/**\n * \"Decipher\" reveal for mono text \u2014 glyphs cycle through a random set and lock\n * into the real character left to right, staggered.\n *\n * Follows the approved reference choreography. There is no ScrambleTextPlugin here\n * on purpose: that one is a paid Club GSAP add-on, and Webflow's native toggle\n * only serves the free build. This is the hand-rolled equivalent the designer's\n * prototype already uses, so the timing matches what she signed off on.\n */\n\nconst SCRAMBLE_CHARS = '!<>-_\\\\/[]{}=+*^?#$%01';\n\n/**\n * Tracks which reveal \"generation\" owns an element's spans. A relaunch has to\n * invalidate a still-running pass, otherwise two rAF loops fight over the same\n * element and the text never settles.\n */\nconst tokens = new WeakMap<HTMLElement, symbol>();\n\nexport type ScrambleOptions = {\n /** Total run length in ms. */\n duration?: number;\n /** How much of `duration` is spent staggering the lock-in, 0\u20131. */\n stagger?: number;\n /** Overrides the cached text. Pass it when the copy changes between reveals. */\n text?: string;\n /** Skip the animation and settle immediately. */\n reducedMotion?: boolean;\n};\n\nexport const scrambleReveal = (\n element: HTMLElement,\n { duration = 1050, stagger = 0.75, text: forcedText, reducedMotion = false }: ScrambleOptions = {}\n): void => {\n if (reducedMotion) {\n if (forcedText !== undefined) element.textContent = forcedText;\n return;\n }\n\n // The canonical text is cached: a relaunch mid-animation would otherwise\n // re-read whatever scrambled state happens to be on screen and lock that in.\n if (forcedText !== undefined) {\n element.dataset.scrambleText = forcedText;\n } else if (!element.dataset.scrambleText) {\n const initial = element.textContent;\n if (!initial || !initial.trim()) return;\n element.dataset.scrambleText = initial;\n }\n\n const text = element.dataset.scrambleText;\n if (!text || !text.trim()) return;\n\n const chars = Array.from(text);\n const count = chars.length;\n\n element.textContent = '';\n const fragment = document.createDocumentFragment();\n const spans = chars.map((char) => {\n const span = document.createElement('span');\n span.textContent = char;\n fragment.append(span);\n return span;\n });\n element.append(fragment);\n\n const token = Symbol('scramble');\n tokens.set(element, token);\n\n const lockTimes = chars.map((_, i) => (count <= 1 ? 0 : (i / (count - 1)) * duration * stagger));\n const start = performance.now();\n\n const tick = (now: number): void => {\n if (tokens.get(element) !== token) return; // superseded by a newer reveal\n\n const elapsed = now - start;\n let allLocked = true;\n\n chars.forEach((char, i) => {\n if (char === ' ') return;\n if (elapsed >= lockTimes[i]) {\n spans[i].textContent = char;\n } else {\n allLocked = false;\n spans[i].textContent = SCRAMBLE_CHARS[(Math.random() * SCRAMBLE_CHARS.length) | 0];\n }\n });\n\n if (!allLocked) requestAnimationFrame(tick);\n };\n\n requestAnimationFrame(tick);\n};\n\n/**\n * Hovering settled text relaunches it with the options it was first revealed\n * with. Bound once per element; safe to call on every reveal.\n */\nexport const bindScrambleRelaunch = (element: HTMLElement, options: ScrambleOptions = {}): void => {\n if (element.dataset.scrambleHoverBound) return;\n element.dataset.scrambleHoverBound = 'true';\n element.addEventListener('pointerenter', () => scrambleReveal(element, options));\n};\n", "import { scrambleReveal } from '$lib/scramble';\n\nimport { SLIDE_ENTER } from './carousel';\nimport { registerAnimation } from './registry';\n\n/**\n * The scan-evidence stack builds itself piece by piece: each slot grows in, in\n * DOM order, one finishing before the next starts.\n *\n * `.evidence` is bottom-anchored, so a growing height pushes the revealed-so-far\n * block UP rather than shifting anything below it \u2014 the stack visibly builds\n * upward even though slots open top to bottom.\n *\n * This animates each slot WRAPPER's height, never the tile inside it. The tile\n * keeps its real height and padding throughout, so every label and image stays\n * correctly positioned, and the wrapper's overflow:hidden just clips how much of\n * that already-correct box shows. Animating the tile's own height was tried in\n * the prototype first: padding cannot compress below itself, so \"collapsed\"\n * tiles never reached zero.\n *\n * GSAP resolves `height: 'auto'` by measuring it, so \u2014 unlike a plain CSS\n * transition \u2014 it interpolates real pixel values instead of snapping.\n *\n * Follows the approved scan-evidence reveal choreography.\n */\n\nconst SLOT_DURATION = 0.5;\nconst SLOT_EASE = 'power2.out';\nconst SCRAMBLE_SELECTOR = '.status-pill, .evidence_label, .evidence_sku';\n\nregisterAnimation('evidence', (element, { gsap, reducedMotion }) => {\n const slots = Array.from(element.children) as HTMLElement[];\n if (!slots.length) return;\n\n if (reducedMotion) {\n for (const slot of slots) {\n slot.style.height = 'auto';\n slot.style.opacity = '1';\n }\n return;\n }\n\n const play = (): void => {\n gsap.killTweensOf(slots);\n gsap.set(slots, { height: 0, opacity: 0, overflow: 'hidden' });\n\n const timeline = gsap.timeline();\n\n for (const slot of slots) {\n // The mono-text decipher starts the instant its slot begins opening, so\n // the label decodes in lockstep with the piece it sits on.\n timeline.call(() => {\n for (const text of slot.querySelectorAll<HTMLElement>(SCRAMBLE_SELECTOR)) {\n scrambleReveal(text, { duration: 630, stagger: 0.6 });\n }\n });\n\n timeline.to(slot, {\n height: 'auto',\n opacity: 1,\n duration: SLOT_DURATION,\n ease: SLOT_EASE,\n // NOT clearProps: the head block's default for a slot is height:0, so\n // clearing the inline height would re-collapse the slot the instant its\n // own tween finished, before the next one even started.\n onComplete: () => gsap.set(slot, { height: 'auto' }),\n });\n }\n };\n\n // Two independent conditions have to hold before the stack may open: the\n // slide has to be the current one, and the section has to have actually\n // reached the viewport. The carousel makes a slide current the moment the\n // page loads, long before this section is on screen, so without the second\n // condition the stack would already be sitting open when it arrives.\n //\n // They must be tracked separately rather than checked once. `runAnimations`\n // walks the DOM in order, so the carousel on the track initialises before the\n // evidence inside a card, and it sets `is-current` from its own observer a\n // frame later. An arrival callback that checks `is-current` inline therefore\n // loses the race on the first card, and \u2014 with the section already in view \u2014\n // no further intersection entry ever arrives to retry. That deadlock is\n // exactly what shipped in the first pass: card one stayed collapsed while\n // every card reached by a tab click played correctly.\n let inViewport = false;\n let hasPlayed = false;\n\n const maybePlay = (): void => {\n if (!inViewport) return;\n const slide = element.closest<HTMLElement>('[data-slide]');\n if (slide && !slide.classList.contains('is-current')) return;\n hasPlayed = true;\n play();\n };\n\n if ('IntersectionObserver' in window) {\n const arrival = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (!entry.isIntersecting) continue;\n inViewport = true;\n maybePlay();\n if (hasPlayed) arrival.disconnect();\n }\n },\n { threshold: 0.2 }\n );\n arrival.observe(element);\n } else {\n inViewport = true;\n maybePlay();\n }\n\n // A slide becoming current is the other half of the gate: on load it fires\n // before the section is on screen and does nothing, and on a genuine switch\n // it replays the stack.\n element.closest<HTMLElement>('[data-slide]')?.addEventListener(SLIDE_ENTER, maybePlay);\n});\n", "import { registerAnimation } from './registry';\n\nconst LINE = 'split_line';\nconst WORD = 'split_word';\nconst INNER = 'split_word-inner';\n\n/**\n * Wraps every word in an overflow-hidden span so it can slide up into view.\n * `<br>` starts a new line; existing child spans (accent colours) survive as\n * single words and keep their class. The class names are real Webflow styles \u2014\n * a class that only ever exists at runtime gets stripped from the published CSS.\n */\nconst splitHeadline = (element: HTMLElement): HTMLElement[] => {\n const fragment = document.createDocumentFragment();\n const words: HTMLElement[] = [];\n let line = document.createElement('span');\n line.className = LINE;\n fragment.appendChild(line);\n\n const addWord = (text: string, className?: string): void => {\n if (!text) return;\n\n const outer = document.createElement('span');\n outer.className = WORD;\n\n const inner = document.createElement('span');\n inner.className = className ? `${INNER} ${className}` : INNER;\n inner.textContent = text;\n\n outer.appendChild(inner);\n line.appendChild(outer);\n words.push(inner);\n };\n\n const addChunks = (text: string, className?: string): void => {\n for (const chunk of text.split(/(\\s+)/)) {\n if (!chunk) continue;\n if (/^\\s+$/.test(chunk)) line.appendChild(document.createTextNode(' '));\n else addWord(chunk, className);\n }\n };\n\n for (const node of Array.from(element.childNodes)) {\n if (node.nodeType === Node.TEXT_NODE) {\n addChunks(node.textContent ?? '');\n } else if (node instanceof HTMLBRElement) {\n line = document.createElement('span');\n line.className = LINE;\n fragment.appendChild(line);\n } else if (node instanceof HTMLElement) {\n addChunks(node.textContent ?? '', node.className || undefined);\n }\n }\n\n element.textContent = '';\n element.appendChild(fragment);\n\n return words;\n};\n\nconst TWEEN = { yPercent: 0, duration: 0.9, ease: 'power4.out', stagger: 0.035 } as const;\n\nregisterAnimation('headline', (element, { gsap, ScrollTrigger, reducedMotion }) => {\n if (reducedMotion) {\n element.style.visibility = 'visible';\n return;\n }\n\n const words = splitHeadline(element);\n gsap.set(words, { yPercent: 110 });\n element.style.visibility = 'visible';\n\n // The hero headline is above the fold, so it plays on load; everything else\n // waits for the scroll to reach it.\n if (element.dataset.animTrigger === 'load' || !ScrollTrigger) {\n gsap.to(words, { ...TWEEN, delay: 0.3 });\n return;\n }\n\n gsap.to(words, { ...TWEEN, scrollTrigger: { trigger: element, start: 'top 85%' } });\n});\n", "import { registerAnimation } from './registry';\n\nconst DEFAULT_DURATION = 26;\n\n/**\n * Infinite logo strip. The track holds the same set of logos repeated N times,\n * so translating by exactly one set's width lands on an identical frame \u2014 no\n * measuring loop, no drift. N comes from the DOM, not a constant, so adding a\n * logo in the Designer cannot desync the loop.\n */\nregisterAnimation('marquee', (element, { gsap, reducedMotion }) => {\n if (reducedMotion) return;\n\n const sets = Number(element.dataset.animSets ?? 4);\n const duration = Number(element.dataset.animDuration ?? DEFAULT_DURATION);\n if (!Number.isFinite(sets) || sets < 2) return;\n\n const distance = () => element.scrollWidth / sets;\n\n const tween = gsap.to(element, {\n x: () => -distance(),\n duration,\n ease: 'none',\n repeat: -1,\n modifiers: {\n // wrap inside one set so the tween never accumulates a huge offset\n x: (value: string) => `${Number.parseFloat(value) % distance()}px`,\n },\n });\n\n const parent = element.parentElement;\n if (!parent) return;\n\n parent.addEventListener('pointerenter', () => tween.pause());\n parent.addEventListener('pointerleave', () => tween.resume());\n});\n", "import { registerAnimation } from './registry';\n\nconst VISIBLE = 'is-visible';\n\n/**\n * Fade-and-rise on entry. The motion itself is a CSS transition on\n * `.hero_media`; this only decides when to add the state class, so the\n * client can restyle the easing in the Designer without touching the bundle.\n */\nregisterAnimation('reveal', (element, { reducedMotion }) => {\n const delay = Number(element.dataset.animDelay ?? 0);\n if (delay) element.style.transitionDelay = `${delay}ms`;\n\n if (reducedMotion || !('IntersectionObserver' in window)) {\n element.classList.add(VISIBLE);\n return;\n }\n\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (!entry.isIntersecting) continue;\n entry.target.classList.add(VISIBLE);\n observer.unobserve(entry.target);\n }\n },\n { threshold: 0.15 }\n );\n\n observer.observe(element);\n});\n", "import { bindScrambleRelaunch, scrambleReveal } from '$lib/scramble';\n\nimport { registerAnimation } from './registry';\n\n/**\n * Standalone decipher reveal for a single piece of mono text, for markup that\n * wants the effect without an evidence stack around it.\n *\n * Fires once on entry, then relaunches on hover \u2014 the same behaviour the\n * prototype gives every `.scan-evidence__label`.\n *\n * Tunable from the Designer:\n * data-anim-duration=\"1050\" total run length in ms\n * data-anim-stagger=\"0.75\" share of the run spent staggering the lock-in\n */\nregisterAnimation('scramble', (element, { reducedMotion }) => {\n const options = {\n duration: Number(element.dataset.animDuration ?? 1050),\n stagger: Number(element.dataset.animStagger ?? 0.75),\n reducedMotion,\n };\n\n if (reducedMotion || !('IntersectionObserver' in window)) {\n scrambleReveal(element, options);\n return;\n }\n\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (!entry.isIntersecting) continue;\n scrambleReveal(element, options);\n bindScrambleRelaunch(element, options);\n observer.unobserve(entry.target);\n }\n },\n { threshold: 0.2 }\n );\n\n observer.observe(element);\n});\n", "import type { gsap as GsapStatic } from 'gsap';\nimport type { ScrollTrigger as ScrollTriggerStatic } from 'gsap/ScrollTrigger';\n\ndeclare global {\n interface Window {\n gsap?: typeof GsapStatic;\n ScrollTrigger?: typeof ScrollTriggerStatic;\n }\n}\n\nexport type Gsap = typeof GsapStatic;\nexport type ScrollTriggerType = typeof ScrollTriggerStatic;\n\n/**\n * GSAP ships from Webflow's native Site Settings toggle, not from this bundle.\n * A site transfer wipes Site Settings, so the toggle can silently disappear and\n * take every animation with it. Fail loudly here instead.\n */\nexport const requireGsap = (): { gsap: Gsap; ScrollTrigger?: ScrollTriggerType } => {\n const { gsap, ScrollTrigger } = window;\n\n if (!gsap) {\n throw new Error(\n '[shopic] GSAP is not on the page. Enable it in Webflow \u2192 Site Settings \u2192 Custom Code \u2192 GSAP.'\n );\n }\n\n if (ScrollTrigger) gsap.registerPlugin(ScrollTrigger);\n\n return { gsap, ScrollTrigger };\n};\n\n/**\n * Whether the visitor asked the OS to cut down on motion.\n */\nexport const prefersReducedMotion = (): boolean =>\n window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n", "import { runAnimations } from '$anim/index';\nimport { prefersReducedMotion, requireGsap } from '$lib/gsap';\n\nwindow.Webflow ||= [];\nwindow.Webflow.push(() => {\n const { gsap, ScrollTrigger } = requireGsap();\n\n runAnimations({ gsap, ScrollTrigger, reducedMotion: prefersReducedMotion() });\n});\n"],
5
+ "mappings": ";;;AAAA,MAAI,YAAY,GAAG,uBAAY,UAAU,EAAE,iBAAiB,UAAU,MAAM,SAAS,OAAO,CAAC;;;ACQtF,MAAM,YAAY,CAAC,OAAe,WACrC,QAAQ,QAAS,SAAS;;;ACC9B,MAAM,WAAW,oBAAI,IAA2B;AAOzC,MAAM,oBAAoB,CAAC,MAAc,SAA8B;AAC5E,QAAI,SAAS,IAAI,IAAI,GAAG;AACtB,YAAM,IAAI,MAAM,uBAAuB,IAAI,wBAAwB;AAAA,IACrE;AAEA,aAAS,IAAI,MAAM,IAAI;AAAA,EACzB;AAcO,MAAM,gBAAgB,CAAC,SAA2B,OAAmB,aAAmB;AAC7F,UAAM,WAAW,KAAK,iBAA8B,aAAa;AAEjE,eAAW,WAAW,UAAU;AAC9B,YAAM,OAAO,QAAQ,QAAQ;AAC7B,UAAI,CAAC,KAAM;AACX,UAAI,QAAQ,QAAQ,cAAc,QAAS;AAE3C,YAAM,OAAO,SAAS,IAAI,IAAI;AAE9B,UAAI,CAAC,MAAM;AAET,gBAAQ,KAAK,mDAAmD,IAAI,MAAM,OAAO;AACjF;AAAA,MACF;AAEA,cAAQ,QAAQ,YAAY;AAC5B,WAAK,SAAS,OAAO;AAAA,IACvB;AAAA,EACF;;;ACpCA,MAAM,kBAAkB;AACxB,MAAM,mBAAmB;AAEzB,MAAM,oBAAoB;AAE1B,MAAM,oBAAoB;AAC1B,MAAM,oBAAoB;AAC1B,MAAM,kBAAkB;AACxB,MAAM,cAAc;AACpB,MAAM,mBAAmB,CAAC,GAAG,MAAM,KAAK,KAAK,KAAK,KAAK,KAAK,CAAC;AAGtD,MAAM,cAAc;AAE3B,oBAAkB,YAAY,CAAC,OAAO,EAAE,MAAM,cAAc,MAAM;AAChE,UAAM,aAAa,MAAM,KAAK,MAAM,iBAA8B,uBAAuB,CAAC;AAC1F,QAAI,WAAW,SAAS,EAAG;AAE3B,UAAM,QAAQ,MAAM,QAAqB,SAAS,KAAK,MAAM,iBAAiB,SAAS;AACvF,UAAM,OAAO,MAAM,KAAK,MAAM,iBAA8B,YAAY,CAAC;AACzE,UAAM,iBAAiB,gBAAgB,IAAI;AAE3C,UAAM,aAAa,WAAW,WAAW,SAAS,CAAC,EAAE,UAAU,IAAI;AACnE,UAAM,WAAW,WAAW,CAAC,EAAE,UAAU,IAAI;AAE7C,eAAW,SAAS,CAAC,YAAY,QAAQ,GAAG;AAC1C,YAAM,QAAQ,aAAa;AAC3B,YAAM,aAAa,eAAe,MAAM;AAGxC,iBAAW,UAAU,MAAM,iBAA8B,aAAa,GAAG;AACvE,eAAO,QAAQ,YAAY;AAAA,MAC7B;AAAA,IACF;AAEA,UAAM,aAAa,YAAY,WAAW,CAAC,CAAC;AAC5C,UAAM,OAAO,QAAQ;AAIrB,UAAM,SAAS,MAAM,KAAK,MAAM,iBAA8B,uBAAuB,CAAC;AACtF,UAAM,eAAe,IAAI,IAAI,OAAO,IAAI,CAAC,OAAO,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC;AAEzE,UAAM,oBAAoB,oBAAI,IAAoB;AAClD,eAAW,QAAQ,CAAC,OAAO,UAAU;AACnC,YAAM,OAAO,MAAM,QAAQ;AAC3B,UAAI,QAAQ,CAAC,kBAAkB,IAAI,IAAI,EAAG,mBAAkB,IAAI,MAAM,KAAK;AAAA,IAC7E,CAAC;AAKD,QAAI,YAAsB,CAAC;AAE3B,UAAM,UAAU,MAAY;AAC1B,YAAM,YAAY,MAAM,sBAAsB,EAAE;AAChD,YAAM,SAAS,MAAM;AACrB,YAAM,SAAS,WAAW,iBAAiB,KAAK,EAAE,WAAW,KAAK;AAClE,kBAAY,OAAO;AAAA,QACjB,CAAC,UAAU,MAAM,sBAAsB,EAAE,OAAO,YAAY,SAAS;AAAA,MACvE;AAAA,IACF;AAEA,UAAM,aAAa,CAAC,UAA+B,UAAU,aAAa,IAAI,KAAK,KAAK,CAAC;AAEzF,UAAM,eAAe,CAAC,SAAuB;AAC3C,iBAAW,OAAO,MAAM;AACtB,cAAM,SAAS,IAAI,QAAQ,QAAQ;AACnC,YAAI,UAAU,OAAO,aAAa,MAAM;AAGxC,YAAI,OAAQ,KAAI,aAAa,gBAAgB,MAAM;AAAA,YAC9C,KAAI,gBAAgB,cAAc;AAAA,MACzC;AAAA,IACF;AAEA,QAAI,eAAe;AACnB,QAAI,UAA8B;AAClC,QAAI,WAAW;AACf,QAAI,oBAAoB;AACxB,QAAI;AACJ,QAAI;AACJ,QAAI,iBAAiB;AACrB,QAAI,kBAAiC;AAErC,UAAM,WAAW,MAAe,oBAAoB;AAEpD,UAAM,gBAAgB,MAAY;AAChC,UAAI,kBAAkB,QAAW;AAC/B,qBAAa,aAAa;AAC1B,wBAAgB;AAAA,MAClB;AAAA,IACF;AAEA,UAAM,cAAc,MAClB,CAAC,iBACD,CAAC,SAAS,UACV,qBACA,CAAC,SAAS,KACV,CAAC,YACD,CAAC,MAAM,SAAS,SAAS,aAAa;AAExC,UAAM,mBAAmB,MAAY;AACnC,oBAAc;AACd,UAAI,CAAC,YAAY,EAAG;AAEpB,sBAAgB,WAAW,MAAM;AAC/B,wBAAgB;AAChB,YAAI,CAAC,YAAY,EAAG;AAEpB,cAAM,YAAY,UAAU,eAAe,GAAG,WAAW,MAAM;AAI/D,uBAAe,cAAc,IAAI,WAAW,WAAW,SAAS,CAAC;AAAA,MACnE,GAAG,iBAAiB;AAAA,IACtB;AAEA,eAAW,OAAO,MAAM;AACtB,UAAI,iBAAiB,SAAS,CAAC,UAAU;AACvC,cAAM,eAAe;AACrB,cAAM,OAAO,IAAI,QAAQ;AACzB,cAAM,QAAQ,SAAS,SAAY,SAAY,kBAAkB,IAAI,IAAI;AACzE,YAAI,UAAU,UAAa,SAAS,OAAW;AAE/C,uBAAe;AACf,qBAAa,IAAI;AACjB,sBAAc;AACd,uBAAe,WAAW,KAAK,CAAC;AAAA,MAClC,CAAC;AAAA,IACH;AAKA,UAAM,SAAS,IAAI,IAAyB,OAAO,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC;AAE7E,UAAM,WAAW,IAAI;AAAA,MACnB,CAAC,YAAY;AACX,mBAAW,SAAS,SAAS;AAC3B,iBAAO,IAAI,MAAM,QAAuB,MAAM,iBAAiB,MAAM,oBAAoB,CAAC;AAAA,QAC5F;AAEA,YAAI,cAAkC;AACtC,YAAI,OAAO;AACX,mBAAW,SAAS,QAAQ;AAC1B,gBAAM,QAAQ,OAAO,IAAI,KAAK,KAAK;AACnC,cAAI,QAAQ,MAAM;AAChB,mBAAO;AACP,0BAAc;AAAA,UAChB;AAAA,QACF;AAIA,YAAI,CAAC,eAAe,gBAAgB,QAAS;AAE7C,kBAAU;AACV,cAAM,OAAO,YAAY,QAAQ;AACjC,YAAI,MAAM;AACR,uBAAa,IAAI;AACjB,gBAAM,YAAY,kBAAkB,IAAI,IAAI;AAC5C,cAAI,cAAc,OAAW,gBAAe;AAAA,QAC9C;AACA,mBAAW,SAAS,OAAQ,OAAM,UAAU,OAAO,cAAc,UAAU,WAAW;AAEtF,oBAAY,cAAc,IAAI,YAAY,WAAW,CAAC;AAAA,MACxD;AAAA,MACA,EAAE,MAAM,OAAO,WAAW,iBAAiB;AAAA,IAC7C;AAEA,eAAW,SAAS,OAAQ,UAAS,QAAQ,KAAK;AAElD,UAAM,eAAe,MAAmB;AACtC,YAAM,SAAS,MAAM;AACrB,UAAI,UAAU,OAAO,CAAC;AACtB,UAAI,kBAAkB;AACtB,iBAAW,CAAC,OAAO,KAAK,KAAK,OAAO,QAAQ,GAAG;AAC7C,cAAM,WAAW,KAAK,IAAI,UAAU,KAAK,IAAI,MAAM;AACnD,YAAI,WAAW,iBAAiB;AAC9B,4BAAkB;AAClB,oBAAU;AAAA,QACZ;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAEA,UAAM,eAAe,CAAC,WAA8B;AAMlD,UAAI,OAAO,QAAQ,YAAY;AAC7B,cAAM,gBAAgB,aAAa,IAAI,MAAM,KAAK,KAAK;AACvD,cAAM,aAAa,WAAW,WAAW,UAAU,cAAc,WAAW,MAAM,CAAC,CAAC;AAAA,MACtF;AAEA,uBAAiB;AACjB,uBAAiB;AAAA,IACnB;AAEA,aAAS,eAAe,OAA0B;AAChD,mBAAa,WAAW;AACxB,WAAK,aAAa,KAAK;AACvB,uBAAiB;AAEjB,UAAI,mBAAmB,GAAG;AACxB,cAAM,aAAa,WAAW,KAAK;AACnC,qBAAa,KAAK;AAClB;AAAA,MACF;AAEA,WAAK,GAAG,OAAO;AAAA,QACb,YAAY,WAAW,KAAK;AAAA,QAC5B,UAAU;AAAA,QACV,MAAM;AAAA,QACN,WAAW;AAAA,QACX,YAAY,MAAM,aAAa,KAAK;AAAA,QACpC,aAAa,MAAM;AACjB,2BAAiB;AAAA,QACnB;AAAA,MACF,CAAC;AAAA,IACH;AAEA,UAAM,kBAAkB,MAAY;AAClC,YAAM,SAAS,aAAa;AAI5B,UAAI,KAAK,IAAI,WAAW,MAAM,IAAI,MAAM,UAAU,IAAI,mBAAmB;AACvE,yBAAiB;AACjB;AAAA,MACF;AAEA,qBAAe,MAAM;AAAA,IACvB;AAIA,YAAQ;AACR,UAAM,MAAM,iBAAiB;AAC7B,UAAM,MAAM,cAAc;AAC1B,UAAM,aAAa,WAAW,WAAW,CAAC,CAAC;AAI3C,UAAM,iBAAiB,IAAI,eAAe,MAAM,QAAQ,CAAC;AACzD,mBAAe,QAAQ,KAAK;AAC5B,eAAW,SAAS,OAAQ,gBAAe,QAAQ,KAAK;AASxD,QAAI,iBAAiB,QAAQ;AAC3B,YAAM,iBAAiB,aAAa,MAAM;AACxC,YAAI,kBAAkB,SAAS,EAAG;AAClC,wBAAgB;AAAA,MAClB,CAAC;AAAA,IACH,OAAO;AACL,YAAM,iBAAiB,UAAU,MAAM;AACrC,YAAI,kBAAkB,SAAS,EAAG;AAClC,qBAAa,WAAW;AACxB,sBAAc,WAAW,iBAAiB,gBAAgB;AAAA,MAC5D,CAAC;AAAA,IACH;AAEA,QAAI,aAAa;AACjB,QAAI,kBAAkB;AACtB,QAAI,UAAU;AACd,QAAI,gBAAgB;AAKpB,UAAM,iBAAiB,eAAe,CAAC,UAAU;AAC/C,oBAAc;AACd,mBAAa,WAAW;AACxB,WAAK,aAAa,KAAK;AACvB,uBAAiB;AACjB,wBAAkB,MAAM;AACxB,gBAAU;AACV,sBAAgB;AAChB,mBAAa,MAAM;AACnB,wBAAkB,MAAM;AAAA,IAC1B,CAAC;AAED,UAAM,iBAAiB,eAAe,CAAC,UAAU;AAC/C,UAAI,MAAM,cAAc,gBAAiB;AAEzC,YAAM,YAAY,MAAM,UAAU;AAElC,UAAI,CAAC,SAAS;AACZ,YAAI,KAAK,IAAI,SAAS,IAAI,kBAAmB;AAC7C,kBAAU;AACV,cAAM,UAAU,IAAI,aAAa;AAGjC,cAAM,kBAAkB,MAAM,SAAS;AAAA,MACzC;AAEA,YAAM,aAAa,kBAAkB,YAAY;AACjD,UAAI,MAAM,gBAAgB,QAAS,OAAM,eAAe;AAAA,IAC1D,CAAC;AAED,UAAM,yBAAyB,CAAC,UAA8B;AAC5D,UAAI,MAAM,cAAc,gBAAiB;AAEzC,wBAAkB;AAClB,UAAI,MAAM,kBAAkB,MAAM,SAAS,EAAG,OAAM,sBAAsB,MAAM,SAAS;AAEzF,UAAI,SAAS;AACX,kBAAU;AACV,wBAAgB;AAChB,cAAM,UAAU,OAAO,aAAa;AAAA,MACtC;AAMA,sBAAgB;AAAA,IAClB;AAEA,UAAM,iBAAiB,aAAa,sBAAsB;AAC1D,UAAM,iBAAiB,iBAAiB,sBAAsB;AAK9D,UAAM;AAAA,MACJ;AAAA,MACA,CAAC,UAAU;AACT,YAAI,CAAC,cAAe;AACpB,wBAAgB;AAChB,cAAM,eAAe;AACrB,cAAM,gBAAgB;AAAA,MACxB;AAAA,MACA;AAAA,IACF;AAIA,UAAM,iBAAiB,gBAAgB,CAAC,UAAU;AAChD,UAAI,MAAM,gBAAgB,QAAS;AACnC,iBAAW;AACX,oBAAc;AAAA,IAChB,CAAC;AAED,UAAM,iBAAiB,gBAAgB,CAAC,UAAU;AAChD,UAAI,MAAM,gBAAgB,QAAS;AACnC,iBAAW;AACX,uBAAiB;AAAA,IACnB,CAAC;AAED,UAAM,iBAAiB,WAAW,aAAa;AAC/C,UAAM,iBAAiB,YAAY,MAAM;AAGvC,qBAAe,gBAAgB;AAAA,IACjC,CAAC;AAED,aAAS,iBAAiB,oBAAoB,MAAM;AAClD,UAAI,SAAS,OAAQ,eAAc;AAAA,UAC9B,kBAAiB;AAAA,IACxB,CAAC;AAED,UAAM,qBAAqB,IAAI;AAAA,MAC7B,CAAC,YAAY;AACX,4BAAoB,QAAQ,KAAK,CAAC,UAAU,MAAM,cAAc;AAChE,YAAI,kBAAmB,kBAAiB;AAAA,YACnC,eAAc;AAAA,MACrB;AAAA,MACA,EAAE,WAAW,IAAI;AAAA,IACnB;AACA,uBAAmB,QAAQ,KAAK;AAAA,EAClC,CAAC;;;ACtYD,MAAM,iBAAiB;AAOvB,MAAM,SAAS,oBAAI,QAA6B;AAazC,MAAM,iBAAiB,CAC5B,SACA,EAAE,WAAW,MAAM,UAAU,MAAM,MAAM,YAAY,gBAAgB,MAAM,IAAqB,CAAC,MACxF;AACT,QAAI,eAAe;AACjB,UAAI,eAAe,OAAW,SAAQ,cAAc;AACpD;AAAA,IACF;AAIA,QAAI,eAAe,QAAW;AAC5B,cAAQ,QAAQ,eAAe;AAAA,IACjC,WAAW,CAAC,QAAQ,QAAQ,cAAc;AACxC,YAAM,UAAU,QAAQ;AACxB,UAAI,CAAC,WAAW,CAAC,QAAQ,KAAK,EAAG;AACjC,cAAQ,QAAQ,eAAe;AAAA,IACjC;AAEA,UAAM,OAAO,QAAQ,QAAQ;AAC7B,QAAI,CAAC,QAAQ,CAAC,KAAK,KAAK,EAAG;AAE3B,UAAM,QAAQ,MAAM,KAAK,IAAI;AAC7B,UAAM,QAAQ,MAAM;AAEpB,YAAQ,cAAc;AACtB,UAAM,WAAW,SAAS,uBAAuB;AACjD,UAAM,QAAQ,MAAM,IAAI,CAAC,SAAS;AAChC,YAAM,OAAO,SAAS,cAAc,MAAM;AAC1C,WAAK,cAAc;AACnB,eAAS,OAAO,IAAI;AACpB,aAAO;AAAA,IACT,CAAC;AACD,YAAQ,OAAO,QAAQ;AAEvB,UAAM,QAAQ,OAAO,UAAU;AAC/B,WAAO,IAAI,SAAS,KAAK;AAEzB,UAAM,YAAY,MAAM,IAAI,CAAC,GAAG,MAAO,SAAS,IAAI,IAAK,KAAK,QAAQ,KAAM,WAAW,OAAQ;AAC/F,UAAM,QAAQ,YAAY,IAAI;AAE9B,UAAM,OAAO,CAAC,QAAsB;AAClC,UAAI,OAAO,IAAI,OAAO,MAAM,MAAO;AAEnC,YAAM,UAAU,MAAM;AACtB,UAAI,YAAY;AAEhB,YAAM,QAAQ,CAAC,MAAM,MAAM;AACzB,YAAI,SAAS,IAAK;AAClB,YAAI,WAAW,UAAU,CAAC,GAAG;AAC3B,gBAAM,CAAC,EAAE,cAAc;AAAA,QACzB,OAAO;AACL,sBAAY;AACZ,gBAAM,CAAC,EAAE,cAAc,eAAgB,KAAK,OAAO,IAAI,eAAe,SAAU,CAAC;AAAA,QACnF;AAAA,MACF,CAAC;AAED,UAAI,CAAC,UAAW,uBAAsB,IAAI;AAAA,IAC5C;AAEA,0BAAsB,IAAI;AAAA,EAC5B;AAMO,MAAM,uBAAuB,CAAC,SAAsB,UAA2B,CAAC,MAAY;AACjG,QAAI,QAAQ,QAAQ,mBAAoB;AACxC,YAAQ,QAAQ,qBAAqB;AACrC,YAAQ,iBAAiB,gBAAgB,MAAM,eAAe,SAAS,OAAO,CAAC;AAAA,EACjF;;;AC3EA,MAAM,gBAAgB;AACtB,MAAM,YAAY;AAClB,MAAM,oBAAoB;AAE1B,oBAAkB,YAAY,CAAC,SAAS,EAAE,MAAM,cAAc,MAAM;AAClE,UAAM,QAAQ,MAAM,KAAK,QAAQ,QAAQ;AACzC,QAAI,CAAC,MAAM,OAAQ;AAEnB,QAAI,eAAe;AACjB,iBAAW,QAAQ,OAAO;AACxB,aAAK,MAAM,SAAS;AACpB,aAAK,MAAM,UAAU;AAAA,MACvB;AACA;AAAA,IACF;AAEA,UAAM,OAAO,MAAY;AACvB,WAAK,aAAa,KAAK;AACvB,WAAK,IAAI,OAAO,EAAE,QAAQ,GAAG,SAAS,GAAG,UAAU,SAAS,CAAC;AAE7D,YAAM,WAAW,KAAK,SAAS;AAE/B,iBAAW,QAAQ,OAAO;AAGxB,iBAAS,KAAK,MAAM;AAClB,qBAAW,QAAQ,KAAK,iBAA8B,iBAAiB,GAAG;AACxE,2BAAe,MAAM,EAAE,UAAU,KAAK,SAAS,IAAI,CAAC;AAAA,UACtD;AAAA,QACF,CAAC;AAED,iBAAS,GAAG,MAAM;AAAA,UAChB,QAAQ;AAAA,UACR,SAAS;AAAA,UACT,UAAU;AAAA,UACV,MAAM;AAAA;AAAA;AAAA;AAAA,UAIN,YAAY,MAAM,KAAK,IAAI,MAAM,EAAE,QAAQ,OAAO,CAAC;AAAA,QACrD,CAAC;AAAA,MACH;AAAA,IACF;AAgBA,QAAI,aAAa;AACjB,QAAI,YAAY;AAEhB,UAAM,YAAY,MAAY;AAC5B,UAAI,CAAC,WAAY;AACjB,YAAM,QAAQ,QAAQ,QAAqB,cAAc;AACzD,UAAI,SAAS,CAAC,MAAM,UAAU,SAAS,YAAY,EAAG;AACtD,kBAAY;AACZ,WAAK;AAAA,IACP;AAEA,QAAI,0BAA0B,QAAQ;AACpC,YAAM,UAAU,IAAI;AAAA,QAClB,CAAC,YAAY;AACX,qBAAW,SAAS,SAAS;AAC3B,gBAAI,CAAC,MAAM,eAAgB;AAC3B,yBAAa;AACb,sBAAU;AACV,gBAAI,UAAW,SAAQ,WAAW;AAAA,UACpC;AAAA,QACF;AAAA,QACA,EAAE,WAAW,IAAI;AAAA,MACnB;AACA,cAAQ,QAAQ,OAAO;AAAA,IACzB,OAAO;AACL,mBAAa;AACb,gBAAU;AAAA,IACZ;AAKA,YAAQ,QAAqB,cAAc,GAAG,iBAAiB,aAAa,SAAS;AAAA,EACvF,CAAC;;;ACnHD,MAAM,OAAO;AACb,MAAM,OAAO;AACb,MAAM,QAAQ;AAQd,MAAM,gBAAgB,CAAC,YAAwC;AAC7D,UAAM,WAAW,SAAS,uBAAuB;AACjD,UAAM,QAAuB,CAAC;AAC9B,QAAI,OAAO,SAAS,cAAc,MAAM;AACxC,SAAK,YAAY;AACjB,aAAS,YAAY,IAAI;AAEzB,UAAM,UAAU,CAAC,MAAc,cAA6B;AAC1D,UAAI,CAAC,KAAM;AAEX,YAAM,QAAQ,SAAS,cAAc,MAAM;AAC3C,YAAM,YAAY;AAElB,YAAM,QAAQ,SAAS,cAAc,MAAM;AAC3C,YAAM,YAAY,YAAY,GAAG,KAAK,IAAI,SAAS,KAAK;AACxD,YAAM,cAAc;AAEpB,YAAM,YAAY,KAAK;AACvB,WAAK,YAAY,KAAK;AACtB,YAAM,KAAK,KAAK;AAAA,IAClB;AAEA,UAAM,YAAY,CAAC,MAAc,cAA6B;AAC5D,iBAAW,SAAS,KAAK,MAAM,OAAO,GAAG;AACvC,YAAI,CAAC,MAAO;AACZ,YAAI,QAAQ,KAAK,KAAK,EAAG,MAAK,YAAY,SAAS,eAAe,GAAG,CAAC;AAAA,YACjE,SAAQ,OAAO,SAAS;AAAA,MAC/B;AAAA,IACF;AAEA,eAAW,QAAQ,MAAM,KAAK,QAAQ,UAAU,GAAG;AACjD,UAAI,KAAK,aAAa,KAAK,WAAW;AACpC,kBAAU,KAAK,eAAe,EAAE;AAAA,MAClC,WAAW,gBAAgB,eAAe;AACxC,eAAO,SAAS,cAAc,MAAM;AACpC,aAAK,YAAY;AACjB,iBAAS,YAAY,IAAI;AAAA,MAC3B,WAAW,gBAAgB,aAAa;AACtC,kBAAU,KAAK,eAAe,IAAI,KAAK,aAAa,MAAS;AAAA,MAC/D;AAAA,IACF;AAEA,YAAQ,cAAc;AACtB,YAAQ,YAAY,QAAQ;AAE5B,WAAO;AAAA,EACT;AAEA,MAAM,QAAQ,EAAE,UAAU,GAAG,UAAU,KAAK,MAAM,cAAc,SAAS,MAAM;AAE/E,oBAAkB,YAAY,CAAC,SAAS,EAAE,MAAM,eAAe,cAAc,MAAM;AACjF,QAAI,eAAe;AACjB,cAAQ,MAAM,aAAa;AAC3B;AAAA,IACF;AAEA,UAAM,QAAQ,cAAc,OAAO;AACnC,SAAK,IAAI,OAAO,EAAE,UAAU,IAAI,CAAC;AACjC,YAAQ,MAAM,aAAa;AAI3B,QAAI,QAAQ,QAAQ,gBAAgB,UAAU,CAAC,eAAe;AAC5D,WAAK,GAAG,OAAO,EAAE,GAAG,OAAO,OAAO,IAAI,CAAC;AACvC;AAAA,IACF;AAEA,SAAK,GAAG,OAAO,EAAE,GAAG,OAAO,eAAe,EAAE,SAAS,SAAS,OAAO,UAAU,EAAE,CAAC;AAAA,EACpF,CAAC;;;AC9ED,MAAM,mBAAmB;AAQzB,oBAAkB,WAAW,CAAC,SAAS,EAAE,MAAM,cAAc,MAAM;AACjE,QAAI,cAAe;AAEnB,UAAM,OAAO,OAAO,QAAQ,QAAQ,YAAY,CAAC;AACjD,UAAM,WAAW,OAAO,QAAQ,QAAQ,gBAAgB,gBAAgB;AACxE,QAAI,CAAC,OAAO,SAAS,IAAI,KAAK,OAAO,EAAG;AAExC,UAAM,WAAW,MAAM,QAAQ,cAAc;AAE7C,UAAM,QAAQ,KAAK,GAAG,SAAS;AAAA,MAC7B,GAAG,MAAM,CAAC,SAAS;AAAA,MACnB;AAAA,MACA,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,WAAW;AAAA;AAAA,QAET,GAAG,CAAC,UAAkB,GAAG,OAAO,WAAW,KAAK,IAAI,SAAS,CAAC;AAAA,MAChE;AAAA,IACF,CAAC;AAED,UAAM,SAAS,QAAQ;AACvB,QAAI,CAAC,OAAQ;AAEb,WAAO,iBAAiB,gBAAgB,MAAM,MAAM,MAAM,CAAC;AAC3D,WAAO,iBAAiB,gBAAgB,MAAM,MAAM,OAAO,CAAC;AAAA,EAC9D,CAAC;;;ACjCD,MAAM,UAAU;AAOhB,oBAAkB,UAAU,CAAC,SAAS,EAAE,cAAc,MAAM;AAC1D,UAAM,QAAQ,OAAO,QAAQ,QAAQ,aAAa,CAAC;AACnD,QAAI,MAAO,SAAQ,MAAM,kBAAkB,GAAG,KAAK;AAEnD,QAAI,iBAAiB,EAAE,0BAA0B,SAAS;AACxD,cAAQ,UAAU,IAAI,OAAO;AAC7B;AAAA,IACF;AAEA,UAAM,WAAW,IAAI;AAAA,MACnB,CAAC,YAAY;AACX,mBAAW,SAAS,SAAS;AAC3B,cAAI,CAAC,MAAM,eAAgB;AAC3B,gBAAM,OAAO,UAAU,IAAI,OAAO;AAClC,mBAAS,UAAU,MAAM,MAAM;AAAA,QACjC;AAAA,MACF;AAAA,MACA,EAAE,WAAW,KAAK;AAAA,IACpB;AAEA,aAAS,QAAQ,OAAO;AAAA,EAC1B,CAAC;;;ACfD,oBAAkB,YAAY,CAAC,SAAS,EAAE,cAAc,MAAM;AAC5D,UAAM,UAAU;AAAA,MACd,UAAU,OAAO,QAAQ,QAAQ,gBAAgB,IAAI;AAAA,MACrD,SAAS,OAAO,QAAQ,QAAQ,eAAe,IAAI;AAAA,MACnD;AAAA,IACF;AAEA,QAAI,iBAAiB,EAAE,0BAA0B,SAAS;AACxD,qBAAe,SAAS,OAAO;AAC/B;AAAA,IACF;AAEA,UAAM,WAAW,IAAI;AAAA,MACnB,CAAC,YAAY;AACX,mBAAW,SAAS,SAAS;AAC3B,cAAI,CAAC,MAAM,eAAgB;AAC3B,yBAAe,SAAS,OAAO;AAC/B,+BAAqB,SAAS,OAAO;AACrC,mBAAS,UAAU,MAAM,MAAM;AAAA,QACjC;AAAA,MACF;AAAA,MACA,EAAE,WAAW,IAAI;AAAA,IACnB;AAEA,aAAS,QAAQ,OAAO;AAAA,EAC1B,CAAC;;;ACtBM,MAAM,cAAc,MAAyD;AAClF,UAAM,EAAE,MAAM,cAAc,IAAI;AAEhC,QAAI,CAAC,MAAM;AACT,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAEA,QAAI,cAAe,MAAK,eAAe,aAAa;AAEpD,WAAO,EAAE,MAAM,cAAc;AAAA,EAC/B;AAKO,MAAM,uBAAuB,MAClC,OAAO,WAAW,kCAAkC,EAAE;;;ACjCxD,SAAO,YAAY,CAAC;AACpB,SAAO,QAAQ,KAAK,MAAM;AACxB,UAAM,EAAE,MAAM,cAAc,IAAI,YAAY;AAE5C,kBAAc,EAAE,MAAM,eAAe,eAAe,qBAAqB,EAAE,CAAC;AAAA,EAC9E,CAAC;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shopic",
3
- "version": "0.0.2",
3
+ "version": "0.1.1",
4
4
  "description": "Custom code bundle for the Shopic Webflow site.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -13,11 +13,12 @@
13
13
  "dev": "cross-env NODE_ENV=development node ./bin/build.js",
14
14
  "build": "cross-env NODE_ENV=production node ./bin/build.js",
15
15
  "prepublishOnly": "npm run check && npm run lint && npm run build",
16
- "lint": "eslint ./src && prettier --check ./src",
17
- "lint:fix": "eslint ./src --fix",
16
+ "lint": "eslint ./src ./scripts && prettier --check ./src ./scripts",
17
+ "lint:fix": "eslint ./src ./scripts --fix",
18
18
  "check": "tsc --noEmit",
19
- "format": "prettier --write ./src",
19
+ "format": "prettier --write ./src ./scripts",
20
20
  "test": "playwright test",
21
+ "test:unit": "node --test \"tests/unit/**/*.test.js\"",
21
22
  "test:ui": "playwright test --ui",
22
23
  "update": "pnpm update -i -L -r"
23
24
  },