>4}:${n>>4}:${a>>4}`,s=i.get(o)||[0,0,0,0];s[0]+=t,s[1]+=n,s[2]+=a,s[3]+=1,i.set(o,s)}let a=[...i.values()].map(([e,t,n,r])=>{let i=[e/r,t/r,n/r],a=Math.max(...i)-Math.min(...i),o=(Math.max(...i)+Math.min(...i))/2;return{rgb:i,count:r,accent:a*2+Math.abs(o-200)*.4}}).sort((e,t)=>t.count-e.count).slice(0,32);if(!a.length)return null;let o=(e,t)=>(e[0]-t[0])**2+(e[1]-t[1])**2+(e[2]-t[2])**2,s=[a[0],a[0]],c=-1;return a.forEach((e,t)=>a.slice(t+1).forEach(t=>{let n=Math.sqrt(e.count*t.count)*o(e.rgb,t.rgb);n>c&&(s=[e,t],c=n)})),s.sort((e,t)=>t.accent-e.accent),s.map(({rgb:e})=>`rgb(${e.map(e=>Math.round(e)).join(` `)})`)}catch{return null}},el=e=>{let t=e?.parentElement;for(;t;){let e=getComputedStyle(t).backgroundColor,n=Number(e.match(/rgba?\([^/]*[,/]\s*([\d.]+)\s*\)$/)?.[1]??1),r=Xc(e);if(r&&n>.05)return r;t=t.parentElement}return null},tl=e=>{let[t,n,r]=Zc(Qc(e)||el(e)||[255,91,28]),i=Y(n<18?64:n,48,82),a=Y(r<18?45:r>82?56:r,36,66),o=Math.round(Math.random()*16-8);return[`hsl(${Math.round(t)} ${Math.round(i)}% ${Math.round(a)}%)`,`hsl(${Math.round((t+28+o+360)%360)} ${Math.round(Y(i+7,48,88))}% ${Math.round(Y(a+7,38,72))}%)`,`hsl(${Math.round((t-34+o+360)%360)} ${Math.round(Y(i-4,44,80))}% ${Math.round(Y(a-6,32,64))}%)`]},nl={defer:!0,create(e,t={}){let n=W().reducedMotion,r=t.color||`#ff5b1c`,i=t.color2||`#12141a`,a=Jc.has(t.colorMode)?t.colorMode:Yc(t.colors).length?`palette`:`pair`,o=Yc(t.colors),s=[`left`,`right`,`up`,`down`,`random`].includes(t.direction)?t.direction:`right`,c=Math.max(.05,Number(t.duration??.7)),l=Math.max(0,Number(t.delay??0)),u=t.ease?V(t.ease):`cubic-bezier(.77,0,.18,1)`,d=Y(Math.round(Number(t.layers??2)),1,3),f=t.mask===!0,p=Math.max(0,d-+!!f),m=Math.max(0,Number(t.stagger??120)),h=t.lines===!0,g=()=>s===`random`?[`left`,`right`,`up`,`down`][Math.floor(Math.random()*4)]:s,_=e=>({right:`translateX(101%)`,left:`translateX(-101%)`,down:`translateY(101%)`,up:`translateY(-101%)`})[e],v=[],y=[],b=e,x=null,S=!0,C=t=>a===`single`?[r]:a===`pair`?[r,i]:a===`palette`?o.length?o:[r,i]:$c(t)||$c(e)||tl(t||e),w=e=>{let t=C(e.container),n=a===`palette`?Math.floor(Math.random()*t.length):0;e.panels.forEach((e,r)=>{let i=a===`pair`?d>1&&r===d-1?t[1]:t[0]:t[(n+r)%t.length];e.style.background=i})},T=e=>{e.panels=[];for(let t=0;t{let n=e.style.position,r=e.style.overflow;getComputedStyle(e).position===`static`&&(e.style.position=`relative`),e.style.overflow=`hidden`;let i={container:e,content:t,panels:[],restorePosition:n,restoreOverflow:r,restoreClipPath:e.style.clipPath,restoreWebkitClipPath:e.style.webkitClipPath,restoreTransition:e.style.transition};return T(i),y.push(i),i.panels},D=null,O=()=>{let t=getComputedStyle(e),n=e.tagName===`IMG`||t.display.startsWith(`inline`),r=document.createElement(`div`);r.className=`kt-cover-wrap`,r.style.cssText=`position:relative;overflow:hidden;display:${n?`inline-block`:`block`};width:${n?`auto`:`100%`};height:${n?`auto`:`100%`};min-width:0;min-height:0;border-radius:${t.borderRadius};`,e.parentNode.insertBefore(r,e),r.appendChild(e),b=r,x=()=>{r.parentNode&&(r.parentNode.insertBefore(e,r),r.remove())},E(r,e)};function k(){let t=e.textContent,n=t.split(/\s+/).filter(e=>e.length);if(n.length<1)return!1;D=t,e.textContent=``;let r=n.map((t,r)=>{let i=document.createElement(`span`);return i.textContent=t,e.appendChild(i),r{let t=Math.round(e.getBoundingClientRect().top);(o===null||Math.abs(t-o)>3)&&(a=[],i.push(a),o=t),a.push(e)}),e.textContent=``,i.forEach(t=>{let n=document.createElement(`span`);n.className=`kt-cover-line`,n.style.cssText=`position:relative;display:block;overflow:hidden;width:max-content;max-width:100%;`;let r=document.createElement(`span`);r.style.display=`block`,r.textContent=t.map(e=>e.textContent).join(` `),n.appendChild(r),e.appendChild(n),E(n,r)}),!0}h&&k()||O();let A=!1,j=null,M=null,N=e=>({right:`inset(0 0 0 100%)`,left:`inset(0 100% 0 0)`,down:`inset(100% 0 0 0)`,up:`inset(0 0 100% 0)`})[e]||`inset(0 0 0 100%)`,P=[`left`,`right`,`up`,`down`].includes(t.maskDirection)?t.maskDirection:null,F=e=>{e.container.style.clipPath=e.restoreClipPath,e.container.style.webkitClipPath=e.restoreWebkitClipPath,e.container.style.transition=e.restoreTransition},I=()=>{if(!S||A)return;A=!0;let n=g(),r=_(n);f&&y.forEach(e=>{let t=e.container;t.style.clipPath=N(P||n),t.style.webkitClipPath=t.style.clipPath,t.style.transition=`clip-path ${c}s ${u},-webkit-clip-path ${c}s ${u}`}),e.offsetWidth,requestAnimationFrame(()=>{y.forEach((e,t)=>{let n=l+(h?t*m:0);f&&v.push(setTimeout(()=>{S&&(e.container.style.clipPath=`inset(0 0 0 0)`,e.container.style.webkitClipPath=`inset(0 0 0 0)`)},n)),e.panels.forEach((e,t)=>{let i=Math.max(0,d-1-t);v.push(setTimeout(()=>{S&&(e.style.transform=r)},n+i*m))})})});let i=h?Math.max(0,y.length-1):0,a=l+i*m+Math.max(0,d-1)*m+c*1e3+80;v.push(setTimeout(()=>{S&&(y.forEach(e=>{e.panels.forEach(e=>e.remove()),f&&F(e)}),t.onComplete?.(e))},a))},L=t.waitForImage!==!1,R=h?null:e.tagName===`IMG`?e:e.querySelector&&e.querySelector(`img`),z=()=>{if(L&&R&&!(R.complete&&R.naturalWidth)){let e=!1,t=()=>{!e&&S&&(e=!0,R.removeEventListener(`load`,t),R.removeEventListener(`error`,t),a===`auto`&&y.forEach(w),I())};try{R.decode&&R.decode().then(t,t)}catch{}R.addEventListener(`load`,t,{once:!0}),R.addEventListener(`error`,t,{once:!0}),v.push(setTimeout(t,4e3))}else a===`auto`&&y.forEach(w),I()};n?y.forEach(e=>e.panels.forEach(e=>e.remove())):typeof IntersectionObserver<`u`?M=requestAnimationFrame(()=>{if(!S)return;let e=b.getBoundingClientRect();if(e.bottom>0&&e.right>0&&e.top{for(let t of e)if(t.isIntersecting){j.disconnect(),j=null,z();break}},{threshold:Y(Number(t.threshold??.2),0,1)}),j.observe(b)}):z();let B=null,H=0;t.watch===!0&&typeof MutationObserver<`u`&&(B=new MutationObserver(()=>{S&&!n&&(H&&cancelAnimationFrame(H),H=requestAnimationFrame(()=>{H=0,S&&U.replay()}))}),B.observe(e,{childList:!0,subtree:!1,characterData:!0}));let U={el:e,type:`coverReveal`,replay(){S&&(A=!1,v.forEach(clearTimeout),v=[],!n&&(y.forEach(e=>{e.panels.forEach(e=>e.remove()),T(e)}),requestAnimationFrame(I)))},exit(){return!S||n?Promise.resolve():(A=!1,v.forEach(clearTimeout),v=[],y.forEach(e=>{e.panels.forEach(e=>e.remove()),T(e),f&&F(e)}),new Promise(e=>requestAnimationFrame(()=>e())))},async refresh(t){await this.exit(),typeof t==`function`&&await t(e),this.replay()},pause(){},resume(){},destroy(){S=!1,j?.disconnect(),B?.disconnect(),H&&cancelAnimationFrame(H),M!=null&&cancelAnimationFrame(M),v.forEach(clearTimeout),y.forEach(e=>{e.panels.forEach(e=>e.remove()),F(e),e.container.style.overflow=e.restoreOverflow,e.container.style.position=e.restorePosition}),x?.(),D!=null&&(e.textContent=D)}};return U},reduced(e,t){return this.create(e,t)}};function rl(e,{threshold:t,max:n,resistance:r,duration:i,labels:a,onRefresh:o}){let s=Math.max(20,Number(t??64)),c=Math.max(s,Number(n??s*1.8)),l=Y(Number(r??.7),.05,1),u=Math.max(0,Number(i??.32)),d=se({pull:`Pull to refresh`,release:`Release to refresh`,busy:`Refreshing`},a),f=$(e,[`transform`,`transition`,`overscroll-behavior`,`touch-action`]);e.style.overscrollBehavior=`contain`;let p=document.createElement(`span`);p.className=`kt-pull-indicator`,p.setAttribute(`role`,`status`),p.setAttribute(`aria-live`,`polite`),p.style.cssText=`position:absolute;left:50%;top:0;translate:-50% 0;display:grid;place-items:center;width:34px;height:34px;pointer-events:none;opacity:0;color:currentColor;`,p.innerHTML=` `;let m=p.querySelector(`.kt-pull-arc`),h=p.querySelector(`.kt-pull-label`),g=e.parentElement||e,_=$(g,[`position`]);getComputedStyle(g).position===`static`&&(g.style.position=`relative`),g.appendChild(p);let v=e=>{let t=e*l;if(t<=s)return t;let n=Math.max(1,c-s);return s+n*(1-Math.exp(-(t-s)/n))},y=!1,b=!1,x=0,S=0,C=null,w=(t,n)=>{S=t;let r=Y(S/s,0,1);e.style.transition=n?`transform ${u}s cubic-bezier(.22,.8,.3,1)`:`none`,e.style.transform=S?`translateY(${S.toFixed(1)}px)`:``,p.style.opacity=String(Y(S/(s*.6),0,1)),p.style.translate=`-50% ${Math.max(0,S-34).toFixed(1)}px`,m.setAttribute(`stroke-dashoffset`,String(94.2*(1-r))),p.style.rotate=b?``:`${r*180}deg`;let i=d(b?`busy`:r>=1?`release`:`pull`);h.textContent!==i&&(h.textContent=i)},T=()=>{if(!y)return;if(y=!1,C=null,SA.done())},E=t=>{b||e.scrollTop>0||(C=t.pointerId,x=t.clientY,y=!0)},D=e=>{if(!y||e.pointerId!==C)return;let t=e.clientY-x;if(t<=0){S&&w(0,!1);return}w(v(t),!1)},O=()=>T(),k=e=>{y&&S>0&&e.preventDefault()};e.addEventListener(`pointerdown`,E,{passive:!0}),e.addEventListener(`pointermove`,D,{passive:!0}),e.addEventListener(`pointerup`,O),e.addEventListener(`pointercancel`,O),e.addEventListener(`touchmove`,k,{passive:!1});let A={el:e,type:`gesture`,get refreshing(){return b},refresh(){b||(y=!0,S=s,T())},done(){b&&(b=!1,e.classList.remove(`kt-pull-loading`),p.classList.remove(`kt-pull-spinning`),w(0,!0))},pause(){},resume(){},destroy(){e.removeEventListener(`pointerdown`,E),e.removeEventListener(`pointermove`,D),e.removeEventListener(`pointerup`,O),e.removeEventListener(`pointercancel`,O),e.removeEventListener(`touchmove`,k),e.classList.remove(`kt-pull-loading`),p.remove(),f(),_()}};return A}function il(e,{hoverScale:t,tapScale:n,lift:r,duration:i,ease:a,hoverEase:o,pressEase:s,origin:c}){if(W().reducedMotion)return{el:e,type:`gesture`,pause(){},resume(){},destroy(){}};let l=Number(t??1.04),u=Number(n??.96),d=Number(r??0),f=Math.max(0,Number(i??.22)),p=a?V(a):U.spring?`cubic-bezier(.34,1.8,.5,1)`:`cubic-bezier(.34,1.56,.64,1)`,m=o?V(o):p,h=s?V(s):p,g=c||`center`,_=e.style.transition,v=e.style.transform,y=e.style.transformOrigin,b=e.style.willChange;e.style.transition=`transform ${f}s ${p}`,e.style.transformOrigin=g,e.style.willChange=`transform`;let x=!1,S=!1,C=()=>{let t=S?u:x?l:1,n=x&&!S?-d:0;e.style.transition=`transform ${f}s ${S?h:m}`,e.style.transform=`translateY(${n}px) scale(${t})`},w=()=>{x=!0,C()},T=()=>{x=!1,S=!1,C()},E=()=>{S=!0,C()},D=()=>{S=!1,C()},O=()=>{x=!0,C()},k=()=>{x=!1,S=!1,C()},A=e=>{(e.key===` `||e.key===`Enter`)&&(S=!0,C())},j=e=>{(e.key===` `||e.key===`Enter`)&&(S=!1,C())};return e.addEventListener(`pointerenter`,w),e.addEventListener(`pointerleave`,T),e.addEventListener(`pointerdown`,E),e.addEventListener(`pointerup`,D),e.addEventListener(`pointercancel`,D),e.addEventListener(`focus`,O),e.addEventListener(`blur`,k),e.addEventListener(`keydown`,A),e.addEventListener(`keyup`,j),{el:e,type:`gesture`,pause(){},resume(){},destroy(){e.removeEventListener(`pointerenter`,w),e.removeEventListener(`pointerleave`,T),e.removeEventListener(`pointerdown`,E),e.removeEventListener(`pointerup`,D),e.removeEventListener(`pointercancel`,D),e.removeEventListener(`focus`,O),e.removeEventListener(`blur`,k),e.removeEventListener(`keydown`,A),e.removeEventListener(`keyup`,j),e.style.transition=_,e.style.transform=v,e.style.transformOrigin=y,e.style.willChange=b}}}var al={create(e,t={}){return(t.preset===`pull`||t.effect===`pull`?`pull`:`spring`)==`pull`?rl(e,{threshold:t.threshold,max:t.max,resistance:t.resistance,duration:t.duration,labels:t.labels,onRefresh:t.onRefresh}):il(e,{hoverScale:t.hoverScale,tapScale:t.tapScale,lift:t.lift,duration:t.duration,ease:t.ease,hoverEase:t.hoverEase,pressEase:t.pressEase,origin:t.origin})},reduced(e,t={}){return t.preset!==`pull`&&t.effect!==`pull`?{el:e,type:`gesture`,pause(){},resume(){},destroy(){}}:this.create(e,{...t,duration:0})}},ol={create(e,t={}){let n=[`x`,`y`,`both`].includes(t.axis)?t.axis:`both`,r=t.bounds,i=t.snapBack===!0,a=t.inertia!==!1&&!i,o=t.handle&&e.querySelector(t.handle)||e,s=e.style.transform,c=e.style.transition,l=e.style.touchAction,u=o.style.cursor;e.style.touchAction=n===`x`?`pan-y`:n===`y`?`pan-x`:`none`,o.style.cursor=`grab`;let d=0,f=0,p=!1,m=0,h=0,g=0,_=0,v=0,y=0,b=0,x=0,S=0,C=null,w=()=>{if(r!==`parent`)return null;let t=e.offsetParent||e.parentElement;if(!t)return null;let n=t.getBoundingClientRect(),i=e.getBoundingClientRect(),a=i.left-d,o=i.top-f;return{minX:n.left-a,maxX:n.right-(a+i.width),minY:n.top-o,maxY:n.bottom-(o+i.height)}},T=(t,r)=>{n===`y`&&(t=0),n===`x`&&(r=0);let i=w();i&&(t=Y(t,i.minX,i.maxX),r=Y(r,i.minY,i.maxY)),d=t,f=r,e.style.transform=`translate(${d}px, ${f}px)`},E=t=>{(t.button==null||t.button===0)&&(p=!0,e.style.transition=`none`,o.style.cursor=`grabbing`,m=t.clientX,h=t.clientY,g=d,_=f,v=t.clientX,y=t.clientY,b=performance.now(),C&&=(cancelAnimationFrame(C),null))},D=e=>{if(!p)return;T(g+(e.clientX-m),_+(e.clientY-h));let t=performance.now(),n=t-b||16;x=(e.clientX-v)/n,S=(e.clientY-y)/n,v=e.clientX,y=e.clientY,b=t},O=()=>{if(p){if(p=!1,o.style.cursor=`grab`,i)e.style.transition=`transform .42s cubic-bezier(.22,.8,.3,1)`,T(0,0);else if(a&&(Math.abs(x)>.02||Math.abs(S)>.02)){let e=()=>{x*=.92,S*=.92,T(d+x*16,f+S*16),C=Math.abs(x)>.02||Math.abs(S)>.02?requestAnimationFrame(e):null};C=requestAnimationFrame(e)}}},k=t=>{if(t.target!==e&&t.target!==o)return;let n=t.shiftKey?20:6,r=!0;e.style.transition=`transform .12s var(--kt-ease-ui, ease)`,t.key===`ArrowLeft`?T(d-n,f):t.key===`ArrowRight`?T(d+n,f):t.key===`ArrowUp`?T(d,f-n):t.key===`ArrowDown`?T(d,f+n):r=!1,r&&t.preventDefault()};o.addEventListener(`pointerdown`,E),window.addEventListener(`pointermove`,D),window.addEventListener(`pointerup`,O),window.addEventListener(`pointercancel`,O);let A=Se(e,[`tabindex`]);return e.hasAttribute(`tabindex`)||(e.tabIndex=0),e.addEventListener(`keydown`,k),{el:e,type:`drag`,reset(){e.style.transition=`transform .42s cubic-bezier(.22,.8,.3,1)`,T(0,0)},pause(){},resume(){},destroy(){C&&cancelAnimationFrame(C),o.removeEventListener(`pointerdown`,E),window.removeEventListener(`pointermove`,D),window.removeEventListener(`pointerup`,O),window.removeEventListener(`pointercancel`,O),e.removeEventListener(`keydown`,k),A(),e.style.transform=s,e.style.transition=c,e.style.touchAction=l,o.style.cursor=u}}},reduced(e,t){return this.create(e,t)}},sl=new Set([`content`,`html`]);function cl(e){return e.getAttribute(`aria-label`)?.trim()||e.getAttribute(`aria-labelledby`)?.trim()||e.textContent.trim()?!0:!!e.querySelector(`img[alt]:not([alt=""]), [aria-label]:not([aria-label=""])`)}var ll={create(e,t={}){let n=W().reducedMotion,r=e.getAttribute(`title`),i=t.content||e.getAttribute(`data-kt-title`)||r||e.getAttribute(`aria-label`)||``;if(!i)return null;let a=r!=null&&r.trim()!==``&&!cl(e);a&&e.setAttribute(`aria-label`,r),r!=null&&e.removeAttribute(`title`);let o=[`top`,`bottom`,`left`,`right`].includes(t.placement)?t.placement:`top`,s=[`hover`,`focus`,`click`,`manual`].includes(t.trigger)?t.trigger:`hover`,c=Math.max(0,Number(t.delay??120)),l=Math.max(0,Number(t.hideDelay??80)),u=Number(t.offset??8),d=Math.max(0,Number(t.duration??.16)),f=t.interactive===!0,p=t.html===!0,m=[`fade`,`scale`,`shift`,`none`].includes(t.effect)?t.effect:`fade`,h=m===`scale`?{opacity:0,transform:`scale(0.9)`}:m===`shift`?{opacity:0,transform:`translateY(5px)`}:{opacity:0,transform:`none`},g={opacity:1,transform:`none`},_=document.createElement(`div`);_.className=`kt-tooltip`,_.setAttribute(`role`,`tooltip`),_.id=`kt-tooltip-${Math.random().toString(36).slice(2,8)}`,_.hidden=!0,_.style.position=`fixed`,_.style.opacity=`0`,p?_.innerHTML=i:_.textContent=i;let v=document.createElement(`span`);v.className=`kt-tooltip__arrow`,v.setAttribute(`aria-hidden`,`true`),_.appendChild(v),document.body.appendChild(_);let y=e.getAttribute(`aria-describedby`);e.setAttribute(`aria-describedby`,y?`${y} ${_.id}`:_.id);let b=!1,x=null,S=null,C=null,w=()=>{let t=e.getBoundingClientRect(),n=_.offsetWidth,r=_.offsetHeight,i=window.innerWidth,a=window.innerHeight,s=o;s===`top`&&t.top-r-u<0?s=`bottom`:s===`bottom`&&t.bottom+r+u>a?s=`top`:s===`left`&&t.left-n-u<0?s=`right`:s===`right`&&t.right+n+u>i&&(s=`left`);let c,l;s===`top`?(c=t.left+t.width/2-n/2,l=t.top-r-u):s===`bottom`?(c=t.left+t.width/2-n/2,l=t.bottom+u):s===`left`?(c=t.left-n-u,l=t.top+t.height/2-r/2):(c=t.right+u,l=t.top+t.height/2-r/2),c=Y(c,4,i-n-4),l=Y(l,4,a-r-4),_.dataset.placement=s,_.style.left=`${Math.round(c)}px`,_.style.top=`${Math.round(l)}px`},T=()=>{clearTimeout(S),!b&&(b=!0,_.hidden=!1,w(),C&&C.cancel(),_.style.opacity=`1`,!n&&m!==`none`&&(C=_.animate([h,g],{duration:d*1e3,easing:`ease`})),window.addEventListener(`scroll`,w,{capture:!0,passive:!0}),window.addEventListener(`resize`,w),document.addEventListener(`keydown`,P))},E=()=>{if(clearTimeout(x),!b)return;b=!1;let e=()=>{b||(_.hidden=!0,_.style.opacity=`0`)};C&&C.cancel(),_.style.opacity=`0`,!n&&m!==`none`?(C=_.animate([g,h],{duration:d*700,easing:`ease`}),C.onfinish=e,C.oncancel=e):e(),window.removeEventListener(`scroll`,w,!0),window.removeEventListener(`resize`,w),document.removeEventListener(`keydown`,P)},D=()=>{clearTimeout(S),x=setTimeout(T,c)},O=()=>{clearTimeout(x),S=setTimeout(E,l)},k=()=>D(),A=()=>O(),j=()=>T(),M=()=>E(),N=()=>{b?E():T()},P=e=>{e.key===`Escape`&&b&&E()},F=t=>{b&&!e.contains(t.target)&&!_.contains(t.target)&&E()},I=()=>clearTimeout(S);return s===`hover`?(e.addEventListener(`pointerenter`,k),e.addEventListener(`pointerleave`,A),e.addEventListener(`focus`,j),e.addEventListener(`blur`,M),_.style.pointerEvents=`auto`,_.addEventListener(`pointerenter`,I),_.addEventListener(`pointerleave`,O)):s===`focus`?(e.addEventListener(`focus`,j),e.addEventListener(`blur`,M)):s===`click`&&(e.addEventListener(`click`,N),document.addEventListener(`pointerdown`,F,!0)),f&&s!==`hover`&&(_.style.pointerEvents=`auto`),{el:e,type:`tooltip`,show:T,hide:E,update(e={}){return!Object.keys(e).some(e=>!sl.has(e))&&(e.content!=null&&((e.html==null?p:e.html===!0)?_.innerHTML=String(e.content):_.textContent=String(e.content),_.appendChild(v)),b&&w(),!0)},pause(){},resume(){},destroy(){clearTimeout(x),clearTimeout(S),window.removeEventListener(`scroll`,w,!0),window.removeEventListener(`resize`,w),e.removeEventListener(`pointerenter`,k),e.removeEventListener(`pointerleave`,A),e.removeEventListener(`focus`,j),e.removeEventListener(`blur`,M),e.removeEventListener(`click`,N),document.removeEventListener(`pointerdown`,F,!0),document.removeEventListener(`keydown`,P),_.removeEventListener(`pointerenter`,I),_.removeEventListener(`pointerleave`,O),_.remove();let t=(e.getAttribute(`aria-describedby`)||``).split(/\s+/).filter(e=>e&&e!==_.id).join(` `);t?e.setAttribute(`aria-describedby`,t):e.removeAttribute(`aria-describedby`),a&&e.getAttribute(`aria-label`)===r&&e.removeAttribute(`aria-label`),r!=null&&e.setAttribute(`title`,r)}}},reduced(e,t){return this.create(e,t)}},ul={create(e,t={}){let n=W().reducedMotion,r=Math.max(14,Number(t.size??24)),i=t.onColor||`var(--kt-switch-on, #ff5b1c)`,a=t.offColor||`var(--kt-switch-off, color-mix(in srgb, currentColor 55%, transparent))`,o=t.thumbColor||`var(--kt-switch-thumb, #fff)`,s=Math.max(0,Number(t.duration??.22)),c=e.tagName===`INPUT`?null:e.querySelector(`input[type="checkbox"], input[type="radio"]`);c&&(c.style.position=`absolute`,c.style.opacity=`0`,c.style.pointerEvents=`none`,c.style.width=`0`,c.style.height=`0`,c.tabIndex=-1);let l=t.checked===!0||(c?c.checked:e.getAttribute(`aria-checked`)===`true`||e.hasAttribute(`checked`)),u=e.getAttribute(`style`),d=Math.round(r*.16),f=Math.round(r*.8);e.classList.add(`kt-switch`),e.setAttribute(`role`,`switch`);let p=Se(e,[`tabindex`]);e.tagName!==`BUTTON`&&e.tagName!==`INPUT`&&!e.hasAttribute(`tabindex`)&&(e.tabIndex=0),e.style.display=`inline-flex`,e.style.alignItems=`center`,e.style.boxSizing=`content-box`,e.style.width=`${r+f}px`,e.style.height=`${r}px`,e.style.padding=`${d}px`,e.style.borderRadius=`${r}px`,e.style.border=`0`,e.style.cursor=`pointer`,e.style.transition=`background-color ${s}s ease`,e.style.verticalAlign=`middle`;let m=document.createElement(`span`);m.className=`kt-switch__thumb`,m.setAttribute(`aria-hidden`,`true`),m.style.cssText=`width:${r}px;height:${r}px;border-radius:50%;background:${o};box-shadow:0 1px 3px rgba(0,0,0,.3);transition:transform ${n?0:s}s cubic-bezier(.22,.8,.3,1);will-change:transform;flex:0 0 auto;`,e.appendChild(m);let h=()=>{e.setAttribute(`aria-checked`,l?`true`:`false`),e.classList.toggle(`kt-on`,l),e.style.backgroundColor=l?i:a,m.style.transform=l?`translateX(${f}px)`:`translateX(0)`,c&&c.checked!==l&&(c.checked=l)},g=()=>{if(l=!l,h(),c)try{c.dispatchEvent(new Event(`change`,{bubbles:!0})),c.dispatchEvent(new Event(`input`,{bubbles:!0}))}catch{}t.onChange?.(l,e);try{e.dispatchEvent(new CustomEvent(`kt-switch-change`,{bubbles:!0,detail:{checked:l}}))}catch{}},_=e=>{e.target!==c&&(e.preventDefault(),g())},v=e=>{(e.key===` `||e.key===`Enter`)&&(e.preventDefault(),g())},y=()=>{c&&c.checked!==l&&(l=c.checked,h())};return e.addEventListener(`click`,_),e.addEventListener(`keydown`,v),c&&c.addEventListener(`change`,y),h(),{el:e,type:`switch`,toggle:g,set(e){l=!!e,h()},get checked(){return l},pause(){},resume(){},destroy(){e.removeEventListener(`click`,_),e.removeEventListener(`keydown`,v),c&&(c.removeEventListener(`change`,y),c.style.position=``,c.style.opacity=``,c.style.pointerEvents=``,c.style.width=``,c.style.height=``,c.removeAttribute(`tabindex`)),m.remove(),e.classList.remove(`kt-switch`,`kt-on`),e.removeAttribute(`role`),e.removeAttribute(`aria-checked`),p(),u==null?e.removeAttribute(`style`):e.setAttribute(`style`,u)}}},reduced(e,t){return this.create(e,t)}},dl={create(e,t={}){let n=W().reducedMotion,r=Math.max(0,Number(t.duration??.4)),i=t.ease||`cubic-bezier(.22,.8,.3,1)`,a=Math.max(0,Number(t.stagger??0)),o=t.item||null,s=()=>o?Array.from(e.querySelectorAll(o)):Array.from(e.children),c=new Set,l=(e,t)=>{let n=e.cloneNode(!0);n.removeAttribute(`id`),n.querySelectorAll?.(`[id]`).forEach(e=>e.removeAttribute(`id`));let r=[e,...e.querySelectorAll(`*`)],i=[n,...n.querySelectorAll(`*`)];return r.forEach((e,t)=>{let n=getComputedStyle(e);for(let e=0;e{u=new WeakMap,d=new WeakSet,s().forEach(e=>{u.set(e,e.getBoundingClientRect()),d.add(e)})},p=[`none`,`slide`,`fade`,`crossfade`,`fade-slide`,`scale`,`fold`].includes(t.mode)?t.mode:`slide`,m=Math.max(0,Number(t.foldBlur??12)),h=()=>p===`fade`||p===`crossfade`?[{opacity:0},{opacity:1}]:[{opacity:0,transform:`scale(.92)`},{opacity:1,transform:`none`}],g=(e,t,n,r)=>{let i=`translate(${e}px, ${t}px) scale(${n}, ${r})`;return p===`fade`?[{transform:i,opacity:1,offset:0},{transform:i,opacity:0,offset:.44},{transform:`none`,opacity:0,offset:.56},{transform:`none`,opacity:1,offset:1}]:p===`fade-slide`?[{transform:i,opacity:.15},{transform:`none`,opacity:1}]:p===`scale`?[{transform:i,offset:0},{transform:`translate(${e}px, ${t}px) scale(.18)`,offset:.46},{transform:`scale(.18)`,offset:.54},{transform:`none`,offset:1}]:[{transform:i},{transform:`none`}]},_=()=>{if(n||r===0||p===`none`){f();return}let e=0;s().forEach(t=>{let n=u.get(t),o=t.getBoundingClientRect();if(!n||!d.has(t)){t.animate(h(),{duration:r*1e3,easing:i,delay:e*a*1e3}),e+=1;return}let s=n.left-o.left,f=n.top-o.top,_=o.width?n.width/o.width:1,v=o.height?n.height/o.height:1;if(!(Math.abs(s)<1&&Math.abs(f)<1&&Math.abs(_-1)<.01&&Math.abs(v-1)<.01)){if(p===`crossfade`||p===`fold`){let o={duration:r*1e3,easing:i,delay:e*a*1e3},u=`translate(${s}px, ${f}px) scale(${_}, ${v})`,d=l(t,n),h=d.animate(p===`fold`?[{opacity:1,filter:`blur(0px)`,offset:0},{opacity:0,filter:`blur(${m}px)`,offset:.62},{opacity:0,filter:`blur(${m}px)`,offset:1}]:[{opacity:1},{opacity:0}],o),g=t.animate(p===`fold`?[{transform:u,opacity:0,filter:`blur(${m}px)`,offset:0},{opacity:1,offset:.62},{transform:`none`,opacity:1,filter:`blur(0px)`,offset:1}]:[{opacity:0},{opacity:1}],o);Promise.allSettled([h.finished,g.finished]).then(()=>{c.delete(d),d.remove()}),e+=1;return}t.animate(g(s,f,_,v),{duration:r*1e3,easing:i,delay:e*a*1e3}),e+=1}}),f()},v=null,y=()=>{v&&t.watch!==!1&&v.observe(e,{childList:!0,subtree:!1})};t.watch!==!1&&typeof MutationObserver<`u`&&(v=new MutationObserver(()=>_()),y()),f();let b=r=>{v?.disconnect(),f();let i=document.createDocumentFragment();r.forEach(e=>i.appendChild(e));let a=t.viewTransition===!0&&!n&&typeof document.startViewTransition==`function`,o=[],s=new Set;a&&r.forEach(e=>{let t=e.getAttribute?.(`data-kt-layout-id`)?.trim();if(!t)return;let n=`kt-${t.replace(/[^a-zA-Z0-9_-]+/g,`-`)}`;n&&!s.has(n)&&(s.add(n),o.push({item:e,authored:e.style.viewTransitionName,name:n}),e.style.viewTransitionName=n)});let c=()=>e.appendChild(i);if(a&&o.length){let e=!1;try{let t=document.startViewTransition(()=>{e=!0,c(),f(),y()});return Promise.resolve(t?.finished).catch(()=>{}).finally(()=>{o.forEach(({item:e,authored:t})=>{t?e.style.viewTransitionName=t:e.style.removeProperty(`view-transition-name`)})}),r}catch{e||c(),o.forEach(({item:e,authored:t})=>{t?e.style.viewTransitionName=t:e.style.removeProperty(`view-transition-name`)})}}else c();return requestAnimationFrame(()=>{_(),y()}),r};return{el:e,type:`flip`,record:f,play:_,reorder:b,shuffle:()=>{let e=s();for(let t=e.length-1;t>0;--t){let n=Math.floor(Math.random()*(t+1));[e[t],e[n]]=[e[n],e[t]]}return b(e)},sort:(e=`asc`,t={})=>{let n=s();if(typeof e==`function`)return b(n.sort(e));let r=String(e||`asc`).toLowerCase(),i=(t.order||(r===`desc`?`desc`:`asc`))===`desc`?-1:1,a=t.key||(r===`date`?`date`:r===`category`?`category`:``),o=t.getValue||(e=>a?e.dataset?.[a]??e.getAttribute(`data-${a}`)??``:e.textContent?.trim()||``),c=Array.isArray(t.categoryOrder)?t.categoryOrder:null,l=new Intl.Collator(t.locale,{numeric:!0,sensitivity:`base`});return n.sort((e,t)=>{let n=o(e),a=o(t);if(r===`date`)return((Date.parse(n)||0)-(Date.parse(a)||0))*i;if(r===`category`&&c){let e=c.indexOf(n),t=c.indexOf(a),r=e<0?c.length:e,o=t<0?c.length:t;if(r!==o)return(r-o)*i}return l.compare(String(n),String(a))*i}),b(n)},pause(){v?.disconnect()},resume(){y()},destroy(){v?.disconnect(),v=null,c.forEach(e=>e.remove()),c.clear()}}},reduced(e,t){return this.create(e,t)}};function fl(e){let t=String(e||`cubic-out`).trim(),n=t.match(/^cubic-bezier\(\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*\)$/i);return n?B(...n.slice(1).map(Number)):t===`ease`?B(.25,.1,.25,1):t===`ease-in`?B(.42,0,1,1):t===`ease-out`?B(0,0,.58,1):t===`ease-in-out`?B(.42,0,.58,1):z(t)}var pl={defer:!0,create(e,t={}){let n=t.axis===`horizontal`||t.axis===`x`?`horizontal`:`vertical`,r=Math.max(4,Number(t.size??44)),i=t.mode===`mask`?`mask`:`shadow`,a=n===`horizontal`,o=t.transitionMode===`instant`?`instant`:`smooth`,s=Number(t.transition),c=o===`instant`?0:Math.max(0,Number(t.transitionDuration??(Number.isFinite(s)?s/1e3:.18)))*1e3,l=fl(t.ease||`cubic-out`),u=null,d=()=>{let t=a?e.scrollLeft:e.scrollTop,r=Math.max(0,a?e.scrollWidth-e.clientWidth:e.scrollHeight-e.clientHeight);return{axis:n,position:t,max:r,progress:r>0?t/r:0,atStart:t<=1,atEnd:r<=1||t>=r-1,canScrollStart:t>1,canScrollEnd:r>1&&t{let n=d();e.classList.toggle(`kt-at-start`,n.atStart),e.classList.toggle(`kt-at-end`,n.atEnd),e.classList.toggle(`kt-can-scroll-start`,n.canScrollStart),e.classList.toggle(`kt-can-scroll-end`,n.canScrollEnd),e.style.setProperty(`--kt-scroll-shadow-progress`,n.progress.toFixed(4));let r=`${n.atStart}:${n.atEnd}:${n.canScrollStart}:${n.canScrollEnd}`;if(r!==u){u=r,t.onChange?.(n,e);let i=e.ownerDocument?.defaultView?.CustomEvent||globalThis.CustomEvent;i&&e.dispatchEvent(new i(`kineto:scroll-shadows-change`,{detail:n}))}return n},p=$(e,[`overflowX`,`overflowY`]);if(a?getComputedStyle(e).overflowX===`visible`&&(e.style.overflowX=`auto`):getComputedStyle(e).overflowY===`visible`&&(e.style.overflowY=`auto`),i===`mask`){let t=$(e,[`maskImage`,`webkitMaskImage`]),n=a?`to right`:`to bottom`,i=null,o=null,s=null,u=0,m=0,h=0,g=0,_=0,v=()=>{e.style.setProperty(`--kt-scroll-shadow-start`,`${o.toFixed(2)}px`),e.style.setProperty(`--kt-scroll-shadow-end`,`${s.toFixed(2)}px`);let t=`linear-gradient(${n}, transparent 0, #000 min(var(--kt-scroll-shadow-size, ${r}px), var(--kt-scroll-shadow-start)), #000 calc(100% - min(var(--kt-scroll-shadow-size, ${r}px), var(--kt-scroll-shadow-end, ${r}px))), transparent 100%)`;e.style.maskImage=t,e.style.webkitMaskImage=t,f()},y=(e=performance.now())=>{if(i=null,o==null||c===0){o=u,s=m,v();return}let t=Math.max(0,e-_),n=Math.min(1,t/c),r=l(n);o=h+(u-h)*r,s=g+(m-g)*r,v(),n<1&&(i=requestAnimationFrame(y))},b=(t=!1)=>{let n=a?e.scrollLeft:e.scrollTop,l=a?e.scrollWidth-e.clientWidth:e.scrollHeight-e.clientHeight,d=Math.max(0,Math.min(r,n)),f=Math.max(0,Math.min(r,l-n));(o==null||t||c===0)&&(o=d,s=f),h=o,g=s,u=d,m=f,_=performance.now(),i!=null&&cancelAnimationFrame(i),i=requestAnimationFrame(y)},x=()=>b(!1);return b(!0),e.addEventListener(`scroll`,x,{passive:!0}),window.addEventListener(`resize`,x,{passive:!0}),{el:e,type:`scrollShadows`,get state(){return d()},refresh(){return b(!0),d()},pause(){},resume(){},destroy(){i!=null&&cancelAnimationFrame(i),e.removeEventListener(`scroll`,x),window.removeEventListener(`resize`,x),e.classList.remove(`kt-at-start`,`kt-at-end`,`kt-can-scroll-start`,`kt-can-scroll-end`),e.style.removeProperty(`--kt-scroll-shadow-progress`),e.style.removeProperty(`--kt-scroll-shadow-start`),e.style.removeProperty(`--kt-scroll-shadow-end`),t(),p()}}}let m=Math.round(r*.34),h=typeof getComputedStyle<`u`?getComputedStyle(e).backgroundColor:``,g=h&&h!==`rgba(0, 0, 0, 0)`&&h!==`transparent`,_=`var(--kt-scroll-shadow-cover, ${t.color||(g?h:`Canvas`)})`,v=`var(--kt-scroll-shadow, ${t.shadow||`rgba(0, 0, 0, 0.24)`})`,y=Math.max(0,Math.min(1,Number(t.opacity??1))),b=y<1?`color-mix(in srgb, ${v} ${Math.round(y*100)}%, transparent)`:v,x=t.shape===`linear`,S=$(e,[`backgroundImage`,`backgroundRepeat`,`backgroundSize`,`backgroundPosition`,`backgroundAttachment`,`backgroundColor`]),C=a?[`linear-gradient(to right, ${_} 30%, rgba(0,0,0,0))`,`linear-gradient(to left, ${_} 30%, rgba(0,0,0,0))`]:[`linear-gradient(${_} 30%, rgba(0,0,0,0))`,`linear-gradient(rgba(0,0,0,0), ${_} 70%)`],w=x?a?[`linear-gradient(to right, ${b}, rgba(0,0,0,0))`,`linear-gradient(to left, ${b}, rgba(0,0,0,0))`]:[`linear-gradient(to bottom, ${b}, rgba(0,0,0,0))`,`linear-gradient(to top, ${b}, rgba(0,0,0,0))`]:a?[`radial-gradient(farthest-side at 0 50%, ${b}, rgba(0,0,0,0))`,`radial-gradient(farthest-side at 100% 50%, ${b}, rgba(0,0,0,0))`]:[`radial-gradient(farthest-side at 50% 0, ${b}, rgba(0,0,0,0))`,`radial-gradient(farthest-side at 50% 100%, ${b}, rgba(0,0,0,0))`];e.style.backgroundImage=[...C,...w].join(`, `),e.style.backgroundRepeat=`no-repeat`,e.style.backgroundColor=_,a?(e.style.backgroundSize=`var(--kt-scroll-shadow-size, ${r}px) 100%, var(--kt-scroll-shadow-size, ${r}px) 100%, var(--kt-scroll-shadow-shade, ${m}px) 100%, var(--kt-scroll-shadow-shade, ${m}px) 100%`,e.style.backgroundAttachment=`local, local, scroll, scroll`,e.style.backgroundPosition=`left center, right center, left center, right center`):(e.style.backgroundSize=`100% var(--kt-scroll-shadow-size, ${r}px), 100% var(--kt-scroll-shadow-size, ${r}px), 100% var(--kt-scroll-shadow-shade, ${m}px), 100% var(--kt-scroll-shadow-shade, ${m}px)`,e.style.backgroundAttachment=`local, local, scroll, scroll`,e.style.backgroundPosition=`center top, center bottom, center top, center bottom`);let T=()=>f();return e.addEventListener(`scroll`,T,{passive:!0}),window.addEventListener(`resize`,T,{passive:!0}),f(),{el:e,type:`scrollShadows`,get state(){return d()},refresh(){return f()},pause(){},resume(){},destroy(){e.removeEventListener(`scroll`,T),window.removeEventListener(`resize`,T),e.classList.remove(`kt-at-start`,`kt-at-end`,`kt-can-scroll-start`,`kt-can-scroll-end`),e.style.removeProperty(`--kt-scroll-shadow-progress`),S(),p()}}},reduced(e,t){return this.create(e,t)}},ml=Object.freeze([`squircle`,`round`,`bevel`,`scoop`,`notch`,`square`]),hl=Object.freeze({square:1/0,squircle:2,round:1,bevel:0,scoop:-1,notch:-1/0}),gl=12;function _l(e,t){let n=Number.isFinite(Number(t))?Number(t):hl[e]??hl.squircle;return Math.max(-12,Math.min(gl,n))}function vl(e){let t=2/2**Math.abs(e),n=e<0;return{concave:n,at(e,r){let i=e===0?1:e===r?0:Math.cos(e/r*(Math.PI/2)),a=e===0?0:e===r?1:Math.sin(e/r*(Math.PI/2));return n?[1-a**+t,1-i**+t]:[i**+t,a**+t]}}}function yl(e){return e>=0?1-2**(-1/2**e):2**(-1/2**-e)}function bl(e,t,n,r,i,a,o,s,c,l){for(let u=0;u<=l;u+=1){let[d,f]=c.at(s?l-u:u,l);e.push([t+a*r*d,n+o*i*f])}}function xl(e,t,n){let[r,i,a,o]=e,s=Math.min(1,t/Math.max(1e-6,r+i),t/Math.max(1e-6,o+a),n/Math.max(1e-6,r+o),n/Math.max(1e-6,i+a));return e.map(e=>Math.max(0,e*s))}function Sl(e,t,n,r,i){let[a,o,s,c]=xl(n,e,t),l=Math.max(2,Math.round(i??Math.min(48,Math.max(8,Math.ceil(Math.max(a,o,s,c)/2))))),u=vl(r),d=[];return bl(d,a,a,a,a,-1,-1,!1,u,l),bl(d,e-o,o,o,o,1,-1,!0,u,l),bl(d,e-s,t-s,s,s,1,1,!1,u,l),bl(d,c,t-c,c,c,-1,1,!0,u,l),d}function Cl(e,t,n,r,i){return`polygon(${Sl(e,t,n,r,i).map(([e,t])=>`${e.toFixed(2)}px ${t.toFixed(2)}px`).join(`,`)})`}function wl(e,t,n,r,i){let[a,...o]=Sl(e,t,n,r,i);return`M${a[0].toFixed(2)} ${a[1].toFixed(2)}${o.map(([e,t])=>`L${e.toFixed(2)} ${t.toFixed(2)}`).join(``)}Z`}var Tl=`kt-squircle-border`,El=`http://www.w3.org/2000/svg`;function Dl(){return typeof CSS<`u`&&typeof CSS.supports==`function`&&CSS.supports(`corner-shape`,`squircle`)}function Ol(e,t,n){if(e==null||e===``||e===`auto`)return null;let r=String(e).trim().split(/\s+/).slice(0,4),i=(e,t)=>{let n=/%$/.test(e),r=parseFloat(e);return Number.isFinite(r)?n?r/100*t:r:null},a=Math.min(t,n),o=r.map(e=>i(e,a));if(o.some(e=>e==null))return null;let[s,c=s,l=s,u=c]=o;return[s,c,l,u]}function kl(e,t,n){let r=Math.min(t,n),i=e=>{let t=String(e||`0`).trim().split(/\s+/)[0],n=parseFloat(t);return Number.isFinite(n)?/%$/.test(t)?n/100*r:n:0};return[i(e.borderTopLeftRadius),i(e.borderTopRightRadius),i(e.borderBottomRightRadius),i(e.borderBottomLeftRadius)]}function Al(e,t){return Number.isFinite(Number(t))?`superellipse(${Number(t)})`:e}function jl(){let e=document.createElementNS(El,`svg`);e.setAttribute(`class`,Tl),e.setAttribute(`aria-hidden`,`true`),e.setAttribute(`preserveAspectRatio`,`none`),e.style.cssText=`position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none;overflow:visible;`;let t=document.createElementNS(El,`path`);return t.setAttribute(`fill`,`none`),e.appendChild(t),{svg:e,path:t}}var Ml={defer:!0,create(e,t={}){let n=ml.includes(t.preset)?t.preset:`squircle`,r=t.superellipse,i=_l(n,r),a=t.nativeShape!==`off`&&t.nativeShape!==!1&&Dl(),o=t.borderFollow!==`off`&&t.borderFollow!==!1,s=Number.isFinite(Number(t.cornerSamples))?Math.round(Number(t.cornerSamples)):void 0,c=$(e,[`border-radius`,`corner-shape`,`clip-path`,`position`,`border-color`]),l=null,u=null,d=!1,f=``,p={width:0,height:0,radii:[0,0,0,0]},m=()=>{let n=e.getBoundingClientRect(),r=getComputedStyle(e),i=Math.max(0,n.width),a=Math.max(0,n.height),o=Ol(t.cornerRadius,i,a),s=kl(r,i,a);return{width:i,height:a,radii:o||(s.some(e=>e>0)?s:[24,24,24,24]),computed:r}},h=({width:t,height:n,radii:r,computed:c})=>{let u=parseFloat(c.borderTopWidth)||0;if(!(o&&!a&&u>0&&c.borderTopStyle!==`none`)){l&&=(l.svg.remove(),null);return}l||(c.position===`static`&&(e.style.position=`relative`),l=jl(),e.appendChild(l.svg)),l.svg.setAttribute(`viewBox`,`0 0 ${t} ${n}`),l.path.setAttribute(`d`,wl(t,n,r,i,s)),l.path.setAttribute(`stroke`,c.borderTopColor),l.path.setAttribute(`stroke-width`,String(u*2)),e.style.borderColor=`transparent`},g=()=>{if(d)return;let t=m(),{width:o,height:c,radii:l}=t;if(o<=0||c<=0)return;let u=`${o}x${c}|${l.join(`,`)}`;u!==f&&(f=u,p={width:o,height:c,radii:[...l]},a?(e.style.borderRadius=l.map(e=>`${e}px`).join(` `),e.style.cornerShape=Al(n,r)):(e.style.borderRadius=`0px`,e.style.clipPath=Cl(o,c,l,i,s)),h(t))};return g(),typeof ResizeObserver<`u`&&(u=new ResizeObserver(()=>g()),u.observe(e)),{el:e,type:`squircle`,get renderer(){return a?`native`:`polyfill`},get shape(){return{preset:n,k:i,diagonal:yl(i),...p}},refresh(){f=``,g()},pause(){},resume(){},destroy(){d||(d=!0,u?.disconnect(),u=null,l?.svg.remove(),l=null,c())}}},reduced(e,t){return this.create(e,t)}},Nl={create(e,t={}){let n=Math.max(0,Number(t.offset??8)),r=Math.max(1,Number(t.distance??120)),i=t.shrink!==!1,a=t.shadow!==!1,o=t.activeClass||`kt-stuck`;e.classList.add(`kt-sticky-header`),i&&e.classList.add(`kt-sh-shrink`),a&&e.classList.add(`kt-sh-shadow`);let s=(e=>{let t=e.parentElement;for(;t&&t!==document.body&&t!==document.documentElement;){let e=getComputedStyle(t).overflowY;if((e===`auto`||e===`scroll`)&&t.scrollHeight>t.clientHeight+1)return t;t=t.parentElement}return window})(e),c=()=>s===window?window.scrollY||document.documentElement.scrollTop||0:s.scrollTop,l=!1,u=!1,d=null,f=!1,p=()=>{if(u=!1,d=null,f)return;let i=c(),a=Y(i/r,0,1);e.style.setProperty(`--kt-header-progress`,a.toFixed(4));let s=i>n;s!==l&&(l=s,e.classList.toggle(o,l),t.onChange?.(l,a,e))},m=()=>{u||(u=!0,d=requestAnimationFrame(p))};return p(),s.addEventListener(`scroll`,m,{passive:!0}),window.addEventListener(`resize`,m,{passive:!0}),{el:e,type:`stickyHeader`,pause(){s.removeEventListener(`scroll`,m)},resume(){s.addEventListener(`scroll`,m,{passive:!0})},destroy(){f=!0,d!=null&&cancelAnimationFrame(d),s.removeEventListener(`scroll`,m),window.removeEventListener(`resize`,m),e.classList.remove(`kt-sticky-header`,`kt-sh-shrink`,`kt-sh-shadow`,o),e.style.removeProperty(`--kt-header-progress`)}}},reduced(e,t){return this.create(e,t)}},Pl={create(e,t={}){let n=t.height||`100vh`,r=t.top||`calc((100svh - ${n}) / 2)`,i=t.smooth===!0?.12:typeof t.smooth==`number`?Y(t.smooth,.02,1):0;if(!e.parentNode)return null;let a=Array.from(e.childNodes),o=e.getAttribute(`style`),s=e.children.length===1&&e.firstElementChild?.classList.contains(`hscroll-track`)?e.firstElementChild:null,c=document.createElement(`div`);c.className=`kt-hscroll-viewport`;let l=s||document.createElement(`div`),u=l.getAttribute(`style`);s||(l.className=`kt-hscroll-track`,a.forEach(e=>l.appendChild(e))),c.appendChild(l),e.appendChild(c),e.classList.add(`kt-hscroll`),e.style.position=`relative`,e.style.width=`100%`,e.style.maxWidth=`100%`,e.style.minWidth=`0`,e.style.boxSizing=`border-box`,c.style.cssText=`position:sticky;top:${r};width:100%;max-width:100%;min-width:0;height:${n};overflow:hidden;display:flex;align-items:center;box-sizing:border-box;`,l.style.display=`flex`,l.style.flex=`0 0 auto`,l.style.width=`max-content`,l.style.minWidth=`max-content`,l.style.willChange=`transform`;let d=0,f=0,p=0,m=null,h=!1,g=()=>{let t=c.clientWidth;d=Math.max(0,l.scrollWidth-t),e.style.height=`calc(${n} + ${d}px)`},_=()=>{let t=e.getBoundingClientRect(),n=c.clientHeight,r=e.offsetHeight-n,i=Y((Number.parseFloat(getComputedStyle(c).top)||0)-t.top,0,r);f=(r>0?i/r:0)*d},v=ie(),y=e=>{p=i?J(p,f,re(i,v.tick(e))):f,l.style.transform=`translate3d(${-p}px,0,0)`,i&&Math.abs(p-f)>.2?m=requestAnimationFrame(y):(p=f,l.style.transform=`translate3d(${-p}px,0,0)`,m=null)},b=()=>{h&&(_(),i?m??=requestAnimationFrame(y):y())},x=!1,S=()=>{x||(x=!0,requestAnimationFrame(()=>{x=!1,b()}))},C=()=>{g(),b()};h=!0,g(),b(),window.addEventListener(`scroll`,S,{passive:!0}),window.addEventListener(`resize`,C,{passive:!0});let w=typeof ResizeObserver<`u`?new ResizeObserver(C):null;return w?.observe(l),{el:e,type:`horizontalScroll`,pause(){h=!1},resume(){h=!0,b()},destroy:()=>{h=!1,m!=null&&cancelAnimationFrame(m),window.removeEventListener(`scroll`,S),window.removeEventListener(`resize`,C),w?.disconnect(),s?(e.insertBefore(l,c),u==null?l.removeAttribute(`style`):l.setAttribute(`style`,u)):Array.from(l.childNodes).forEach(t=>e.insertBefore(t,c)),c.remove(),e.classList.remove(`kt-hscroll`),o==null?e.removeAttribute(`style`):e.setAttribute(`style`,o)}}},reduced(e){return e.style.overflowX=`auto`,{el:e,type:`horizontalScroll`,pause(){},resume(){},destroy(){e.style.overflowX=``}}}},Fl=new Map,Il=new Map,Ll=/^[a-z][a-z0-9-]{0,63}$/,Rl=Object.freeze([`2d`,`webgl`,`webgl2`]),zl=Object.freeze([`setup`,`resize`,`frame`,`destroy`,`uniforms`]);function Bl(e,t){throw TypeError(`[Kineto/canvasEffect] ${e}: ${t}`)}function Vl(e,t=`effect`){(!e||typeof e!=`object`)&&Bl(t,`a definition must be an object`);let n=typeof e.fragment==`string`&&e.fragment.trim()?e.fragment:null,r=e.context??(n?`webgl`:`2d`);Rl.includes(r)||Bl(t,`context must be one of ${Rl.join(`, `)}`),n&&r===`2d`&&Bl(t,`a fragment shader needs a webgl or webgl2 context`),!n&&typeof e.frame!=`function`&&Bl(t,`needs a frame(api) function or a fragment shader`),zl.forEach(n=>{e[n]!=null&&typeof e[n]!=`function`&&Bl(t,`${n} must be a function`)});let i=e.options&&typeof e.options==`object`?{...e.options}:{};return Object.freeze({...e,context:r,fragment:n,options:Object.freeze(i)})}function Hl(e,t){let n=String(e??``);Ll.test(n)||Bl(n||`(no name)`,`use lower-case letters, digits and dashes, starting with a letter`),Fl.set(n,Vl(t,n));let r=Il.get(n);return r&&(Il.delete(n),r.forEach((e,t)=>{t.isConnected&&e()})),n}function Ul(){return Array.from(Fl,([e,t])=>({name:e,context:t.context,shader:!!t.fragment,options:{...t.options}}))}function Wl(e){return typeof e==`string`&&Fl.get(e)||null}function Gl(e,t,n){let r=String(e??``);if(!Ll.test(r))return;Il.has(r)||Il.set(r,new Map);let i=Il.get(r);i.forEach((e,t)=>{t.isConnected||i.delete(t)}),i.set(t,n)}var Kl=Object.freeze({low:Object.freeze({maxDpr:1,fps:30}),medium:Object.freeze({maxDpr:1.5,fps:60}),high:Object.freeze({maxDpr:2,fps:0})});function ql(e,{performance:t,touch:n}){return Object.prototype.hasOwnProperty.call(Kl,e)?e:t===`low`?`low`:t===`mid`||n?`medium`:`high`}function Jl({el:e,target:t,onInput:n}){let r={clientX:0,clientY:0,down:!1,seen:!1,overHost:!1,lastInput:0},i={x:0,y:0,nx:.5,ny:.5,vx:0,vy:0,inside:!1,down:!1,active:!1};if(t===`none`||typeof window>`u`)return{view:i,sample(){},destroy(){}};let a=t===`window`?window:e,o=e=>{r.clientX=e.clientX,r.clientY=e.clientY,r.seen=!0,r.lastInput=performance.now(),n()},s=e=>{r.down=!0,o(e)},c=e=>{r.down=!1,o(e)},l=()=>{r.overHost=!0,n()},u=()=>{r.overHost=!1,r.down=!1,n()},d=[[`pointermove`,o],[`pointerdown`,s],[`pointerup`,c],[`pointercancel`,c]];d.forEach(([e,t])=>a.addEventListener(e,t,{passive:!0})),e.addEventListener(`pointerenter`,l,{passive:!0}),e.addEventListener(`pointerleave`,u,{passive:!0});let f=null,p=null;return{view:i,sample(e,t){if(!r.seen)return;let n=r.clientX-e.left,a=r.clientY-e.top;f!=null&&t>0&&(i.vx=(n-f)/t,i.vy=(a-p)/t),f=n,p=a,i.x=n,i.y=a,i.nx=e.width?n/e.width:.5,i.ny=e.height?a/e.height:.5,i.inside=r.overHost||n>=0&&a>=0&&n<=e.width&&a<=e.height,i.down=r.down,i.active=performance.now()-r.lastInput<500},destroy(){d.forEach(([e,t])=>a.removeEventListener(e,t)),e.removeEventListener(`pointerenter`,l),e.removeEventListener(`pointerleave`,u)}}}function Yl({enabled:e,onInput:t}){let n={progress:0,velocity:0};if(!e||typeof window>`u`)return{view:n,sample(){},destroy(){}};let r=()=>t();window.addEventListener(`scroll`,r,{passive:!0});let i=null;return{view:n,sample(e,t){let r=window.innerHeight||1,a=r+e.height;n.progress=a>0?Math.min(1,Math.max(0,(r-e.top)/a)):0;let o=window.scrollY;n.velocity=i!=null&&t>0?(o-i)/t:0,i=o},destroy(){window.removeEventListener(`scroll`,r)}}}var Xl=`attribute vec2 aPosition;void main(){gl_Position=vec4(aPosition,0.0,1.0);}`,Zl=`#version 300 es
+in vec2 aPosition;void main(){gl_Position=vec4(aPosition,0.0,1.0);}`,Ql=`uniform float iTime;uniform vec3 iResolution;uniform vec4 iMouse;
+`,$l=`
+void main(){mainImage(gl_FragColor,gl_FragCoord.xy);}`,eu=`#ifdef GL_ES
precision highp float;
#endif
-`;function Zl(e,t){if(t)return e;let n=/\bmainImage\s*\(/.test(e)&&!/\bvoid\s+main\s*\(/.test(e),r=/\bprecision\s+(lowp|mediump|highp)\s+float/.test(e)?``:Xl;return n?`${r}${Jl}${e}${Yl}`:`${r}${e}`}function Ql(e,t,n){let r=e.createShader(t);if(e.shaderSource(r,n),e.compileShader(r),!e.getShaderParameter(r,e.COMPILE_STATUS)){let n=e.getShaderInfoLog(r)||`unknown error`;throw e.deleteShader(r),Error(`[Kineto/canvasEffect] ${t===e.VERTEX_SHADER?`vertex`:`fragment`} shader failed to compile:\n${n}`)}return r}var $l=e=>`u${e[0].toUpperCase()}${e.slice(1)}`;function eu(e,t,n){let r=globalThis.WebGL2RenderingContext,i=typeof r==`function`&&e instanceof r,a=Ql(e,e.VERTEX_SHADER,i?ql:Kl),o=Ql(e,e.FRAGMENT_SHADER,Zl(t,i)),s=e.createProgram();if(e.attachShader(s,a),e.attachShader(s,o),e.linkProgram(s),!e.getProgramParameter(s,e.LINK_STATUS))throw Error(`[Kineto/canvasEffect] shader program failed to link:\n${e.getProgramInfoLog(s)||`unknown error`}`);let c=e.createBuffer();e.bindBuffer(e.ARRAY_BUFFER,c),e.bufferData(e.ARRAY_BUFFER,new Float32Array([-1,-1,3,-1,-1,3]),e.STATIC_DRAW);let l=e.getAttribLocation(s,`aPosition`),u=new Map,d=t=>(u.has(t)||u.set(t,e.getUniformLocation(s,t)),u.get(t)),f=(t,r)=>{let i=d(t);if(i!=null&&r!=null){if(typeof r==`number`)e.uniform1f(i,r);else if(typeof r==`boolean`)e.uniform1f(i,+!!r);else if(typeof r==`string`){let t=n(r);t&&e.uniform3f(i,t[0],t[1],t[2])}else if(Array.isArray(r)||ArrayBuffer.isView(r)){let t=Array.from(r,Number);t.length===2?e.uniform2f(i,t[0],t[1]):t.length===3?e.uniform3f(i,t[0],t[1],t[2]):t.length===4&&e.uniform4f(i,t[0],t[1],t[2],t[3])}}};return{draw(t,n){e.viewport(0,0,t.canvas.width,t.canvas.height),e.useProgram(s),e.bindBuffer(e.ARRAY_BUFFER,c),e.enableVertexAttribArray(l),e.vertexAttribPointer(l,2,e.FLOAT,!1,0,0);let{width:r,height:i}=t.canvas,{nx:a,ny:o,x:u,y:d,down:p}=t.pointer;f(`uTime`,t.time),f(`uResolution`,[r,i]),f(`uPointer`,[a,1-o]),f(`uPointerDown`,p),f(`uScroll`,t.scroll.progress),f(`iTime`,t.time),f(`iResolution`,[r,i,1]),f(`iMouse`,[u*t.dpr,i-d*t.dpr,+!!p,0]),Object.entries(t.options).forEach(([e,t])=>f($l(e),t)),n&&Object.entries(n).forEach(([e,t])=>f(e,t)),e.drawArrays(e.TRIANGLES,0,3)},destroy(){e.deleteBuffer(c),e.deleteProgram(s),e.deleteShader(a),e.deleteShader(o)}}}var tu=`kt-canvas-effect`,nu=`kt-canvas-effect__canvas`,ru={still:`is-still`,unsupported:`is-unsupported`,failed:`is-failed`},iu=1/15,au=null,ou=`#010203`;function su(e){if(typeof document>`u`||(au||=document.createElement(`canvas`).getContext(`2d`),!au))return null;au.fillStyle=ou,au.fillStyle=e;let t=String(au.fillStyle);if(t===ou&&e.trim().toLowerCase()!==ou)return null;if(t[0]===`#`){let e=parseInt(t.slice(1,7),16);return[(e>>16&255)/255,(e>>8&255)/255,(e&255)/255,1]}let n=t.match(/[\d.]+/g);return n&&n.length>=3?[n[0]/255,n[1]/255,n[2]/255,n[3]==null?1:Number(n[3])]:null}function cu(e){return{color:e.color,color2:e.color2,background:e.background,speed:e.speed,density:e.density,size:e.size,strength:e.strength,distortion:e.distortion}}function lu(e,t){let n={};return Object.entries(e).forEach(([e,r])=>{let i=t[e];i==null||i===``?n[e]=r:typeof r==`number`?n[e]=X(i,r):typeof r==`boolean`?n[e]=i===!0||i===`true`?!0:i===!1||i===`false`?!1:r:typeof r==`string`?n[e]=String(i):Array.isArray(r)?n[e]=Array.isArray(i)&&i.length===r.length&&i.every(e=>Number.isFinite(Number(e)))?i.map(Number):r:n[e]=i}),n}var uu=new Set;function du(e,t,n){Object.entries(cu(n)).forEach(([n,r])=>{if(r===void 0||Object.prototype.hasOwnProperty.call(t.options,n))return;let i=`${e}:${n}`;uu.has(i)||(uu.add(i),console.warn(`[Kineto/canvasEffect] "${e}" does not read "${n}" (it reads: ${Object.keys(t.options).join(`, `)||`no options`}).`))})}function fu(e,t,n,r){let i=t.effect??t.preset;if(typeof i!=`string`)return i!=null&&console.warn("[Kineto/canvasEffect] `effect` must name an effect registered with Kineto.defineCanvasEffect(); definitions are not accepted as options."),null;let a=Bl(i);if(!a)return Vl(i,e,()=>n.create(`canvasEffect`,e,t)),null;du(i,a,t);let o=Ul(t.quality,{performance:n.performance,touch:!!n.env?.touch}),s=Hl[o],c={maxDpr:X(t.maxDpr,s.maxDpr,.5,4),fps:X(t.fps,s.fps,0,240),pointer:[`host`,`window`,`none`].includes(t.pointer)?t.pointer:`host`},l=e.tagName===`CANVAS`,u=l?e:document.createElement(`canvas`),d=$(e,[`position`,`isolation`]),f=e.classList.contains(tu);e.classList.add(tu),l||(getComputedStyle(e).position===`static`&&(e.style.position=`relative`),e.style.isolation=`isolate`,u.className=nu,u.setAttribute(`aria-hidden`,`true`),e.insertBefore(u,e.firstChild));let p=!0,m=!1,h=!0,g=!1,_=!1,v=null,y=null,b=0,x=null,S=null,C=!1,w=new Map,T=t=>{if(Array.isArray(t))return t;let n=String(t??``).trim();if(!w.has(n)){let t=/^var\(\s*(--[\w-]+)\s*(?:,\s*(.+))?\)$/.exec(n),r=t?getComputedStyle(e).getPropertyValue(t[1]).trim()||t[2]||``:n;w.set(n,r?su(r):null)}return w.get(n)},E=Wl({el:e,target:c.pointer,onInput:()=>I()}),D=Gl({enabled:t.scroll!==!1,onInput:()=>I()}),O={el:e,canvas:u,context:a.context,ctx:null,gl:null,options:lu(a.options,t),state:void 0,width:0,height:0,dpr:1,time:0,delta:0,frame:0,pointer:E.view,scroll:D.view,quality:o,reducedMotion:r,color:T,wake:()=>I()},k=(t,n)=>e.classList.toggle(ru[t],n),A=n=>{g=!0,L(),k(`failed`,!0),console.error(`[Kineto/canvasEffect] "${i}" stopped:`,n);try{t.onError?.(n,e)}catch{}},j=()=>{let e=a.context!==`2d`,t=e?{alpha:!0,antialias:!1,depth:!1,stencil:!1,premultipliedAlpha:!0,preserveDrawingBuffer:!1,powerPreference:o===`high`?`default`:`low-power`}:{alpha:!0},n=u.getContext(a.context,t);return n?(x=n,O.ctx=n,O.gl=e?n:null,a.fragment&&(S=eu(n,a.fragment,T)),!0):!1},M=()=>j()?(O.state=a.setup?a.setup(O):void 0,C=!0,!0):(k(`unsupported`,!0),!1),N=(e,t)=>{let n=Math.min(typeof window<`u`&&window.devicePixelRatio||1,c.maxDpr),r=Math.max(1,Math.round(e*n)),i=Math.max(1,Math.round(t*n));O.width=e,O.height=t,O.dpr=n,u.width!==r&&(u.width=r),u.height!==i&&(u.height=i),C&&a.resize?.(O)},P=t=>{let n=e.getBoundingClientRect(),i=y==null?0:Math.min(iu,Math.max(0,(t-y)/1e3));y=t,O.delta=r?0:i,O.time+=O.delta,O.frame+=1,E.sample(n,i),D.sample(n,i);let o=a.frame?a.frame(O):void 0;return S&&S.draw(O,a.uniforms?a.uniforms(O):null),S&&!a.frame&&(o=void 0),o},F=e=>{if(v=null,!p||m||g||_){h=!0;return}if(c.fps>0&&b&&e-b<1e3/c.fps-1){v=requestAnimationFrame(F);return}b=e;let t;try{t=P(e)}catch(e){A(e);return}if(r||t===!1){h=!0,y=null;return}v=requestAnimationFrame(F)};function I(){p&&!m&&!g&&!_&&C&&h&&(h=!1,v=requestAnimationFrame(F))}function L(){v!=null&&cancelAnimationFrame(v),v=null,h=!0,y=null}let R=e=>{e.preventDefault(),_=!0,L()},z=()=>{_=!1,S=null;try{M(),N(O.width,O.height),I()}catch(e){A(e)}};a.context!==`2d`&&(u.addEventListener(`webglcontextlost`,R),u.addEventListener(`webglcontextrestored`,z));let B=null,V=(e,t)=>{try{if(N(e,t),!C&&!M())return;if(a.resize?.(O),k(`still`,r),C&&r){P(typeof performance<`u`?performance.now():0);return}I()}catch(e){A(e)}};return typeof ResizeObserver<`u`?(B=new ResizeObserver(()=>{if(m)return;let t={width:e.clientWidth,height:e.clientHeight};if(!C&&!g){V(t.width,t.height);return}if(!g&&C&&(t.width!==Math.round(O.width)||t.height!==Math.round(O.height)))try{N(t.width,t.height),r?P(performance.now()):I()}catch(e){A(e)}}),B.observe(e)):V(e.clientWidth,e.clientHeight),{el:e,type:`canvasEffect`,get effect(){return i},get quality(){return o},redraw(){if(r&&C){try{P(performance.now())}catch(e){A(e)}return}I()},pause(){p=!1,L()},resume(){p||(p=!0,I())},update(e,t){if((e.effect!=null||e.preset!=null)&&Bl(t.effect??t.preset)!==a)return!1;if(O.options=lu(a.options,t),w.clear(),!C)return!0;try{a.resize?.(O),r?P(performance.now()):I()}catch(e){A(e)}return!0},destroy(){if(!m){m=!0,L(),B?.disconnect(),E.destroy(),D.destroy(),u.removeEventListener(`webglcontextlost`,R),u.removeEventListener(`webglcontextrestored`,z);try{C&&a.destroy?.(O)}catch(e){console.error(`[Kineto/canvasEffect] destroy() failed:`,e)}S?.destroy(),O.gl&&O.gl.getExtension(`WEBGL_lose_context`)?.loseContext(),l?x&&a.context===`2d`&&x.clearRect(0,0,u.width,u.height):u.remove(),Object.values(ru).forEach(t=>e.classList.remove(t)),f||e.classList.remove(tu),d()}}}}var pu={offscreen:e=>e.pauseOffscreen===!1?null:`pause`,defer:!0,create(e,t={},n){return fu(e,t,n,!1)},reduced(e,t={},n){return fu(e,t,n,t.reducedMotion!==`run`)},define:Rl,list:zl},mu=e=>`${X(e)}px`;function hu(e={}){if(!e||typeof e!=`object`)throw TypeError(`Kineto.states state values must be objects.`);let t={};e.opacity!=null&&(t.opacity=String(Math.max(0,Math.min(1,X(e.opacity,1)))));let n=[];e.x!=null&&n.push(`translateX(${mu(e.x)})`),e.y!=null&&n.push(`translateY(${mu(e.y)})`),e.scale!=null&&n.push(`scale(${X(e.scale,1)})`),e.rotate!=null&&n.push(`rotate(${X(e.rotate)}deg)`),e.skewX!=null&&n.push(`skewX(${X(e.skewX)}deg)`),e.skewY!=null&&n.push(`skewY(${X(e.skewY)}deg)`),e.transform!=null&&n.push(String(e.transform)),n.length&&(t.transform=n.join(` `));let r=[];return e.blur!=null&&r.push(`blur(${mu(e.blur)})`),e.brightness!=null&&r.push(`brightness(${X(e.brightness,1)})`),e.filter!=null&&r.push(String(e.filter)),r.length&&(t.filter=r.join(` `)),t}function gu(e){return de(e).filter(e=>e?.nodeType===1)}function _u(e,t){if(!t)return[];if(typeof t!=`string`)return gu(t);let n=[];return e.forEach(e=>{try{n.push(...e.querySelectorAll(t))}catch{}}),[...new Set(n)]}function vu(e,t){let n={...t,...e};return{duration:Math.max(0,X(n.duration,300)),delay:Math.max(0,X(n.delay,0)),stagger:Math.max(0,X(n.stagger,0)),ease:V(n.ease||`ease`),initial:n.initial,beforeChildren:n.beforeChildren===!0,afterChildren:n.afterChildren===!0,delayChildren:Math.max(0,X(n.delayChildren,0)),reducedMotion:n.reducedMotion}}function yu(e={},t={},n=null){if(!e||typeof e!=`object`||Array.isArray(e))throw TypeError(`Kineto.states() expects a named state object.`);let r=new Map(Object.entries(e).map(([e,t])=>[e,hu(t)]));if(!r.size)throw TypeError(`Kineto.states() needs at least one named state.`);let i=new Map,a=new Map,o=new Set,s=null,c=!1,l=e=>{i.has(e)||i.set(e,e.getAttribute(`style`))},u=(e,t)=>{l(e),Object.entries(t).forEach(([t,n])=>{e.style[t]=n})},d=()=>{i.forEach((e,t)=>{(t?.isConnected||t?.style)&&(e==null?t.removeAttribute(`style`):t.setAttribute(`style`,e))}),i.clear()},f=(e,t)=>{e.status||(e.status=t,e.entries.forEach(t=>{a.get(t.el)===e&&a.delete(t.el),t.timer&&clearTimeout(t.timer);try{t.animation?.cancel?.()}catch{}}),o.delete(e),e.resolve({status:t}))},p=e=>f(e,`cancelled`),m=(e,t)=>{let n=e.entries.find(e=>e.el===t&&!e.done);if(n){n.done=!0,e.interrupted=!0,n.timer&&clearTimeout(n.timer);try{n.animation?.cancel?.()}catch{}a.get(t)===e&&a.delete(t),--e.remaining,e.remaining||f(e,`cancelled`)}},h=(e,t,n,r,i,o)=>{l(t);let s=a.get(t);s&&s!==e&&m(s,t),a.set(t,e);let c={el:t,animation:null,timer:null,done:!1};e.entries.push(c);let d=()=>{c.done||e.status||(c.done=!0,u(t,r),a.get(t)===e&&a.delete(t),--e.remaining,e.remaining||f(e,e.interrupted?`cancelled`:`finished`))},h=i.delay+i.stagger*o;if(e.reduced||!i.duration){c.timer=setTimeout(d,h),h||d();return}if(typeof t.animate==`function`){let a=n?[n,r]:[{},r];try{let n=t.animate(a,{duration:i.duration,delay:h,easing:i.ease,fill:`both`});c.animation=n,n.finished.then(d,()=>{!c.done&&!e.status&&p(e)});return}catch{}}c.timer=setTimeout(d,h+i.duration)},g=(e,i,a={})=>{if(c)return Promise.resolve({status:`cancelled`});let l=r.get(i);if(!l)return Promise.reject(RangeError(`Unknown Kineto state: ${i}`));let d=gu(e),f=_u(d,a.children),m=vu(a,t),g=m.beforeChildren?[...f,...d]:[...d,...f],_=[...new Set(g)],v=m.initial,y=v&&v!==!1?r.get(v):null;if(y&&_.forEach(e=>u(e,y)),!_.length)return Promise.resolve({status:`finished`});let b={entries:[],remaining:_.length,status:null,interrupted:!1,resolve:null,reduced:!!(m.reducedMotion===`final`||n?.prefersReducedMotion||W().reducedMotion)},x=new Promise(e=>{b.resolve=e});x.cancel=()=>p(b),o.add(b),s={target:e,stateName:i,options:{...a,initial:!1}};let S=m.duration+m.stagger*Math.max(0,d.length-1),C=m.duration+m.stagger*Math.max(0,f.length-1),w={...m,delay:m.delay+m.delayChildren+(m.afterChildren?S:0)},T={...m,delay:m.delay+(m.beforeChildren?C+m.delayChildren:0)};return _.forEach(e=>{let t=f.includes(e),n=t?w:T,r=t?f.indexOf(e):d.indexOf(e);h(b,e,y||null,l,n,Math.max(0,r))}),x},_={apply:g,replay(e=s?.target,t=s?.stateName,n=s?.options||{}){return!e||!t?Promise.resolve({status:`finished`}):g(e,t,{...n,initial:!1})},scan(e=typeof document<`u`?document:null,t={}){let n=[];return e?.querySelectorAll&&e.querySelectorAll(`[data-kt-state]`).forEach(e=>{let i=e.getAttribute(`data-kt-state`);i&&r.has(i)&&n.push(g(e,i,t))}),Promise.all(n).then(e=>e.at(-1)||{status:`finished`})},destroy(){return c?_:(c=!0,[...o].forEach(p),d(),s=null,_)},get stateNames(){return[...r.keys()]}};return W().ssr&&(_.ssr=!0),_}return Object.entries({parallax:Dn,mouseParallax:On,reveal:Bn,counter:$n,dateTime:ir,lazy:mi,stylize:Ei,textSplit:ji,blurText:Mi,typewriter:Ni,textReveal:Ri,textTransition:qi,magnetic:Zi,marquee:ea,overflowText:ua,loader:Oa,loadingIndicator:vo,tilt:Co,cursor:Xo,textFill:Zo,stickyStack:$o,scrollVelocity:ts,progress:is,slider:ss,ambientMedia:us,pageReveal:ps,glitch:gs,cardGlow:Ds,lightbox:Ls,pageTransition:Ks,vibrate:Js,ripple:Ys,cssScroll:tc,scrollSequence:nc,brushReveal:ic,fullpage:sc,confetti:cc,accordion:lc,hold:fc,megaMenu:Sc,toast:Nc,bottomSheet:Rc,tabs:Vc,radial:Uc,coverReveal:Qc,gesture:tl,drag:nl,tooltip:al,switch:ol,flip:sl,scrollShadows:ll,squircle:Ol,stickyHeader:kl,horizontalScroll:Al,canvasEffect:pu}).forEach(([e,t])=>En.register(e,t)),En.listTerminalFramePresets=qa,En.defineCanvasEffect=pu.define,En.listCanvasEffects=pu.list,En.states=(e,t={})=>yu(e,t,En),pu.define,pu.list,En});
\ No newline at end of file
+`;function tu(e,t){if(t)return e;let n=/\bmainImage\s*\(/.test(e)&&!/\bvoid\s+main\s*\(/.test(e),r=/\bprecision\s+(lowp|mediump|highp)\s+float/.test(e)?``:eu;return n?`${r}${Ql}${e}${$l}`:`${r}${e}`}function nu(e,t,n){let r=e.createShader(t);if(e.shaderSource(r,n),e.compileShader(r),!e.getShaderParameter(r,e.COMPILE_STATUS)){let n=e.getShaderInfoLog(r)||`unknown error`;throw e.deleteShader(r),Error(`[Kineto/canvasEffect] ${t===e.VERTEX_SHADER?`vertex`:`fragment`} shader failed to compile:\n${n}`)}return r}var ru=e=>`u${e[0].toUpperCase()}${e.slice(1)}`;function iu(e,t,n){let r=globalThis.WebGL2RenderingContext,i=typeof r==`function`&&e instanceof r,a=nu(e,e.VERTEX_SHADER,i?Zl:Xl),o=nu(e,e.FRAGMENT_SHADER,tu(t,i)),s=e.createProgram();if(e.attachShader(s,a),e.attachShader(s,o),e.linkProgram(s),!e.getProgramParameter(s,e.LINK_STATUS))throw Error(`[Kineto/canvasEffect] shader program failed to link:\n${e.getProgramInfoLog(s)||`unknown error`}`);let c=e.createBuffer();e.bindBuffer(e.ARRAY_BUFFER,c),e.bufferData(e.ARRAY_BUFFER,new Float32Array([-1,-1,3,-1,-1,3]),e.STATIC_DRAW);let l=e.getAttribLocation(s,`aPosition`),u=new Map,d=t=>(u.has(t)||u.set(t,e.getUniformLocation(s,t)),u.get(t)),f=(t,r)=>{let i=d(t);if(i!=null&&r!=null){if(typeof r==`number`)e.uniform1f(i,r);else if(typeof r==`boolean`)e.uniform1f(i,+!!r);else if(typeof r==`string`){let t=n(r);t&&e.uniform3f(i,t[0],t[1],t[2])}else if(Array.isArray(r)||ArrayBuffer.isView(r)){let t=Array.from(r,Number);t.length===2?e.uniform2f(i,t[0],t[1]):t.length===3?e.uniform3f(i,t[0],t[1],t[2]):t.length===4&&e.uniform4f(i,t[0],t[1],t[2],t[3])}}};return{draw(t,n){e.viewport(0,0,t.canvas.width,t.canvas.height),e.useProgram(s),e.bindBuffer(e.ARRAY_BUFFER,c),e.enableVertexAttribArray(l),e.vertexAttribPointer(l,2,e.FLOAT,!1,0,0);let{width:r,height:i}=t.canvas,{nx:a,ny:o,x:u,y:d,down:p}=t.pointer;f(`uTime`,t.time),f(`uResolution`,[r,i]),f(`uPointer`,[a,1-o]),f(`uPointerDown`,p),f(`uScroll`,t.scroll.progress),f(`iTime`,t.time),f(`iResolution`,[r,i,1]),f(`iMouse`,[u*t.dpr,i-d*t.dpr,+!!p,0]),Object.entries(t.options).forEach(([e,t])=>f(ru(e),t)),n&&Object.entries(n).forEach(([e,t])=>f(e,t)),e.drawArrays(e.TRIANGLES,0,3)},destroy(){e.deleteBuffer(c),e.deleteProgram(s),e.deleteShader(a),e.deleteShader(o)}}}var au=`kt-canvas-effect`,ou=`kt-canvas-effect__canvas`,su={still:`is-still`,unsupported:`is-unsupported`,failed:`is-failed`},cu=1/15,lu=null,uu=`#010203`;function du(e){if(typeof document>`u`||(lu||=document.createElement(`canvas`).getContext(`2d`),!lu))return null;lu.fillStyle=uu,lu.fillStyle=e;let t=String(lu.fillStyle);if(t===uu&&e.trim().toLowerCase()!==uu)return null;if(t[0]===`#`){let e=parseInt(t.slice(1,7),16);return[(e>>16&255)/255,(e>>8&255)/255,(e&255)/255,1]}let n=t.match(/[\d.]+/g);return n&&n.length>=3?[n[0]/255,n[1]/255,n[2]/255,n[3]==null?1:Number(n[3])]:null}function fu(e){return{color:e.color,color2:e.color2,background:e.background,speed:e.speed,density:e.density,size:e.size,strength:e.strength,distortion:e.distortion}}function pu(e,t){let n={};return Object.entries(e).forEach(([e,r])=>{let i=t[e];i==null||i===``?n[e]=r:typeof r==`number`?n[e]=X(i,r):typeof r==`boolean`?n[e]=i===!0||i===`true`?!0:i===!1||i===`false`?!1:r:typeof r==`string`?n[e]=String(i):Array.isArray(r)?n[e]=Array.isArray(i)&&i.length===r.length&&i.every(e=>Number.isFinite(Number(e)))?i.map(Number):r:n[e]=i}),n}var mu=new Set;function hu(e,t,n){Object.entries(fu(n)).forEach(([n,r])=>{if(r===void 0||Object.prototype.hasOwnProperty.call(t.options,n))return;let i=`${e}:${n}`;mu.has(i)||(mu.add(i),console.warn(`[Kineto/canvasEffect] "${e}" does not read "${n}" (it reads: ${Object.keys(t.options).join(`, `)||`no options`}).`))})}function gu(e,t,n,r){let i=t.effect??t.preset;if(typeof i!=`string`)return i!=null&&console.warn("[Kineto/canvasEffect] `effect` must name an effect registered with Kineto.defineCanvasEffect(); definitions are not accepted as options."),null;let a=Wl(i);if(!a)return Gl(i,e,()=>n.create(`canvasEffect`,e,t)),null;hu(i,a,t);let o=ql(t.quality,{performance:n.performance,touch:!!n.env?.touch}),s=Kl[o],c={maxDpr:X(t.maxDpr,s.maxDpr,.5,4),fps:X(t.fps,s.fps,0,240),pointer:[`host`,`window`,`none`].includes(t.pointer)?t.pointer:`host`},l=e.tagName===`CANVAS`,u=l?e:document.createElement(`canvas`),d=$(e,[`position`,`isolation`]),f=e.classList.contains(au);e.classList.add(au),l||(getComputedStyle(e).position===`static`&&(e.style.position=`relative`),e.style.isolation=`isolate`,u.className=ou,u.setAttribute(`aria-hidden`,`true`),e.insertBefore(u,e.firstChild));let p=!0,m=!1,h=!0,g=!1,_=!1,v=null,y=null,b=0,x=null,S=null,C=!1,w=new Map,T=t=>{if(Array.isArray(t))return t;let n=String(t??``).trim();if(!w.has(n)){let t=/^var\(\s*(--[\w-]+)\s*(?:,\s*(.+))?\)$/.exec(n),r=t?getComputedStyle(e).getPropertyValue(t[1]).trim()||t[2]||``:n;w.set(n,r?du(r):null)}return w.get(n)},E=Jl({el:e,target:c.pointer,onInput:()=>I()}),D=Yl({enabled:t.scroll!==!1,onInput:()=>I()}),O={el:e,canvas:u,context:a.context,ctx:null,gl:null,options:pu(a.options,t),state:void 0,width:0,height:0,dpr:1,time:0,delta:0,frame:0,pointer:E.view,scroll:D.view,quality:o,reducedMotion:r,color:T,wake:()=>I()},k=(t,n)=>e.classList.toggle(su[t],n),A=n=>{g=!0,L(),k(`failed`,!0),console.error(`[Kineto/canvasEffect] "${i}" stopped:`,n);try{t.onError?.(n,e)}catch{}},j=()=>{let e=a.context!==`2d`,t=e?{alpha:!0,antialias:!1,depth:!1,stencil:!1,premultipliedAlpha:!0,preserveDrawingBuffer:!1,powerPreference:o===`high`?`default`:`low-power`}:{alpha:!0},n=u.getContext(a.context,t);return n?(x=n,O.ctx=n,O.gl=e?n:null,a.fragment&&(S=iu(n,a.fragment,T)),!0):!1},M=()=>j()?(O.state=a.setup?a.setup(O):void 0,C=!0,!0):(k(`unsupported`,!0),!1),N=(e,t)=>{let n=Math.min(typeof window<`u`&&window.devicePixelRatio||1,c.maxDpr),r=Math.max(1,Math.round(e*n)),i=Math.max(1,Math.round(t*n));O.width=e,O.height=t,O.dpr=n,u.width!==r&&(u.width=r),u.height!==i&&(u.height=i),C&&a.resize?.(O)},P=t=>{let n=e.getBoundingClientRect(),i=y==null?0:Math.min(cu,Math.max(0,(t-y)/1e3));y=t,O.delta=r?0:i,O.time+=O.delta,O.frame+=1,E.sample(n,i),D.sample(n,i);let o=a.frame?a.frame(O):void 0;return S&&S.draw(O,a.uniforms?a.uniforms(O):null),S&&!a.frame&&(o=void 0),o},F=e=>{if(v=null,!p||m||g||_){h=!0;return}if(c.fps>0&&b&&e-b<1e3/c.fps-1){v=requestAnimationFrame(F);return}b=e;let t;try{t=P(e)}catch(e){A(e);return}if(r||t===!1){h=!0,y=null;return}v=requestAnimationFrame(F)};function I(){p&&!m&&!g&&!_&&C&&h&&(h=!1,v=requestAnimationFrame(F))}function L(){v!=null&&cancelAnimationFrame(v),v=null,h=!0,y=null}let R=e=>{e.preventDefault(),_=!0,L()},z=()=>{_=!1,S=null;try{M(),N(O.width,O.height),I()}catch(e){A(e)}};a.context!==`2d`&&(u.addEventListener(`webglcontextlost`,R),u.addEventListener(`webglcontextrestored`,z));let B=null,V=(e,t)=>{try{if(N(e,t),!C&&!M())return;if(a.resize?.(O),k(`still`,r),C&&r){P(typeof performance<`u`?performance.now():0);return}I()}catch(e){A(e)}};return typeof ResizeObserver<`u`?(B=new ResizeObserver(()=>{if(m)return;let t={width:e.clientWidth,height:e.clientHeight};if(!C&&!g){V(t.width,t.height);return}if(!g&&C&&(t.width!==Math.round(O.width)||t.height!==Math.round(O.height)))try{N(t.width,t.height),r?P(performance.now()):I()}catch(e){A(e)}}),B.observe(e)):V(e.clientWidth,e.clientHeight),{el:e,type:`canvasEffect`,get effect(){return i},get quality(){return o},redraw(){if(r&&C){try{P(performance.now())}catch(e){A(e)}return}I()},pause(){p=!1,L()},resume(){p||(p=!0,I())},update(e,t){if((e.effect!=null||e.preset!=null)&&Wl(t.effect??t.preset)!==a)return!1;if(O.options=pu(a.options,t),w.clear(),!C)return!0;try{a.resize?.(O),r?P(performance.now()):I()}catch(e){A(e)}return!0},destroy(){if(!m){m=!0,L(),B?.disconnect(),E.destroy(),D.destroy(),u.removeEventListener(`webglcontextlost`,R),u.removeEventListener(`webglcontextrestored`,z);try{C&&a.destroy?.(O)}catch(e){console.error(`[Kineto/canvasEffect] destroy() failed:`,e)}S?.destroy(),O.gl&&O.gl.getExtension(`WEBGL_lose_context`)?.loseContext(),l?x&&a.context===`2d`&&x.clearRect(0,0,u.width,u.height):u.remove(),Object.values(su).forEach(t=>e.classList.remove(t)),f||e.classList.remove(au),d()}}}}var _u={offscreen:e=>e.pauseOffscreen===!1?null:`pause`,defer:!0,create(e,t={},n){return gu(e,t,n,!1)},reduced(e,t={},n){return gu(e,t,n,t.reducedMotion!==`run`)},define:Hl,list:Ul},vu=e=>`${X(e)}px`;function yu(e={}){if(!e||typeof e!=`object`)throw TypeError(`Kineto.states state values must be objects.`);let t={};e.opacity!=null&&(t.opacity=String(Math.max(0,Math.min(1,X(e.opacity,1)))));let n=[];e.x!=null&&n.push(`translateX(${vu(e.x)})`),e.y!=null&&n.push(`translateY(${vu(e.y)})`),e.scale!=null&&n.push(`scale(${X(e.scale,1)})`),e.rotate!=null&&n.push(`rotate(${X(e.rotate)}deg)`),e.skewX!=null&&n.push(`skewX(${X(e.skewX)}deg)`),e.skewY!=null&&n.push(`skewY(${X(e.skewY)}deg)`),e.transform!=null&&n.push(String(e.transform)),n.length&&(t.transform=n.join(` `));let r=[];return e.blur!=null&&r.push(`blur(${vu(e.blur)})`),e.brightness!=null&&r.push(`brightness(${X(e.brightness,1)})`),e.filter!=null&&r.push(String(e.filter)),r.length&&(t.filter=r.join(` `)),t}function bu(e){return pe(e).filter(e=>e?.nodeType===1)}function xu(e,t){if(!t)return[];if(typeof t!=`string`)return bu(t);let n=[];return e.forEach(e=>{try{n.push(...e.querySelectorAll(t))}catch{}}),[...new Set(n)]}function Su(e,t){let n={...t,...e};return{duration:Math.max(0,X(n.duration,300)),delay:Math.max(0,X(n.delay,0)),stagger:Math.max(0,X(n.stagger,0)),ease:V(n.ease||`ease`),initial:n.initial,beforeChildren:n.beforeChildren===!0,afterChildren:n.afterChildren===!0,delayChildren:Math.max(0,X(n.delayChildren,0)),reducedMotion:n.reducedMotion}}function Cu(e={},t={},n=null){if(!e||typeof e!=`object`||Array.isArray(e))throw TypeError(`Kineto.states() expects a named state object.`);let r=new Map(Object.entries(e).map(([e,t])=>[e,yu(t)]));if(!r.size)throw TypeError(`Kineto.states() needs at least one named state.`);let i=new Map,a=new Map,o=new Set,s=null,c=!1,l=e=>{i.has(e)||i.set(e,e.getAttribute(`style`))},u=(e,t)=>{l(e),Object.entries(t).forEach(([t,n])=>{e.style[t]=n})},d=()=>{i.forEach((e,t)=>{(t?.isConnected||t?.style)&&(e==null?t.removeAttribute(`style`):t.setAttribute(`style`,e))}),i.clear()},f=(e,t)=>{e.status||(e.status=t,e.entries.forEach(t=>{a.get(t.el)===e&&a.delete(t.el),t.timer&&clearTimeout(t.timer);try{t.animation?.cancel?.()}catch{}}),o.delete(e),e.resolve({status:t}))},p=e=>f(e,`cancelled`),m=(e,t)=>{let n=e.entries.find(e=>e.el===t&&!e.done);if(n){n.done=!0,e.interrupted=!0,n.timer&&clearTimeout(n.timer);try{n.animation?.cancel?.()}catch{}a.get(t)===e&&a.delete(t),--e.remaining,e.remaining||f(e,`cancelled`)}},h=(e,t,n,r,i,o)=>{l(t);let s=a.get(t);s&&s!==e&&m(s,t),a.set(t,e);let c={el:t,animation:null,timer:null,done:!1};e.entries.push(c);let d=()=>{c.done||e.status||(c.done=!0,u(t,r),a.get(t)===e&&a.delete(t),--e.remaining,e.remaining||f(e,e.interrupted?`cancelled`:`finished`))},h=i.delay+i.stagger*o;if(e.reduced||!i.duration){c.timer=setTimeout(d,h),h||d();return}if(typeof t.animate==`function`){let a=n?[n,r]:[{},r];try{let n=t.animate(a,{duration:i.duration,delay:h,easing:i.ease,fill:`both`});c.animation=n,n.finished.then(d,()=>{!c.done&&!e.status&&p(e)});return}catch{}}c.timer=setTimeout(d,h+i.duration)},g=(e,i,a={})=>{if(c)return Promise.resolve({status:`cancelled`});let l=r.get(i);if(!l)return Promise.reject(RangeError(`Unknown Kineto state: ${i}`));let d=bu(e),f=xu(d,a.children),m=Su(a,t),g=m.beforeChildren?[...f,...d]:[...d,...f],_=[...new Set(g)],v=m.initial,y=v&&v!==!1?r.get(v):null;if(y&&_.forEach(e=>u(e,y)),!_.length)return Promise.resolve({status:`finished`});let b={entries:[],remaining:_.length,status:null,interrupted:!1,resolve:null,reduced:!!(m.reducedMotion===`final`||n?.prefersReducedMotion||W().reducedMotion)},x=new Promise(e=>{b.resolve=e});x.cancel=()=>p(b),o.add(b),s={target:e,stateName:i,options:{...a,initial:!1}};let S=m.duration+m.stagger*Math.max(0,d.length-1),C=m.duration+m.stagger*Math.max(0,f.length-1),w={...m,delay:m.delay+m.delayChildren+(m.afterChildren?S:0)},T={...m,delay:m.delay+(m.beforeChildren?C+m.delayChildren:0)};return _.forEach(e=>{let t=f.includes(e),n=t?w:T,r=t?f.indexOf(e):d.indexOf(e);h(b,e,y||null,l,n,Math.max(0,r))}),x},_={apply:g,replay(e=s?.target,t=s?.stateName,n=s?.options||{}){return!e||!t?Promise.resolve({status:`finished`}):g(e,t,{...n,initial:!1})},scan(e=typeof document<`u`?document:null,t={}){let n=[];return e?.querySelectorAll&&e.querySelectorAll(`[data-kt-state]`).forEach(e=>{let i=e.getAttribute(`data-kt-state`);i&&r.has(i)&&n.push(g(e,i,t))}),Promise.all(n).then(e=>e.at(-1)||{status:`finished`})},destroy(){return c?_:(c=!0,[...o].forEach(p),d(),s=null,_)},get stateNames(){return[...r.keys()]}};return W().ssr&&(_.ssr=!0),_}return Object.entries({parallax:jn,mouseParallax:Mn,reveal:Wn,counter:rr,dateTime:cr,lazy:vi,stylize:Ai,textSplit:Fi,blurText:Ii,typewriter:Li,textReveal:Hi,textTransition:Zi,magnetic:ta,marquee:ia,overflowText:ma,loader:Ma,loadingIndicator:So,tilt:Do,cursor:es,textFill:ts,stickyStack:rs,scrollVelocity:as,progress:cs,slider:ds,ambientMedia:ms,pageReveal:_s,glitch:bs,cardGlow:js,lightbox:Vs,pageTransition:Xs,vibrate:Qs,ripple:$s,cssScroll:ac,scrollSequence:oc,brushReveal:cc,fullpage:dc,confetti:fc,accordion:pc,hold:gc,megaMenu:Ec,toast:Lc,bottomSheet:Hc,tabs:Gc,radial:qc,coverReveal:nl,gesture:al,drag:ol,tooltip:ll,switch:ul,flip:dl,scrollShadows:pl,squircle:Ml,stickyHeader:Nl,horizontalScroll:Pl,canvasEffect:_u}).forEach(([e,t])=>An.register(e,t)),An.listTerminalFramePresets=Za,An.defineCanvasEffect=_u.define,An.listCanvasEffects=_u.list,An.states=(e,t={})=>Cu(e,t,An),_u.define,_u.list,An});
\ No newline at end of file
diff --git a/site/llms.txt b/site/llms.txt
index 7d6beaf..e63ec9c 100644
--- a/site/llms.txt
+++ b/site/llms.txt
@@ -1,6 +1,6 @@
# Kineto
-> @dong-gri/kineto v0.13.0 — vanilla-first web motion & media effects library (53 modules, React/Vue/jQuery adapters, zero runtime dependencies). Attach effects with data-kt-* attributes on any markup, including shadcn/ui, Bootstrap, MUI, Mantine, Chakra, Ant Design, daisyUI, Nuxt UI, PrimeVue and Vuetify components.
+> @dong-gri/kineto v0.13.1 — vanilla-first web motion & media effects library (53 modules, React/Vue/jQuery adapters, zero runtime dependencies). Attach effects with data-kt-* attributes on any markup, including shadcn/ui, Bootstrap, MUI, Mantine, Chakra, Ant Design, daisyUI, Nuxt UI, PrimeVue and Vuetify components.
## Start here
diff --git a/site/r/ai-rules.json b/site/r/ai-rules.json
index acd98c2..32faeac 100644
--- a/site/r/ai-rules.json
+++ b/site/r/ai-rules.json
@@ -10,13 +10,13 @@
"path": "ai/cursor/kineto.mdc",
"type": "registry:file",
"target": "~/.cursor/rules/kineto.mdc",
- "content": "---\ndescription: Use Kineto (@dong-gri/kineto) motion and media effects correctly next to shadcn/ui, Bootstrap, MUI, Mantine, Chakra, Ant Design, daisyUI, Nuxt UI, PrimeVue and Vuetify, including Figma-to-code flows.\nglobs: [\"**/*.tsx\", \"**/*.jsx\", \"**/*.vue\", \"**/*.html\", \"**/*.astro\", \"**/*.svelte\"]\nalwaysApply: false\n---\n\n# Kineto rules for coding agents (v0.13.0)\n\nKineto (`@dong-gri/kineto`) is a vanilla-first motion/effects library: 55 modules activated by `data-kt-*` attributes or `Kineto.create()`, with React/Vue/jQuery adapters. Use these rules whenever you generate UI with any component library (shadcn/ui, Bootstrap, MUI, Mantine, Chakra, Ant Design, daisyUI, Nuxt UI, PrimeVue, Vuetify) or from a Figma frame.\n\n## Principles\n\n- Kineto adds motion and media effects to markup you already own; it does not replace the component library's behaviour (dialogs, menus, forms, routing).\n- Prefer the library's own component when it already provides the behaviour (toast, tooltip, tabs, accordion, dialog, carousel). Use Kineto for what the library does not ship: entrance motion, text motion, counters, image loading effects, hover/pointer effects, marquee, page reveal, cursor, scroll storytelling.\n- Never put two things that animate the same property on one element: a Tailwind/tw-animate-css enter animation plus a Kineto reveal, a Bootstrap carousel plus a Kineto slider, two Kineto modules that both write transform (see docs/rfc/module-composition.md).\n- Attach with `data-kt-*` attributes on the element that reaches the DOM, call `Kineto.observe()` once at startup so markup added later (framework renders, portals, modals) is picked up and cleaned up, and reach for the React/Vue adapters only when you need the instance (replay, pause, presence).\n- Respect `prefers-reduced-motion`: Kineto already renders the meaningful end state for every module, so do not add a second reduced-motion branch around it.\n- Read the exact option names from kineto.features.json (or `npx @dong-gri/kineto-mcp`); do not invent options.\n\n## Setup (once per app)\n\n```js\nimport '@dong-gri/kineto/style.css';\nimport Kineto from '@dong-gri/kineto';\nKineto.observe(); // scans now and follows later DOM changes\n```\n\nReact: mount a client `KinetoProvider` that calls `Kineto.observe()` in an effect (shadcn registry: `npx shadcn@latest add @kineto/provider`). Vue/Nuxt: a client plugin that calls `Kineto.observe()`. Static HTML: the CDN snippet with `Kineto.observe()`.\n\n## Attachment grammar\n\n- Activation: `data-kt-=\"\"` (omit the value for the default variant). Options: `data-kt-=\"value\"`; booleans as bare attributes; arrays/objects as JSON strings.\n- JavaScript: `Kineto.create('', element, { : '', ...options })`. React: `useKineto('', options)` / `\" options={...}>`. Vue: `v-motion=\"{ type, options }\"` / `useKineto()`.\n- Component libraries forward unknown props/attrs to their root DOM node, so `data-kt-*` on ``, ``, ``, `` … reaches the DOM. If a component does not forward attributes, wrap it in a plain element or use a ref.\n- One transform-writing module per element (tilt, gesture, magnetic, scrollVelocity, parallax, flip). One reveal per section, staggered, rather than one per item.\n\n## Decide by intent\n\n| Intent | Default recipe | When the library already has it |\n|---|---|---|\n| hero-headline — Hero headline entrance (hero, headline, key visual, kv) | `data-kt-text-reveal=\"stream\" data-kt-speed=\"30\"` | Kineto provides it |\n| section-entrance — Section / card entrance on scroll (fade in, scroll reveal, entrance, appear) | `data-kt-reveal=\"fade-up\" data-kt-once data-kt-duration=\"0.7\"` | Kineto provides it |\n| card-hover — Card hover: tilt, glow, spring (card hover, 3d tilt, glow, spotlight) | `data-kt-tilt=\"tilt-glare\" data-kt-max=\"8\"` | Kineto provides it |\n| cta-button — Call-to-action button feedback (button, cta, click feedback, ripple) | `data-kt-magnetic=\"pointer\" data-kt-strength=\"0.3\"` | Kineto provides it |\n| kpi-number — KPI / statistic count-up (counter, count up, kpi, stat) | `data-kt-counter=\"slot\" data-kt-to=\"128000\" data-kt-format=\",\"` | Kineto provides it |\n| countdown — Countdown / elapsed clock (countdown, timer, until, clock) | `data-kt-counter=\"clock\" data-kt-until=\"2026-12-31T00:00:00+09:00\" data-kt-show-days` | Kineto provides it |\n| image-loading — Image loading effect / placeholder (lazy load, skeleton, blur up, placeholder) | `data-kt-lazy=\"skeleton\" data-kt-skeleton-variant=\"shimmer\"` | Kineto provides it |\n| media-texture — Graphic texture on an image or video (dither, ascii art, halftone, duotone) | `data-kt-stylize=\"dither\" data-kt-cell-size=\"8\" data-kt-dither-type=\"4x4\"` | Kineto provides it |\n| gallery-lightbox — Image gallery with lightbox (lightbox, gallery, zoom, viewer) | `data-kt-lightbox=\"grouped\" data-kt-group=\"gallery\"` | Kineto provides it |\n| interactive-reveal — Brush / scratch-to-reveal media (brush reveal, scratch, scratch card, drag to reveal) | `data-kt-brush-reveal data-kt-radius=\"48\" data-kt-softness=\"0.6\"` | Kineto provides it |\n| carousel — Carousel / slider (carousel, slider, swiper, slides) | `data-kt-slider=\"slide\" data-kt-loop data-kt-autoplay=\"4000\"` | prefer the library (shadcn: Carousel (Embla); bootstrap: .carousel; mantine: @mantine/carousel; antd: Carousel; daisyui: .carousel; nuxt-ui: UCarousel; primevue: Carousel; vuetify: v-carousel) |\n| marquee-ticker — Marquee / ticker (marquee, ticker, logo wall, scrolling text) | `data-kt-marquee=\"left\" data-kt-speed=\"60\" data-kt-pause-on-hover` | Kineto provides it |\n| toast — Toast notification (toast, snackbar, notification, 알림) | `data-kt-toast data-kt-position=\"bottom-right\" data-kt-duration=\"4000\"` | prefer the library (shadcn: Sonner / Toast; bootstrap: .toast; mui: Snackbar; mantine: @mantine/notifications; chakra: Toaster; antd: message / notification; daisyui: .toast (static); nuxt-ui: useToast; primevue: Toast; vuetify: v-snackbar) |\n| tooltip — Tooltip (tooltip, hint, 툴팁) | `data-kt-tooltip data-kt-placement=\"top\"` | prefer the library (shadcn: Tooltip; bootstrap: data-bs-toggle=\"tooltip\"; mui: Tooltip; mantine: Tooltip; chakra: Tooltip; antd: Tooltip; daisyui: .tooltip; nuxt-ui: UTooltip; primevue: v-tooltip; vuetify: v-tooltip) |\n| dialog-enter-exit — Dialog / modal enter-exit motion (dialog, modal, enter exit, mount unmount) | `data-kt-presence` | keep the library component, animate its content (shadcn: Dialog (tw-animate-css); bootstrap: .modal.fade; mui: Dialog + Grow; mantine: Modal; chakra: Dialog; antd: Modal; nuxt-ui: UModal; primevue: Dialog; vuetify: v-dialog) |\n| bottom-sheet — Mobile bottom sheet (bottom sheet, drawer, sheet, 바텀시트) | `data-kt-bottom-sheet data-kt-resizable data-kt-handle` | prefer the library (shadcn: Drawer (vaul) / Sheet; bootstrap: .offcanvas; mui: SwipeableDrawer; mantine: Drawer; chakra: Drawer; antd: Drawer; nuxt-ui: USlideover; primevue: Sidebar; vuetify: v-bottom-sheet) |\n| page-transition — Page transition and first-paint reveal (page transition, route transition, page reveal, curtain) | `data-kt-page-reveal=\"curtain\" data-kt-color=\"#0a0908\"` | Kineto provides it |\n| scroll-progress — Reading / scroll progress (scroll progress, reading progress, progress bar, back to top) | `data-kt-progress=\"page:scaleX\" data-kt-position=\"top\"` | Kineto provides it |\n| sticky-header — Sticky header that shrinks or hides on scroll (sticky header, navbar, shrink, hide on scroll) | `data-kt-sticky-header=\"shrink\" data-kt-distance=\"80\"` | keep the library component, animate its content (bootstrap: .navbar; shadcn: NavigationMenu; mui: AppBar; vuetify: v-app-bar) |\n| parallax-depth — Parallax and pointer depth (parallax, depth, layers, mouse parallax) | `data-kt-parallax=\"y\" data-kt-speed=\"0.3\"` | Kineto provides it |\n| scroll-storytelling — Scroll storytelling (sequence, pinned stacks, horizontal) (scrollytelling, image sequence, pinned, sticky stack) | `data-kt-scroll-sequence=\"cover\" data-kt-frames=\"120\"` | Kineto provides it |\n| loading-state — Inline loading indicator / real progress (spinner, loading, progress, shimmer) | `data-kt-loading-indicator=\"spinner\"` | prefer the library (shadcn: Spinner / Progress; bootstrap: .spinner-border; mui: CircularProgress; mantine: Loader; chakra: Spinner; antd: Spin; daisyui: .loading; nuxt-ui: UProgress; primevue: ProgressSpinner; vuetify: v-progress-circular) |\n| text-rotation — Rotating words / typewriter (rotating text, typewriter, word swap, typing) | `data-kt-text-transition=\"slide-up\" data-kt-texts=\"["design","motion","ship"]\" data-kt-hold=\"1.4\"` | Kineto provides it |\n| glitch-accent — Glitch / CRT accent (glitch, crt, vhs, datamosh) | `data-kt-glitch=\"rgb\"` | Kineto provides it |\n| custom-cursor — Custom cursor (cursor, custom cursor, pointer, 커서) | `data-kt-cursor=\"ring\"` | Kineto provides it |\n| celebration — Success celebration (confetti, celebrate, success, reward) | `data-kt-confetti data-kt-trigger=\"click\"` | Kineto provides it |\n| overflow-shadows — Scrollable area edge shadows (scroll shadow, overflow, scroll area, fade edges) | `data-kt-scroll-shadows=\"horizontal\"` | keep the library component, animate its content (shadcn: ScrollArea; mantine: ScrollArea; chakra: ScrollArea) |\n| corner-shape — iOS-style corner shape (squircle) (squircle, superellipse, corner shape, corner smoothing) | `data-kt-squircle=\"squircle\"` | keep the library component, animate its content (tailwind: rounded-* (circular corners only); shadcn: --radius (circular corners only); mui: shape.borderRadius (circular corners only)) |\n| interactive-background — Interactive canvas / WebGL background (React Bits–style) (react bits, background effect, interactive background, shader) | `data-kt-canvas-effect` | Kineto provides it |\n| confirm-hold — Hold-to-confirm and mash gauges (hold to confirm, long press, mash, confirm) | `data-kt-hold=\"confirm\" data-kt-duration=\"1.2\"` | Kineto provides it |\n| disclosure — Accordion, tabs, switch, mega menu (behaviour components) (accordion, tabs, switch, toggle) | `data-kt-accordion data-kt-single` | prefer the library (shadcn: Accordion / Tabs / Switch / NavigationMenu; bootstrap: .accordion / .nav-tabs / .form-switch / .navbar; mui: Accordion / Tabs / Switch / Menu; mantine: Accordion / Tabs / Switch; chakra: Accordion / Tabs / Switch; antd: Collapse / Tabs / Switch / Menu; daisyui: .collapse / .tabs / .toggle; nuxt-ui: UAccordion / UTabs / USwitch; primevue: Accordion / Tabs / ToggleSwitch / MegaMenu; vuetify: v-expansion-panels / v-tabs / v-switch) |\n| layout-change — Animated reorder / add / remove (FLIP) (flip, reorder, list animation, layout animation) | `data-kt-flip=\"layout\" data-kt-watch` | Kineto provides it |\n| drag-and-fullpage — Draggable elements and fullpage sections (drag, draggable, fullpage, snap sections) | `data-kt-drag data-kt-inertia data-kt-snap-back` | Kineto provides it |\n\n## Figma layer → intent\n\n- node is a TEXT layer with font size ≥ 40 or named like a heading → `hero-headline`\n- node is an IMAGE fill (RECTANGLE/FRAME with image paint) or named image/photo/thumbnail → `image-loading`\n- node repeats as a grid/list of similar frames (auto-layout children with the same component) → `section-entrance`\n- node is a COMPONENT INSTANCE named Button/CTA → `cta-button`\n- node text is mostly digits (counts, KRW/USD, percentages) → `kpi-number`\n- frame is wider than the viewport and clips horizontally repeating children → `marquee-ticker`\n- frame has prototype interaction 'On drag' or scroll horizontally → `carousel`\n- Layer-name regexes per intent: hero-headline (/hero/i /kv/i /key ?visual/i /headline/i /title/i /h1/i); section-entrance (/section/i /card/i /feature/i /grid/i /list item/i /row/i); card-hover (/card/i /product card/i /tile/i /thumbnail/i); cta-button (/button/i /cta/i /primary button/i); kpi-number (/stat/i /kpi/i /number/i /metric/i /counter/i /\\d{2,}/i); countdown (/countdown/i /timer/i /d-day/i /clock/i); image-loading (/image/i /photo/i /thumbnail/i /cover/i /gallery/i); media-texture (/dither/i /halftone/i /ascii/i /texture/i /grain/i /duotone/i /poster/i /print/i /newsprint/i /screen/i); gallery-lightbox (/gallery/i /lightbox/i /grid/i /masonry/i /photo/i); interactive-reveal (/scratch/i /brush/i /reveal/i /mask/i); carousel (/carousel/i /slider/i /swiper/i /slides/i); marquee-ticker (/marquee/i /ticker/i /logo/i /partners/i /ranking/i); toast (/toast/i /snackbar/i /notification/i /alert/i); tooltip (/tooltip/i /hint/i); dialog-enter-exit (/modal/i /dialog/i /overlay/i /popup/i); bottom-sheet (/bottom sheet/i /sheet/i /drawer/i); page-transition (/intro/i /splash/i /loading/i /transition/i); scroll-progress (/progress/i /reading bar/i /back to top/i); sticky-header (/header/i /navbar/i /gnb/i /top bar/i); parallax-depth (/layer/i /bg/i /background/i /fg/i /foreground/i /parallax/i); scroll-storytelling (/sequence/i /frames/i /pinned/i /story/i /horizontal/i); loading-state (/spinner/i /loading/i /loader/i /progress/i); text-rotation (/typing/i /rotating/i /tagline/i); glitch-accent (/glitch/i /crt/i /vhs/i /neon/i /cyber/i /game/i); custom-cursor (/cursor/i); celebration (/success/i /complete/i /reward/i /confetti/i); overflow-shadows (/list/i /scroll area/i /table/i /chips/i); corner-shape (/card/i /button/i /app icon/i /avatar/i /tile/i /sheet/i); interactive-background (/hero/i /background/i /banner/i /section/i /card/i); confirm-hold (/confirm/i /delete/i /hold/i); disclosure (/accordion/i /faq/i /tabs/i /segmented/i /switch/i /toggle/i /menu/i); layout-change (/list/i /grid/i /kanban/i /board/i); drag-and-fullpage (/draggable/i /fullpage/i /sections/i)\n\n## Per-ecosystem notes\n\n- **vanilla** (data-attributes; provides: nothing — vanilla): Every Kineto module is available; there is no library component to defer to. Multi-page sites can add `data-kt-page-transition` on for same-origin page transitions and `data-kt-page-reveal` for the first-paint overlay.\n- **shadcn** (props-passthrough; provides: accordion, tabs, tooltip, toast, dialog, sheet, carousel, skeleton, progress, switch, navigation-menu, scroll-area): tw-animate-css (`animate-in`, `fade-in-0`, `zoom-in-95` …) already animates Dialog/Sheet/Popover content — do not add a Kineto reveal to the same node; animate the children instead. shadcn `Carousel` (Embla) owns the track; put Kineto reveal/tilt on the slides, never `data-kt-slider` on the same container. shadcn `Skeleton` is a layout placeholder; for images prefer `data-kt-lazy=\"skeleton\"` on the ` ` itself so the placeholder resolves into the real picture. Next.js App Router: the provider is a client component; server components can still render `data-kt-*` attributes. Tailwind v4 utilities and Kineto never fight over layout: Kineto only writes transform/opacity/filter/clip on the host and creates its own wrapper layers.\n- **tailwind** (data-attributes; provides: nothing — vanilla): `animate-*` keyframe utilities and `transition-transform` on the same element as `data-kt-reveal`, `data-kt-gesture` or `data-kt-tilt` double-animate the host. daisyUI, Flowbite and Preline are plain class-based markup on top of Tailwind; everything in this entry applies to them.\n- **bootstrap** (data-attributes; provides: accordion, collapse, tabs, tooltip, popover, toast, modal, offcanvas, carousel, placeholder, spinner, progress, dropdown, navbar): `.carousel` (Bootstrap) and `data-kt-slider` on the same element: choose one. Bootstrap carousel for content slides, Kineto slider when you want coverflow/cube/cards effects. `.placeholder` / `.placeholder-glow` and `data-kt-lazy=\"skeleton\"`: choose one placeholder per image. `data-bs-toggle=\"tooltip\"` and `data-kt-tooltip` on one element create two tooltips. `.accordion` (Bootstrap collapse) and `data-kt-accordion`: Bootstrap owns open/close; add Kineto reveal to the panel content instead. Bootstrap's `.fade` modal transition stays; animate the modal body with `data-kt-reveal` or `data-kt-text-split` for richer entrances. Bootstrap's grid and utilities are unaffected; Kineto never sets layout properties on the host.\n- **mui** (props-passthrough; provides: accordion, tabs, tooltip, snackbar, dialog, drawer, skeleton, progress, switch, menu): MUI `Grow`/`Fade`/`Slide` transitions wrap Dialog and Menu content; do not add a Kineto reveal to the same node. MUI `ButtonBase` already renders a Material ripple (`TouchRipple`); use `data-kt-magnetic` or `data-kt-gesture` on buttons, not `data-kt-ripple`. Emotion class names are left untouched; Kineto writes inline styles it later restores on destroy.\n- **mantine** (props-passthrough; provides: accordion, tabs, tooltip, notifications, modal, drawer, skeleton, progress, switch, carousel): `@mantine/carousel` (Embla) and `data-kt-slider` on one element: choose one. Mantine `Transition`/`Collapse` already animate their child; add Kineto to the content, not the transition node.\n- **chakra** (props-passthrough; provides: accordion, tabs, tooltip, toaster, dialog, drawer, skeleton, progress, switch, menu): Chakra `Presence`/`Dialog` animate their positioner via `data-state` keyframes; do not add a Kineto reveal to the same node.\n- **antd** (props-passthrough-or-wrapper; provides: collapse, tabs, tooltip, message, notification, modal, drawer, skeleton, progress, switch, carousel, dropdown): Ant Design `Carousel` (react-slick) and `data-kt-slider` on one element: choose one. `Statistic` has its own count-up animation when `formatter` uses CountUp; use either that or `data-kt-counter`, not both.\n- **daisyui** (data-attributes; provides: skeleton, tooltip, tabs, collapse, toast, carousel, progress, modal, drawer): daisyUI `carousel` is CSS scroll-snap; `data-kt-slider` on the same element takes over the track — choose one. daisyUI `skeleton` boxes stay for text; use `data-kt-lazy=\"skeleton\"` on images.\n- **nuxt-ui** (attrs-fallthrough; provides: accordion, tabs, tooltip, toast, modal, slideover, skeleton, progress, switch, carousel, navigation-menu): Nuxt UI `UCarousel` (Embla) and `data-kt-slider`: choose one per container. Reka UI content nodes animate through `data-state`; add Kineto to their children. Components rendered on the server keep the attributes, so hydration sees the same markup the observer will scan.\n- **primevue** (attrs-fallthrough; provides: accordion, tabs, tooltip, toast, dialog, sidebar, skeleton, progress, toggleswitch, carousel, galleria, megamenu): PrimeVue `Carousel`/`Galleria` and `data-kt-slider` on the same element: choose one. PrimeVue `Ripple` directive and `data-kt-ripple` on one button: choose one.\n- **vuetify** (attrs-fallthrough; provides: expansion-panels, tabs, tooltip, snackbar, dialog, navigation-drawer, skeleton-loader, progress, switch, carousel, menu): `v-carousel` and `data-kt-slider` on one element: choose one. `v-btn` already has a ripple; use `data-kt-magnetic`/`data-kt-gesture` instead of `data-kt-ripple`.\n\n## Option lookup\n\nExact option names, variants and defaults live in `kineto.features.json` (https://raw.githubusercontent.com/catgarret/kineto/main/kineto.features.json) and the generated reference (https://github.com/catgarret/kineto/blob/main/docs/module-reference.md). With the Kineto MCP server (`npx @dong-gri/kineto-mcp`) call `kineto_suggest`, `kineto_snippet` and `kineto_validate_options` instead of guessing.\n\n## Do not generate these any more\n\n| Instead of | Generate | Why |\n|---|---|---|\n| `data-kt-lazy=\"dither\"` | `data-kt-stylize=\"dither\"` | Lazy fetches an image; these redraw pixels that are already there. They are the Stylize module now, and Stylize adds the living look (motion, pointer) and the crisp dissolve reveal that the alias cannot reach. |\n| `data-kt-lazy=\"ascii\"` | `data-kt-stylize=\"ascii\"` | Lazy fetches an image; these redraw pixels that are already there. They are the Stylize module now, and Stylize adds the living look (motion, pointer) and the crisp dissolve reveal that the alias cannot reach. |\n| `data-kt-lazy=\"halftone\"` | `data-kt-stylize=\"halftone\"` | Lazy fetches an image; these redraw pixels that are already there. They are the Stylize module now, and Stylize adds the living look (motion, pointer) and the crisp dissolve reveal that the alias cannot reach. |\n\n- Stylize does not load anything: use src, not data-src, and data-kt-mode=\"persist|reveal\" instead of data-kt-persist. Both modules can sit on one element when the image also needs lazy loading.\n\nThe old markup keeps working until next major and emits a recoverable `KT_DEPRECATED` diagnostic when `debug` is on.\n\n## Modules with their variant keys\n\n- `data-kt-ambient-media` — variants: image-clone, video-sample, color (JS key `preset`)\n- `data-kt-blur-text` — variants: blur (JS key `preset`)\n- `data-kt-brush-reveal` — variants: brush (JS key `preset`)\n- `data-kt-card-glow` — variants: spotlight, edge, border, comet, aurora, shine, glass (JS key `preset`)\n- `data-kt-counter` — variants: slot, plain, digit, pop, flip, clock (JS key `preset`)\n- `data-kt-date-time` — variants: relative, absolute, both (JS key `preset`)\n- `data-kt-css-scroll` — variants: progress-property, css-animation-timeline, scroll-timeline (JS key `preset`)\n- `data-kt-cursor` — variants: dot, ring, blob, crosshair, text, trail, orbit, snake, sparkle, image, custom (JS key `preset`)\n- `data-kt-fullpage` — variants: transform, snap (JS key `mode`)\n- `data-kt-glitch` — variants: rgb, pixel, noise, crt, wave, image, datamosh, reveal, vcr, rgb-slice-burst (JS key `preset`)\n- `data-kt-lazy` — variants: fade, blur-up, wave, grain, skeleton, pixelate, print, dissolve, flicker, polaroid, crt, data-mosaic, rgb-slice-burst, dither, ascii, halftone (JS key `preset`)\n- `data-kt-stylize` — variants: dither, ascii, halftone (JS key `preset`)\n- `data-kt-lightbox` — variants: viewer, grouped (JS key `preset`)\n- `data-kt-loader` — variants: slot, circular, bar (JS key `preset`)\n- `data-kt-loading-indicator` — variants: spinner, dots, bar, shimmer, shimmer-wave, terminal (JS key `preset`)\n- `data-kt-magnetic` — variants: pointer, dock (JS key `preset`)\n- `data-kt-marquee` — variants: left, right, reverse-on-scroll (JS key `preset`)\n- `data-kt-mouse-parallax` — variants: pointer, gyro, compass (JS key `preset`)\n- `data-kt-overflow-text` — variants: loop, bounce, rewind, once, page, flip, dissolve, page-roll, rolling, fade, scroll-fade (JS key `preset`)\n- `data-kt-page-reveal` — variants: curtain, split, blinds, diagonal, shutter, fade, zoom, iris, flash, center-slit, data-mosaic, curve, dissolve, push, grid, fold (JS key `preset`)\n- `data-kt-page-transition` — variants: same-origin (JS key `effect`)\n- `data-kt-parallax` — variants: x, y (JS key `preset`)\n- `data-kt-progress` — variants: page:scaleX, page:width, element:scaleX, element:width (JS key `preset`)\n- `data-kt-reveal` — variants: fade, fade-up, fade-down, fade-left, fade-right, slide-up, slide-down, slide-left, slide-right, zoom-in, zoom-out, blur, rise, soft, flip-x, flip-y, rotate, mask, wipe, class, clock, swing, skew (JS key `preset`)\n- `data-kt-radial` — variants: bottom, top, left, right (JS key `preset`)\n- `data-kt-ripple` — variants: material (JS key `preset`)\n- `data-kt-scroll-sequence` — variants: cover, contain (JS key `preset`)\n- `data-kt-scroll-velocity` — variants: skew, rotate, scale, blur, translate, combo (JS key `preset`)\n- `data-kt-slider` — variants: slide, fade, dissolve, wipe, coverflow, flip, cube, cards, creative, radial (JS key `preset`)\n- `data-kt-sticky-stack` — variants: vertical, horizontal, zindex, floating (JS key `preset`)\n- `data-kt-text-fill` — variants: scroll-fill (JS key `preset`)\n- `data-kt-text-reveal` — variants: stream, char, word, line, bounce, hangul, decode, flicker, shuffle (JS key `preset`)\n- `data-kt-text-split` — variants: char, word (JS key `preset`)\n- `data-kt-text-transition` — variants: slide-up, flip, rise, fade, blur, scale, clip, dissolve, shimmer, pop (JS key `preset`)\n- `data-kt-tilt` — variants: tilt-glare, tilt, x-only, y-only, reverse (JS key `preset`)\n- `data-kt-typewriter` — variants: type-erase (JS key `preset`)\n- `data-kt-vibrate` — variants: tap, double-tap, soft, rigid, heavy, success, warning, error, ratchet, heartbeat, long-press (JS key `preset`)\n- `data-kt-confetti` — variants: burst (JS key `preset`)\n- `data-kt-accordion` — variants: panel (JS key `effect`)\n- `data-kt-hold` — variants: confirm, tap (JS key `mode`)\n- `data-kt-mega-menu` — variants: dropdown, mega, radial (JS key `preset`)\n- `data-kt-toast` — variants: stack (JS key `type`)\n- `data-kt-bottom-sheet` — variants: sheet (JS key `preset`)\n- `data-kt-tabs` — variants: line (JS key `effect`)\n- `data-kt-cover-reveal` — variants: block (JS key `preset`)\n- `data-kt-gesture` — variants: spring, pull (JS key `preset`)\n- `data-kt-drag` — variants: free (JS key `preset`)\n- `data-kt-tooltip` — variants: default (JS key `effect`)\n- `data-kt-switch` — variants: toggle (JS key `preset`)\n- `data-kt-flip` — variants: layout, none, slide, fade, crossfade, fade-slide, scale, fold (JS key `mode`)\n- `data-kt-scroll-shadows` — variants: vertical, horizontal, mask (JS key `mode`)\n- `data-kt-squircle` — variants: squircle, round, bevel, scoop, notch, square (JS key `preset`)\n- `data-kt-sticky-header` — variants: shrink, toggle (JS key `preset`)\n- `data-kt-horizontal-scroll` — variants: pin (JS key `preset`)\n- `data-kt-canvas-effect` — variants: custom (JS key `preset`)\n"
+ "content": "---\ndescription: Use Kineto (@dong-gri/kineto) motion and media effects correctly next to shadcn/ui, Bootstrap, MUI, Mantine, Chakra, Ant Design, daisyUI, Nuxt UI, PrimeVue and Vuetify, including Figma-to-code flows.\nglobs: [\"**/*.tsx\", \"**/*.jsx\", \"**/*.vue\", \"**/*.html\", \"**/*.astro\", \"**/*.svelte\"]\nalwaysApply: false\n---\n\n# Kineto rules for coding agents (v0.13.1)\n\nKineto (`@dong-gri/kineto`) is a vanilla-first motion/effects library: 55 modules activated by `data-kt-*` attributes or `Kineto.create()`, with React/Vue/jQuery adapters. Use these rules whenever you generate UI with any component library (shadcn/ui, Bootstrap, MUI, Mantine, Chakra, Ant Design, daisyUI, Nuxt UI, PrimeVue, Vuetify) or from a Figma frame.\n\n## Principles\n\n- Kineto adds motion and media effects to markup you already own; it does not replace the component library's behaviour (dialogs, menus, forms, routing).\n- Prefer the library's own component when it already provides the behaviour (toast, tooltip, tabs, accordion, dialog, carousel). Use Kineto for what the library does not ship: entrance motion, text motion, counters, image loading effects, hover/pointer effects, marquee, page reveal, cursor, scroll storytelling.\n- Never put two things that animate the same property on one element: a Tailwind/tw-animate-css enter animation plus a Kineto reveal, a Bootstrap carousel plus a Kineto slider, two Kineto modules that both write transform (see docs/rfc/module-composition.md).\n- Attach with `data-kt-*` attributes on the element that reaches the DOM, call `Kineto.observe()` once at startup so markup added later (framework renders, portals, modals) is picked up and cleaned up, and reach for the React/Vue adapters only when you need the instance (replay, pause, presence).\n- Respect `prefers-reduced-motion`: Kineto already renders the meaningful end state for every module, so do not add a second reduced-motion branch around it.\n- Read the exact option names from kineto.features.json (or `npx @dong-gri/kineto-mcp`); do not invent options.\n\n## Setup (once per app)\n\n```js\nimport '@dong-gri/kineto/style.css';\nimport Kineto from '@dong-gri/kineto';\nKineto.observe(); // scans now and follows later DOM changes\n```\n\nReact: mount a client `KinetoProvider` that calls `Kineto.observe()` in an effect (shadcn registry: `npx shadcn@latest add @kineto/provider`). Vue/Nuxt: a client plugin that calls `Kineto.observe()`. Static HTML: the CDN snippet with `Kineto.observe()`.\n\n## Attachment grammar\n\n- Activation: `data-kt-=\"\"` (omit the value for the default variant). Options: `data-kt-=\"value\"`; booleans as bare attributes; arrays/objects as JSON strings.\n- JavaScript: `Kineto.create('', element, { : '', ...options })`. React: `useKineto('', options)` / `\" options={...}>`. Vue: `v-motion=\"{ type, options }\"` / `useKineto()`.\n- Component libraries forward unknown props/attrs to their root DOM node, so `data-kt-*` on ``, ``, ``, `` … reaches the DOM. If a component does not forward attributes, wrap it in a plain element or use a ref.\n- One transform-writing module per element (tilt, gesture, magnetic, scrollVelocity, parallax, flip). One reveal per section, staggered, rather than one per item.\n\n## Decide by intent\n\n| Intent | Default recipe | When the library already has it |\n|---|---|---|\n| hero-headline — Hero headline entrance (hero, headline, key visual, kv) | `data-kt-text-reveal=\"stream\" data-kt-speed=\"30\"` | Kineto provides it |\n| section-entrance — Section / card entrance on scroll (fade in, scroll reveal, entrance, appear) | `data-kt-reveal=\"fade-up\" data-kt-once data-kt-duration=\"0.7\"` | Kineto provides it |\n| card-hover — Card hover: tilt, glow, spring (card hover, 3d tilt, glow, spotlight) | `data-kt-tilt=\"tilt-glare\" data-kt-max=\"8\"` | Kineto provides it |\n| cta-button — Call-to-action button feedback (button, cta, click feedback, ripple) | `data-kt-magnetic=\"pointer\" data-kt-strength=\"0.3\"` | Kineto provides it |\n| kpi-number — KPI / statistic count-up (counter, count up, kpi, stat) | `data-kt-counter=\"slot\" data-kt-to=\"128000\" data-kt-format=\",\"` | Kineto provides it |\n| countdown — Countdown / elapsed clock (countdown, timer, until, clock) | `data-kt-counter=\"clock\" data-kt-until=\"2026-12-31T00:00:00+09:00\" data-kt-show-days` | Kineto provides it |\n| image-loading — Image loading effect / placeholder (lazy load, skeleton, blur up, placeholder) | `data-kt-lazy=\"skeleton\" data-kt-skeleton-variant=\"shimmer\"` | Kineto provides it |\n| media-texture — Graphic texture on an image or video (dither, ascii art, halftone, duotone) | `data-kt-stylize=\"dither\" data-kt-cell-size=\"8\" data-kt-dither-type=\"4x4\"` | Kineto provides it |\n| gallery-lightbox — Image gallery with lightbox (lightbox, gallery, zoom, viewer) | `data-kt-lightbox=\"grouped\" data-kt-group=\"gallery\"` | Kineto provides it |\n| interactive-reveal — Brush / scratch-to-reveal media (brush reveal, scratch, scratch card, drag to reveal) | `data-kt-brush-reveal data-kt-radius=\"48\" data-kt-softness=\"0.6\"` | Kineto provides it |\n| carousel — Carousel / slider (carousel, slider, swiper, slides) | `data-kt-slider=\"slide\" data-kt-loop data-kt-autoplay=\"4000\"` | prefer the library (shadcn: Carousel (Embla); bootstrap: .carousel; mantine: @mantine/carousel; antd: Carousel; daisyui: .carousel; nuxt-ui: UCarousel; primevue: Carousel; vuetify: v-carousel) |\n| marquee-ticker — Marquee / ticker (marquee, ticker, logo wall, scrolling text) | `data-kt-marquee=\"left\" data-kt-speed=\"60\" data-kt-pause-on-hover` | Kineto provides it |\n| toast — Toast notification (toast, snackbar, notification, 알림) | `data-kt-toast data-kt-position=\"bottom-right\" data-kt-duration=\"4000\"` | prefer the library (shadcn: Sonner / Toast; bootstrap: .toast; mui: Snackbar; mantine: @mantine/notifications; chakra: Toaster; antd: message / notification; daisyui: .toast (static); nuxt-ui: useToast; primevue: Toast; vuetify: v-snackbar) |\n| tooltip — Tooltip (tooltip, hint, 툴팁) | `data-kt-tooltip data-kt-placement=\"top\"` | prefer the library (shadcn: Tooltip; bootstrap: data-bs-toggle=\"tooltip\"; mui: Tooltip; mantine: Tooltip; chakra: Tooltip; antd: Tooltip; daisyui: .tooltip; nuxt-ui: UTooltip; primevue: v-tooltip; vuetify: v-tooltip) |\n| dialog-enter-exit — Dialog / modal enter-exit motion (dialog, modal, enter exit, mount unmount) | `data-kt-presence` | keep the library component, animate its content (shadcn: Dialog (tw-animate-css); bootstrap: .modal.fade; mui: Dialog + Grow; mantine: Modal; chakra: Dialog; antd: Modal; nuxt-ui: UModal; primevue: Dialog; vuetify: v-dialog) |\n| bottom-sheet — Mobile bottom sheet (bottom sheet, drawer, sheet, 바텀시트) | `data-kt-bottom-sheet data-kt-resizable data-kt-handle` | prefer the library (shadcn: Drawer (vaul) / Sheet; bootstrap: .offcanvas; mui: SwipeableDrawer; mantine: Drawer; chakra: Drawer; antd: Drawer; nuxt-ui: USlideover; primevue: Sidebar; vuetify: v-bottom-sheet) |\n| page-transition — Page transition and first-paint reveal (page transition, route transition, page reveal, curtain) | `data-kt-page-reveal=\"curtain\" data-kt-color=\"#0a0908\"` | Kineto provides it |\n| scroll-progress — Reading / scroll progress (scroll progress, reading progress, progress bar, back to top) | `data-kt-progress=\"page:scaleX\" data-kt-position=\"top\"` | Kineto provides it |\n| sticky-header — Sticky header that shrinks or hides on scroll (sticky header, navbar, shrink, hide on scroll) | `data-kt-sticky-header=\"shrink\" data-kt-distance=\"80\"` | keep the library component, animate its content (bootstrap: .navbar; shadcn: NavigationMenu; mui: AppBar; vuetify: v-app-bar) |\n| parallax-depth — Parallax and pointer depth (parallax, depth, layers, mouse parallax) | `data-kt-parallax=\"y\" data-kt-speed=\"0.3\"` | Kineto provides it |\n| scroll-storytelling — Scroll storytelling (sequence, pinned stacks, horizontal) (scrollytelling, image sequence, pinned, sticky stack) | `data-kt-scroll-sequence=\"cover\" data-kt-frames=\"120\"` | Kineto provides it |\n| loading-state — Inline loading indicator / real progress (spinner, loading, progress, shimmer) | `data-kt-loading-indicator=\"spinner\"` | prefer the library (shadcn: Spinner / Progress; bootstrap: .spinner-border; mui: CircularProgress; mantine: Loader; chakra: Spinner; antd: Spin; daisyui: .loading; nuxt-ui: UProgress; primevue: ProgressSpinner; vuetify: v-progress-circular) |\n| text-rotation — Rotating words / typewriter (rotating text, typewriter, word swap, typing) | `data-kt-text-transition=\"slide-up\" data-kt-texts=\"["design","motion","ship"]\" data-kt-hold=\"1.4\"` | Kineto provides it |\n| glitch-accent — Glitch / CRT accent (glitch, crt, vhs, datamosh) | `data-kt-glitch=\"rgb\"` | Kineto provides it |\n| custom-cursor — Custom cursor (cursor, custom cursor, pointer, 커서) | `data-kt-cursor=\"ring\"` | Kineto provides it |\n| celebration — Success celebration (confetti, celebrate, success, reward) | `data-kt-confetti data-kt-trigger=\"click\"` | Kineto provides it |\n| overflow-shadows — Scrollable area edge shadows (scroll shadow, overflow, scroll area, fade edges) | `data-kt-scroll-shadows=\"horizontal\"` | keep the library component, animate its content (shadcn: ScrollArea; mantine: ScrollArea; chakra: ScrollArea) |\n| corner-shape — iOS-style corner shape (squircle) (squircle, superellipse, corner shape, corner smoothing) | `data-kt-squircle=\"squircle\"` | keep the library component, animate its content (tailwind: rounded-* (circular corners only); shadcn: --radius (circular corners only); mui: shape.borderRadius (circular corners only)) |\n| interactive-background — Interactive canvas / WebGL background (React Bits–style) (react bits, background effect, interactive background, shader) | `data-kt-canvas-effect` | Kineto provides it |\n| confirm-hold — Hold-to-confirm and mash gauges (hold to confirm, long press, mash, confirm) | `data-kt-hold=\"confirm\" data-kt-duration=\"1.2\"` | Kineto provides it |\n| disclosure — Accordion, tabs, switch, mega menu (behaviour components) (accordion, tabs, switch, toggle) | `data-kt-accordion data-kt-single` | prefer the library (shadcn: Accordion / Tabs / Switch / NavigationMenu; bootstrap: .accordion / .nav-tabs / .form-switch / .navbar; mui: Accordion / Tabs / Switch / Menu; mantine: Accordion / Tabs / Switch; chakra: Accordion / Tabs / Switch; antd: Collapse / Tabs / Switch / Menu; daisyui: .collapse / .tabs / .toggle; nuxt-ui: UAccordion / UTabs / USwitch; primevue: Accordion / Tabs / ToggleSwitch / MegaMenu; vuetify: v-expansion-panels / v-tabs / v-switch) |\n| layout-change — Animated reorder / add / remove (FLIP) (flip, reorder, list animation, layout animation) | `data-kt-flip=\"layout\" data-kt-watch` | Kineto provides it |\n| drag-and-fullpage — Draggable elements and fullpage sections (drag, draggable, fullpage, snap sections) | `data-kt-drag data-kt-inertia data-kt-snap-back` | Kineto provides it |\n\n## Figma layer → intent\n\n- node is a TEXT layer with font size ≥ 40 or named like a heading → `hero-headline`\n- node is an IMAGE fill (RECTANGLE/FRAME with image paint) or named image/photo/thumbnail → `image-loading`\n- node repeats as a grid/list of similar frames (auto-layout children with the same component) → `section-entrance`\n- node is a COMPONENT INSTANCE named Button/CTA → `cta-button`\n- node text is mostly digits (counts, KRW/USD, percentages) → `kpi-number`\n- frame is wider than the viewport and clips horizontally repeating children → `marquee-ticker`\n- frame has prototype interaction 'On drag' or scroll horizontally → `carousel`\n- Layer-name regexes per intent: hero-headline (/hero/i /kv/i /key ?visual/i /headline/i /title/i /h1/i); section-entrance (/section/i /card/i /feature/i /grid/i /list item/i /row/i); card-hover (/card/i /product card/i /tile/i /thumbnail/i); cta-button (/button/i /cta/i /primary button/i); kpi-number (/stat/i /kpi/i /number/i /metric/i /counter/i /\\d{2,}/i); countdown (/countdown/i /timer/i /d-day/i /clock/i); image-loading (/image/i /photo/i /thumbnail/i /cover/i /gallery/i); media-texture (/dither/i /halftone/i /ascii/i /texture/i /grain/i /duotone/i /poster/i /print/i /newsprint/i /screen/i); gallery-lightbox (/gallery/i /lightbox/i /grid/i /masonry/i /photo/i); interactive-reveal (/scratch/i /brush/i /reveal/i /mask/i); carousel (/carousel/i /slider/i /swiper/i /slides/i); marquee-ticker (/marquee/i /ticker/i /logo/i /partners/i /ranking/i); toast (/toast/i /snackbar/i /notification/i /alert/i); tooltip (/tooltip/i /hint/i); dialog-enter-exit (/modal/i /dialog/i /overlay/i /popup/i); bottom-sheet (/bottom sheet/i /sheet/i /drawer/i); page-transition (/intro/i /splash/i /loading/i /transition/i); scroll-progress (/progress/i /reading bar/i /back to top/i); sticky-header (/header/i /navbar/i /gnb/i /top bar/i); parallax-depth (/layer/i /bg/i /background/i /fg/i /foreground/i /parallax/i); scroll-storytelling (/sequence/i /frames/i /pinned/i /story/i /horizontal/i); loading-state (/spinner/i /loading/i /loader/i /progress/i); text-rotation (/typing/i /rotating/i /tagline/i); glitch-accent (/glitch/i /crt/i /vhs/i /neon/i /cyber/i /game/i); custom-cursor (/cursor/i); celebration (/success/i /complete/i /reward/i /confetti/i); overflow-shadows (/list/i /scroll area/i /table/i /chips/i); corner-shape (/card/i /button/i /app icon/i /avatar/i /tile/i /sheet/i); interactive-background (/hero/i /background/i /banner/i /section/i /card/i); confirm-hold (/confirm/i /delete/i /hold/i); disclosure (/accordion/i /faq/i /tabs/i /segmented/i /switch/i /toggle/i /menu/i); layout-change (/list/i /grid/i /kanban/i /board/i); drag-and-fullpage (/draggable/i /fullpage/i /sections/i)\n\n## Per-ecosystem notes\n\n- **vanilla** (data-attributes; provides: nothing — vanilla): Every Kineto module is available; there is no library component to defer to. Multi-page sites can add `data-kt-page-transition` on for same-origin page transitions and `data-kt-page-reveal` for the first-paint overlay.\n- **shadcn** (props-passthrough; provides: accordion, tabs, tooltip, toast, dialog, sheet, carousel, skeleton, progress, switch, navigation-menu, scroll-area): tw-animate-css (`animate-in`, `fade-in-0`, `zoom-in-95` …) already animates Dialog/Sheet/Popover content — do not add a Kineto reveal to the same node; animate the children instead. shadcn `Carousel` (Embla) owns the track; put Kineto reveal/tilt on the slides, never `data-kt-slider` on the same container. shadcn `Skeleton` is a layout placeholder; for images prefer `data-kt-lazy=\"skeleton\"` on the ` ` itself so the placeholder resolves into the real picture. Next.js App Router: the provider is a client component; server components can still render `data-kt-*` attributes. Tailwind v4 utilities and Kineto never fight over layout: Kineto only writes transform/opacity/filter/clip on the host and creates its own wrapper layers.\n- **tailwind** (data-attributes; provides: nothing — vanilla): `animate-*` keyframe utilities and `transition-transform` on the same element as `data-kt-reveal`, `data-kt-gesture` or `data-kt-tilt` double-animate the host. daisyUI, Flowbite and Preline are plain class-based markup on top of Tailwind; everything in this entry applies to them.\n- **bootstrap** (data-attributes; provides: accordion, collapse, tabs, tooltip, popover, toast, modal, offcanvas, carousel, placeholder, spinner, progress, dropdown, navbar): `.carousel` (Bootstrap) and `data-kt-slider` on the same element: choose one. Bootstrap carousel for content slides, Kineto slider when you want coverflow/cube/cards effects. `.placeholder` / `.placeholder-glow` and `data-kt-lazy=\"skeleton\"`: choose one placeholder per image. `data-bs-toggle=\"tooltip\"` and `data-kt-tooltip` on one element create two tooltips. `.accordion` (Bootstrap collapse) and `data-kt-accordion`: Bootstrap owns open/close; add Kineto reveal to the panel content instead. Bootstrap's `.fade` modal transition stays; animate the modal body with `data-kt-reveal` or `data-kt-text-split` for richer entrances. Bootstrap's grid and utilities are unaffected; Kineto never sets layout properties on the host.\n- **mui** (props-passthrough; provides: accordion, tabs, tooltip, snackbar, dialog, drawer, skeleton, progress, switch, menu): MUI `Grow`/`Fade`/`Slide` transitions wrap Dialog and Menu content; do not add a Kineto reveal to the same node. MUI `ButtonBase` already renders a Material ripple (`TouchRipple`); use `data-kt-magnetic` or `data-kt-gesture` on buttons, not `data-kt-ripple`. Emotion class names are left untouched; Kineto writes inline styles it later restores on destroy.\n- **mantine** (props-passthrough; provides: accordion, tabs, tooltip, notifications, modal, drawer, skeleton, progress, switch, carousel): `@mantine/carousel` (Embla) and `data-kt-slider` on one element: choose one. Mantine `Transition`/`Collapse` already animate their child; add Kineto to the content, not the transition node.\n- **chakra** (props-passthrough; provides: accordion, tabs, tooltip, toaster, dialog, drawer, skeleton, progress, switch, menu): Chakra `Presence`/`Dialog` animate their positioner via `data-state` keyframes; do not add a Kineto reveal to the same node.\n- **antd** (props-passthrough-or-wrapper; provides: collapse, tabs, tooltip, message, notification, modal, drawer, skeleton, progress, switch, carousel, dropdown): Ant Design `Carousel` (react-slick) and `data-kt-slider` on one element: choose one. `Statistic` has its own count-up animation when `formatter` uses CountUp; use either that or `data-kt-counter`, not both.\n- **daisyui** (data-attributes; provides: skeleton, tooltip, tabs, collapse, toast, carousel, progress, modal, drawer): daisyUI `carousel` is CSS scroll-snap; `data-kt-slider` on the same element takes over the track — choose one. daisyUI `skeleton` boxes stay for text; use `data-kt-lazy=\"skeleton\"` on images.\n- **nuxt-ui** (attrs-fallthrough; provides: accordion, tabs, tooltip, toast, modal, slideover, skeleton, progress, switch, carousel, navigation-menu): Nuxt UI `UCarousel` (Embla) and `data-kt-slider`: choose one per container. Reka UI content nodes animate through `data-state`; add Kineto to their children. Components rendered on the server keep the attributes, so hydration sees the same markup the observer will scan.\n- **primevue** (attrs-fallthrough; provides: accordion, tabs, tooltip, toast, dialog, sidebar, skeleton, progress, toggleswitch, carousel, galleria, megamenu): PrimeVue `Carousel`/`Galleria` and `data-kt-slider` on the same element: choose one. PrimeVue `Ripple` directive and `data-kt-ripple` on one button: choose one.\n- **vuetify** (attrs-fallthrough; provides: expansion-panels, tabs, tooltip, snackbar, dialog, navigation-drawer, skeleton-loader, progress, switch, carousel, menu): `v-carousel` and `data-kt-slider` on one element: choose one. `v-btn` already has a ripple; use `data-kt-magnetic`/`data-kt-gesture` instead of `data-kt-ripple`.\n\n## Option lookup\n\nExact option names, variants and defaults live in `kineto.features.json` (https://raw.githubusercontent.com/catgarret/kineto/main/kineto.features.json) and the generated reference (https://github.com/catgarret/kineto/blob/main/docs/module-reference.md). With the Kineto MCP server (`npx @dong-gri/kineto-mcp`) call `kineto_suggest`, `kineto_snippet` and `kineto_validate_options` instead of guessing.\n\n## Do not generate these any more\n\n| Instead of | Generate | Why |\n|---|---|---|\n| `data-kt-lazy=\"dither\"` | `data-kt-stylize=\"dither\"` | Lazy fetches an image; these redraw pixels that are already there. They are the Stylize module now, and Stylize adds the living look (motion, pointer) and the crisp dissolve reveal that the alias cannot reach. |\n| `data-kt-lazy=\"ascii\"` | `data-kt-stylize=\"ascii\"` | Lazy fetches an image; these redraw pixels that are already there. They are the Stylize module now, and Stylize adds the living look (motion, pointer) and the crisp dissolve reveal that the alias cannot reach. |\n| `data-kt-lazy=\"halftone\"` | `data-kt-stylize=\"halftone\"` | Lazy fetches an image; these redraw pixels that are already there. They are the Stylize module now, and Stylize adds the living look (motion, pointer) and the crisp dissolve reveal that the alias cannot reach. |\n\n- Stylize does not load anything: use src, not data-src, and data-kt-mode=\"persist|reveal\" instead of data-kt-persist. Both modules can sit on one element when the image also needs lazy loading.\n\nThe old markup keeps working until next major and emits a recoverable `KT_DEPRECATED` diagnostic when `debug` is on.\n\n## Modules with their variant keys\n\n- `data-kt-ambient-media` — variants: image-clone, video-sample, color (JS key `preset`)\n- `data-kt-blur-text` — variants: blur (JS key `preset`)\n- `data-kt-brush-reveal` — variants: brush (JS key `preset`)\n- `data-kt-card-glow` — variants: spotlight, edge, border, comet, aurora, shine, glass (JS key `preset`)\n- `data-kt-counter` — variants: slot, plain, digit, pop, flip, clock (JS key `preset`)\n- `data-kt-date-time` — variants: relative, absolute, both (JS key `preset`)\n- `data-kt-css-scroll` — variants: progress-property, css-animation-timeline, scroll-timeline (JS key `preset`)\n- `data-kt-cursor` — variants: dot, ring, blob, crosshair, text, trail, orbit, snake, sparkle, image, custom (JS key `preset`)\n- `data-kt-fullpage` — variants: transform, snap (JS key `mode`)\n- `data-kt-glitch` — variants: rgb, pixel, noise, crt, wave, image, datamosh, reveal, vcr, rgb-slice-burst (JS key `preset`)\n- `data-kt-lazy` — variants: fade, blur-up, wave, grain, skeleton, pixelate, print, dissolve, flicker, polaroid, crt, data-mosaic, rgb-slice-burst, dither, ascii, halftone (JS key `preset`)\n- `data-kt-stylize` — variants: dither, ascii, halftone (JS key `preset`)\n- `data-kt-lightbox` — variants: viewer, grouped (JS key `preset`)\n- `data-kt-loader` — variants: slot, circular, bar (JS key `preset`)\n- `data-kt-loading-indicator` — variants: spinner, dots, bar, shimmer, shimmer-wave, terminal (JS key `preset`)\n- `data-kt-magnetic` — variants: pointer, dock (JS key `preset`)\n- `data-kt-marquee` — variants: left, right, reverse-on-scroll (JS key `preset`)\n- `data-kt-mouse-parallax` — variants: pointer, gyro, compass (JS key `preset`)\n- `data-kt-overflow-text` — variants: loop, bounce, rewind, once, page, flip, dissolve, page-roll, rolling, fade, scroll-fade (JS key `preset`)\n- `data-kt-page-reveal` — variants: curtain, split, blinds, diagonal, shutter, fade, zoom, iris, flash, center-slit, data-mosaic, curve, dissolve, push, grid, fold (JS key `preset`)\n- `data-kt-page-transition` — variants: same-origin (JS key `effect`)\n- `data-kt-parallax` — variants: x, y (JS key `preset`)\n- `data-kt-progress` — variants: page:scaleX, page:width, element:scaleX, element:width (JS key `preset`)\n- `data-kt-reveal` — variants: fade, fade-up, fade-down, fade-left, fade-right, slide-up, slide-down, slide-left, slide-right, zoom-in, zoom-out, blur, rise, soft, flip-x, flip-y, rotate, mask, wipe, class, clock, swing, skew (JS key `preset`)\n- `data-kt-radial` — variants: bottom, top, left, right (JS key `preset`)\n- `data-kt-ripple` — variants: material (JS key `preset`)\n- `data-kt-scroll-sequence` — variants: cover, contain (JS key `preset`)\n- `data-kt-scroll-velocity` — variants: skew, rotate, scale, blur, translate, combo (JS key `preset`)\n- `data-kt-slider` — variants: slide, fade, dissolve, wipe, coverflow, flip, cube, cards, creative, radial (JS key `preset`)\n- `data-kt-sticky-stack` — variants: vertical, horizontal, zindex, floating (JS key `preset`)\n- `data-kt-text-fill` — variants: scroll-fill (JS key `preset`)\n- `data-kt-text-reveal` — variants: stream, char, word, line, bounce, hangul, decode, flicker, shuffle (JS key `preset`)\n- `data-kt-text-split` — variants: char, word (JS key `preset`)\n- `data-kt-text-transition` — variants: slide-up, flip, rise, fade, blur, scale, clip, dissolve, shimmer, pop (JS key `preset`)\n- `data-kt-tilt` — variants: tilt-glare, tilt, x-only, y-only, reverse (JS key `preset`)\n- `data-kt-typewriter` — variants: type-erase (JS key `preset`)\n- `data-kt-vibrate` — variants: tap, double-tap, soft, rigid, heavy, success, warning, error, ratchet, heartbeat, long-press (JS key `preset`)\n- `data-kt-confetti` — variants: burst (JS key `preset`)\n- `data-kt-accordion` — variants: panel (JS key `effect`)\n- `data-kt-hold` — variants: confirm, tap (JS key `mode`)\n- `data-kt-mega-menu` — variants: dropdown, mega, radial (JS key `preset`)\n- `data-kt-toast` — variants: stack (JS key `type`)\n- `data-kt-bottom-sheet` — variants: sheet (JS key `preset`)\n- `data-kt-tabs` — variants: line (JS key `effect`)\n- `data-kt-cover-reveal` — variants: block (JS key `preset`)\n- `data-kt-gesture` — variants: spring, pull (JS key `preset`)\n- `data-kt-drag` — variants: free (JS key `preset`)\n- `data-kt-tooltip` — variants: default (JS key `effect`)\n- `data-kt-switch` — variants: toggle (JS key `preset`)\n- `data-kt-flip` — variants: layout, none, slide, fade, crossfade, fade-slide, scale, fold (JS key `mode`)\n- `data-kt-scroll-shadows` — variants: vertical, horizontal, mask (JS key `mode`)\n- `data-kt-squircle` — variants: squircle, round, bevel, scoop, notch, square (JS key `preset`)\n- `data-kt-sticky-header` — variants: shrink, toggle (JS key `preset`)\n- `data-kt-horizontal-scroll` — variants: pin (JS key `preset`)\n- `data-kt-canvas-effect` — variants: custom (JS key `preset`)\n"
},
{
"path": "ai/kineto.rules.md",
"type": "registry:file",
"target": "~/docs/kineto-ai-rules.md",
- "content": "# Kineto rules for coding agents (v0.13.0)\n\nKineto (`@dong-gri/kineto`) is a vanilla-first motion/effects library: 55 modules activated by `data-kt-*` attributes or `Kineto.create()`, with React/Vue/jQuery adapters. Use these rules whenever you generate UI with any component library (shadcn/ui, Bootstrap, MUI, Mantine, Chakra, Ant Design, daisyUI, Nuxt UI, PrimeVue, Vuetify) or from a Figma frame.\n\n## Principles\n\n- Kineto adds motion and media effects to markup you already own; it does not replace the component library's behaviour (dialogs, menus, forms, routing).\n- Prefer the library's own component when it already provides the behaviour (toast, tooltip, tabs, accordion, dialog, carousel). Use Kineto for what the library does not ship: entrance motion, text motion, counters, image loading effects, hover/pointer effects, marquee, page reveal, cursor, scroll storytelling.\n- Never put two things that animate the same property on one element: a Tailwind/tw-animate-css enter animation plus a Kineto reveal, a Bootstrap carousel plus a Kineto slider, two Kineto modules that both write transform (see docs/rfc/module-composition.md).\n- Attach with `data-kt-*` attributes on the element that reaches the DOM, call `Kineto.observe()` once at startup so markup added later (framework renders, portals, modals) is picked up and cleaned up, and reach for the React/Vue adapters only when you need the instance (replay, pause, presence).\n- Respect `prefers-reduced-motion`: Kineto already renders the meaningful end state for every module, so do not add a second reduced-motion branch around it.\n- Read the exact option names from kineto.features.json (or `npx @dong-gri/kineto-mcp`); do not invent options.\n\n## Setup (once per app)\n\n```js\nimport '@dong-gri/kineto/style.css';\nimport Kineto from '@dong-gri/kineto';\nKineto.observe(); // scans now and follows later DOM changes\n```\n\nReact: mount a client `KinetoProvider` that calls `Kineto.observe()` in an effect (shadcn registry: `npx shadcn@latest add @kineto/provider`). Vue/Nuxt: a client plugin that calls `Kineto.observe()`. Static HTML: the CDN snippet with `Kineto.observe()`.\n\n## Attachment grammar\n\n- Activation: `data-kt-=\"\"` (omit the value for the default variant). Options: `data-kt-=\"value\"`; booleans as bare attributes; arrays/objects as JSON strings.\n- JavaScript: `Kineto.create('', element, { : '', ...options })`. React: `useKineto('', options)` / `\" options={...}>`. Vue: `v-motion=\"{ type, options }\"` / `useKineto()`.\n- Component libraries forward unknown props/attrs to their root DOM node, so `data-kt-*` on ``, ``, ``, `` … reaches the DOM. If a component does not forward attributes, wrap it in a plain element or use a ref.\n- One transform-writing module per element (tilt, gesture, magnetic, scrollVelocity, parallax, flip). One reveal per section, staggered, rather than one per item.\n\n## Decide by intent\n\n| Intent | Default recipe | When the library already has it |\n|---|---|---|\n| hero-headline — Hero headline entrance (hero, headline, key visual, kv) | `data-kt-text-reveal=\"stream\" data-kt-speed=\"30\"` | Kineto provides it |\n| section-entrance — Section / card entrance on scroll (fade in, scroll reveal, entrance, appear) | `data-kt-reveal=\"fade-up\" data-kt-once data-kt-duration=\"0.7\"` | Kineto provides it |\n| card-hover — Card hover: tilt, glow, spring (card hover, 3d tilt, glow, spotlight) | `data-kt-tilt=\"tilt-glare\" data-kt-max=\"8\"` | Kineto provides it |\n| cta-button — Call-to-action button feedback (button, cta, click feedback, ripple) | `data-kt-magnetic=\"pointer\" data-kt-strength=\"0.3\"` | Kineto provides it |\n| kpi-number — KPI / statistic count-up (counter, count up, kpi, stat) | `data-kt-counter=\"slot\" data-kt-to=\"128000\" data-kt-format=\",\"` | Kineto provides it |\n| countdown — Countdown / elapsed clock (countdown, timer, until, clock) | `data-kt-counter=\"clock\" data-kt-until=\"2026-12-31T00:00:00+09:00\" data-kt-show-days` | Kineto provides it |\n| image-loading — Image loading effect / placeholder (lazy load, skeleton, blur up, placeholder) | `data-kt-lazy=\"skeleton\" data-kt-skeleton-variant=\"shimmer\"` | Kineto provides it |\n| media-texture — Graphic texture on an image or video (dither, ascii art, halftone, duotone) | `data-kt-stylize=\"dither\" data-kt-cell-size=\"8\" data-kt-dither-type=\"4x4\"` | Kineto provides it |\n| gallery-lightbox — Image gallery with lightbox (lightbox, gallery, zoom, viewer) | `data-kt-lightbox=\"grouped\" data-kt-group=\"gallery\"` | Kineto provides it |\n| interactive-reveal — Brush / scratch-to-reveal media (brush reveal, scratch, scratch card, drag to reveal) | `data-kt-brush-reveal data-kt-radius=\"48\" data-kt-softness=\"0.6\"` | Kineto provides it |\n| carousel — Carousel / slider (carousel, slider, swiper, slides) | `data-kt-slider=\"slide\" data-kt-loop data-kt-autoplay=\"4000\"` | prefer the library (shadcn: Carousel (Embla); bootstrap: .carousel; mantine: @mantine/carousel; antd: Carousel; daisyui: .carousel; nuxt-ui: UCarousel; primevue: Carousel; vuetify: v-carousel) |\n| marquee-ticker — Marquee / ticker (marquee, ticker, logo wall, scrolling text) | `data-kt-marquee=\"left\" data-kt-speed=\"60\" data-kt-pause-on-hover` | Kineto provides it |\n| toast — Toast notification (toast, snackbar, notification, 알림) | `data-kt-toast data-kt-position=\"bottom-right\" data-kt-duration=\"4000\"` | prefer the library (shadcn: Sonner / Toast; bootstrap: .toast; mui: Snackbar; mantine: @mantine/notifications; chakra: Toaster; antd: message / notification; daisyui: .toast (static); nuxt-ui: useToast; primevue: Toast; vuetify: v-snackbar) |\n| tooltip — Tooltip (tooltip, hint, 툴팁) | `data-kt-tooltip data-kt-placement=\"top\"` | prefer the library (shadcn: Tooltip; bootstrap: data-bs-toggle=\"tooltip\"; mui: Tooltip; mantine: Tooltip; chakra: Tooltip; antd: Tooltip; daisyui: .tooltip; nuxt-ui: UTooltip; primevue: v-tooltip; vuetify: v-tooltip) |\n| dialog-enter-exit — Dialog / modal enter-exit motion (dialog, modal, enter exit, mount unmount) | `data-kt-presence` | keep the library component, animate its content (shadcn: Dialog (tw-animate-css); bootstrap: .modal.fade; mui: Dialog + Grow; mantine: Modal; chakra: Dialog; antd: Modal; nuxt-ui: UModal; primevue: Dialog; vuetify: v-dialog) |\n| bottom-sheet — Mobile bottom sheet (bottom sheet, drawer, sheet, 바텀시트) | `data-kt-bottom-sheet data-kt-resizable data-kt-handle` | prefer the library (shadcn: Drawer (vaul) / Sheet; bootstrap: .offcanvas; mui: SwipeableDrawer; mantine: Drawer; chakra: Drawer; antd: Drawer; nuxt-ui: USlideover; primevue: Sidebar; vuetify: v-bottom-sheet) |\n| page-transition — Page transition and first-paint reveal (page transition, route transition, page reveal, curtain) | `data-kt-page-reveal=\"curtain\" data-kt-color=\"#0a0908\"` | Kineto provides it |\n| scroll-progress — Reading / scroll progress (scroll progress, reading progress, progress bar, back to top) | `data-kt-progress=\"page:scaleX\" data-kt-position=\"top\"` | Kineto provides it |\n| sticky-header — Sticky header that shrinks or hides on scroll (sticky header, navbar, shrink, hide on scroll) | `data-kt-sticky-header=\"shrink\" data-kt-distance=\"80\"` | keep the library component, animate its content (bootstrap: .navbar; shadcn: NavigationMenu; mui: AppBar; vuetify: v-app-bar) |\n| parallax-depth — Parallax and pointer depth (parallax, depth, layers, mouse parallax) | `data-kt-parallax=\"y\" data-kt-speed=\"0.3\"` | Kineto provides it |\n| scroll-storytelling — Scroll storytelling (sequence, pinned stacks, horizontal) (scrollytelling, image sequence, pinned, sticky stack) | `data-kt-scroll-sequence=\"cover\" data-kt-frames=\"120\"` | Kineto provides it |\n| loading-state — Inline loading indicator / real progress (spinner, loading, progress, shimmer) | `data-kt-loading-indicator=\"spinner\"` | prefer the library (shadcn: Spinner / Progress; bootstrap: .spinner-border; mui: CircularProgress; mantine: Loader; chakra: Spinner; antd: Spin; daisyui: .loading; nuxt-ui: UProgress; primevue: ProgressSpinner; vuetify: v-progress-circular) |\n| text-rotation — Rotating words / typewriter (rotating text, typewriter, word swap, typing) | `data-kt-text-transition=\"slide-up\" data-kt-texts=\"["design","motion","ship"]\" data-kt-hold=\"1.4\"` | Kineto provides it |\n| glitch-accent — Glitch / CRT accent (glitch, crt, vhs, datamosh) | `data-kt-glitch=\"rgb\"` | Kineto provides it |\n| custom-cursor — Custom cursor (cursor, custom cursor, pointer, 커서) | `data-kt-cursor=\"ring\"` | Kineto provides it |\n| celebration — Success celebration (confetti, celebrate, success, reward) | `data-kt-confetti data-kt-trigger=\"click\"` | Kineto provides it |\n| overflow-shadows — Scrollable area edge shadows (scroll shadow, overflow, scroll area, fade edges) | `data-kt-scroll-shadows=\"horizontal\"` | keep the library component, animate its content (shadcn: ScrollArea; mantine: ScrollArea; chakra: ScrollArea) |\n| corner-shape — iOS-style corner shape (squircle) (squircle, superellipse, corner shape, corner smoothing) | `data-kt-squircle=\"squircle\"` | keep the library component, animate its content (tailwind: rounded-* (circular corners only); shadcn: --radius (circular corners only); mui: shape.borderRadius (circular corners only)) |\n| interactive-background — Interactive canvas / WebGL background (React Bits–style) (react bits, background effect, interactive background, shader) | `data-kt-canvas-effect` | Kineto provides it |\n| confirm-hold — Hold-to-confirm and mash gauges (hold to confirm, long press, mash, confirm) | `data-kt-hold=\"confirm\" data-kt-duration=\"1.2\"` | Kineto provides it |\n| disclosure — Accordion, tabs, switch, mega menu (behaviour components) (accordion, tabs, switch, toggle) | `data-kt-accordion data-kt-single` | prefer the library (shadcn: Accordion / Tabs / Switch / NavigationMenu; bootstrap: .accordion / .nav-tabs / .form-switch / .navbar; mui: Accordion / Tabs / Switch / Menu; mantine: Accordion / Tabs / Switch; chakra: Accordion / Tabs / Switch; antd: Collapse / Tabs / Switch / Menu; daisyui: .collapse / .tabs / .toggle; nuxt-ui: UAccordion / UTabs / USwitch; primevue: Accordion / Tabs / ToggleSwitch / MegaMenu; vuetify: v-expansion-panels / v-tabs / v-switch) |\n| layout-change — Animated reorder / add / remove (FLIP) (flip, reorder, list animation, layout animation) | `data-kt-flip=\"layout\" data-kt-watch` | Kineto provides it |\n| drag-and-fullpage — Draggable elements and fullpage sections (drag, draggable, fullpage, snap sections) | `data-kt-drag data-kt-inertia data-kt-snap-back` | Kineto provides it |\n\n## Figma layer → intent\n\n- node is a TEXT layer with font size ≥ 40 or named like a heading → `hero-headline`\n- node is an IMAGE fill (RECTANGLE/FRAME with image paint) or named image/photo/thumbnail → `image-loading`\n- node repeats as a grid/list of similar frames (auto-layout children with the same component) → `section-entrance`\n- node is a COMPONENT INSTANCE named Button/CTA → `cta-button`\n- node text is mostly digits (counts, KRW/USD, percentages) → `kpi-number`\n- frame is wider than the viewport and clips horizontally repeating children → `marquee-ticker`\n- frame has prototype interaction 'On drag' or scroll horizontally → `carousel`\n- Layer-name regexes per intent: hero-headline (/hero/i /kv/i /key ?visual/i /headline/i /title/i /h1/i); section-entrance (/section/i /card/i /feature/i /grid/i /list item/i /row/i); card-hover (/card/i /product card/i /tile/i /thumbnail/i); cta-button (/button/i /cta/i /primary button/i); kpi-number (/stat/i /kpi/i /number/i /metric/i /counter/i /\\d{2,}/i); countdown (/countdown/i /timer/i /d-day/i /clock/i); image-loading (/image/i /photo/i /thumbnail/i /cover/i /gallery/i); media-texture (/dither/i /halftone/i /ascii/i /texture/i /grain/i /duotone/i /poster/i /print/i /newsprint/i /screen/i); gallery-lightbox (/gallery/i /lightbox/i /grid/i /masonry/i /photo/i); interactive-reveal (/scratch/i /brush/i /reveal/i /mask/i); carousel (/carousel/i /slider/i /swiper/i /slides/i); marquee-ticker (/marquee/i /ticker/i /logo/i /partners/i /ranking/i); toast (/toast/i /snackbar/i /notification/i /alert/i); tooltip (/tooltip/i /hint/i); dialog-enter-exit (/modal/i /dialog/i /overlay/i /popup/i); bottom-sheet (/bottom sheet/i /sheet/i /drawer/i); page-transition (/intro/i /splash/i /loading/i /transition/i); scroll-progress (/progress/i /reading bar/i /back to top/i); sticky-header (/header/i /navbar/i /gnb/i /top bar/i); parallax-depth (/layer/i /bg/i /background/i /fg/i /foreground/i /parallax/i); scroll-storytelling (/sequence/i /frames/i /pinned/i /story/i /horizontal/i); loading-state (/spinner/i /loading/i /loader/i /progress/i); text-rotation (/typing/i /rotating/i /tagline/i); glitch-accent (/glitch/i /crt/i /vhs/i /neon/i /cyber/i /game/i); custom-cursor (/cursor/i); celebration (/success/i /complete/i /reward/i /confetti/i); overflow-shadows (/list/i /scroll area/i /table/i /chips/i); corner-shape (/card/i /button/i /app icon/i /avatar/i /tile/i /sheet/i); interactive-background (/hero/i /background/i /banner/i /section/i /card/i); confirm-hold (/confirm/i /delete/i /hold/i); disclosure (/accordion/i /faq/i /tabs/i /segmented/i /switch/i /toggle/i /menu/i); layout-change (/list/i /grid/i /kanban/i /board/i); drag-and-fullpage (/draggable/i /fullpage/i /sections/i)\n\n## Per-ecosystem notes\n\n- **vanilla** (data-attributes; provides: nothing — vanilla): Every Kineto module is available; there is no library component to defer to. Multi-page sites can add `data-kt-page-transition` on for same-origin page transitions and `data-kt-page-reveal` for the first-paint overlay.\n- **shadcn** (props-passthrough; provides: accordion, tabs, tooltip, toast, dialog, sheet, carousel, skeleton, progress, switch, navigation-menu, scroll-area): tw-animate-css (`animate-in`, `fade-in-0`, `zoom-in-95` …) already animates Dialog/Sheet/Popover content — do not add a Kineto reveal to the same node; animate the children instead. shadcn `Carousel` (Embla) owns the track; put Kineto reveal/tilt on the slides, never `data-kt-slider` on the same container. shadcn `Skeleton` is a layout placeholder; for images prefer `data-kt-lazy=\"skeleton\"` on the ` ` itself so the placeholder resolves into the real picture. Next.js App Router: the provider is a client component; server components can still render `data-kt-*` attributes. Tailwind v4 utilities and Kineto never fight over layout: Kineto only writes transform/opacity/filter/clip on the host and creates its own wrapper layers.\n- **tailwind** (data-attributes; provides: nothing — vanilla): `animate-*` keyframe utilities and `transition-transform` on the same element as `data-kt-reveal`, `data-kt-gesture` or `data-kt-tilt` double-animate the host. daisyUI, Flowbite and Preline are plain class-based markup on top of Tailwind; everything in this entry applies to them.\n- **bootstrap** (data-attributes; provides: accordion, collapse, tabs, tooltip, popover, toast, modal, offcanvas, carousel, placeholder, spinner, progress, dropdown, navbar): `.carousel` (Bootstrap) and `data-kt-slider` on the same element: choose one. Bootstrap carousel for content slides, Kineto slider when you want coverflow/cube/cards effects. `.placeholder` / `.placeholder-glow` and `data-kt-lazy=\"skeleton\"`: choose one placeholder per image. `data-bs-toggle=\"tooltip\"` and `data-kt-tooltip` on one element create two tooltips. `.accordion` (Bootstrap collapse) and `data-kt-accordion`: Bootstrap owns open/close; add Kineto reveal to the panel content instead. Bootstrap's `.fade` modal transition stays; animate the modal body with `data-kt-reveal` or `data-kt-text-split` for richer entrances. Bootstrap's grid and utilities are unaffected; Kineto never sets layout properties on the host.\n- **mui** (props-passthrough; provides: accordion, tabs, tooltip, snackbar, dialog, drawer, skeleton, progress, switch, menu): MUI `Grow`/`Fade`/`Slide` transitions wrap Dialog and Menu content; do not add a Kineto reveal to the same node. MUI `ButtonBase` already renders a Material ripple (`TouchRipple`); use `data-kt-magnetic` or `data-kt-gesture` on buttons, not `data-kt-ripple`. Emotion class names are left untouched; Kineto writes inline styles it later restores on destroy.\n- **mantine** (props-passthrough; provides: accordion, tabs, tooltip, notifications, modal, drawer, skeleton, progress, switch, carousel): `@mantine/carousel` (Embla) and `data-kt-slider` on one element: choose one. Mantine `Transition`/`Collapse` already animate their child; add Kineto to the content, not the transition node.\n- **chakra** (props-passthrough; provides: accordion, tabs, tooltip, toaster, dialog, drawer, skeleton, progress, switch, menu): Chakra `Presence`/`Dialog` animate their positioner via `data-state` keyframes; do not add a Kineto reveal to the same node.\n- **antd** (props-passthrough-or-wrapper; provides: collapse, tabs, tooltip, message, notification, modal, drawer, skeleton, progress, switch, carousel, dropdown): Ant Design `Carousel` (react-slick) and `data-kt-slider` on one element: choose one. `Statistic` has its own count-up animation when `formatter` uses CountUp; use either that or `data-kt-counter`, not both.\n- **daisyui** (data-attributes; provides: skeleton, tooltip, tabs, collapse, toast, carousel, progress, modal, drawer): daisyUI `carousel` is CSS scroll-snap; `data-kt-slider` on the same element takes over the track — choose one. daisyUI `skeleton` boxes stay for text; use `data-kt-lazy=\"skeleton\"` on images.\n- **nuxt-ui** (attrs-fallthrough; provides: accordion, tabs, tooltip, toast, modal, slideover, skeleton, progress, switch, carousel, navigation-menu): Nuxt UI `UCarousel` (Embla) and `data-kt-slider`: choose one per container. Reka UI content nodes animate through `data-state`; add Kineto to their children. Components rendered on the server keep the attributes, so hydration sees the same markup the observer will scan.\n- **primevue** (attrs-fallthrough; provides: accordion, tabs, tooltip, toast, dialog, sidebar, skeleton, progress, toggleswitch, carousel, galleria, megamenu): PrimeVue `Carousel`/`Galleria` and `data-kt-slider` on the same element: choose one. PrimeVue `Ripple` directive and `data-kt-ripple` on one button: choose one.\n- **vuetify** (attrs-fallthrough; provides: expansion-panels, tabs, tooltip, snackbar, dialog, navigation-drawer, skeleton-loader, progress, switch, carousel, menu): `v-carousel` and `data-kt-slider` on one element: choose one. `v-btn` already has a ripple; use `data-kt-magnetic`/`data-kt-gesture` instead of `data-kt-ripple`.\n\n## Option lookup\n\nExact option names, variants and defaults live in `kineto.features.json` (https://raw.githubusercontent.com/catgarret/kineto/main/kineto.features.json) and the generated reference (https://github.com/catgarret/kineto/blob/main/docs/module-reference.md). With the Kineto MCP server (`npx @dong-gri/kineto-mcp`) call `kineto_suggest`, `kineto_snippet` and `kineto_validate_options` instead of guessing.\n\n## Do not generate these any more\n\n| Instead of | Generate | Why |\n|---|---|---|\n| `data-kt-lazy=\"dither\"` | `data-kt-stylize=\"dither\"` | Lazy fetches an image; these redraw pixels that are already there. They are the Stylize module now, and Stylize adds the living look (motion, pointer) and the crisp dissolve reveal that the alias cannot reach. |\n| `data-kt-lazy=\"ascii\"` | `data-kt-stylize=\"ascii\"` | Lazy fetches an image; these redraw pixels that are already there. They are the Stylize module now, and Stylize adds the living look (motion, pointer) and the crisp dissolve reveal that the alias cannot reach. |\n| `data-kt-lazy=\"halftone\"` | `data-kt-stylize=\"halftone\"` | Lazy fetches an image; these redraw pixels that are already there. They are the Stylize module now, and Stylize adds the living look (motion, pointer) and the crisp dissolve reveal that the alias cannot reach. |\n\n- Stylize does not load anything: use src, not data-src, and data-kt-mode=\"persist|reveal\" instead of data-kt-persist. Both modules can sit on one element when the image also needs lazy loading.\n\nThe old markup keeps working until next major and emits a recoverable `KT_DEPRECATED` diagnostic when `debug` is on.\n\n## Modules with their variant keys\n\n- `data-kt-ambient-media` — variants: image-clone, video-sample, color (JS key `preset`)\n- `data-kt-blur-text` — variants: blur (JS key `preset`)\n- `data-kt-brush-reveal` — variants: brush (JS key `preset`)\n- `data-kt-card-glow` — variants: spotlight, edge, border, comet, aurora, shine, glass (JS key `preset`)\n- `data-kt-counter` — variants: slot, plain, digit, pop, flip, clock (JS key `preset`)\n- `data-kt-date-time` — variants: relative, absolute, both (JS key `preset`)\n- `data-kt-css-scroll` — variants: progress-property, css-animation-timeline, scroll-timeline (JS key `preset`)\n- `data-kt-cursor` — variants: dot, ring, blob, crosshair, text, trail, orbit, snake, sparkle, image, custom (JS key `preset`)\n- `data-kt-fullpage` — variants: transform, snap (JS key `mode`)\n- `data-kt-glitch` — variants: rgb, pixel, noise, crt, wave, image, datamosh, reveal, vcr, rgb-slice-burst (JS key `preset`)\n- `data-kt-lazy` — variants: fade, blur-up, wave, grain, skeleton, pixelate, print, dissolve, flicker, polaroid, crt, data-mosaic, rgb-slice-burst, dither, ascii, halftone (JS key `preset`)\n- `data-kt-stylize` — variants: dither, ascii, halftone (JS key `preset`)\n- `data-kt-lightbox` — variants: viewer, grouped (JS key `preset`)\n- `data-kt-loader` — variants: slot, circular, bar (JS key `preset`)\n- `data-kt-loading-indicator` — variants: spinner, dots, bar, shimmer, shimmer-wave, terminal (JS key `preset`)\n- `data-kt-magnetic` — variants: pointer, dock (JS key `preset`)\n- `data-kt-marquee` — variants: left, right, reverse-on-scroll (JS key `preset`)\n- `data-kt-mouse-parallax` — variants: pointer, gyro, compass (JS key `preset`)\n- `data-kt-overflow-text` — variants: loop, bounce, rewind, once, page, flip, dissolve, page-roll, rolling, fade, scroll-fade (JS key `preset`)\n- `data-kt-page-reveal` — variants: curtain, split, blinds, diagonal, shutter, fade, zoom, iris, flash, center-slit, data-mosaic, curve, dissolve, push, grid, fold (JS key `preset`)\n- `data-kt-page-transition` — variants: same-origin (JS key `effect`)\n- `data-kt-parallax` — variants: x, y (JS key `preset`)\n- `data-kt-progress` — variants: page:scaleX, page:width, element:scaleX, element:width (JS key `preset`)\n- `data-kt-reveal` — variants: fade, fade-up, fade-down, fade-left, fade-right, slide-up, slide-down, slide-left, slide-right, zoom-in, zoom-out, blur, rise, soft, flip-x, flip-y, rotate, mask, wipe, class, clock, swing, skew (JS key `preset`)\n- `data-kt-radial` — variants: bottom, top, left, right (JS key `preset`)\n- `data-kt-ripple` — variants: material (JS key `preset`)\n- `data-kt-scroll-sequence` — variants: cover, contain (JS key `preset`)\n- `data-kt-scroll-velocity` — variants: skew, rotate, scale, blur, translate, combo (JS key `preset`)\n- `data-kt-slider` — variants: slide, fade, dissolve, wipe, coverflow, flip, cube, cards, creative, radial (JS key `preset`)\n- `data-kt-sticky-stack` — variants: vertical, horizontal, zindex, floating (JS key `preset`)\n- `data-kt-text-fill` — variants: scroll-fill (JS key `preset`)\n- `data-kt-text-reveal` — variants: stream, char, word, line, bounce, hangul, decode, flicker, shuffle (JS key `preset`)\n- `data-kt-text-split` — variants: char, word (JS key `preset`)\n- `data-kt-text-transition` — variants: slide-up, flip, rise, fade, blur, scale, clip, dissolve, shimmer, pop (JS key `preset`)\n- `data-kt-tilt` — variants: tilt-glare, tilt, x-only, y-only, reverse (JS key `preset`)\n- `data-kt-typewriter` — variants: type-erase (JS key `preset`)\n- `data-kt-vibrate` — variants: tap, double-tap, soft, rigid, heavy, success, warning, error, ratchet, heartbeat, long-press (JS key `preset`)\n- `data-kt-confetti` — variants: burst (JS key `preset`)\n- `data-kt-accordion` — variants: panel (JS key `effect`)\n- `data-kt-hold` — variants: confirm, tap (JS key `mode`)\n- `data-kt-mega-menu` — variants: dropdown, mega, radial (JS key `preset`)\n- `data-kt-toast` — variants: stack (JS key `type`)\n- `data-kt-bottom-sheet` — variants: sheet (JS key `preset`)\n- `data-kt-tabs` — variants: line (JS key `effect`)\n- `data-kt-cover-reveal` — variants: block (JS key `preset`)\n- `data-kt-gesture` — variants: spring, pull (JS key `preset`)\n- `data-kt-drag` — variants: free (JS key `preset`)\n- `data-kt-tooltip` — variants: default (JS key `effect`)\n- `data-kt-switch` — variants: toggle (JS key `preset`)\n- `data-kt-flip` — variants: layout, none, slide, fade, crossfade, fade-slide, scale, fold (JS key `mode`)\n- `data-kt-scroll-shadows` — variants: vertical, horizontal, mask (JS key `mode`)\n- `data-kt-squircle` — variants: squircle, round, bevel, scoop, notch, square (JS key `preset`)\n- `data-kt-sticky-header` — variants: shrink, toggle (JS key `preset`)\n- `data-kt-horizontal-scroll` — variants: pin (JS key `preset`)\n- `data-kt-canvas-effect` — variants: custom (JS key `preset`)\n"
+ "content": "# Kineto rules for coding agents (v0.13.1)\n\nKineto (`@dong-gri/kineto`) is a vanilla-first motion/effects library: 55 modules activated by `data-kt-*` attributes or `Kineto.create()`, with React/Vue/jQuery adapters. Use these rules whenever you generate UI with any component library (shadcn/ui, Bootstrap, MUI, Mantine, Chakra, Ant Design, daisyUI, Nuxt UI, PrimeVue, Vuetify) or from a Figma frame.\n\n## Principles\n\n- Kineto adds motion and media effects to markup you already own; it does not replace the component library's behaviour (dialogs, menus, forms, routing).\n- Prefer the library's own component when it already provides the behaviour (toast, tooltip, tabs, accordion, dialog, carousel). Use Kineto for what the library does not ship: entrance motion, text motion, counters, image loading effects, hover/pointer effects, marquee, page reveal, cursor, scroll storytelling.\n- Never put two things that animate the same property on one element: a Tailwind/tw-animate-css enter animation plus a Kineto reveal, a Bootstrap carousel plus a Kineto slider, two Kineto modules that both write transform (see docs/rfc/module-composition.md).\n- Attach with `data-kt-*` attributes on the element that reaches the DOM, call `Kineto.observe()` once at startup so markup added later (framework renders, portals, modals) is picked up and cleaned up, and reach for the React/Vue adapters only when you need the instance (replay, pause, presence).\n- Respect `prefers-reduced-motion`: Kineto already renders the meaningful end state for every module, so do not add a second reduced-motion branch around it.\n- Read the exact option names from kineto.features.json (or `npx @dong-gri/kineto-mcp`); do not invent options.\n\n## Setup (once per app)\n\n```js\nimport '@dong-gri/kineto/style.css';\nimport Kineto from '@dong-gri/kineto';\nKineto.observe(); // scans now and follows later DOM changes\n```\n\nReact: mount a client `KinetoProvider` that calls `Kineto.observe()` in an effect (shadcn registry: `npx shadcn@latest add @kineto/provider`). Vue/Nuxt: a client plugin that calls `Kineto.observe()`. Static HTML: the CDN snippet with `Kineto.observe()`.\n\n## Attachment grammar\n\n- Activation: `data-kt-=\"\"` (omit the value for the default variant). Options: `data-kt-=\"value\"`; booleans as bare attributes; arrays/objects as JSON strings.\n- JavaScript: `Kineto.create('', element, { : '', ...options })`. React: `useKineto('', options)` / `\" options={...}>`. Vue: `v-motion=\"{ type, options }\"` / `useKineto()`.\n- Component libraries forward unknown props/attrs to their root DOM node, so `data-kt-*` on ``, ``, ``, `` … reaches the DOM. If a component does not forward attributes, wrap it in a plain element or use a ref.\n- One transform-writing module per element (tilt, gesture, magnetic, scrollVelocity, parallax, flip). One reveal per section, staggered, rather than one per item.\n\n## Decide by intent\n\n| Intent | Default recipe | When the library already has it |\n|---|---|---|\n| hero-headline — Hero headline entrance (hero, headline, key visual, kv) | `data-kt-text-reveal=\"stream\" data-kt-speed=\"30\"` | Kineto provides it |\n| section-entrance — Section / card entrance on scroll (fade in, scroll reveal, entrance, appear) | `data-kt-reveal=\"fade-up\" data-kt-once data-kt-duration=\"0.7\"` | Kineto provides it |\n| card-hover — Card hover: tilt, glow, spring (card hover, 3d tilt, glow, spotlight) | `data-kt-tilt=\"tilt-glare\" data-kt-max=\"8\"` | Kineto provides it |\n| cta-button — Call-to-action button feedback (button, cta, click feedback, ripple) | `data-kt-magnetic=\"pointer\" data-kt-strength=\"0.3\"` | Kineto provides it |\n| kpi-number — KPI / statistic count-up (counter, count up, kpi, stat) | `data-kt-counter=\"slot\" data-kt-to=\"128000\" data-kt-format=\",\"` | Kineto provides it |\n| countdown — Countdown / elapsed clock (countdown, timer, until, clock) | `data-kt-counter=\"clock\" data-kt-until=\"2026-12-31T00:00:00+09:00\" data-kt-show-days` | Kineto provides it |\n| image-loading — Image loading effect / placeholder (lazy load, skeleton, blur up, placeholder) | `data-kt-lazy=\"skeleton\" data-kt-skeleton-variant=\"shimmer\"` | Kineto provides it |\n| media-texture — Graphic texture on an image or video (dither, ascii art, halftone, duotone) | `data-kt-stylize=\"dither\" data-kt-cell-size=\"8\" data-kt-dither-type=\"4x4\"` | Kineto provides it |\n| gallery-lightbox — Image gallery with lightbox (lightbox, gallery, zoom, viewer) | `data-kt-lightbox=\"grouped\" data-kt-group=\"gallery\"` | Kineto provides it |\n| interactive-reveal — Brush / scratch-to-reveal media (brush reveal, scratch, scratch card, drag to reveal) | `data-kt-brush-reveal data-kt-radius=\"48\" data-kt-softness=\"0.6\"` | Kineto provides it |\n| carousel — Carousel / slider (carousel, slider, swiper, slides) | `data-kt-slider=\"slide\" data-kt-loop data-kt-autoplay=\"4000\"` | prefer the library (shadcn: Carousel (Embla); bootstrap: .carousel; mantine: @mantine/carousel; antd: Carousel; daisyui: .carousel; nuxt-ui: UCarousel; primevue: Carousel; vuetify: v-carousel) |\n| marquee-ticker — Marquee / ticker (marquee, ticker, logo wall, scrolling text) | `data-kt-marquee=\"left\" data-kt-speed=\"60\" data-kt-pause-on-hover` | Kineto provides it |\n| toast — Toast notification (toast, snackbar, notification, 알림) | `data-kt-toast data-kt-position=\"bottom-right\" data-kt-duration=\"4000\"` | prefer the library (shadcn: Sonner / Toast; bootstrap: .toast; mui: Snackbar; mantine: @mantine/notifications; chakra: Toaster; antd: message / notification; daisyui: .toast (static); nuxt-ui: useToast; primevue: Toast; vuetify: v-snackbar) |\n| tooltip — Tooltip (tooltip, hint, 툴팁) | `data-kt-tooltip data-kt-placement=\"top\"` | prefer the library (shadcn: Tooltip; bootstrap: data-bs-toggle=\"tooltip\"; mui: Tooltip; mantine: Tooltip; chakra: Tooltip; antd: Tooltip; daisyui: .tooltip; nuxt-ui: UTooltip; primevue: v-tooltip; vuetify: v-tooltip) |\n| dialog-enter-exit — Dialog / modal enter-exit motion (dialog, modal, enter exit, mount unmount) | `data-kt-presence` | keep the library component, animate its content (shadcn: Dialog (tw-animate-css); bootstrap: .modal.fade; mui: Dialog + Grow; mantine: Modal; chakra: Dialog; antd: Modal; nuxt-ui: UModal; primevue: Dialog; vuetify: v-dialog) |\n| bottom-sheet — Mobile bottom sheet (bottom sheet, drawer, sheet, 바텀시트) | `data-kt-bottom-sheet data-kt-resizable data-kt-handle` | prefer the library (shadcn: Drawer (vaul) / Sheet; bootstrap: .offcanvas; mui: SwipeableDrawer; mantine: Drawer; chakra: Drawer; antd: Drawer; nuxt-ui: USlideover; primevue: Sidebar; vuetify: v-bottom-sheet) |\n| page-transition — Page transition and first-paint reveal (page transition, route transition, page reveal, curtain) | `data-kt-page-reveal=\"curtain\" data-kt-color=\"#0a0908\"` | Kineto provides it |\n| scroll-progress — Reading / scroll progress (scroll progress, reading progress, progress bar, back to top) | `data-kt-progress=\"page:scaleX\" data-kt-position=\"top\"` | Kineto provides it |\n| sticky-header — Sticky header that shrinks or hides on scroll (sticky header, navbar, shrink, hide on scroll) | `data-kt-sticky-header=\"shrink\" data-kt-distance=\"80\"` | keep the library component, animate its content (bootstrap: .navbar; shadcn: NavigationMenu; mui: AppBar; vuetify: v-app-bar) |\n| parallax-depth — Parallax and pointer depth (parallax, depth, layers, mouse parallax) | `data-kt-parallax=\"y\" data-kt-speed=\"0.3\"` | Kineto provides it |\n| scroll-storytelling — Scroll storytelling (sequence, pinned stacks, horizontal) (scrollytelling, image sequence, pinned, sticky stack) | `data-kt-scroll-sequence=\"cover\" data-kt-frames=\"120\"` | Kineto provides it |\n| loading-state — Inline loading indicator / real progress (spinner, loading, progress, shimmer) | `data-kt-loading-indicator=\"spinner\"` | prefer the library (shadcn: Spinner / Progress; bootstrap: .spinner-border; mui: CircularProgress; mantine: Loader; chakra: Spinner; antd: Spin; daisyui: .loading; nuxt-ui: UProgress; primevue: ProgressSpinner; vuetify: v-progress-circular) |\n| text-rotation — Rotating words / typewriter (rotating text, typewriter, word swap, typing) | `data-kt-text-transition=\"slide-up\" data-kt-texts=\"["design","motion","ship"]\" data-kt-hold=\"1.4\"` | Kineto provides it |\n| glitch-accent — Glitch / CRT accent (glitch, crt, vhs, datamosh) | `data-kt-glitch=\"rgb\"` | Kineto provides it |\n| custom-cursor — Custom cursor (cursor, custom cursor, pointer, 커서) | `data-kt-cursor=\"ring\"` | Kineto provides it |\n| celebration — Success celebration (confetti, celebrate, success, reward) | `data-kt-confetti data-kt-trigger=\"click\"` | Kineto provides it |\n| overflow-shadows — Scrollable area edge shadows (scroll shadow, overflow, scroll area, fade edges) | `data-kt-scroll-shadows=\"horizontal\"` | keep the library component, animate its content (shadcn: ScrollArea; mantine: ScrollArea; chakra: ScrollArea) |\n| corner-shape — iOS-style corner shape (squircle) (squircle, superellipse, corner shape, corner smoothing) | `data-kt-squircle=\"squircle\"` | keep the library component, animate its content (tailwind: rounded-* (circular corners only); shadcn: --radius (circular corners only); mui: shape.borderRadius (circular corners only)) |\n| interactive-background — Interactive canvas / WebGL background (React Bits–style) (react bits, background effect, interactive background, shader) | `data-kt-canvas-effect` | Kineto provides it |\n| confirm-hold — Hold-to-confirm and mash gauges (hold to confirm, long press, mash, confirm) | `data-kt-hold=\"confirm\" data-kt-duration=\"1.2\"` | Kineto provides it |\n| disclosure — Accordion, tabs, switch, mega menu (behaviour components) (accordion, tabs, switch, toggle) | `data-kt-accordion data-kt-single` | prefer the library (shadcn: Accordion / Tabs / Switch / NavigationMenu; bootstrap: .accordion / .nav-tabs / .form-switch / .navbar; mui: Accordion / Tabs / Switch / Menu; mantine: Accordion / Tabs / Switch; chakra: Accordion / Tabs / Switch; antd: Collapse / Tabs / Switch / Menu; daisyui: .collapse / .tabs / .toggle; nuxt-ui: UAccordion / UTabs / USwitch; primevue: Accordion / Tabs / ToggleSwitch / MegaMenu; vuetify: v-expansion-panels / v-tabs / v-switch) |\n| layout-change — Animated reorder / add / remove (FLIP) (flip, reorder, list animation, layout animation) | `data-kt-flip=\"layout\" data-kt-watch` | Kineto provides it |\n| drag-and-fullpage — Draggable elements and fullpage sections (drag, draggable, fullpage, snap sections) | `data-kt-drag data-kt-inertia data-kt-snap-back` | Kineto provides it |\n\n## Figma layer → intent\n\n- node is a TEXT layer with font size ≥ 40 or named like a heading → `hero-headline`\n- node is an IMAGE fill (RECTANGLE/FRAME with image paint) or named image/photo/thumbnail → `image-loading`\n- node repeats as a grid/list of similar frames (auto-layout children with the same component) → `section-entrance`\n- node is a COMPONENT INSTANCE named Button/CTA → `cta-button`\n- node text is mostly digits (counts, KRW/USD, percentages) → `kpi-number`\n- frame is wider than the viewport and clips horizontally repeating children → `marquee-ticker`\n- frame has prototype interaction 'On drag' or scroll horizontally → `carousel`\n- Layer-name regexes per intent: hero-headline (/hero/i /kv/i /key ?visual/i /headline/i /title/i /h1/i); section-entrance (/section/i /card/i /feature/i /grid/i /list item/i /row/i); card-hover (/card/i /product card/i /tile/i /thumbnail/i); cta-button (/button/i /cta/i /primary button/i); kpi-number (/stat/i /kpi/i /number/i /metric/i /counter/i /\\d{2,}/i); countdown (/countdown/i /timer/i /d-day/i /clock/i); image-loading (/image/i /photo/i /thumbnail/i /cover/i /gallery/i); media-texture (/dither/i /halftone/i /ascii/i /texture/i /grain/i /duotone/i /poster/i /print/i /newsprint/i /screen/i); gallery-lightbox (/gallery/i /lightbox/i /grid/i /masonry/i /photo/i); interactive-reveal (/scratch/i /brush/i /reveal/i /mask/i); carousel (/carousel/i /slider/i /swiper/i /slides/i); marquee-ticker (/marquee/i /ticker/i /logo/i /partners/i /ranking/i); toast (/toast/i /snackbar/i /notification/i /alert/i); tooltip (/tooltip/i /hint/i); dialog-enter-exit (/modal/i /dialog/i /overlay/i /popup/i); bottom-sheet (/bottom sheet/i /sheet/i /drawer/i); page-transition (/intro/i /splash/i /loading/i /transition/i); scroll-progress (/progress/i /reading bar/i /back to top/i); sticky-header (/header/i /navbar/i /gnb/i /top bar/i); parallax-depth (/layer/i /bg/i /background/i /fg/i /foreground/i /parallax/i); scroll-storytelling (/sequence/i /frames/i /pinned/i /story/i /horizontal/i); loading-state (/spinner/i /loading/i /loader/i /progress/i); text-rotation (/typing/i /rotating/i /tagline/i); glitch-accent (/glitch/i /crt/i /vhs/i /neon/i /cyber/i /game/i); custom-cursor (/cursor/i); celebration (/success/i /complete/i /reward/i /confetti/i); overflow-shadows (/list/i /scroll area/i /table/i /chips/i); corner-shape (/card/i /button/i /app icon/i /avatar/i /tile/i /sheet/i); interactive-background (/hero/i /background/i /banner/i /section/i /card/i); confirm-hold (/confirm/i /delete/i /hold/i); disclosure (/accordion/i /faq/i /tabs/i /segmented/i /switch/i /toggle/i /menu/i); layout-change (/list/i /grid/i /kanban/i /board/i); drag-and-fullpage (/draggable/i /fullpage/i /sections/i)\n\n## Per-ecosystem notes\n\n- **vanilla** (data-attributes; provides: nothing — vanilla): Every Kineto module is available; there is no library component to defer to. Multi-page sites can add `data-kt-page-transition` on for same-origin page transitions and `data-kt-page-reveal` for the first-paint overlay.\n- **shadcn** (props-passthrough; provides: accordion, tabs, tooltip, toast, dialog, sheet, carousel, skeleton, progress, switch, navigation-menu, scroll-area): tw-animate-css (`animate-in`, `fade-in-0`, `zoom-in-95` …) already animates Dialog/Sheet/Popover content — do not add a Kineto reveal to the same node; animate the children instead. shadcn `Carousel` (Embla) owns the track; put Kineto reveal/tilt on the slides, never `data-kt-slider` on the same container. shadcn `Skeleton` is a layout placeholder; for images prefer `data-kt-lazy=\"skeleton\"` on the ` ` itself so the placeholder resolves into the real picture. Next.js App Router: the provider is a client component; server components can still render `data-kt-*` attributes. Tailwind v4 utilities and Kineto never fight over layout: Kineto only writes transform/opacity/filter/clip on the host and creates its own wrapper layers.\n- **tailwind** (data-attributes; provides: nothing — vanilla): `animate-*` keyframe utilities and `transition-transform` on the same element as `data-kt-reveal`, `data-kt-gesture` or `data-kt-tilt` double-animate the host. daisyUI, Flowbite and Preline are plain class-based markup on top of Tailwind; everything in this entry applies to them.\n- **bootstrap** (data-attributes; provides: accordion, collapse, tabs, tooltip, popover, toast, modal, offcanvas, carousel, placeholder, spinner, progress, dropdown, navbar): `.carousel` (Bootstrap) and `data-kt-slider` on the same element: choose one. Bootstrap carousel for content slides, Kineto slider when you want coverflow/cube/cards effects. `.placeholder` / `.placeholder-glow` and `data-kt-lazy=\"skeleton\"`: choose one placeholder per image. `data-bs-toggle=\"tooltip\"` and `data-kt-tooltip` on one element create two tooltips. `.accordion` (Bootstrap collapse) and `data-kt-accordion`: Bootstrap owns open/close; add Kineto reveal to the panel content instead. Bootstrap's `.fade` modal transition stays; animate the modal body with `data-kt-reveal` or `data-kt-text-split` for richer entrances. Bootstrap's grid and utilities are unaffected; Kineto never sets layout properties on the host.\n- **mui** (props-passthrough; provides: accordion, tabs, tooltip, snackbar, dialog, drawer, skeleton, progress, switch, menu): MUI `Grow`/`Fade`/`Slide` transitions wrap Dialog and Menu content; do not add a Kineto reveal to the same node. MUI `ButtonBase` already renders a Material ripple (`TouchRipple`); use `data-kt-magnetic` or `data-kt-gesture` on buttons, not `data-kt-ripple`. Emotion class names are left untouched; Kineto writes inline styles it later restores on destroy.\n- **mantine** (props-passthrough; provides: accordion, tabs, tooltip, notifications, modal, drawer, skeleton, progress, switch, carousel): `@mantine/carousel` (Embla) and `data-kt-slider` on one element: choose one. Mantine `Transition`/`Collapse` already animate their child; add Kineto to the content, not the transition node.\n- **chakra** (props-passthrough; provides: accordion, tabs, tooltip, toaster, dialog, drawer, skeleton, progress, switch, menu): Chakra `Presence`/`Dialog` animate their positioner via `data-state` keyframes; do not add a Kineto reveal to the same node.\n- **antd** (props-passthrough-or-wrapper; provides: collapse, tabs, tooltip, message, notification, modal, drawer, skeleton, progress, switch, carousel, dropdown): Ant Design `Carousel` (react-slick) and `data-kt-slider` on one element: choose one. `Statistic` has its own count-up animation when `formatter` uses CountUp; use either that or `data-kt-counter`, not both.\n- **daisyui** (data-attributes; provides: skeleton, tooltip, tabs, collapse, toast, carousel, progress, modal, drawer): daisyUI `carousel` is CSS scroll-snap; `data-kt-slider` on the same element takes over the track — choose one. daisyUI `skeleton` boxes stay for text; use `data-kt-lazy=\"skeleton\"` on images.\n- **nuxt-ui** (attrs-fallthrough; provides: accordion, tabs, tooltip, toast, modal, slideover, skeleton, progress, switch, carousel, navigation-menu): Nuxt UI `UCarousel` (Embla) and `data-kt-slider`: choose one per container. Reka UI content nodes animate through `data-state`; add Kineto to their children. Components rendered on the server keep the attributes, so hydration sees the same markup the observer will scan.\n- **primevue** (attrs-fallthrough; provides: accordion, tabs, tooltip, toast, dialog, sidebar, skeleton, progress, toggleswitch, carousel, galleria, megamenu): PrimeVue `Carousel`/`Galleria` and `data-kt-slider` on the same element: choose one. PrimeVue `Ripple` directive and `data-kt-ripple` on one button: choose one.\n- **vuetify** (attrs-fallthrough; provides: expansion-panels, tabs, tooltip, snackbar, dialog, navigation-drawer, skeleton-loader, progress, switch, carousel, menu): `v-carousel` and `data-kt-slider` on one element: choose one. `v-btn` already has a ripple; use `data-kt-magnetic`/`data-kt-gesture` instead of `data-kt-ripple`.\n\n## Option lookup\n\nExact option names, variants and defaults live in `kineto.features.json` (https://raw.githubusercontent.com/catgarret/kineto/main/kineto.features.json) and the generated reference (https://github.com/catgarret/kineto/blob/main/docs/module-reference.md). With the Kineto MCP server (`npx @dong-gri/kineto-mcp`) call `kineto_suggest`, `kineto_snippet` and `kineto_validate_options` instead of guessing.\n\n## Do not generate these any more\n\n| Instead of | Generate | Why |\n|---|---|---|\n| `data-kt-lazy=\"dither\"` | `data-kt-stylize=\"dither\"` | Lazy fetches an image; these redraw pixels that are already there. They are the Stylize module now, and Stylize adds the living look (motion, pointer) and the crisp dissolve reveal that the alias cannot reach. |\n| `data-kt-lazy=\"ascii\"` | `data-kt-stylize=\"ascii\"` | Lazy fetches an image; these redraw pixels that are already there. They are the Stylize module now, and Stylize adds the living look (motion, pointer) and the crisp dissolve reveal that the alias cannot reach. |\n| `data-kt-lazy=\"halftone\"` | `data-kt-stylize=\"halftone\"` | Lazy fetches an image; these redraw pixels that are already there. They are the Stylize module now, and Stylize adds the living look (motion, pointer) and the crisp dissolve reveal that the alias cannot reach. |\n\n- Stylize does not load anything: use src, not data-src, and data-kt-mode=\"persist|reveal\" instead of data-kt-persist. Both modules can sit on one element when the image also needs lazy loading.\n\nThe old markup keeps working until next major and emits a recoverable `KT_DEPRECATED` diagnostic when `debug` is on.\n\n## Modules with their variant keys\n\n- `data-kt-ambient-media` — variants: image-clone, video-sample, color (JS key `preset`)\n- `data-kt-blur-text` — variants: blur (JS key `preset`)\n- `data-kt-brush-reveal` — variants: brush (JS key `preset`)\n- `data-kt-card-glow` — variants: spotlight, edge, border, comet, aurora, shine, glass (JS key `preset`)\n- `data-kt-counter` — variants: slot, plain, digit, pop, flip, clock (JS key `preset`)\n- `data-kt-date-time` — variants: relative, absolute, both (JS key `preset`)\n- `data-kt-css-scroll` — variants: progress-property, css-animation-timeline, scroll-timeline (JS key `preset`)\n- `data-kt-cursor` — variants: dot, ring, blob, crosshair, text, trail, orbit, snake, sparkle, image, custom (JS key `preset`)\n- `data-kt-fullpage` — variants: transform, snap (JS key `mode`)\n- `data-kt-glitch` — variants: rgb, pixel, noise, crt, wave, image, datamosh, reveal, vcr, rgb-slice-burst (JS key `preset`)\n- `data-kt-lazy` — variants: fade, blur-up, wave, grain, skeleton, pixelate, print, dissolve, flicker, polaroid, crt, data-mosaic, rgb-slice-burst, dither, ascii, halftone (JS key `preset`)\n- `data-kt-stylize` — variants: dither, ascii, halftone (JS key `preset`)\n- `data-kt-lightbox` — variants: viewer, grouped (JS key `preset`)\n- `data-kt-loader` — variants: slot, circular, bar (JS key `preset`)\n- `data-kt-loading-indicator` — variants: spinner, dots, bar, shimmer, shimmer-wave, terminal (JS key `preset`)\n- `data-kt-magnetic` — variants: pointer, dock (JS key `preset`)\n- `data-kt-marquee` — variants: left, right, reverse-on-scroll (JS key `preset`)\n- `data-kt-mouse-parallax` — variants: pointer, gyro, compass (JS key `preset`)\n- `data-kt-overflow-text` — variants: loop, bounce, rewind, once, page, flip, dissolve, page-roll, rolling, fade, scroll-fade (JS key `preset`)\n- `data-kt-page-reveal` — variants: curtain, split, blinds, diagonal, shutter, fade, zoom, iris, flash, center-slit, data-mosaic, curve, dissolve, push, grid, fold (JS key `preset`)\n- `data-kt-page-transition` — variants: same-origin (JS key `effect`)\n- `data-kt-parallax` — variants: x, y (JS key `preset`)\n- `data-kt-progress` — variants: page:scaleX, page:width, element:scaleX, element:width (JS key `preset`)\n- `data-kt-reveal` — variants: fade, fade-up, fade-down, fade-left, fade-right, slide-up, slide-down, slide-left, slide-right, zoom-in, zoom-out, blur, rise, soft, flip-x, flip-y, rotate, mask, wipe, class, clock, swing, skew (JS key `preset`)\n- `data-kt-radial` — variants: bottom, top, left, right (JS key `preset`)\n- `data-kt-ripple` — variants: material (JS key `preset`)\n- `data-kt-scroll-sequence` — variants: cover, contain (JS key `preset`)\n- `data-kt-scroll-velocity` — variants: skew, rotate, scale, blur, translate, combo (JS key `preset`)\n- `data-kt-slider` — variants: slide, fade, dissolve, wipe, coverflow, flip, cube, cards, creative, radial (JS key `preset`)\n- `data-kt-sticky-stack` — variants: vertical, horizontal, zindex, floating (JS key `preset`)\n- `data-kt-text-fill` — variants: scroll-fill (JS key `preset`)\n- `data-kt-text-reveal` — variants: stream, char, word, line, bounce, hangul, decode, flicker, shuffle (JS key `preset`)\n- `data-kt-text-split` — variants: char, word (JS key `preset`)\n- `data-kt-text-transition` — variants: slide-up, flip, rise, fade, blur, scale, clip, dissolve, shimmer, pop (JS key `preset`)\n- `data-kt-tilt` — variants: tilt-glare, tilt, x-only, y-only, reverse (JS key `preset`)\n- `data-kt-typewriter` — variants: type-erase (JS key `preset`)\n- `data-kt-vibrate` — variants: tap, double-tap, soft, rigid, heavy, success, warning, error, ratchet, heartbeat, long-press (JS key `preset`)\n- `data-kt-confetti` — variants: burst (JS key `preset`)\n- `data-kt-accordion` — variants: panel (JS key `effect`)\n- `data-kt-hold` — variants: confirm, tap (JS key `mode`)\n- `data-kt-mega-menu` — variants: dropdown, mega, radial (JS key `preset`)\n- `data-kt-toast` — variants: stack (JS key `type`)\n- `data-kt-bottom-sheet` — variants: sheet (JS key `preset`)\n- `data-kt-tabs` — variants: line (JS key `effect`)\n- `data-kt-cover-reveal` — variants: block (JS key `preset`)\n- `data-kt-gesture` — variants: spring, pull (JS key `preset`)\n- `data-kt-drag` — variants: free (JS key `preset`)\n- `data-kt-tooltip` — variants: default (JS key `effect`)\n- `data-kt-switch` — variants: toggle (JS key `preset`)\n- `data-kt-flip` — variants: layout, none, slide, fade, crossfade, fade-slide, scale, fold (JS key `mode`)\n- `data-kt-scroll-shadows` — variants: vertical, horizontal, mask (JS key `mode`)\n- `data-kt-squircle` — variants: squircle, round, bevel, scoop, notch, square (JS key `preset`)\n- `data-kt-sticky-header` — variants: shrink, toggle (JS key `preset`)\n- `data-kt-horizontal-scroll` — variants: pin (JS key `preset`)\n- `data-kt-canvas-effect` — variants: custom (JS key `preset`)\n"
}
],
"docs": "Cursor reads .cursor/rules/kineto.mdc automatically. For Claude Code or Codex, reference docs/kineto-ai-rules.md from CLAUDE.md / AGENTS.md.",
diff --git a/site/variant-catalog.js b/site/variant-catalog.js
index 0cc51a8..f4074a7 100644
--- a/site/variant-catalog.js
+++ b/site/variant-catalog.js
@@ -1 +1 @@
-window.KINETO_VARIANT_CATALOG=Object.freeze({schemaVersion:`1.0.0`,libraryVersion:`0.13.0`,moduleCount:38,variantCount:228,capabilities:{any:`No requirement — works on any element.`,image:`Needs an (the element itself or a descendant).`,text:`Needs rendered text and no image to operate on.`,video:`Needs a element.`,media:`Needs an or (the element itself or a descendant).`,items:`Needs two or more element children to sequence.`,track:`Needs a .kt-slider-track descendant for track-based slider effects.`},modules:{ambientMedia:{attribute:`data-kt-ambient-media`,category:`effects`,preview:`link`,reason:`variant 가 소스 종류(복제할 이미지·샘플링할 영상·단색)에서 파생됩니다. 옵션 하나로 바꿀 수 없어 모듈 데모의 세 카드가 그대로 비교 화면입니다.`,carrier:``,carrierAttribute:``,clearAttributes:[],defaultVariant:`image-clone`,deprecatedVariants:[],optionAttributes:[`data-kt-allow-overflow`,`data-kt-ambient-src`,`data-kt-ambient-srcset`,`data-kt-blur`,`data-kt-brightness`,`data-kt-color`,`data-kt-fallback-color`,`data-kt-hide-on-pause`,`data-kt-inset`,`data-kt-opacity`,`data-kt-radius`,`data-kt-sample-fps`,`data-kt-sample-height`,`data-kt-sample-width`,`data-kt-saturation`,`data-kt-scale`,`data-kt-source`,`data-kt-src`],variants:[{name:`image-clone`,requires:`any`,deprecated:!1},{name:`video-sample`,requires:`any`,deprecated:!1},{name:`color`,requires:`any`,deprecated:!1}]},cardGlow:{attribute:`data-kt-card-glow`,category:`pointer`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-mode`],defaultVariant:`spotlight`,deprecatedVariants:[],optionAttributes:`data-kt-always-on.data-kt-blend-mode.data-kt-blur.data-kt-border-blur.data-kt-border-color.data-kt-border-color2.data-kt-border-glow.data-kt-border-inset.data-kt-border-opacity.data-kt-border-radius.data-kt-border-width.data-kt-color.data-kt-color1.data-kt-color2.data-kt-cycle-duration.data-kt-disable-on-mobile.data-kt-duration.data-kt-ease.data-kt-follow.data-kt-glass-blur.data-kt-glass-depth.data-kt-glass-refraction.data-kt-glass-rim.data-kt-glass-rim-opacity.data-kt-glass-saturate.data-kt-glass-sheen.data-kt-glass-tint.data-kt-halo.data-kt-intensity.data-kt-luminous-border.data-kt-mode.data-kt-opacity.data-kt-preset.data-kt-radius.data-kt-reflection.data-kt-sensitivity.data-kt-shadow.data-kt-shadow-blur.data-kt-shadow-color.data-kt-shadow-css.data-kt-shadow-follow.data-kt-shadow-hover-only.data-kt-shadow-inset.data-kt-shadow-opacity.data-kt-shadow-spread.data-kt-shadow-x.data-kt-shadow-y.data-kt-smoothing.data-kt-speed.data-kt-spread.data-kt-surface.data-kt-surface-blend.data-kt-surface-blur.data-kt-surface-color.data-kt-surface-color2.data-kt-surface-gradient.data-kt-surface-inset.data-kt-surface-opacity.data-kt-surface-size`.split(`.`),variants:[{name:`spotlight`,requires:`any`,deprecated:!1},{name:`edge`,requires:`any`,deprecated:!1},{name:`border`,requires:`any`,deprecated:!1},{name:`comet`,requires:`any`,deprecated:!1},{name:`aurora`,requires:`any`,deprecated:!1},{name:`shine`,requires:`any`,deprecated:!1},{name:`glass`,requires:`any`,deprecated:!1}]},counter:{attribute:`data-kt-counter`,category:`text`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-mode`,`data-kt-style`],defaultVariant:`slot`,deprecatedVariants:[],optionAttributes:`data-kt-blink.data-kt-blink-separators.data-kt-clock-separator.data-kt-clock-style.data-kt-comma.data-kt-days-label.data-kt-decimals.data-kt-delay.data-kt-duration.data-kt-ease.data-kt-format.data-kt-from.data-kt-gap.data-kt-grouping.data-kt-hour12.data-kt-line-height.data-kt-locale.data-kt-loops.data-kt-mode.data-kt-on-complete.data-kt-once.data-kt-pop-align.data-kt-pop-duration.data-kt-pop-scale.data-kt-prefix.data-kt-preset.data-kt-roll-direction.data-kt-roll-duration.data-kt-seam-color.data-kt-seconds.data-kt-seconds-digits.data-kt-seconds-label.data-kt-seconds-only.data-kt-separator.data-kt-separator-color.data-kt-shadow.data-kt-show-days.data-kt-since.data-kt-stagger.data-kt-start.data-kt-style.data-kt-suffix.data-kt-tile-color.data-kt-tile-radius.data-kt-tile-text-color.data-kt-to.data-kt-until`.split(`.`),variants:[{name:`slot`,requires:`any`,deprecated:!1},{name:`plain`,requires:`any`,deprecated:!1},{name:`digit`,requires:`any`,deprecated:!1},{name:`pop`,requires:`any`,deprecated:!1},{name:`flip`,requires:`any`,deprecated:!1},{name:`clock`,requires:`any`,deprecated:!1}]},dateTime:{attribute:`data-kt-date-time`,category:`text`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-mode`],defaultVariant:`relative`,deprecatedVariants:[],optionAttributes:[`data-kt-date`,`data-kt-date-style`,`data-kt-datetime`,`data-kt-fallback`,`data-kt-live`,`data-kt-locale`,`data-kt-mode`,`data-kt-now`,`data-kt-numeric`,`data-kt-preset`,`data-kt-relative-cutoff`,`data-kt-relative-cutoff-unit`,`data-kt-relative-rounding`,`data-kt-relative-style`,`data-kt-relative-unit`,`data-kt-source`,`data-kt-time-style`,`data-kt-time-zone`,`data-kt-update-interval`,`data-kt-value`],variants:[{name:`relative`,requires:`any`,deprecated:!1},{name:`absolute`,requires:`any`,deprecated:!1},{name:`both`,requires:`any`,deprecated:!1}]},cssScroll:{attribute:`data-kt-css-scroll`,category:`scroll`,preview:`link`,reason:`variant 는 어떤 타임라인 옵션을 채웠는지의 결과입니다. 속성 값은 이름표일 뿐이라 값만 바꿔서는 다른 variant 가 되지 않습니다.`,carrier:``,carrierAttribute:``,clearAttributes:[],defaultVariant:`progress-property`,deprecatedVariants:[],optionAttributes:[`data-kt-axis`,`data-kt-css-animation`,`data-kt-end`,`data-kt-on-update`,`data-kt-property`,`data-kt-range-end`,`data-kt-range-start`,`data-kt-start`,`data-kt-timeline`],variants:[{name:`progress-property`,requires:`any`,deprecated:!1},{name:`css-animation-timeline`,requires:`any`,deprecated:!1},{name:`scroll-timeline`,requires:`any`,deprecated:!1}]},cursor:{attribute:`data-kt-cursor`,category:`pointer`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-type`],defaultVariant:`dot`,deprecatedVariants:[],optionAttributes:`data-kt-backdrop-filter.data-kt-background.data-kt-blur.data-kt-border-color.data-kt-border-width.data-kt-class-name.data-kt-click-image.data-kt-click-image-duration.data-kt-click-image-size.data-kt-click-sprite.data-kt-click-sprite-duration.data-kt-click-sprite-frames.data-kt-click-sprite-height.data-kt-click-sprite-width.data-kt-color.data-kt-crosshair-size.data-kt-dot.data-kt-dot-color.data-kt-dot-shadow.data-kt-dot-size.data-kt-ease.data-kt-follower.data-kt-follower-size.data-kt-full.data-kt-global.data-kt-height.data-kt-hidden-selector.data-kt-hide-dot-on-hover.data-kt-hover-background.data-kt-hover-class.data-kt-hover-color.data-kt-hover-dot-opacity.data-kt-hover-dot-size.data-kt-hover-effect.data-kt-hover-label.data-kt-hover-scale.data-kt-hover-selector.data-kt-hover-shadow.data-kt-hover-src.data-kt-hover-template.data-kt-html.data-kt-label.data-kt-label-color.data-kt-label-size.data-kt-mix-blend-mode.data-kt-on-enter.data-kt-on-leave.data-kt-opacity.data-kt-orbit-hover-scale.data-kt-orbit-radius.data-kt-orbit-speed.data-kt-orbit-squash.data-kt-orbit-text.data-kt-preset.data-kt-press-scale.data-kt-radius.data-kt-rotate.data-kt-rotate-duration.data-kt-rotate-text.data-kt-shadow.data-kt-shape.data-kt-smoothing.data-kt-snake-gap.data-kt-snake-min-scale.data-kt-snake-scale-ease.data-kt-snake-text.data-kt-sparkle-color.data-kt-sparkle-color2.data-kt-sparkle-duration.data-kt-sparkle-size.data-kt-sparkle-symbols.data-kt-sparkle-throttle.data-kt-speed.data-kt-spring.data-kt-src.data-kt-template.data-kt-text.data-kt-text-color.data-kt-trail-color.data-kt-trail-count.data-kt-trail-size.data-kt-type.data-kt-width.data-kt-z-index`.split(`.`),variants:[{name:`dot`,requires:`any`,deprecated:!1},{name:`ring`,requires:`any`,deprecated:!1},{name:`blob`,requires:`any`,deprecated:!1},{name:`crosshair`,requires:`any`,deprecated:!1},{name:`text`,requires:`any`,deprecated:!1},{name:`trail`,requires:`any`,deprecated:!1},{name:`orbit`,requires:`any`,deprecated:!1},{name:`snake`,requires:`any`,deprecated:!1},{name:`sparkle`,requires:`any`,deprecated:!1},{name:`image`,requires:`any`,deprecated:!1},{name:`custom`,requires:`any`,deprecated:!1}]},fullpage:{attribute:`data-kt-fullpage`,category:`scroll`,preview:`link`,reason:`페이지 전체의 스크롤을 가져가는 모듈이라 한 페이지에 여러 개를 둘 수 없습니다.`,carrier:``,carrierAttribute:``,clearAttributes:[],defaultVariant:`transform`,deprecatedVariants:[],optionAttributes:[`data-kt-auto-advance`,`data-kt-axis`,`data-kt-dots`,`data-kt-drag`,`data-kt-duration`,`data-kt-ease`,`data-kt-height`,`data-kt-initial`,`data-kt-keyboard`,`data-kt-labels`,`data-kt-loop`,`data-kt-mode`,`data-kt-on-change`,`data-kt-on-leave`,`data-kt-section-selector`,`data-kt-threshold`,`data-kt-touch`,`data-kt-wheel`],variants:[{name:`transform`,requires:`any`,deprecated:!1},{name:`snap`,requires:`any`,deprecated:!1}]},glitch:{attribute:`data-kt-glitch`,category:`effects`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-type`],defaultVariant:`rgb`,deprecatedVariants:[],optionAttributes:[`data-kt-artifact-count`,`data-kt-artifact-max-size`,`data-kt-artifact-min-size`,`data-kt-blend-mode`,`data-kt-burst-duration-max`,`data-kt-burst-duration-min`,`data-kt-channel-offset`,`data-kt-colors`,`data-kt-delay`,`data-kt-duration`,`data-kt-frequency`,`data-kt-intensity`,`data-kt-interval-max`,`data-kt-interval-min`,`data-kt-loop`,`data-kt-max-slice-offset`,`data-kt-preset`,`data-kt-randomness`,`data-kt-seed`,`data-kt-slice-count`,`data-kt-slice-count-max`,`data-kt-slice-count-min`,`data-kt-speed`,`data-kt-trigger`,`data-kt-type`],variants:[{name:`rgb`,requires:`text`,deprecated:!1},{name:`pixel`,requires:`text`,deprecated:!1},{name:`noise`,requires:`text`,deprecated:!1},{name:`crt`,requires:`image`,deprecated:!1},{name:`wave`,requires:`any`,deprecated:!1},{name:`image`,requires:`image`,deprecated:!1},{name:`datamosh`,requires:`image`,deprecated:!1},{name:`reveal`,requires:`image`,deprecated:!1},{name:`vcr`,requires:`image`,deprecated:!1},{name:`rgb-slice-burst`,requires:`any`,deprecated:!1}]},lazy:{attribute:`data-kt-lazy`,category:`media`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-effect`,`data-kt-variant`],defaultVariant:`fade`,deprecatedVariants:[`dither`,`ascii`,`halftone`],optionAttributes:`data-kt-accent-color.data-kt-animated.data-kt-ascii-chars.data-kt-ascii-font.data-kt-aspect-ratio.data-kt-autoplay.data-kt-blur.data-kt-cell-size.data-kt-color-steps.data-kt-colors.data-kt-cross-origin.data-kt-delay.data-kt-direction.data-kt-display.data-kt-dither-type.data-kt-duration.data-kt-ease.data-kt-edge-opacity.data-kt-edge-width.data-kt-effect.data-kt-fade-duration.data-kt-fallback-src.data-kt-feather.data-kt-flicker-background.data-kt-frame.data-kt-frame-color.data-kt-glitch-strength.data-kt-grain.data-kt-halftone-angle.data-kt-halftone-shape.data-kt-height.data-kt-hold-duration.data-kt-ink-color.data-kt-inverted.data-kt-keep-frame.data-kt-loop.data-kt-max-dpr.data-kt-min-duration.data-kt-muted.data-kt-native-lazy.data-kt-noise.data-kt-noise-blend.data-kt-noise-contrast.data-kt-noise-fps.data-kt-noise-height.data-kt-noise-width.data-kt-object-fit.data-kt-object-position.data-kt-on-error.data-kt-on-load.data-kt-on-progress.data-kt-on-reveal.data-kt-once.data-kt-original-colors.data-kt-paper-color.data-kt-persist.data-kt-pixel-end.data-kt-pixel-start.data-kt-pixel-step-count.data-kt-playsinline.data-kt-preload.data-kt-preset.data-kt-render-fps.data-kt-root-margin.data-kt-rotate.data-kt-seed.data-kt-sizes.data-kt-skeleton-angle.data-kt-skeleton-color.data-kt-skeleton-highlight.data-kt-skeleton-icon.data-kt-skeleton-speed.data-kt-skeleton-variant.data-kt-slice-count.data-kt-src.data-kt-srcset.data-kt-start-scale.data-kt-step-count.data-kt-step-duration.data-kt-steps.data-kt-threshold.data-kt-tile-max.data-kt-tile-min.data-kt-variant.data-kt-wave-amplitude.data-kt-wave-frequency.data-kt-wave-slice-height.data-kt-wave-speed`.split(`.`),variants:[{name:`fade`,requires:`media`,deprecated:!1},{name:`blur-up`,requires:`image`,deprecated:!1},{name:`wave`,requires:`image`,deprecated:!1},{name:`grain`,requires:`image`,deprecated:!1},{name:`skeleton`,requires:`image`,deprecated:!1},{name:`pixelate`,requires:`image`,deprecated:!1},{name:`print`,requires:`image`,deprecated:!1},{name:`dissolve`,requires:`image`,deprecated:!1},{name:`flicker`,requires:`image`,deprecated:!1},{name:`polaroid`,requires:`image`,deprecated:!1},{name:`crt`,requires:`image`,deprecated:!1},{name:`data-mosaic`,requires:`image`,deprecated:!1},{name:`rgb-slice-burst`,requires:`image`,deprecated:!1},{name:`dither`,requires:`media`,deprecated:!0},{name:`ascii`,requires:`media`,deprecated:!0},{name:`halftone`,requires:`media`,deprecated:!0}]},stylize:{attribute:`data-kt-stylize`,category:`effects`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-mode`,`data-kt-effect`],defaultVariant:`dither`,deprecatedVariants:[],optionAttributes:`data-kt-accent-color.data-kt-animated.data-kt-ascii-chars.data-kt-ascii-font.data-kt-aspect-ratio.data-kt-brightness.data-kt-cell-size.data-kt-color-steps.data-kt-contrast.data-kt-delay.data-kt-display.data-kt-dither-type.data-kt-duration.data-kt-ease.data-kt-effect.data-kt-halftone-angle.data-kt-halftone-shape.data-kt-height.data-kt-hold-duration.data-kt-ink-color.data-kt-inverted.data-kt-max-dpr.data-kt-mode.data-kt-motion.data-kt-motion-amount.data-kt-motion-speed.data-kt-object-fit.data-kt-object-position.data-kt-on-complete.data-kt-on-progress.data-kt-original-colors.data-kt-paper-color.data-kt-pointer.data-kt-pointer-cell-size.data-kt-pointer-radius.data-kt-pointer-strength.data-kt-preset.data-kt-render-fps.data-kt-root-margin.data-kt-seed.data-kt-threshold.data-kt-transition.data-kt-trigger`.split(`.`),variants:[{name:`dither`,requires:`media`,deprecated:!1},{name:`ascii`,requires:`media`,deprecated:!1},{name:`halftone`,requires:`media`,deprecated:!1}]},lightbox:{attribute:`data-kt-lightbox`,category:`media`,preview:`link`,reason:"viewer 와 grouped 는 `data-kt-group` 을 함께 쓰는지의 차이입니다. 갤러리 전체가 하나의 소재라 카드 단위로 복제되지 않습니다.",carrier:``,carrierAttribute:``,clearAttributes:[],defaultVariant:`viewer`,deprecatedVariants:[],optionAttributes:`data-kt-alt.data-kt-backdrop-blur.data-kt-backdrop-color.data-kt-backdrop-opacity.data-kt-caption.data-kt-class-name.data-kt-close-on-backdrop.data-kt-cursor.data-kt-description.data-kt-double-click-zoom.data-kt-download.data-kt-duration.data-kt-exif.data-kt-group.data-kt-info.data-kt-labels.data-kt-lazy-effect.data-kt-lazy-options.data-kt-lightbox-duration.data-kt-max-zoom.data-kt-metadata.data-kt-min-zoom.data-kt-minimap.data-kt-on-change.data-kt-on-close.data-kt-on-load.data-kt-on-open.data-kt-radius.data-kt-render-u-i.data-kt-share.data-kt-src.data-kt-thumbnails.data-kt-title.data-kt-toolbar.data-kt-transition.data-kt-ui-template.data-kt-wheel-step.data-kt-zoom.data-kt-zoom-step`.split(`.`),variants:[{name:`viewer`,requires:`any`,deprecated:!1},{name:`grouped`,requires:`any`,deprecated:!1}]},loader:{attribute:`data-kt-loader`,category:`system`,preview:`control`,reason:`전체 화면 로더라 한 번에 하나만 띄울 수 있습니다. 데모의 로더 카드를 그대로 사용합니다.`,carrier:``,carrierAttribute:``,clearAttributes:[],defaultVariant:`bar`,deprecatedVariants:[],optionAttributes:`data-kt-announce.data-kt-aria-label.data-kt-bar-height.data-kt-bar-width.data-kt-class-name.data-kt-color.data-kt-complete-hold.data-kt-complete-on-error.data-kt-duration.data-kt-exit.data-kt-exit-direction.data-kt-exit-duration.data-kt-expected-resources.data-kt-fetch.data-kt-fetch-options.data-kt-fill.data-kt-fill-color.data-kt-hide-scrollbar.data-kt-label.data-kt-label-blend.data-kt-label-color.data-kt-linecap.data-kt-manual-duration.data-kt-min-duration.data-kt-on-cancel.data-kt-on-complete.data-kt-on-error.data-kt-on-hide.data-kt-on-progress.data-kt-on-show.data-kt-on-start.data-kt-on-state-change.data-kt-percent.data-kt-preset.data-kt-progress.data-kt-progress-output.data-kt-progress-scope.data-kt-progress-source.data-kt-progress-template.data-kt-promise.data-kt-promise-ceiling.data-kt-promise-start.data-kt-radius.data-kt-render-u-i.data-kt-resource-selector.data-kt-resources.data-kt-reveal-effect.data-kt-show-percent.data-kt-size.data-kt-smoothing.data-kt-source.data-kt-stroke.data-kt-track-color.data-kt-transition.data-kt-type.data-kt-url`.split(`.`),variants:[{name:`slot`,requires:`any`,deprecated:!1},{name:`circular`,requires:`any`,deprecated:!1},{name:`bar`,requires:`any`,deprecated:!1}]},loadingIndicator:{attribute:`data-kt-loading-indicator`,category:`system`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-type`],defaultVariant:`spinner`,deprecatedVariants:[],optionAttributes:`data-kt-aria-label.data-kt-ascii-only.data-kt-auto-complete.data-kt-bar-height.data-kt-bar-mode.data-kt-bar-width.data-kt-base-color.data-kt-class-name.data-kt-color.data-kt-complete-hold.data-kt-complete-on-error.data-kt-cursor-char.data-kt-direction.data-kt-dot-count.data-kt-dot-gap.data-kt-dot-size.data-kt-dot-style.data-kt-empty-char.data-kt-exit-duration.data-kt-fill-char.data-kt-fixed-width.data-kt-font-family.data-kt-font-weight.data-kt-frame-interval.data-kt-frames.data-kt-glow.data-kt-glow-color.data-kt-glow-size.data-kt-hide-on-complete.data-kt-highlight-color.data-kt-indeterminate.data-kt-label.data-kt-letter-spacing.data-kt-line-height.data-kt-motion-duration.data-kt-on-complete.data-kt-on-error.data-kt-on-hide.data-kt-on-progress.data-kt-on-show.data-kt-on-start.data-kt-on-state-change.data-kt-preset.data-kt-progress.data-kt-progress-output.data-kt-progress-scope.data-kt-progress-source.data-kt-progress-template.data-kt-render-u-i.data-kt-rotate-spokes.data-kt-secondary-color.data-kt-show-label.data-kt-show-spinner.data-kt-show-status.data-kt-size.data-kt-spinner-mode.data-kt-spinner-style.data-kt-spread.data-kt-step-total.data-kt-stroke.data-kt-terminal-style.data-kt-text.data-kt-text-size.data-kt-track.data-kt-track-color.data-kt-transform-origin.data-kt-type.data-kt-viewport-width`.split(`.`),variants:[{name:`spinner`,requires:`any`,deprecated:!1},{name:`dots`,requires:`any`,deprecated:!1},{name:`bar`,requires:`any`,deprecated:!1},{name:`shimmer`,requires:`any`,deprecated:!1},{name:`shimmer-wave`,requires:`any`,deprecated:!1},{name:`terminal`,requires:`any`,deprecated:!1}]},magnetic:{attribute:`data-kt-magnetic`,category:`pointer`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-effect`],defaultVariant:`pointer`,deprecatedVariants:[],optionAttributes:[`data-kt-axis`,`data-kt-ease`,`data-kt-effect`,`data-kt-item`,`data-kt-lift`,`data-kt-max-scale`,`data-kt-preset`,`data-kt-radius`,`data-kt-range`,`data-kt-strength`],variants:[{name:`pointer`,requires:`any`,deprecated:!1},{name:`dock`,requires:`any`,deprecated:!1}]},marquee:{attribute:`data-kt-marquee`,category:`text`,preview:`grid`,reason:``,carrier:`direction`,carrierAttribute:`data-kt-direction`,clearAttributes:[`data-kt-direction`],defaultVariant:`left`,deprecatedVariants:[],optionAttributes:[`data-kt-clones`,`data-kt-direction`,`data-kt-fade`,`data-kt-pause-on-hover`,`data-kt-reverse-on-scroll-up`,`data-kt-scroll-acceleration`,`data-kt-skew`,`data-kt-speed`],variants:[{name:`left`,requires:`any`,deprecated:!1},{name:`right`,requires:`any`,deprecated:!1},{name:`reverse-on-scroll`,requires:`any`,deprecated:!1}]},mouseParallax:{attribute:`data-kt-mouse-parallax`,category:`pointer`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-mode`],defaultVariant:`pointer`,deprecatedVariants:[],optionAttributes:[`data-kt-compass-range`,`data-kt-ease`,`data-kt-global`,`data-kt-gyro`,`data-kt-max-x`,`data-kt-max-y`,`data-kt-mode`,`data-kt-preset`,`data-kt-rotate-offset`,`data-kt-sensitivity`,`data-kt-smoothing`,`data-kt-speed`],variants:[{name:`pointer`,requires:`any`,deprecated:!1},{name:`gyro`,requires:`any`,deprecated:!1},{name:`compass`,requires:`any`,deprecated:!1}]},overflowText:{attribute:`data-kt-overflow-text`,category:`text`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-mode`],defaultVariant:`loop`,deprecatedVariants:[],optionAttributes:`data-kt-aria-live.data-kt-crossfade.data-kt-delay.data-kt-direction.data-kt-dissolve-duration.data-kt-easing.data-kt-ellipsis.data-kt-end-pause.data-kt-flip-direction.data-kt-flip-duration.data-kt-force.data-kt-gap.data-kt-hold-duration.data-kt-hover-target.data-kt-items.data-kt-jitter.data-kt-loop-on-hover.data-kt-mask-direction.data-kt-mask-duration.data-kt-mask-ease.data-kt-mode.data-kt-on-change.data-kt-on-page.data-kt-page-duration.data-kt-page-overlap.data-kt-pause-on-hover.data-kt-perspective.data-kt-preset.data-kt-repeat.data-kt-restart-delay.data-kt-restore-direction.data-kt-restore-on-leave.data-kt-role.data-kt-roll-direction.data-kt-roll-duration.data-kt-speed.data-kt-text.data-kt-threshold.data-kt-title.data-kt-transition-direction.data-kt-trigger`.split(`.`),variants:[{name:`loop`,requires:`any`,deprecated:!1},{name:`bounce`,requires:`any`,deprecated:!1},{name:`rewind`,requires:`any`,deprecated:!1},{name:`once`,requires:`any`,deprecated:!1},{name:`page`,requires:`any`,deprecated:!1},{name:`flip`,requires:`any`,deprecated:!1},{name:`dissolve`,requires:`any`,deprecated:!1},{name:`page-roll`,requires:`any`,deprecated:!1},{name:`rolling`,requires:`any`,deprecated:!1},{name:`fade`,requires:`any`,deprecated:!1},{name:`scroll-fade`,requires:`any`,deprecated:!1}]},pageReveal:{attribute:`data-kt-page-reveal`,category:`system`,preview:`control`,reason:`화면 전체를 덮는 오버레이라 한 페이지에 여러 개를 띄울 수 없습니다. 데모의 재생 버튼을 그대로 사용합니다.`,carrier:``,carrierAttribute:``,clearAttributes:[],defaultVariant:`curtain`,deprecatedVariants:[],optionAttributes:`data-kt-angle.data-kt-axis.data-kt-cleanup-duration.data-kt-color.data-kt-color2.data-kt-count.data-kt-delay.data-kt-density.data-kt-direction.data-kt-duration.data-kt-ease.data-kt-effect.data-kt-horizontal-duration.data-kt-large-tile-chance.data-kt-line-height.data-kt-line-width.data-kt-noise-duration.data-kt-on-complete.data-kt-overscan.data-kt-preset.data-kt-reverse.data-kt-seed.data-kt-small-tile-chance.data-kt-stagger.data-kt-tile-max.data-kt-tile-min.data-kt-vertical-duration`.split(`.`),variants:[{name:`curtain`,requires:`any`,deprecated:!1},{name:`split`,requires:`any`,deprecated:!1},{name:`blinds`,requires:`any`,deprecated:!1},{name:`diagonal`,requires:`any`,deprecated:!1},{name:`shutter`,requires:`any`,deprecated:!1},{name:`fade`,requires:`any`,deprecated:!1},{name:`zoom`,requires:`any`,deprecated:!1},{name:`iris`,requires:`any`,deprecated:!1},{name:`flash`,requires:`any`,deprecated:!1},{name:`center-slit`,requires:`any`,deprecated:!1},{name:`data-mosaic`,requires:`any`,deprecated:!1},{name:`curve`,requires:`any`,deprecated:!1},{name:`dissolve`,requires:`any`,deprecated:!1},{name:`push`,requires:`any`,deprecated:!1},{name:`grid`,requires:`any`,deprecated:!1},{name:`fold`,requires:`any`,deprecated:!1}]},parallax:{attribute:`data-kt-parallax`,category:`scroll`,preview:`grid`,reason:``,carrier:`axis`,carrierAttribute:`data-kt-axis`,clearAttributes:[`data-kt-axis`],defaultVariant:`y`,deprecatedVariants:[],optionAttributes:[`data-kt-axis`,`data-kt-distance`,`data-kt-end`,`data-kt-on-update`,`data-kt-scrub`,`data-kt-speed`,`data-kt-start`],variants:[{name:`x`,requires:`any`,deprecated:!1},{name:`y`,requires:`any`,deprecated:!1}]},progress:{attribute:`data-kt-progress`,category:`scroll`,preview:`link`,reason:`variant 가 대상(page·element)과 표현(scaleX·width) 두 옵션의 조합이고, page 계열은 문서 전체에 하나만 존재할 수 있습니다.`,carrier:``,carrierAttribute:``,clearAttributes:[],defaultVariant:`page:scaleX`,deprecatedVariants:[],optionAttributes:[`data-kt-attach`,`data-kt-click-to-top`,`data-kt-color`,`data-kt-color2`,`data-kt-hide-at-end`,`data-kt-label`,`data-kt-offset`,`data-kt-on-update`,`data-kt-position`,`data-kt-property`,`data-kt-radius`,`data-kt-show-after`,`data-kt-show-percent`,`data-kt-size`,`data-kt-smoothing`,`data-kt-stroke`,`data-kt-target`,`data-kt-thickness`,`data-kt-track-color`,`data-kt-ui`,`data-kt-z-index`],variants:[{name:`page:scaleX`,requires:`any`,deprecated:!1},{name:`page:width`,requires:`any`,deprecated:!1},{name:`element:scaleX`,requires:`any`,deprecated:!1},{name:`element:width`,requires:`any`,deprecated:!1}]},reveal:{attribute:`data-kt-reveal`,category:`scroll`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`],defaultVariant:`fade-up`,deprecatedVariants:[],optionAttributes:`data-kt-active-class.data-kt-class-only.data-kt-clock-direction.data-kt-delay.data-kt-direction.data-kt-distance.data-kt-duration.data-kt-ease.data-kt-end.data-kt-enter-class.data-kt-enter-ease.data-kt-leave-class.data-kt-on-class-change.data-kt-on-complete.data-kt-on-enter.data-kt-on-enter-back.data-kt-on-leave.data-kt-on-leave-back.data-kt-once.data-kt-order.data-kt-preset.data-kt-remove-class-on-leave.data-kt-root-margin.data-kt-spring.data-kt-stagger.data-kt-start.data-kt-start-angle.data-kt-threshold`.split(`.`),variants:[{name:`fade`,requires:`any`,deprecated:!1},{name:`fade-up`,requires:`any`,deprecated:!1},{name:`fade-down`,requires:`any`,deprecated:!1},{name:`fade-left`,requires:`any`,deprecated:!1},{name:`fade-right`,requires:`any`,deprecated:!1},{name:`slide-up`,requires:`any`,deprecated:!1},{name:`slide-down`,requires:`any`,deprecated:!1},{name:`slide-left`,requires:`any`,deprecated:!1},{name:`slide-right`,requires:`any`,deprecated:!1},{name:`zoom-in`,requires:`any`,deprecated:!1},{name:`zoom-out`,requires:`any`,deprecated:!1},{name:`blur`,requires:`any`,deprecated:!1},{name:`rise`,requires:`any`,deprecated:!1},{name:`soft`,requires:`any`,deprecated:!1},{name:`flip-x`,requires:`any`,deprecated:!1},{name:`flip-y`,requires:`any`,deprecated:!1},{name:`rotate`,requires:`any`,deprecated:!1},{name:`mask`,requires:`any`,deprecated:!1},{name:`wipe`,requires:`any`,deprecated:!1},{name:`class`,requires:`any`,deprecated:!1},{name:`clock`,requires:`any`,deprecated:!1},{name:`swing`,requires:`any`,deprecated:!1},{name:`skew`,requires:`any`,deprecated:!1}]},radial:{attribute:`data-kt-radial`,category:`media`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-position`],defaultVariant:`bottom`,deprecatedVariants:[],optionAttributes:[`data-kt-active-angle`,`data-kt-active-class`,`data-kt-align`,`data-kt-autoplay`,`data-kt-controls`,`data-kt-damping`,`data-kt-drag`,`data-kt-duration`,`data-kt-loop`,`data-kt-mass`,`data-kt-position`,`data-kt-radius`,`data-kt-smoothing`,`data-kt-spring`,`data-kt-step`,`data-kt-stiffness`],variants:[{name:`bottom`,requires:`any`,deprecated:!1},{name:`top`,requires:`any`,deprecated:!1},{name:`left`,requires:`any`,deprecated:!1},{name:`right`,requires:`any`,deprecated:!1}]},scrollSequence:{attribute:`data-kt-scroll-sequence`,category:`scroll`,preview:`grid`,reason:``,carrier:`fit`,carrierAttribute:`data-kt-fit`,clearAttributes:[`data-kt-fit`],defaultVariant:`cover`,deprecatedVariants:[],optionAttributes:[`data-kt-cross-origin`,`data-kt-end`,`data-kt-extension`,`data-kt-fit`,`data-kt-frames`,`data-kt-height`,`data-kt-max-dpr`,`data-kt-on-error`,`data-kt-on-frame`,`data-kt-padding`,`data-kt-preload-radius`,`data-kt-scroll-length`,`data-kt-scrub`,`data-kt-start`,`data-kt-top`,`data-kt-url-prefix`,`data-kt-urls`,`data-kt-vh-per-frame`],variants:[{name:`cover`,requires:`any`,deprecated:!1},{name:`contain`,requires:`any`,deprecated:!1}]},scrollVelocity:{attribute:`data-kt-scroll-velocity`,category:`scroll`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-mode`,`data-kt-effect`],defaultVariant:`skew`,deprecatedVariants:[],optionAttributes:[`data-kt-axis`,`data-kt-damping`,`data-kt-decay`,`data-kt-distance`,`data-kt-effect`,`data-kt-elastic`,`data-kt-end`,`data-kt-global`,`data-kt-mass`,`data-kt-max-blur`,`data-kt-max-rotate`,`data-kt-max-scale`,`data-kt-max-skew`,`data-kt-mode`,`data-kt-on-direction`,`data-kt-on-update`,`data-kt-preset`,`data-kt-response`,`data-kt-reverse`,`data-kt-smoothing`,`data-kt-spring`,`data-kt-start`,`data-kt-stiffness`,`data-kt-velocity-divisor`],variants:[{name:`skew`,requires:`any`,deprecated:!1},{name:`rotate`,requires:`any`,deprecated:!1},{name:`scale`,requires:`any`,deprecated:!1},{name:`blur`,requires:`any`,deprecated:!1},{name:`translate`,requires:`any`,deprecated:!1},{name:`combo`,requires:`any`,deprecated:!1}]},slider:{attribute:`data-kt-slider`,category:`media`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-effect`],defaultVariant:`slide`,deprecatedVariants:[],optionAttributes:`data-kt-active-angle.data-kt-active-class.data-kt-active-shadow.data-kt-active-shadow-opacity.data-kt-align.data-kt-auto-height.data-kt-autoplay.data-kt-axis.data-kt-bounce.data-kt-breakpoints.data-kt-controls.data-kt-damping.data-kt-depth.data-kt-dots.data-kt-drag.data-kt-duration.data-kt-effect.data-kt-effect-direction.data-kt-effect-intensity.data-kt-enabled.data-kt-gap.data-kt-grab-cursor.data-kt-index.data-kt-initial.data-kt-initial-index.data-kt-keyboard.data-kt-label.data-kt-labels.data-kt-loop.data-kt-mass.data-kt-min-opacity.data-kt-min-scale.data-kt-momentum.data-kt-next-selector.data-kt-on-before-change.data-kt-on-change.data-kt-on-init.data-kt-opacity-step.data-kt-pause-button.data-kt-pause-on-hover.data-kt-pause-when-offscreen.data-kt-per-group.data-kt-per-view.data-kt-perspective.data-kt-position.data-kt-preset.data-kt-prev-selector.data-kt-progress.data-kt-progress-type.data-kt-radius.data-kt-rotate.data-kt-scale-step.data-kt-scroll-snap.data-kt-slide-to-clicked-slide.data-kt-smoothing.data-kt-spacing.data-kt-speed.data-kt-spring.data-kt-step.data-kt-sticky-snap.data-kt-stiffness.data-kt-sync.data-kt-touch.data-kt-velocity-influence.data-kt-wheel`.split(`.`),variants:[{name:`slide`,requires:`track`,deprecated:!1},{name:`fade`,requires:`track`,deprecated:!1},{name:`dissolve`,requires:`track`,deprecated:!1},{name:`wipe`,requires:`track`,deprecated:!1},{name:`coverflow`,requires:`track`,deprecated:!1},{name:`flip`,requires:`track`,deprecated:!1},{name:`cube`,requires:`track`,deprecated:!1},{name:`cards`,requires:`track`,deprecated:!1},{name:`creative`,requires:`track`,deprecated:!1},{name:`radial`,requires:`items`,deprecated:!1}]},stickyStack:{attribute:`data-kt-sticky-stack`,category:`scroll`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-mode`,`data-kt-type`,`data-kt-effect`],defaultVariant:`vertical`,deprecatedVariants:[],optionAttributes:`data-kt-align.data-kt-blur.data-kt-bottom-space.data-kt-distance.data-kt-ease.data-kt-effect.data-kt-end.data-kt-fade-previous.data-kt-gap.data-kt-item-duration.data-kt-item-height.data-kt-min-height.data-kt-mode.data-kt-offset.data-kt-offset-top.data-kt-offset-y.data-kt-on-progress.data-kt-overlap.data-kt-panel-width.data-kt-perspective.data-kt-pin.data-kt-pin-spacing.data-kt-preset.data-kt-previous-blur.data-kt-previous-opacity.data-kt-previous-scale.data-kt-previous-y.data-kt-reverse-z.data-kt-rotate.data-kt-scale-from.data-kt-scale-previous.data-kt-scroll-length.data-kt-scrub.data-kt-snap.data-kt-start.data-kt-top.data-kt-transform-origin.data-kt-transition-start-offset.data-kt-type`.split(`.`),variants:[{name:`vertical`,requires:`any`,deprecated:!1},{name:`horizontal`,requires:`any`,deprecated:!1},{name:`zindex`,requires:`any`,deprecated:!1},{name:`floating`,requires:`any`,deprecated:!1}]},textReveal:{attribute:`data-kt-text-reveal`,category:`text`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-mode`],defaultVariant:`stream`,deprecatedVariants:[],optionAttributes:[`data-kt-chars`,`data-kt-delay`,`data-kt-duration`,`data-kt-ease`,`data-kt-flicker-count`,`data-kt-flicker-loop`,`data-kt-hold`,`data-kt-loop`,`data-kt-mode`,`data-kt-on-complete`,`data-kt-preset`,`data-kt-rainbow`,`data-kt-rainbow-colors`,`data-kt-reveal-rate`,`data-kt-root-margin`,`data-kt-scramble-fade`,`data-kt-speed`,`data-kt-stagger`,`data-kt-text`,`data-kt-threshold`],variants:[{name:`stream`,requires:`any`,deprecated:!1},{name:`char`,requires:`any`,deprecated:!1},{name:`word`,requires:`any`,deprecated:!1},{name:`line`,requires:`any`,deprecated:!1},{name:`bounce`,requires:`any`,deprecated:!1},{name:`hangul`,requires:`any`,deprecated:!1},{name:`decode`,requires:`any`,deprecated:!1},{name:`flicker`,requires:`any`,deprecated:!1},{name:`shuffle`,requires:`any`,deprecated:!1}]},textSplit:{attribute:`data-kt-text-split`,category:`text`,preview:`grid`,reason:``,carrier:`by`,carrierAttribute:`data-kt-by`,clearAttributes:[`data-kt-by`,`data-kt-preset`],defaultVariant:`char`,deprecatedVariants:[],optionAttributes:[`data-kt-animation`,`data-kt-by`,`data-kt-delay`,`data-kt-duration`,`data-kt-ease`,`data-kt-hold`,`data-kt-on-complete`,`data-kt-on-swap`,`data-kt-once`,`data-kt-pause`,`data-kt-perspective`,`data-kt-preset`,`data-kt-stagger`,`data-kt-start`,`data-kt-swap-ease`,`data-kt-swap-out`,`data-kt-texts`],variants:[{name:`char`,requires:`text`,deprecated:!1},{name:`word`,requires:`text`,deprecated:!1}]},textTransition:{attribute:`data-kt-text-transition`,category:`text`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-effect`],defaultVariant:`slide-up`,deprecatedVariants:[],optionAttributes:[`data-kt-aria-live`,`data-kt-base-color`,`data-kt-blur`,`data-kt-char-direction`,`data-kt-char-mode`,`data-kt-duration`,`data-kt-ease`,`data-kt-effect`,`data-kt-end-scale`,`data-kt-hold`,`data-kt-jitter`,`data-kt-loop`,`data-kt-min-height`,`data-kt-on-change`,`data-kt-on-complete`,`data-kt-pause`,`data-kt-preset`,`data-kt-shim-color`,`data-kt-shim-speed`,`data-kt-stagger`,`data-kt-start-scale`,`data-kt-texts`],variants:[{name:`slide-up`,requires:`any`,deprecated:!1},{name:`flip`,requires:`any`,deprecated:!1},{name:`rise`,requires:`any`,deprecated:!1},{name:`fade`,requires:`any`,deprecated:!1},{name:`blur`,requires:`any`,deprecated:!1},{name:`scale`,requires:`any`,deprecated:!1},{name:`clip`,requires:`any`,deprecated:!1},{name:`dissolve`,requires:`any`,deprecated:!1},{name:`shimmer`,requires:`any`,deprecated:!1},{name:`pop`,requires:`any`,deprecated:!1}]},tilt:{attribute:`data-kt-tilt`,category:`pointer`,preview:`link`,reason:`variant 가 maxX·maxY·glare·reverse 옵션의 조합에서 파생됩니다. 옵션 하나로 순회할 수 없습니다.`,carrier:``,carrierAttribute:``,clearAttributes:[],defaultVariant:`tilt-glare`,deprecatedVariants:[],optionAttributes:`data-kt-axis.data-kt-disable-on-mobile.data-kt-ease.data-kt-glare.data-kt-glare-blur.data-kt-glare-color.data-kt-glare-opacity.data-kt-glare-radius.data-kt-gyro.data-kt-max.data-kt-max-x.data-kt-max-y.data-kt-perspective.data-kt-reset.data-kt-reverse.data-kt-scale.data-kt-sensitivity.data-kt-smoothing.data-kt-tilt-shadow.data-kt-tilt-shadow-blur.data-kt-tilt-shadow-color.data-kt-tilt-shadow-css.data-kt-tilt-shadow-follow.data-kt-tilt-shadow-hover-only.data-kt-tilt-shadow-inset.data-kt-tilt-shadow-opacity.data-kt-tilt-shadow-spread.data-kt-tilt-shadow-x.data-kt-tilt-shadow-y`.split(`.`),variants:[{name:`tilt-glare`,requires:`any`,deprecated:!1},{name:`tilt`,requires:`any`,deprecated:!1},{name:`x-only`,requires:`any`,deprecated:!1},{name:`y-only`,requires:`any`,deprecated:!1},{name:`reverse`,requires:`any`,deprecated:!1}]},vibrate:{attribute:`data-kt-vibrate`,category:`feedback`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`],defaultVariant:`tap`,deprecatedVariants:[],optionAttributes:[`data-kt-haptic`,`data-kt-pattern`,`data-kt-preset`,`data-kt-threshold`,`data-kt-trigger`],variants:[{name:`tap`,requires:`any`,deprecated:!1},{name:`double-tap`,requires:`any`,deprecated:!1},{name:`soft`,requires:`any`,deprecated:!1},{name:`rigid`,requires:`any`,deprecated:!1},{name:`heavy`,requires:`any`,deprecated:!1},{name:`success`,requires:`any`,deprecated:!1},{name:`warning`,requires:`any`,deprecated:!1},{name:`error`,requires:`any`,deprecated:!1},{name:`ratchet`,requires:`any`,deprecated:!1},{name:`heartbeat`,requires:`any`,deprecated:!1},{name:`long-press`,requires:`any`,deprecated:!1}]},hold:{attribute:`data-kt-hold`,category:`feedback`,preview:`grid`,reason:``,carrier:`mode`,carrierAttribute:`data-kt-mode`,clearAttributes:[`data-kt-mode`],defaultVariant:`confirm`,deprecatedVariants:[],optionAttributes:[`data-kt-action`,`data-kt-blend`,`data-kt-color`,`data-kt-decay`,`data-kt-duration`,`data-kt-labels`,`data-kt-mode`,`data-kt-on-complete`,`data-kt-step`,`data-kt-submit`],variants:[{name:`confirm`,requires:`any`,deprecated:!1},{name:`tap`,requires:`any`,deprecated:!1}]},megaMenu:{attribute:`data-kt-mega-menu`,category:`components`,preview:`grid`,reason:``,carrier:`layout`,carrierAttribute:`data-kt-layout`,clearAttributes:[`data-kt-layout`],defaultVariant:`dropdown`,deprecatedVariants:[],optionAttributes:[`data-kt-close-delay`,`data-kt-duration`,`data-kt-indicator`,`data-kt-layout`,`data-kt-open-delay`,`data-kt-radius`,`data-kt-responsive`,`data-kt-stagger`,`data-kt-start-angle`,`data-kt-sweep`,`data-kt-trigger`],variants:[{name:`dropdown`,requires:`any`,deprecated:!1},{name:`mega`,requires:`any`,deprecated:!1},{name:`radial`,requires:`any`,deprecated:!1}]},gesture:{attribute:`data-kt-gesture`,category:`pointer`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-effect`],defaultVariant:`spring`,deprecatedVariants:[],optionAttributes:[`data-kt-duration`,`data-kt-ease`,`data-kt-effect`,`data-kt-hover-ease`,`data-kt-hover-scale`,`data-kt-labels`,`data-kt-lift`,`data-kt-max`,`data-kt-on-refresh`,`data-kt-origin`,`data-kt-preset`,`data-kt-press-ease`,`data-kt-resistance`,`data-kt-tap-scale`,`data-kt-threshold`],variants:[{name:`spring`,requires:`any`,deprecated:!1},{name:`pull`,requires:`any`,deprecated:!1}]},flip:{attribute:`data-kt-flip`,category:`components`,preview:`grid`,reason:``,carrier:`mode`,carrierAttribute:`data-kt-mode`,clearAttributes:[`data-kt-mode`],defaultVariant:`layout`,deprecatedVariants:[],optionAttributes:[`data-kt-duration`,`data-kt-ease`,`data-kt-fold-blur`,`data-kt-item`,`data-kt-mode`,`data-kt-stagger`,`data-kt-view-transition`,`data-kt-watch`],variants:[{name:`layout`,requires:`any`,deprecated:!1},{name:`none`,requires:`any`,deprecated:!1},{name:`slide`,requires:`any`,deprecated:!1},{name:`fade`,requires:`any`,deprecated:!1},{name:`crossfade`,requires:`any`,deprecated:!1},{name:`fade-slide`,requires:`any`,deprecated:!1},{name:`scale`,requires:`any`,deprecated:!1},{name:`fold`,requires:`any`,deprecated:!1}]},scrollShadows:{attribute:`data-kt-scroll-shadows`,category:`scroll`,preview:`link`,reason:`variant 는 축(vertical·horizontal)과 모드(mask)가 섞인 조합이라 옵션 하나로 순회할 수 없습니다.`,carrier:``,carrierAttribute:``,clearAttributes:[],defaultVariant:`vertical`,deprecatedVariants:[],optionAttributes:[`data-kt-axis`,`data-kt-color`,`data-kt-ease`,`data-kt-mode`,`data-kt-on-change`,`data-kt-opacity`,`data-kt-shadow`,`data-kt-shape`,`data-kt-size`,`data-kt-transition`,`data-kt-transition-duration`,`data-kt-transition-mode`],variants:[{name:`vertical`,requires:`any`,deprecated:!1},{name:`horizontal`,requires:`any`,deprecated:!1},{name:`mask`,requires:`any`,deprecated:!1}]},squircle:{attribute:`data-kt-squircle`,category:`effects`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`],defaultVariant:`squircle`,deprecatedVariants:[],optionAttributes:[`data-kt-border-follow`,`data-kt-corner-radius`,`data-kt-corner-samples`,`data-kt-native-shape`,`data-kt-preset`,`data-kt-superellipse`],variants:[{name:`squircle`,requires:`any`,deprecated:!1},{name:`round`,requires:`any`,deprecated:!1},{name:`bevel`,requires:`any`,deprecated:!1},{name:`scoop`,requires:`any`,deprecated:!1},{name:`notch`,requires:`any`,deprecated:!1},{name:`square`,requires:`any`,deprecated:!1}]},stickyHeader:{attribute:`data-kt-sticky-header`,category:`scroll`,preview:`link`,reason:`뷰포트에 고정되는 헤더라 한 페이지에 여러 개를 나란히 둘 수 없습니다.`,carrier:``,carrierAttribute:``,clearAttributes:[],defaultVariant:`shrink`,deprecatedVariants:[],optionAttributes:[`data-kt-active-class`,`data-kt-distance`,`data-kt-offset`,`data-kt-on-change`,`data-kt-shadow`,`data-kt-shrink`],variants:[{name:`shrink`,requires:`any`,deprecated:!1},{name:`toggle`,requires:`any`,deprecated:!1}]}}});
\ No newline at end of file
+window.KINETO_VARIANT_CATALOG=Object.freeze({schemaVersion:`1.0.0`,libraryVersion:`0.13.1`,moduleCount:38,variantCount:228,capabilities:{any:`No requirement — works on any element.`,image:`Needs an (the element itself or a descendant).`,text:`Needs rendered text and no image to operate on.`,video:`Needs a element.`,media:`Needs an or (the element itself or a descendant).`,items:`Needs two or more element children to sequence.`,track:`Needs a .kt-slider-track descendant for track-based slider effects.`},modules:{ambientMedia:{attribute:`data-kt-ambient-media`,category:`effects`,preview:`link`,reason:`variant 가 소스 종류(복제할 이미지·샘플링할 영상·단색)에서 파생됩니다. 옵션 하나로 바꿀 수 없어 모듈 데모의 세 카드가 그대로 비교 화면입니다.`,carrier:``,carrierAttribute:``,clearAttributes:[],defaultVariant:`image-clone`,deprecatedVariants:[],optionAttributes:[`data-kt-allow-overflow`,`data-kt-ambient-src`,`data-kt-ambient-srcset`,`data-kt-blur`,`data-kt-brightness`,`data-kt-color`,`data-kt-fallback-color`,`data-kt-hide-on-pause`,`data-kt-inset`,`data-kt-opacity`,`data-kt-radius`,`data-kt-sample-fps`,`data-kt-sample-height`,`data-kt-sample-width`,`data-kt-saturation`,`data-kt-scale`,`data-kt-source`,`data-kt-src`],variants:[{name:`image-clone`,requires:`any`,deprecated:!1},{name:`video-sample`,requires:`any`,deprecated:!1},{name:`color`,requires:`any`,deprecated:!1}]},cardGlow:{attribute:`data-kt-card-glow`,category:`pointer`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-mode`],defaultVariant:`spotlight`,deprecatedVariants:[],optionAttributes:`data-kt-always-on.data-kt-blend-mode.data-kt-blur.data-kt-border-blur.data-kt-border-color.data-kt-border-color2.data-kt-border-glow.data-kt-border-inset.data-kt-border-opacity.data-kt-border-radius.data-kt-border-width.data-kt-color.data-kt-color1.data-kt-color2.data-kt-cycle-duration.data-kt-disable-on-mobile.data-kt-duration.data-kt-ease.data-kt-follow.data-kt-glass-blur.data-kt-glass-depth.data-kt-glass-refraction.data-kt-glass-rim.data-kt-glass-rim-opacity.data-kt-glass-saturate.data-kt-glass-sheen.data-kt-glass-tint.data-kt-halo.data-kt-intensity.data-kt-luminous-border.data-kt-mode.data-kt-opacity.data-kt-preset.data-kt-radius.data-kt-reflection.data-kt-sensitivity.data-kt-shadow.data-kt-shadow-blur.data-kt-shadow-color.data-kt-shadow-css.data-kt-shadow-follow.data-kt-shadow-hover-only.data-kt-shadow-inset.data-kt-shadow-opacity.data-kt-shadow-spread.data-kt-shadow-x.data-kt-shadow-y.data-kt-smoothing.data-kt-speed.data-kt-spread.data-kt-surface.data-kt-surface-blend.data-kt-surface-blur.data-kt-surface-color.data-kt-surface-color2.data-kt-surface-gradient.data-kt-surface-inset.data-kt-surface-opacity.data-kt-surface-size`.split(`.`),variants:[{name:`spotlight`,requires:`any`,deprecated:!1},{name:`edge`,requires:`any`,deprecated:!1},{name:`border`,requires:`any`,deprecated:!1},{name:`comet`,requires:`any`,deprecated:!1},{name:`aurora`,requires:`any`,deprecated:!1},{name:`shine`,requires:`any`,deprecated:!1},{name:`glass`,requires:`any`,deprecated:!1}]},counter:{attribute:`data-kt-counter`,category:`text`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-mode`,`data-kt-style`],defaultVariant:`slot`,deprecatedVariants:[],optionAttributes:`data-kt-blink.data-kt-blink-separators.data-kt-clock-separator.data-kt-clock-style.data-kt-comma.data-kt-days-label.data-kt-decimals.data-kt-delay.data-kt-duration.data-kt-ease.data-kt-format.data-kt-from.data-kt-gap.data-kt-grouping.data-kt-hour12.data-kt-line-height.data-kt-locale.data-kt-loops.data-kt-mode.data-kt-on-complete.data-kt-once.data-kt-pop-align.data-kt-pop-duration.data-kt-pop-scale.data-kt-prefix.data-kt-preset.data-kt-roll-direction.data-kt-roll-duration.data-kt-seam-color.data-kt-seconds.data-kt-seconds-digits.data-kt-seconds-label.data-kt-seconds-only.data-kt-separator.data-kt-separator-color.data-kt-shadow.data-kt-show-days.data-kt-since.data-kt-stagger.data-kt-start.data-kt-style.data-kt-suffix.data-kt-tile-color.data-kt-tile-radius.data-kt-tile-text-color.data-kt-to.data-kt-until`.split(`.`),variants:[{name:`slot`,requires:`any`,deprecated:!1},{name:`plain`,requires:`any`,deprecated:!1},{name:`digit`,requires:`any`,deprecated:!1},{name:`pop`,requires:`any`,deprecated:!1},{name:`flip`,requires:`any`,deprecated:!1},{name:`clock`,requires:`any`,deprecated:!1}]},dateTime:{attribute:`data-kt-date-time`,category:`text`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-mode`],defaultVariant:`relative`,deprecatedVariants:[],optionAttributes:[`data-kt-date`,`data-kt-date-style`,`data-kt-datetime`,`data-kt-fallback`,`data-kt-live`,`data-kt-locale`,`data-kt-mode`,`data-kt-now`,`data-kt-numeric`,`data-kt-preset`,`data-kt-relative-cutoff`,`data-kt-relative-cutoff-unit`,`data-kt-relative-rounding`,`data-kt-relative-style`,`data-kt-relative-unit`,`data-kt-source`,`data-kt-time-style`,`data-kt-time-zone`,`data-kt-update-interval`,`data-kt-value`],variants:[{name:`relative`,requires:`any`,deprecated:!1},{name:`absolute`,requires:`any`,deprecated:!1},{name:`both`,requires:`any`,deprecated:!1}]},cssScroll:{attribute:`data-kt-css-scroll`,category:`scroll`,preview:`link`,reason:`variant 는 어떤 타임라인 옵션을 채웠는지의 결과입니다. 속성 값은 이름표일 뿐이라 값만 바꿔서는 다른 variant 가 되지 않습니다.`,carrier:``,carrierAttribute:``,clearAttributes:[],defaultVariant:`progress-property`,deprecatedVariants:[],optionAttributes:[`data-kt-axis`,`data-kt-css-animation`,`data-kt-end`,`data-kt-on-update`,`data-kt-property`,`data-kt-range-end`,`data-kt-range-start`,`data-kt-start`,`data-kt-timeline`],variants:[{name:`progress-property`,requires:`any`,deprecated:!1},{name:`css-animation-timeline`,requires:`any`,deprecated:!1},{name:`scroll-timeline`,requires:`any`,deprecated:!1}]},cursor:{attribute:`data-kt-cursor`,category:`pointer`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-type`],defaultVariant:`dot`,deprecatedVariants:[],optionAttributes:`data-kt-backdrop-filter.data-kt-background.data-kt-blur.data-kt-border-color.data-kt-border-width.data-kt-class-name.data-kt-click-image.data-kt-click-image-duration.data-kt-click-image-size.data-kt-click-sprite.data-kt-click-sprite-duration.data-kt-click-sprite-frames.data-kt-click-sprite-height.data-kt-click-sprite-width.data-kt-color.data-kt-crosshair-size.data-kt-dot.data-kt-dot-color.data-kt-dot-shadow.data-kt-dot-size.data-kt-ease.data-kt-follower.data-kt-follower-size.data-kt-full.data-kt-global.data-kt-height.data-kt-hidden-selector.data-kt-hide-dot-on-hover.data-kt-hover-background.data-kt-hover-class.data-kt-hover-color.data-kt-hover-dot-opacity.data-kt-hover-dot-size.data-kt-hover-effect.data-kt-hover-label.data-kt-hover-scale.data-kt-hover-selector.data-kt-hover-shadow.data-kt-hover-src.data-kt-hover-template.data-kt-html.data-kt-label.data-kt-label-color.data-kt-label-size.data-kt-mix-blend-mode.data-kt-on-enter.data-kt-on-leave.data-kt-opacity.data-kt-orbit-hover-scale.data-kt-orbit-radius.data-kt-orbit-speed.data-kt-orbit-squash.data-kt-orbit-text.data-kt-preset.data-kt-press-scale.data-kt-radius.data-kt-rotate.data-kt-rotate-duration.data-kt-rotate-text.data-kt-shadow.data-kt-shape.data-kt-smoothing.data-kt-snake-gap.data-kt-snake-min-scale.data-kt-snake-scale-ease.data-kt-snake-text.data-kt-sparkle-color.data-kt-sparkle-color2.data-kt-sparkle-duration.data-kt-sparkle-size.data-kt-sparkle-symbols.data-kt-sparkle-throttle.data-kt-speed.data-kt-spring.data-kt-src.data-kt-template.data-kt-text.data-kt-text-color.data-kt-trail-color.data-kt-trail-count.data-kt-trail-size.data-kt-type.data-kt-width.data-kt-z-index`.split(`.`),variants:[{name:`dot`,requires:`any`,deprecated:!1},{name:`ring`,requires:`any`,deprecated:!1},{name:`blob`,requires:`any`,deprecated:!1},{name:`crosshair`,requires:`any`,deprecated:!1},{name:`text`,requires:`any`,deprecated:!1},{name:`trail`,requires:`any`,deprecated:!1},{name:`orbit`,requires:`any`,deprecated:!1},{name:`snake`,requires:`any`,deprecated:!1},{name:`sparkle`,requires:`any`,deprecated:!1},{name:`image`,requires:`any`,deprecated:!1},{name:`custom`,requires:`any`,deprecated:!1}]},fullpage:{attribute:`data-kt-fullpage`,category:`scroll`,preview:`link`,reason:`페이지 전체의 스크롤을 가져가는 모듈이라 한 페이지에 여러 개를 둘 수 없습니다.`,carrier:``,carrierAttribute:``,clearAttributes:[],defaultVariant:`transform`,deprecatedVariants:[],optionAttributes:[`data-kt-auto-advance`,`data-kt-axis`,`data-kt-dots`,`data-kt-drag`,`data-kt-duration`,`data-kt-ease`,`data-kt-height`,`data-kt-initial`,`data-kt-keyboard`,`data-kt-labels`,`data-kt-loop`,`data-kt-mode`,`data-kt-on-change`,`data-kt-on-leave`,`data-kt-section-selector`,`data-kt-threshold`,`data-kt-touch`,`data-kt-wheel`],variants:[{name:`transform`,requires:`any`,deprecated:!1},{name:`snap`,requires:`any`,deprecated:!1}]},glitch:{attribute:`data-kt-glitch`,category:`effects`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-type`],defaultVariant:`rgb`,deprecatedVariants:[],optionAttributes:[`data-kt-artifact-count`,`data-kt-artifact-max-size`,`data-kt-artifact-min-size`,`data-kt-blend-mode`,`data-kt-burst-duration-max`,`data-kt-burst-duration-min`,`data-kt-channel-offset`,`data-kt-colors`,`data-kt-delay`,`data-kt-duration`,`data-kt-frequency`,`data-kt-intensity`,`data-kt-interval-max`,`data-kt-interval-min`,`data-kt-loop`,`data-kt-max-slice-offset`,`data-kt-preset`,`data-kt-randomness`,`data-kt-seed`,`data-kt-slice-count`,`data-kt-slice-count-max`,`data-kt-slice-count-min`,`data-kt-speed`,`data-kt-trigger`,`data-kt-type`],variants:[{name:`rgb`,requires:`text`,deprecated:!1},{name:`pixel`,requires:`text`,deprecated:!1},{name:`noise`,requires:`text`,deprecated:!1},{name:`crt`,requires:`image`,deprecated:!1},{name:`wave`,requires:`any`,deprecated:!1},{name:`image`,requires:`image`,deprecated:!1},{name:`datamosh`,requires:`image`,deprecated:!1},{name:`reveal`,requires:`image`,deprecated:!1},{name:`vcr`,requires:`image`,deprecated:!1},{name:`rgb-slice-burst`,requires:`any`,deprecated:!1}]},lazy:{attribute:`data-kt-lazy`,category:`media`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-effect`,`data-kt-variant`],defaultVariant:`fade`,deprecatedVariants:[`dither`,`ascii`,`halftone`],optionAttributes:`data-kt-accent-color.data-kt-animated.data-kt-ascii-chars.data-kt-ascii-font.data-kt-aspect-ratio.data-kt-autoplay.data-kt-blur.data-kt-cell-size.data-kt-color-steps.data-kt-colors.data-kt-cross-origin.data-kt-delay.data-kt-direction.data-kt-display.data-kt-dither-type.data-kt-duration.data-kt-ease.data-kt-edge-opacity.data-kt-edge-width.data-kt-effect.data-kt-fade-duration.data-kt-fallback-src.data-kt-feather.data-kt-flicker-background.data-kt-frame.data-kt-frame-color.data-kt-glitch-strength.data-kt-grain.data-kt-halftone-angle.data-kt-halftone-shape.data-kt-height.data-kt-hold-duration.data-kt-ink-color.data-kt-inverted.data-kt-keep-frame.data-kt-loop.data-kt-max-dpr.data-kt-min-duration.data-kt-muted.data-kt-native-lazy.data-kt-noise.data-kt-noise-blend.data-kt-noise-contrast.data-kt-noise-fps.data-kt-noise-height.data-kt-noise-width.data-kt-object-fit.data-kt-object-position.data-kt-on-error.data-kt-on-load.data-kt-on-progress.data-kt-on-reveal.data-kt-once.data-kt-original-colors.data-kt-paper-color.data-kt-persist.data-kt-pixel-end.data-kt-pixel-start.data-kt-pixel-step-count.data-kt-playsinline.data-kt-preload.data-kt-preset.data-kt-render-fps.data-kt-root-margin.data-kt-rotate.data-kt-seed.data-kt-sizes.data-kt-skeleton-angle.data-kt-skeleton-color.data-kt-skeleton-highlight.data-kt-skeleton-icon.data-kt-skeleton-speed.data-kt-skeleton-variant.data-kt-slice-count.data-kt-src.data-kt-srcset.data-kt-start-scale.data-kt-step-count.data-kt-step-duration.data-kt-steps.data-kt-threshold.data-kt-tile-max.data-kt-tile-min.data-kt-variant.data-kt-wave-amplitude.data-kt-wave-frequency.data-kt-wave-slice-height.data-kt-wave-speed`.split(`.`),variants:[{name:`fade`,requires:`media`,deprecated:!1},{name:`blur-up`,requires:`image`,deprecated:!1},{name:`wave`,requires:`image`,deprecated:!1},{name:`grain`,requires:`image`,deprecated:!1},{name:`skeleton`,requires:`image`,deprecated:!1},{name:`pixelate`,requires:`image`,deprecated:!1},{name:`print`,requires:`image`,deprecated:!1},{name:`dissolve`,requires:`image`,deprecated:!1},{name:`flicker`,requires:`image`,deprecated:!1},{name:`polaroid`,requires:`image`,deprecated:!1},{name:`crt`,requires:`image`,deprecated:!1},{name:`data-mosaic`,requires:`image`,deprecated:!1},{name:`rgb-slice-burst`,requires:`image`,deprecated:!1},{name:`dither`,requires:`media`,deprecated:!0},{name:`ascii`,requires:`media`,deprecated:!0},{name:`halftone`,requires:`media`,deprecated:!0}]},stylize:{attribute:`data-kt-stylize`,category:`effects`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-mode`,`data-kt-effect`],defaultVariant:`dither`,deprecatedVariants:[],optionAttributes:`data-kt-accent-color.data-kt-animated.data-kt-ascii-chars.data-kt-ascii-font.data-kt-aspect-ratio.data-kt-brightness.data-kt-cell-size.data-kt-color-steps.data-kt-contrast.data-kt-delay.data-kt-display.data-kt-dither-type.data-kt-duration.data-kt-ease.data-kt-effect.data-kt-halftone-angle.data-kt-halftone-shape.data-kt-height.data-kt-hold-duration.data-kt-ink-color.data-kt-inverted.data-kt-max-dpr.data-kt-mode.data-kt-motion.data-kt-motion-amount.data-kt-motion-speed.data-kt-object-fit.data-kt-object-position.data-kt-on-complete.data-kt-on-progress.data-kt-original-colors.data-kt-paper-color.data-kt-pointer.data-kt-pointer-cell-size.data-kt-pointer-radius.data-kt-pointer-strength.data-kt-preset.data-kt-render-fps.data-kt-root-margin.data-kt-seed.data-kt-threshold.data-kt-transition.data-kt-trigger`.split(`.`),variants:[{name:`dither`,requires:`media`,deprecated:!1},{name:`ascii`,requires:`media`,deprecated:!1},{name:`halftone`,requires:`media`,deprecated:!1}]},lightbox:{attribute:`data-kt-lightbox`,category:`media`,preview:`link`,reason:"viewer 와 grouped 는 `data-kt-group` 을 함께 쓰는지의 차이입니다. 갤러리 전체가 하나의 소재라 카드 단위로 복제되지 않습니다.",carrier:``,carrierAttribute:``,clearAttributes:[],defaultVariant:`viewer`,deprecatedVariants:[],optionAttributes:`data-kt-alt.data-kt-backdrop-blur.data-kt-backdrop-color.data-kt-backdrop-opacity.data-kt-caption.data-kt-class-name.data-kt-close-on-backdrop.data-kt-cursor.data-kt-description.data-kt-double-click-zoom.data-kt-download.data-kt-duration.data-kt-exif.data-kt-group.data-kt-info.data-kt-labels.data-kt-lazy-effect.data-kt-lazy-options.data-kt-lightbox-duration.data-kt-max-zoom.data-kt-metadata.data-kt-min-zoom.data-kt-minimap.data-kt-on-change.data-kt-on-close.data-kt-on-load.data-kt-on-open.data-kt-radius.data-kt-render-u-i.data-kt-share.data-kt-src.data-kt-thumbnails.data-kt-title.data-kt-toolbar.data-kt-transition.data-kt-ui-template.data-kt-wheel-step.data-kt-zoom.data-kt-zoom-step`.split(`.`),variants:[{name:`viewer`,requires:`any`,deprecated:!1},{name:`grouped`,requires:`any`,deprecated:!1}]},loader:{attribute:`data-kt-loader`,category:`system`,preview:`control`,reason:`전체 화면 로더라 한 번에 하나만 띄울 수 있습니다. 데모의 로더 카드를 그대로 사용합니다.`,carrier:``,carrierAttribute:``,clearAttributes:[],defaultVariant:`bar`,deprecatedVariants:[],optionAttributes:`data-kt-announce.data-kt-aria-label.data-kt-bar-height.data-kt-bar-width.data-kt-class-name.data-kt-color.data-kt-complete-hold.data-kt-complete-on-error.data-kt-duration.data-kt-exit.data-kt-exit-direction.data-kt-exit-duration.data-kt-expected-resources.data-kt-fetch.data-kt-fetch-options.data-kt-fill.data-kt-fill-color.data-kt-hide-scrollbar.data-kt-label.data-kt-label-blend.data-kt-label-color.data-kt-linecap.data-kt-manual-duration.data-kt-min-duration.data-kt-on-cancel.data-kt-on-complete.data-kt-on-error.data-kt-on-hide.data-kt-on-progress.data-kt-on-show.data-kt-on-start.data-kt-on-state-change.data-kt-percent.data-kt-preset.data-kt-progress.data-kt-progress-output.data-kt-progress-scope.data-kt-progress-source.data-kt-progress-template.data-kt-promise.data-kt-promise-ceiling.data-kt-promise-start.data-kt-radius.data-kt-render-u-i.data-kt-resource-selector.data-kt-resources.data-kt-reveal-effect.data-kt-show-percent.data-kt-size.data-kt-smoothing.data-kt-source.data-kt-stroke.data-kt-track-color.data-kt-transition.data-kt-type.data-kt-url`.split(`.`),variants:[{name:`slot`,requires:`any`,deprecated:!1},{name:`circular`,requires:`any`,deprecated:!1},{name:`bar`,requires:`any`,deprecated:!1}]},loadingIndicator:{attribute:`data-kt-loading-indicator`,category:`system`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-type`],defaultVariant:`spinner`,deprecatedVariants:[],optionAttributes:`data-kt-aria-label.data-kt-ascii-only.data-kt-auto-complete.data-kt-bar-height.data-kt-bar-mode.data-kt-bar-width.data-kt-base-color.data-kt-class-name.data-kt-color.data-kt-complete-hold.data-kt-complete-on-error.data-kt-cursor-char.data-kt-direction.data-kt-dot-count.data-kt-dot-gap.data-kt-dot-size.data-kt-dot-style.data-kt-empty-char.data-kt-exit-duration.data-kt-fill-char.data-kt-fixed-width.data-kt-font-family.data-kt-font-weight.data-kt-frame-interval.data-kt-frames.data-kt-glow.data-kt-glow-color.data-kt-glow-size.data-kt-hide-on-complete.data-kt-highlight-color.data-kt-indeterminate.data-kt-label.data-kt-letter-spacing.data-kt-line-height.data-kt-motion-duration.data-kt-on-complete.data-kt-on-error.data-kt-on-hide.data-kt-on-progress.data-kt-on-show.data-kt-on-start.data-kt-on-state-change.data-kt-preset.data-kt-progress.data-kt-progress-output.data-kt-progress-scope.data-kt-progress-source.data-kt-progress-template.data-kt-render-u-i.data-kt-rotate-spokes.data-kt-secondary-color.data-kt-show-label.data-kt-show-spinner.data-kt-show-status.data-kt-size.data-kt-spinner-mode.data-kt-spinner-style.data-kt-spread.data-kt-step-total.data-kt-stroke.data-kt-terminal-style.data-kt-text.data-kt-text-size.data-kt-track.data-kt-track-color.data-kt-transform-origin.data-kt-type.data-kt-viewport-width`.split(`.`),variants:[{name:`spinner`,requires:`any`,deprecated:!1},{name:`dots`,requires:`any`,deprecated:!1},{name:`bar`,requires:`any`,deprecated:!1},{name:`shimmer`,requires:`any`,deprecated:!1},{name:`shimmer-wave`,requires:`any`,deprecated:!1},{name:`terminal`,requires:`any`,deprecated:!1}]},magnetic:{attribute:`data-kt-magnetic`,category:`pointer`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-effect`],defaultVariant:`pointer`,deprecatedVariants:[],optionAttributes:[`data-kt-axis`,`data-kt-ease`,`data-kt-effect`,`data-kt-item`,`data-kt-lift`,`data-kt-max-scale`,`data-kt-preset`,`data-kt-radius`,`data-kt-range`,`data-kt-strength`],variants:[{name:`pointer`,requires:`any`,deprecated:!1},{name:`dock`,requires:`any`,deprecated:!1}]},marquee:{attribute:`data-kt-marquee`,category:`text`,preview:`grid`,reason:``,carrier:`direction`,carrierAttribute:`data-kt-direction`,clearAttributes:[`data-kt-direction`],defaultVariant:`left`,deprecatedVariants:[],optionAttributes:[`data-kt-clones`,`data-kt-direction`,`data-kt-fade`,`data-kt-pause-on-hover`,`data-kt-reverse-on-scroll-up`,`data-kt-scroll-acceleration`,`data-kt-skew`,`data-kt-speed`],variants:[{name:`left`,requires:`any`,deprecated:!1},{name:`right`,requires:`any`,deprecated:!1},{name:`reverse-on-scroll`,requires:`any`,deprecated:!1}]},mouseParallax:{attribute:`data-kt-mouse-parallax`,category:`pointer`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-mode`],defaultVariant:`pointer`,deprecatedVariants:[],optionAttributes:[`data-kt-compass-range`,`data-kt-ease`,`data-kt-global`,`data-kt-gyro`,`data-kt-max-x`,`data-kt-max-y`,`data-kt-mode`,`data-kt-preset`,`data-kt-rotate-offset`,`data-kt-sensitivity`,`data-kt-smoothing`,`data-kt-speed`],variants:[{name:`pointer`,requires:`any`,deprecated:!1},{name:`gyro`,requires:`any`,deprecated:!1},{name:`compass`,requires:`any`,deprecated:!1}]},overflowText:{attribute:`data-kt-overflow-text`,category:`text`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-mode`],defaultVariant:`loop`,deprecatedVariants:[],optionAttributes:`data-kt-aria-live.data-kt-crossfade.data-kt-delay.data-kt-direction.data-kt-dissolve-duration.data-kt-easing.data-kt-ellipsis.data-kt-end-pause.data-kt-flip-direction.data-kt-flip-duration.data-kt-force.data-kt-gap.data-kt-hold-duration.data-kt-hover-target.data-kt-items.data-kt-jitter.data-kt-loop-on-hover.data-kt-mask-direction.data-kt-mask-duration.data-kt-mask-ease.data-kt-mode.data-kt-on-change.data-kt-on-page.data-kt-page-duration.data-kt-page-overlap.data-kt-pause-on-hover.data-kt-perspective.data-kt-preset.data-kt-repeat.data-kt-restart-delay.data-kt-restore-direction.data-kt-restore-on-leave.data-kt-role.data-kt-roll-direction.data-kt-roll-duration.data-kt-speed.data-kt-text.data-kt-threshold.data-kt-title.data-kt-transition-direction.data-kt-trigger`.split(`.`),variants:[{name:`loop`,requires:`any`,deprecated:!1},{name:`bounce`,requires:`any`,deprecated:!1},{name:`rewind`,requires:`any`,deprecated:!1},{name:`once`,requires:`any`,deprecated:!1},{name:`page`,requires:`any`,deprecated:!1},{name:`flip`,requires:`any`,deprecated:!1},{name:`dissolve`,requires:`any`,deprecated:!1},{name:`page-roll`,requires:`any`,deprecated:!1},{name:`rolling`,requires:`any`,deprecated:!1},{name:`fade`,requires:`any`,deprecated:!1},{name:`scroll-fade`,requires:`any`,deprecated:!1}]},pageReveal:{attribute:`data-kt-page-reveal`,category:`system`,preview:`control`,reason:`화면 전체를 덮는 오버레이라 한 페이지에 여러 개를 띄울 수 없습니다. 데모의 재생 버튼을 그대로 사용합니다.`,carrier:``,carrierAttribute:``,clearAttributes:[],defaultVariant:`curtain`,deprecatedVariants:[],optionAttributes:`data-kt-angle.data-kt-axis.data-kt-cleanup-duration.data-kt-color.data-kt-color2.data-kt-count.data-kt-delay.data-kt-density.data-kt-direction.data-kt-duration.data-kt-ease.data-kt-effect.data-kt-horizontal-duration.data-kt-large-tile-chance.data-kt-line-height.data-kt-line-width.data-kt-noise-duration.data-kt-on-complete.data-kt-overscan.data-kt-preset.data-kt-reverse.data-kt-seed.data-kt-small-tile-chance.data-kt-stagger.data-kt-tile-max.data-kt-tile-min.data-kt-vertical-duration`.split(`.`),variants:[{name:`curtain`,requires:`any`,deprecated:!1},{name:`split`,requires:`any`,deprecated:!1},{name:`blinds`,requires:`any`,deprecated:!1},{name:`diagonal`,requires:`any`,deprecated:!1},{name:`shutter`,requires:`any`,deprecated:!1},{name:`fade`,requires:`any`,deprecated:!1},{name:`zoom`,requires:`any`,deprecated:!1},{name:`iris`,requires:`any`,deprecated:!1},{name:`flash`,requires:`any`,deprecated:!1},{name:`center-slit`,requires:`any`,deprecated:!1},{name:`data-mosaic`,requires:`any`,deprecated:!1},{name:`curve`,requires:`any`,deprecated:!1},{name:`dissolve`,requires:`any`,deprecated:!1},{name:`push`,requires:`any`,deprecated:!1},{name:`grid`,requires:`any`,deprecated:!1},{name:`fold`,requires:`any`,deprecated:!1}]},parallax:{attribute:`data-kt-parallax`,category:`scroll`,preview:`grid`,reason:``,carrier:`axis`,carrierAttribute:`data-kt-axis`,clearAttributes:[`data-kt-axis`],defaultVariant:`y`,deprecatedVariants:[],optionAttributes:[`data-kt-axis`,`data-kt-distance`,`data-kt-end`,`data-kt-on-update`,`data-kt-scrub`,`data-kt-speed`,`data-kt-start`],variants:[{name:`x`,requires:`any`,deprecated:!1},{name:`y`,requires:`any`,deprecated:!1}]},progress:{attribute:`data-kt-progress`,category:`scroll`,preview:`link`,reason:`variant 가 대상(page·element)과 표현(scaleX·width) 두 옵션의 조합이고, page 계열은 문서 전체에 하나만 존재할 수 있습니다.`,carrier:``,carrierAttribute:``,clearAttributes:[],defaultVariant:`page:scaleX`,deprecatedVariants:[],optionAttributes:[`data-kt-attach`,`data-kt-click-to-top`,`data-kt-color`,`data-kt-color2`,`data-kt-hide-at-end`,`data-kt-label`,`data-kt-offset`,`data-kt-on-update`,`data-kt-position`,`data-kt-property`,`data-kt-radius`,`data-kt-show-after`,`data-kt-show-percent`,`data-kt-size`,`data-kt-smoothing`,`data-kt-stroke`,`data-kt-target`,`data-kt-thickness`,`data-kt-track-color`,`data-kt-ui`,`data-kt-z-index`],variants:[{name:`page:scaleX`,requires:`any`,deprecated:!1},{name:`page:width`,requires:`any`,deprecated:!1},{name:`element:scaleX`,requires:`any`,deprecated:!1},{name:`element:width`,requires:`any`,deprecated:!1}]},reveal:{attribute:`data-kt-reveal`,category:`scroll`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`],defaultVariant:`fade-up`,deprecatedVariants:[],optionAttributes:`data-kt-active-class.data-kt-class-only.data-kt-clock-direction.data-kt-delay.data-kt-direction.data-kt-distance.data-kt-duration.data-kt-ease.data-kt-end.data-kt-enter-class.data-kt-enter-ease.data-kt-leave-class.data-kt-on-class-change.data-kt-on-complete.data-kt-on-enter.data-kt-on-enter-back.data-kt-on-leave.data-kt-on-leave-back.data-kt-once.data-kt-order.data-kt-preset.data-kt-remove-class-on-leave.data-kt-root-margin.data-kt-spring.data-kt-stagger.data-kt-start.data-kt-start-angle.data-kt-threshold`.split(`.`),variants:[{name:`fade`,requires:`any`,deprecated:!1},{name:`fade-up`,requires:`any`,deprecated:!1},{name:`fade-down`,requires:`any`,deprecated:!1},{name:`fade-left`,requires:`any`,deprecated:!1},{name:`fade-right`,requires:`any`,deprecated:!1},{name:`slide-up`,requires:`any`,deprecated:!1},{name:`slide-down`,requires:`any`,deprecated:!1},{name:`slide-left`,requires:`any`,deprecated:!1},{name:`slide-right`,requires:`any`,deprecated:!1},{name:`zoom-in`,requires:`any`,deprecated:!1},{name:`zoom-out`,requires:`any`,deprecated:!1},{name:`blur`,requires:`any`,deprecated:!1},{name:`rise`,requires:`any`,deprecated:!1},{name:`soft`,requires:`any`,deprecated:!1},{name:`flip-x`,requires:`any`,deprecated:!1},{name:`flip-y`,requires:`any`,deprecated:!1},{name:`rotate`,requires:`any`,deprecated:!1},{name:`mask`,requires:`any`,deprecated:!1},{name:`wipe`,requires:`any`,deprecated:!1},{name:`class`,requires:`any`,deprecated:!1},{name:`clock`,requires:`any`,deprecated:!1},{name:`swing`,requires:`any`,deprecated:!1},{name:`skew`,requires:`any`,deprecated:!1}]},radial:{attribute:`data-kt-radial`,category:`media`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-position`],defaultVariant:`bottom`,deprecatedVariants:[],optionAttributes:[`data-kt-active-angle`,`data-kt-active-class`,`data-kt-align`,`data-kt-autoplay`,`data-kt-controls`,`data-kt-damping`,`data-kt-drag`,`data-kt-duration`,`data-kt-loop`,`data-kt-mass`,`data-kt-position`,`data-kt-radius`,`data-kt-smoothing`,`data-kt-spring`,`data-kt-step`,`data-kt-stiffness`],variants:[{name:`bottom`,requires:`any`,deprecated:!1},{name:`top`,requires:`any`,deprecated:!1},{name:`left`,requires:`any`,deprecated:!1},{name:`right`,requires:`any`,deprecated:!1}]},scrollSequence:{attribute:`data-kt-scroll-sequence`,category:`scroll`,preview:`grid`,reason:``,carrier:`fit`,carrierAttribute:`data-kt-fit`,clearAttributes:[`data-kt-fit`],defaultVariant:`cover`,deprecatedVariants:[],optionAttributes:[`data-kt-cross-origin`,`data-kt-end`,`data-kt-extension`,`data-kt-fit`,`data-kt-frames`,`data-kt-height`,`data-kt-max-dpr`,`data-kt-on-error`,`data-kt-on-frame`,`data-kt-padding`,`data-kt-preload-radius`,`data-kt-scroll-length`,`data-kt-scrub`,`data-kt-start`,`data-kt-top`,`data-kt-url-prefix`,`data-kt-urls`,`data-kt-vh-per-frame`],variants:[{name:`cover`,requires:`any`,deprecated:!1},{name:`contain`,requires:`any`,deprecated:!1}]},scrollVelocity:{attribute:`data-kt-scroll-velocity`,category:`scroll`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-mode`,`data-kt-effect`],defaultVariant:`skew`,deprecatedVariants:[],optionAttributes:[`data-kt-axis`,`data-kt-damping`,`data-kt-decay`,`data-kt-distance`,`data-kt-effect`,`data-kt-elastic`,`data-kt-end`,`data-kt-global`,`data-kt-mass`,`data-kt-max-blur`,`data-kt-max-rotate`,`data-kt-max-scale`,`data-kt-max-skew`,`data-kt-mode`,`data-kt-on-direction`,`data-kt-on-update`,`data-kt-preset`,`data-kt-response`,`data-kt-reverse`,`data-kt-smoothing`,`data-kt-spring`,`data-kt-start`,`data-kt-stiffness`,`data-kt-velocity-divisor`],variants:[{name:`skew`,requires:`any`,deprecated:!1},{name:`rotate`,requires:`any`,deprecated:!1},{name:`scale`,requires:`any`,deprecated:!1},{name:`blur`,requires:`any`,deprecated:!1},{name:`translate`,requires:`any`,deprecated:!1},{name:`combo`,requires:`any`,deprecated:!1}]},slider:{attribute:`data-kt-slider`,category:`media`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-effect`],defaultVariant:`slide`,deprecatedVariants:[],optionAttributes:`data-kt-active-angle.data-kt-active-class.data-kt-active-shadow.data-kt-active-shadow-opacity.data-kt-align.data-kt-auto-height.data-kt-autoplay.data-kt-axis.data-kt-bounce.data-kt-breakpoints.data-kt-controls.data-kt-damping.data-kt-depth.data-kt-dots.data-kt-drag.data-kt-duration.data-kt-effect.data-kt-effect-direction.data-kt-effect-intensity.data-kt-enabled.data-kt-gap.data-kt-grab-cursor.data-kt-index.data-kt-initial.data-kt-initial-index.data-kt-keyboard.data-kt-label.data-kt-labels.data-kt-loop.data-kt-mass.data-kt-min-opacity.data-kt-min-scale.data-kt-momentum.data-kt-next-selector.data-kt-on-before-change.data-kt-on-change.data-kt-on-init.data-kt-opacity-step.data-kt-pause-button.data-kt-pause-on-hover.data-kt-pause-when-offscreen.data-kt-per-group.data-kt-per-view.data-kt-perspective.data-kt-position.data-kt-preset.data-kt-prev-selector.data-kt-progress.data-kt-progress-type.data-kt-radius.data-kt-rotate.data-kt-scale-step.data-kt-scroll-snap.data-kt-slide-to-clicked-slide.data-kt-smoothing.data-kt-spacing.data-kt-speed.data-kt-spring.data-kt-step.data-kt-sticky-snap.data-kt-stiffness.data-kt-sync.data-kt-touch.data-kt-velocity-influence.data-kt-wheel`.split(`.`),variants:[{name:`slide`,requires:`track`,deprecated:!1},{name:`fade`,requires:`track`,deprecated:!1},{name:`dissolve`,requires:`track`,deprecated:!1},{name:`wipe`,requires:`track`,deprecated:!1},{name:`coverflow`,requires:`track`,deprecated:!1},{name:`flip`,requires:`track`,deprecated:!1},{name:`cube`,requires:`track`,deprecated:!1},{name:`cards`,requires:`track`,deprecated:!1},{name:`creative`,requires:`track`,deprecated:!1},{name:`radial`,requires:`items`,deprecated:!1}]},stickyStack:{attribute:`data-kt-sticky-stack`,category:`scroll`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-mode`,`data-kt-type`,`data-kt-effect`],defaultVariant:`vertical`,deprecatedVariants:[],optionAttributes:`data-kt-align.data-kt-blur.data-kt-bottom-space.data-kt-distance.data-kt-ease.data-kt-effect.data-kt-end.data-kt-fade-previous.data-kt-gap.data-kt-item-duration.data-kt-item-height.data-kt-min-height.data-kt-mode.data-kt-offset.data-kt-offset-top.data-kt-offset-y.data-kt-on-progress.data-kt-overlap.data-kt-panel-width.data-kt-perspective.data-kt-pin.data-kt-pin-spacing.data-kt-preset.data-kt-previous-blur.data-kt-previous-opacity.data-kt-previous-scale.data-kt-previous-y.data-kt-reverse-z.data-kt-rotate.data-kt-scale-from.data-kt-scale-previous.data-kt-scroll-length.data-kt-scrub.data-kt-snap.data-kt-start.data-kt-top.data-kt-transform-origin.data-kt-transition-start-offset.data-kt-type`.split(`.`),variants:[{name:`vertical`,requires:`any`,deprecated:!1},{name:`horizontal`,requires:`any`,deprecated:!1},{name:`zindex`,requires:`any`,deprecated:!1},{name:`floating`,requires:`any`,deprecated:!1}]},textReveal:{attribute:`data-kt-text-reveal`,category:`text`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-mode`],defaultVariant:`stream`,deprecatedVariants:[],optionAttributes:[`data-kt-chars`,`data-kt-delay`,`data-kt-duration`,`data-kt-ease`,`data-kt-flicker-count`,`data-kt-flicker-loop`,`data-kt-hold`,`data-kt-loop`,`data-kt-mode`,`data-kt-on-complete`,`data-kt-preset`,`data-kt-rainbow`,`data-kt-rainbow-colors`,`data-kt-reveal-rate`,`data-kt-root-margin`,`data-kt-scramble-fade`,`data-kt-speed`,`data-kt-stagger`,`data-kt-text`,`data-kt-threshold`],variants:[{name:`stream`,requires:`any`,deprecated:!1},{name:`char`,requires:`any`,deprecated:!1},{name:`word`,requires:`any`,deprecated:!1},{name:`line`,requires:`any`,deprecated:!1},{name:`bounce`,requires:`any`,deprecated:!1},{name:`hangul`,requires:`any`,deprecated:!1},{name:`decode`,requires:`any`,deprecated:!1},{name:`flicker`,requires:`any`,deprecated:!1},{name:`shuffle`,requires:`any`,deprecated:!1}]},textSplit:{attribute:`data-kt-text-split`,category:`text`,preview:`grid`,reason:``,carrier:`by`,carrierAttribute:`data-kt-by`,clearAttributes:[`data-kt-by`,`data-kt-preset`],defaultVariant:`char`,deprecatedVariants:[],optionAttributes:[`data-kt-animation`,`data-kt-by`,`data-kt-delay`,`data-kt-duration`,`data-kt-ease`,`data-kt-hold`,`data-kt-on-complete`,`data-kt-on-swap`,`data-kt-once`,`data-kt-pause`,`data-kt-perspective`,`data-kt-preset`,`data-kt-stagger`,`data-kt-start`,`data-kt-swap-ease`,`data-kt-swap-out`,`data-kt-texts`],variants:[{name:`char`,requires:`text`,deprecated:!1},{name:`word`,requires:`text`,deprecated:!1}]},textTransition:{attribute:`data-kt-text-transition`,category:`text`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-effect`],defaultVariant:`slide-up`,deprecatedVariants:[],optionAttributes:[`data-kt-aria-live`,`data-kt-base-color`,`data-kt-blur`,`data-kt-char-direction`,`data-kt-char-mode`,`data-kt-duration`,`data-kt-ease`,`data-kt-effect`,`data-kt-end-scale`,`data-kt-hold`,`data-kt-jitter`,`data-kt-loop`,`data-kt-min-height`,`data-kt-on-change`,`data-kt-on-complete`,`data-kt-pause`,`data-kt-preset`,`data-kt-shim-color`,`data-kt-shim-speed`,`data-kt-stagger`,`data-kt-start-scale`,`data-kt-texts`],variants:[{name:`slide-up`,requires:`any`,deprecated:!1},{name:`flip`,requires:`any`,deprecated:!1},{name:`rise`,requires:`any`,deprecated:!1},{name:`fade`,requires:`any`,deprecated:!1},{name:`blur`,requires:`any`,deprecated:!1},{name:`scale`,requires:`any`,deprecated:!1},{name:`clip`,requires:`any`,deprecated:!1},{name:`dissolve`,requires:`any`,deprecated:!1},{name:`shimmer`,requires:`any`,deprecated:!1},{name:`pop`,requires:`any`,deprecated:!1}]},tilt:{attribute:`data-kt-tilt`,category:`pointer`,preview:`link`,reason:`variant 가 maxX·maxY·glare·reverse 옵션의 조합에서 파생됩니다. 옵션 하나로 순회할 수 없습니다.`,carrier:``,carrierAttribute:``,clearAttributes:[],defaultVariant:`tilt-glare`,deprecatedVariants:[],optionAttributes:`data-kt-axis.data-kt-disable-on-mobile.data-kt-ease.data-kt-glare.data-kt-glare-blur.data-kt-glare-color.data-kt-glare-opacity.data-kt-glare-radius.data-kt-gyro.data-kt-max.data-kt-max-x.data-kt-max-y.data-kt-perspective.data-kt-reset.data-kt-reverse.data-kt-scale.data-kt-sensitivity.data-kt-smoothing.data-kt-tilt-shadow.data-kt-tilt-shadow-blur.data-kt-tilt-shadow-color.data-kt-tilt-shadow-css.data-kt-tilt-shadow-follow.data-kt-tilt-shadow-hover-only.data-kt-tilt-shadow-inset.data-kt-tilt-shadow-opacity.data-kt-tilt-shadow-spread.data-kt-tilt-shadow-x.data-kt-tilt-shadow-y`.split(`.`),variants:[{name:`tilt-glare`,requires:`any`,deprecated:!1},{name:`tilt`,requires:`any`,deprecated:!1},{name:`x-only`,requires:`any`,deprecated:!1},{name:`y-only`,requires:`any`,deprecated:!1},{name:`reverse`,requires:`any`,deprecated:!1}]},vibrate:{attribute:`data-kt-vibrate`,category:`feedback`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`],defaultVariant:`tap`,deprecatedVariants:[],optionAttributes:[`data-kt-haptic`,`data-kt-pattern`,`data-kt-preset`,`data-kt-threshold`,`data-kt-trigger`],variants:[{name:`tap`,requires:`any`,deprecated:!1},{name:`double-tap`,requires:`any`,deprecated:!1},{name:`soft`,requires:`any`,deprecated:!1},{name:`rigid`,requires:`any`,deprecated:!1},{name:`heavy`,requires:`any`,deprecated:!1},{name:`success`,requires:`any`,deprecated:!1},{name:`warning`,requires:`any`,deprecated:!1},{name:`error`,requires:`any`,deprecated:!1},{name:`ratchet`,requires:`any`,deprecated:!1},{name:`heartbeat`,requires:`any`,deprecated:!1},{name:`long-press`,requires:`any`,deprecated:!1}]},hold:{attribute:`data-kt-hold`,category:`feedback`,preview:`grid`,reason:``,carrier:`mode`,carrierAttribute:`data-kt-mode`,clearAttributes:[`data-kt-mode`],defaultVariant:`confirm`,deprecatedVariants:[],optionAttributes:[`data-kt-action`,`data-kt-blend`,`data-kt-color`,`data-kt-decay`,`data-kt-duration`,`data-kt-labels`,`data-kt-mode`,`data-kt-on-complete`,`data-kt-step`,`data-kt-submit`],variants:[{name:`confirm`,requires:`any`,deprecated:!1},{name:`tap`,requires:`any`,deprecated:!1}]},megaMenu:{attribute:`data-kt-mega-menu`,category:`components`,preview:`grid`,reason:``,carrier:`layout`,carrierAttribute:`data-kt-layout`,clearAttributes:[`data-kt-layout`],defaultVariant:`dropdown`,deprecatedVariants:[],optionAttributes:[`data-kt-close-delay`,`data-kt-duration`,`data-kt-indicator`,`data-kt-layout`,`data-kt-open-delay`,`data-kt-radius`,`data-kt-responsive`,`data-kt-stagger`,`data-kt-start-angle`,`data-kt-sweep`,`data-kt-trigger`],variants:[{name:`dropdown`,requires:`any`,deprecated:!1},{name:`mega`,requires:`any`,deprecated:!1},{name:`radial`,requires:`any`,deprecated:!1}]},gesture:{attribute:`data-kt-gesture`,category:`pointer`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`,`data-kt-effect`],defaultVariant:`spring`,deprecatedVariants:[],optionAttributes:[`data-kt-duration`,`data-kt-ease`,`data-kt-effect`,`data-kt-hover-ease`,`data-kt-hover-scale`,`data-kt-labels`,`data-kt-lift`,`data-kt-max`,`data-kt-on-refresh`,`data-kt-origin`,`data-kt-preset`,`data-kt-press-ease`,`data-kt-resistance`,`data-kt-tap-scale`,`data-kt-threshold`],variants:[{name:`spring`,requires:`any`,deprecated:!1},{name:`pull`,requires:`any`,deprecated:!1}]},flip:{attribute:`data-kt-flip`,category:`components`,preview:`grid`,reason:``,carrier:`mode`,carrierAttribute:`data-kt-mode`,clearAttributes:[`data-kt-mode`],defaultVariant:`layout`,deprecatedVariants:[],optionAttributes:[`data-kt-duration`,`data-kt-ease`,`data-kt-fold-blur`,`data-kt-item`,`data-kt-mode`,`data-kt-stagger`,`data-kt-view-transition`,`data-kt-watch`],variants:[{name:`layout`,requires:`any`,deprecated:!1},{name:`none`,requires:`any`,deprecated:!1},{name:`slide`,requires:`any`,deprecated:!1},{name:`fade`,requires:`any`,deprecated:!1},{name:`crossfade`,requires:`any`,deprecated:!1},{name:`fade-slide`,requires:`any`,deprecated:!1},{name:`scale`,requires:`any`,deprecated:!1},{name:`fold`,requires:`any`,deprecated:!1}]},scrollShadows:{attribute:`data-kt-scroll-shadows`,category:`scroll`,preview:`link`,reason:`variant 는 축(vertical·horizontal)과 모드(mask)가 섞인 조합이라 옵션 하나로 순회할 수 없습니다.`,carrier:``,carrierAttribute:``,clearAttributes:[],defaultVariant:`vertical`,deprecatedVariants:[],optionAttributes:[`data-kt-axis`,`data-kt-color`,`data-kt-ease`,`data-kt-mode`,`data-kt-on-change`,`data-kt-opacity`,`data-kt-shadow`,`data-kt-shape`,`data-kt-size`,`data-kt-transition`,`data-kt-transition-duration`,`data-kt-transition-mode`],variants:[{name:`vertical`,requires:`any`,deprecated:!1},{name:`horizontal`,requires:`any`,deprecated:!1},{name:`mask`,requires:`any`,deprecated:!1}]},squircle:{attribute:`data-kt-squircle`,category:`effects`,preview:`grid`,reason:``,carrier:`activation`,carrierAttribute:``,clearAttributes:[`data-kt-preset`],defaultVariant:`squircle`,deprecatedVariants:[],optionAttributes:[`data-kt-border-follow`,`data-kt-corner-radius`,`data-kt-corner-samples`,`data-kt-native-shape`,`data-kt-preset`,`data-kt-superellipse`],variants:[{name:`squircle`,requires:`any`,deprecated:!1},{name:`round`,requires:`any`,deprecated:!1},{name:`bevel`,requires:`any`,deprecated:!1},{name:`scoop`,requires:`any`,deprecated:!1},{name:`notch`,requires:`any`,deprecated:!1},{name:`square`,requires:`any`,deprecated:!1}]},stickyHeader:{attribute:`data-kt-sticky-header`,category:`scroll`,preview:`link`,reason:`뷰포트에 고정되는 헤더라 한 페이지에 여러 개를 나란히 둘 수 없습니다.`,carrier:``,carrierAttribute:``,clearAttributes:[],defaultVariant:`shrink`,deprecatedVariants:[],optionAttributes:[`data-kt-active-class`,`data-kt-distance`,`data-kt-offset`,`data-kt-on-change`,`data-kt-shadow`,`data-kt-shrink`],variants:[{name:`shrink`,requires:`any`,deprecated:!1},{name:`toggle`,requires:`any`,deprecated:!1}]}}});
\ No newline at end of file
diff --git a/src/core.js b/src/core.js
index 758f5cc..2541b2c 100644
--- a/src/core.js
+++ b/src/core.js
@@ -413,6 +413,41 @@ function coversNode(root, node) {
// Kineto.destroy(root), or removing the root, used to be undone a moment later
// when the engine arrived and the scan created instances on nodes the page had
// already let go of (and that nothing would ever destroy).
+// jsdom 30 and later reject a selector longer than 2048 characters (a guard
+// against runaway selectors; browsers have no limit), and jsdom is where most
+// apps run their unit tests. The built-in registry's activation selector is
+// about 1.1 KB, so it stays one traversal; a page that registers many modules
+// of its own gets a few selectors under the limit instead of a RangeError.
+const MAX_SELECTOR_LENGTH = 2048;
+const DOCUMENT_POSITION_FOLLOWING = 4; // Node.DOCUMENT_POSITION_FOLLOWING
+
+// `root` itself and the elements under it that match any of `parts` (simple
+// selectors), each once and in document order.
+function queryAny(root, parts) {
+ const selectors = [];
+ let current = '';
+ for (const part of parts) {
+ const joined = current ? `${current},${part}` : part;
+ if (current && joined.length > MAX_SELECTOR_LENGTH) {
+ selectors.push(current);
+ current = part;
+ } else current = joined;
+ }
+ if (current) selectors.push(current);
+ const found = selectors.some((selector) => root.matches?.(selector)) ? [root] : [];
+ if (selectors.length === 1) {
+ root.querySelectorAll?.(selectors[0]).forEach((el) => found.push(el));
+ return found;
+ }
+ const seen = new Set(found);
+ selectors.forEach((selector) => root.querySelectorAll?.(selector).forEach((el) => {
+ if (!seen.has(el)) { seen.add(el); found.push(el); }
+ }));
+ // Separate traversals interleave: put the union back in document order, so
+ // each module still creates its elements top to bottom.
+ return found.sort((a, b) => (a.compareDocumentPosition(b) & DOCUMENT_POSITION_FOLLOWING ? -1 : 1));
+}
+
const pendingScans = new Set(); // { root, cancelled, released: [roots destroyed meanwhile] }
function forgetPendingScans(roots = null) {
pendingScans.forEach((pending) => {
@@ -714,7 +749,7 @@ function injectCSSFallback() {
}
const Kineto = {
- version: '0.13.0',
+ version: '0.13.1',
// Central easing subsystem (audit C / J-3). `Kineto.easing(name)` resolves any
// token — CSS keyword, easings.net name, 'elastic-out'/'bounce-in-out'
@@ -921,7 +956,8 @@ const Kineto = {
ensureCoreServices();
const eligible = (el, name) => !getElementMap(el)?.has(name) && !deferral.has(el, name) && !activationIsOwnedOption(el, name);
- // Discover one engine tier with a single selector traversal instead of one
+ // Discover one engine tier with one selector traversal (a few for a very
+ // large registry — see queryAny) instead of one
// querySelectorAll() per registered module. Keep results grouped by module
// so create order remains the registry order, not DOM order across modules.
const discoverModules = (engine) => {
@@ -939,7 +975,6 @@ const Kineto = {
const discovered = new Map(names.map((name) => [name, []]));
if (!names.length) return discovered;
- const selector = names.map((name) => `[data-kt-${dash(name)}]`).join(',');
const collect = (el) => {
const attributes = el.getAttributeNames?.() || [];
attributes.forEach((attribute) => {
@@ -951,8 +986,7 @@ const Kineto = {
// Snapshot the tier before creating from it: a factory may clone markup
// carrying the same activation attribute, and that clone belongs to a
// subsequent scan rather than recursively expanding this one.
- if (root.matches?.(selector)) collect(root);
- root.querySelectorAll?.(selector).forEach(collect);
+ queryAny(root, names.map((name) => `[data-kt-${dash(name)}]`)).forEach(collect);
return discovered;
};
const scanDiscovered = (discovered, keep = null) => {
diff --git a/src/modules/reveal.js b/src/modules/reveal.js
index 28c06ae..fe94905 100644
--- a/src/modules/reveal.js
+++ b/src/modules/reveal.js
@@ -1,4 +1,4 @@
-import { clamp, G, gsapEaseName, latestEntry, motionDefaults, observeOnce, snapshotAttributes, snapshotInlineStyles, ST } from '../utils.js';
+import { clamp, G, gsapEaseName, latestEntry, MAX_FRAME_STEP_MS, motionDefaults, observeOnce, snapshotAttributes, snapshotInlineStyles, ST } from '../utils.js';
import { cubicBezierFn, fn as easingFn } from '../easings.js';
const PRESETS = {
@@ -81,18 +81,31 @@ const removeClasses = (el, opts) => setClasses(el, opts, false);
// A shared, frame-coalesced boundary observer for native timelines. IO wakes it
// on layout changes; captured scroll events include nested scrolling containers.
+//
+// `bounds` measures the box that decides (the native transform path measures
+// the element without the transform it is animating). A ResizeObserver also
+// wakes it: an element created inside a hidden panel (a closed tab, accordion
+// or dialog) measures 0×0, and when the panel opens nothing scrolls, and the
+// IntersectionObserver may never report — a slide-left/right has moved the box
+// it watches one element width aside, and a page that clips horizontal
+// overflow (`overflow-x: clip` on html/body) leaves only a sliver of it, under
+// the threshold. The element's size changing from 0×0 is the signal to measure.
function observeBoundaries(el, opts, clock, boundary, watch, visibleOnly, bounds = () => el.getBoundingClientRect()) {
- let stopped = false, raf = null, zone = null, observer = null;
+ let stopped = false, raf = null, zone = null, observer = null, sizeObserver = null;
const threshold = Number(opts.threshold ?? (clock ? .2 : .1));
const margin = String(opts.rootMargin || (clock ? '0px' : '0px 0px -10% 0px')).trim().split(/\s+/);
+ const side = (index) => margin[index] || margin[index % 2] || margin[0];
const measure = () => {
raf = null;
if (stopped) return;
const width = document.documentElement.clientWidth || window.innerWidth;
const height = document.documentElement.clientHeight || window.innerHeight;
+ // Like IntersectionObserver's rootMargin: a percentage on top/bottom is of
+ // the root's height, on left/right of its width.
const offsets = [0, 1, 2, 3].map((index) => {
- const value = margin[index] || margin[index % 2] || margin[0];
- return Number.parseFloat(value) * (value.endsWith('%') ? width / 100 : 1);
+ const value = side(index);
+ const axis = index % 2 === 0 ? height : width;
+ return Number.parseFloat(value) * (value.endsWith('%') ? axis / 100 : 1);
});
const rect = bounds();
let top = -offsets[0], bottom = height + offsets[2], left = -offsets[3], right = width + offsets[1];
@@ -130,6 +143,10 @@ function observeBoundaries(el, opts, clock, boundary, watch, visibleOnly, bounds
observer = new IntersectionObserver(schedule, { threshold, rootMargin: margin.join(' ') });
observer.observe(el);
} else if (!visibleOnly) boundary(0);
+ if (typeof ResizeObserver !== 'undefined') {
+ sizeObserver = new ResizeObserver(schedule);
+ sizeObserver.observe(el);
+ }
if (watch && !visibleOnly) {
document.addEventListener('scroll', schedule, { passive: true, capture: true });
window.addEventListener('resize', schedule, { passive: true });
@@ -137,6 +154,7 @@ function observeBoundaries(el, opts, clock, boundary, watch, visibleOnly, bounds
return { disconnect() {
stopped = true;
observer?.disconnect();
+ sizeObserver?.disconnect();
if (raf != null) cancelAnimationFrame(raf);
document.removeEventListener('scroll', schedule, true);
window.removeEventListener('resize', schedule);
@@ -198,7 +216,10 @@ function maskedReveal(el, opts, gsap, scrollTrigger, clock, clipAt) {
const frame = (time) => {
raf = null;
if (destroyed || paused) return;
- if (lastTime != null) state.time = clamp(state.time + (time - lastTime) * rate / 1000, 0, total);
+ // One long frame (a busy page load) advances at most MAX_FRAME_STEP_MS, so
+ // a short entrance slows for a moment instead of jumping straight to its end.
+ const step = lastTime == null ? 0 : clamp(time - lastTime, 0, MAX_FRAME_STEP_MS);
+ state.time = clamp(state.time + step * rate / 1000, 0, total);
lastTime = time;
paint();
if (rate > 0 ? state.time < total : state.time > 0) raf = requestAnimationFrame(frame);
diff --git a/src/modules/slider.js b/src/modules/slider.js
index bf4aa44..63497aa 100644
--- a/src/modules/slider.js
+++ b/src/modules/slider.js
@@ -1,4 +1,4 @@
-import { clamp, cssString, env, frameEase, labeller, latestEntry, lerp, numberOption, selectAll, snapshotAttributes, snapshotInlineStyles } from '../utils.js';
+import { MAX_FRAME_STEP_MS, clamp, cssString, env, frameClock, frameEase, labeller, latestEntry, lerp, numberOption, selectAll, snapshotAttributes, snapshotInlineStyles } from '../utils.js';
// Do not rewind presentation owned by a composing module or application.
const snapshotPresentation = (el, properties, classes = [], attributes = []) => {
@@ -177,7 +177,6 @@ export default {
let targetActive = active;
let radialFrame = null;
let radialVelocity = 0;
- let radialLastTime = -16;
let offscreen = false;
let visibilityObserver = null;
const renderRadial = (positionValue) => {
@@ -244,11 +243,14 @@ export default {
return;
}
const from = visualActive;
- const started = performance.now();
- radialLastTime = started - 16;
+ // Time comes from frameClock(), not from `time - started`: a frame
+ // timestamp that repeats (or steps back) counts as one frame instead of
+ // none, and the frame clock is never mixed with performance.now().
+ const clock = frameClock();
+ let elapsed = 0;
const tick = (time) => {
- const dt = Math.min(64, Math.max(0, time - radialLastTime));
- radialLastTime = time;
+ const dt = Math.min(MAX_FRAME_STEP_MS, clock.tick(time));
+ elapsed += dt;
if (radialSpring) {
const seconds = dt / 1000;
const acceleration = ((targetActive - visualActive) * radialStiffness - radialVelocity * radialDamping) / radialMass;
@@ -257,7 +259,7 @@ export default {
} else if (radialSmoothing) {
visualActive = lerp(visualActive, targetActive, frameEase(radialSmoothing, dt));
} else {
- const progress = Math.min(1, (time - started) / (duration * 1000));
+ const progress = Math.min(1, elapsed / (duration * 1000));
const eased = 1 - ((1 - progress) ** 3);
visualActive = from + (targetActive - from) * eased;
}
@@ -266,7 +268,7 @@ export default {
? Math.abs(visualActive - targetActive) <= 0.0015 && Math.abs(radialVelocity) <= 0.0015
: radialSmoothing
? Math.abs(visualActive - targetActive) <= 0.0015
- : (time - started) >= duration * 1000;
+ : elapsed >= duration * 1000;
if (!done) radialFrame = requestAnimationFrame(tick);
else {
visualActive = targetActive;
@@ -629,7 +631,8 @@ export default {
let sampleCount = 0;
let pointerId = null;
let rafId = null;
- let lastFrameTime = -16;
+ // Elapsed time between frames for the settle loop (see tick below).
+ const frameTime = frameClock();
let timer = null;
let timerStartedAt = 0;
let remaining = autoplayDelay;
@@ -901,10 +904,15 @@ export default {
if (!alive) return;
if (offscreen) { rafId = null; return; }
// Normalize the frame lerp to elapsed time so 60/90/120Hz displays
- // settle at the same rate. A long background-tab gap is capped to avoid
- // a single callback teleporting the carousel across its target.
- const dt = Math.min(64, time - lastFrameTime);
- lastFrameTime = time;
+ // settle at the same rate (frameClock + frameEase, like every other
+ // following loop). One callback advances at most MAX_FRAME_STEP_MS, so a
+ // background-tab gap does not teleport the carousel across its target.
+ // A timestamp that repeats or steps back counts as one frame: the old
+ // `time - lastFrameTime` gave 0 there, so a carousel fed the same
+ // timestamp stood still short of its target and kept requesting frames
+ // (tests/browser/motion-timing.mjs). That is the likely cause of a WebKit
+ // CI run where a dissolve stopped at 98.1% — the same value on two runs.
+ const dt = Math.min(MAX_FRAME_STEP_MS, frameTime.tick(time));
const physicsEnabled = springEnabled || bounceActive;
if (physicsEnabled && !dragging) {
// Semi-implicit Euler keeps the public spring controls deterministic
@@ -917,8 +925,7 @@ export default {
position += springVelocity * seconds;
} else {
if (dragging) springVelocity = 0;
- const amount = 1 - ((1 - (dragging ? 0.55 : smoothing)) ** (dt / 16));
- position = lerp(position, target, amount);
+ position = lerp(position, target, frameEase(dragging ? 0.55 : smoothing, dt));
}
render();
const settled = physicsEnabled
diff --git a/src/modules/squircle.js b/src/modules/squircle.js
index 31d1eb2..3def44e 100644
--- a/src/modules/squircle.js
+++ b/src/modules/squircle.js
@@ -185,9 +185,12 @@ export default {
};
apply();
- // The polyfill is drawn in pixels, so it has to be redrawn when the box
- // changes. The native path needs no observer at all.
- if (!useNative && typeof ResizeObserver !== 'undefined') {
+ // Both paths watch the box. The polyfill is drawn in pixels, so every
+ // resize redraws it. The native path writes its radius in pixels too, so a
+ // percentage radius has to follow the box — and an element created inside
+ // a hidden panel (a closed tab, accordion or dialog) measures 0×0 above and
+ // is only shaped once it is shown. apply() skips an unchanged box.
+ if (typeof ResizeObserver !== 'undefined') {
observer = new ResizeObserver(() => apply());
observer.observe(el);
}
diff --git a/src/utils.js b/src/utils.js
index 78c8813..b94c552 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -130,6 +130,17 @@ export function clamp(value, min, max) {
/** 60Hz 기준 한 프레임(ms). 모듈의 `smoothing` 계수는 이 속도에서 맞춰져 있습니다. */
export const FRAME_MS = 1000 / 60;
+/**
+ * 시간 기준 애니메이션이 프레임 하나에서 앞으로 갈 수 있는 최대치: 60Hz 4프레임(약 67ms).
+ *
+ * 긴 작업 하나(페이지 로드 중 이미지 디코드, 폰트, 무거운 스크립트)로 프레임 사이가
+ * 300ms 벌어지면, 흐른 시간을 그대로 쓰는 루프는 0.22초짜리 등장을 한 프레임에 끝내
+ * 버립니다 — 사용자는 움직임 없이 툭 나타나는 것만 봅니다. 이 한도만큼만 가면 그 순간은
+ * 조금 느려질 뿐, 움직임은 보입니다. `frameEase()` 가 따라잡는 한도와 같은 값입니다.
+ */
+export const MAX_FRAME_STEPS = 4;
+export const MAX_FRAME_STEP_MS = FRAME_MS * MAX_FRAME_STEPS;
+
/**
* "매 프레임 남은 거리의 `amount` 만큼 다가간다"를 **시간 기준**으로 바꾼 계수.
*
@@ -142,12 +153,12 @@ export const FRAME_MS = 1000 / 60;
* const k = frameEase(smoothing, elapsedMs);
* current = lerp(current, target, k);
*
- * 한 번에 4프레임 분까지만 따라잡습니다(멈춰 있던 탭이 깨어날 때 한 번에 튀지 않게).
+ * 한 번에 `MAX_FRAME_STEPS`(4프레임) 분까지만 따라잡습니다(멈춰 있던 탭이 깨어날 때 한 번에 튀지 않게).
*/
export function frameEase(amount, elapsedMs = FRAME_MS) {
const step = clamp(Number(amount) || 0, 0, 1);
if (step >= 1) return 1;
- const frames = clamp((Number(elapsedMs) || FRAME_MS) / FRAME_MS, 0.25, 4);
+ const frames = clamp((Number(elapsedMs) || FRAME_MS) / FRAME_MS, 0.25, MAX_FRAME_STEPS);
return 1 - ((1 - step) ** frames);
}
diff --git a/tests/browser/components-a11y.mjs b/tests/browser/components-a11y.mjs
index 9b695f3..973c418 100644
--- a/tests/browser/components-a11y.mjs
+++ b/tests/browser/components-a11y.mjs
@@ -57,6 +57,18 @@ const html = `
};
window.__frames = (count = 2) => new Promise((resolve) => { let left = count; const step = () => (--left <= 0 ? resolve() : requestAnimationFrame(step)); requestAnimationFrame(step); });
window.__wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
+ // Resolves true as soon as check() is true, or false after timeoutMs.
+ // For a result that arrives with an animation's end: a fixed sleep sized
+ // for a normal frame rate failed when a WebKit CI runner stalled rendering.
+ window.__until = (check, timeoutMs = 2000) => new Promise((resolve) => {
+ const started = performance.now();
+ const poll = () => {
+ if (check()) resolve(true);
+ else if (performance.now() - started > timeoutMs) resolve(false);
+ else setTimeout(poll, 16);
+ };
+ poll();
+ });
// A phone's answers to the hover/pointer media queries (utils.canHover()
// asks "(any-hover: hover) and (any-pointer: fine)", then "(any-hover: none)").
// Returns the function that puts the real matchMedia back. (This script is
@@ -350,16 +362,22 @@ const tiltState = await page.evaluate(async () => {
const at = { bubbles: true, clientX: rect.left + 5, clientY: rect.top + 5 };
el.dispatchEvent(new PointerEvent('pointerenter', at));
el.dispatchEvent(new PointerEvent('pointermove', at));
- await window.__wait(1200);
+ // The default smoothing needs about a second of 60 Hz frames to catch up,
+ // and a slow runner drops frames (WebKit on CI still moved at 1.2s). Wait for
+ // the rest instead of guessing a time, but not forever: 4s is a real failure.
+ const movedAt = window.performance.now();
+ await window.__wait(300);
const tilted = /rotateX\((?!0deg)/.test(el.style.transform);
- const frames = await window.__countFrames(10);
+ let frames = await window.__countFrames(10);
+ while (frames > 1 && window.performance.now() - movedAt < 4000) frames = await window.__countFrames(10);
+ const restMs = Math.round(window.performance.now() - movedAt);
const shadowAfter = el.style.getPropertyValue('--kt-tilt-shadow-runtime');
instance.destroy();
- return { tilted, frames, shadowUnchanged: shadowBefore === shadowAfter, pausesOffscreen: window.Kineto.registry.tilt.offscreen === 'pause' };
+ return { tilted, frames, restMs, shadowUnchanged: shadowBefore === shadowAfter, pausesOffscreen: window.Kineto.registry.tilt.offscreen === 'pause' };
});
await check('tilt-rest', async () => {
assert.equal(tiltState.tilted, true, 'tilt must follow the pointer');
- assert.ok(tiltState.frames <= 1, `a tilt under a resting pointer must stop requesting frames (${tiltState.frames})`);
+ assert.ok(tiltState.frames <= 1, `a tilt under a resting pointer must stop requesting frames (${tiltState.frames} still requested ${tiltState.restMs}ms after the last move)`);
});
await check('tilt-shadow-disabled', async () => {
assert.equal(tiltState.shadowUnchanged, true, 'a disabled shadow must not be rewritten while tilting');
@@ -644,8 +662,8 @@ const sheetState = await probe('bottom-sheet-modal', () => page.evaluate(async (
handle.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true, cancelable: true }));
const after = document.getElementById('sheetA').getBoundingClientRect().height;
document.getElementById('closeBtn').click();
- await window.__wait(150);
- const closedByButton = document.getElementById('sheetA').hidden;
+ // The sheet is hidden when its 40 ms close animation finishes.
+ const closedByButton = await window.__until(() => document.getElementById('sheetA').hidden);
const backgroundBack = !document.getElementById('bgLink').closest('[inert]');
b.open();
await window.__wait(120);
diff --git a/tests/browser/motion-timing.mjs b/tests/browser/motion-timing.mjs
index 3a1d8d2..b02ae5a 100644
--- a/tests/browser/motion-timing.mjs
+++ b/tests/browser/motion-timing.mjs
@@ -142,6 +142,49 @@ assert.doesNotMatch(numericEase.magnetic, /NaN/, 'Magnetic must never write NaN'
assert.equal(numericEase.parallaxArrivesInOneFrame, true, 'Mouse Parallax (layers) must honour `smoothing`');
assert.doesNotMatch(numericEase.cursor, /NaN/, `Cursor with a curve-valued ease must not write NaN (${numericEase.cursor})`);
+// Slider: a frame timestamp that repeats counts as one frame. The track loop
+// used `time - lastFrameTime`, which is 0 for a repeated timestamp, so a
+// carousel fed the same timestamp stood still short of its slide and kept
+// requesting frames; the radial loop's duration curve measured `time` against
+// performance.now() and never finished. Five normal frames start each move,
+// then 90 callbacks all carry the same timestamp.
+const repeatedFrames = await page.evaluate(() => {
+ const stuckAt = (time) => window.__runFrames(90, 0, time);
+ const slide = (label) => `${label} `;
+ const track = document.createElement('section');
+ track.innerHTML = `${slide('A')}${slide('B')}${slide('C')}
`;
+ document.getElementById('stage').appendChild(track);
+ const carousel = window.Kineto.create('slider', track, { effect: 'fade', loop: 'off', smoothing: 0.18, momentum: false, pauseWhenOffscreen: false });
+ window.__runFrames(3, 1000 / 60, 20000);
+ carousel.next();
+ stuckAt(window.__runFrames(5, 1000 / 60, 21000));
+ const progress = Number(track.querySelectorAll('.kt-slide')[1].style.getPropertyValue('--kt-slider-slide-progress'));
+ const trackPending = window.__frames.filter(Boolean).length;
+ carousel.destroy();
+ track.remove();
+
+ const orbit = document.createElement('section');
+ orbit.className = 'kt-radial';
+ orbit.style.cssText = 'width:300px;height:300px;position:relative';
+ orbit.innerHTML = ['1', '2', '3', '4'].map((label) => `${label}
`).join('');
+ document.getElementById('stage').appendChild(orbit);
+ const radial = window.Kineto.create('slider', orbit, { effect: 'radial', loop: 'off', duration: 0.4, initialIndex: 0, pauseWhenOffscreen: false });
+ window.__runFrames(3, 1000 / 60, 30000);
+ radial.next();
+ stuckAt(window.__runFrames(5, 1000 / 60, 31000));
+ const item = orbit.querySelectorAll('.orbit-item')[1];
+ const afterStuck = item.style.transform;
+ const pending = window.__frames.filter(Boolean).length;
+ window.__runFrames(40, 1000 / 60, 40000);
+ const radialSettled = { active: item.classList.contains('kt-active'), pending, landed: item.style.transform === afterStuck };
+ radial.destroy();
+ orbit.remove();
+ return { progress, trackPending, radialSettled };
+});
+assert.equal(repeatedFrames.progress, 1, `a slider fed a repeated frame timestamp must still land on its slide (progress ${repeatedFrames.progress})`);
+assert.equal(repeatedFrames.trackPending, 0, 'a landed slider must stop requesting frames');
+assert.deepEqual(repeatedFrames.radialSettled, { active: true, pending: 0, landed: true }, `the radial slider must finish its move on repeated timestamps too (${JSON.stringify(repeatedFrames.radialSettled)})`);
+
assert.deepEqual(errors, [], `page errors:\n${errors.join('\n')}`);
await browser.close();
-console.log(`motion-timing OK (${browserName}) — Tilt covers ${at60.toFixed(2)}° in 150ms at 60Hz and ${at120.toFixed(2)}° at 120Hz; Tilt, Magnetic and Cursor keep moving with a curve-valued ease; Mouse Parallax honours smoothing.`);
+console.log(`motion-timing OK (${browserName}) — Tilt covers ${at60.toFixed(2)}° in 150ms at 60Hz and ${at120.toFixed(2)}° at 120Hz; Tilt, Magnetic and Cursor keep moving with a curve-valued ease; Mouse Parallax honours smoothing; the track and radial Slider land on repeated frame timestamps.`);
diff --git a/tests/browser/reveal-hidden-panel.mjs b/tests/browser/reveal-hidden-panel.mjs
new file mode 100644
index 0000000..5d4f377
--- /dev/null
+++ b/tests/browser/reveal-hidden-panel.mjs
@@ -0,0 +1,85 @@
+// A native slide-left/right Reveal created in a hidden panel plays when the
+// panel opens.
+//
+// The failure this guards (found by `npm run audit:hidden-panel`): without
+// GSAP — blocked or offline, or a data-saver device on the low performance
+// tier — Reveal decides "in view" itself. Its IntersectionObserver watches the
+// element, and a slide-left/right waits one whole element width to the side.
+// On a page that clips horizontal overflow (`overflow-x: clip` on html/body,
+// common exactly where things slide in), a wide card shows the observer only a
+// sliver of that moved box, under the 10% threshold. Created while its panel
+// was closed, the card never got another report when the panel opened: no
+// scroll, no threshold crossing, and it stayed invisible for good. The size
+// change from 0×0 now wakes the check (src/modules/reveal.js observeBoundaries).
+//
+// GSAP's CDN is blocked here so the native path runs deterministically.
+// Run: npm run build && node tests/browser/reveal-hidden-panel.mjs (KT_BROWSER=firefox|webkit)
+import assert from 'node:assert/strict';
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { chromium, firefox, webkit } from 'playwright';
+
+const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
+const browserName = process.env.KT_BROWSER || 'chromium';
+const browserType = { chromium, firefox, webkit }[browserName];
+assert.ok(browserType, `Unsupported KT_BROWSER: ${browserName}`);
+
+const browser = await browserType.launch(browserName === 'chromium'
+ ? { headless: true, ...(process.env.KT_CHROME ? { executablePath: process.env.KT_CHROME } : {}), args: ['--no-sandbox', '--disable-gpu'] }
+ : { headless: true });
+const page = await browser.newPage({ viewport: { width: 1000, height: 800 } });
+const errors = [];
+page.on('pageerror', (error) => errors.push(error.message));
+// No GSAP: every request that leaves the page fails, as on a blocked CDN.
+await page.route(/^https?:\/\//, (route) => route.abort());
+
+// The cards are as wide as the stage, which is centred with a small gutter, so
+// after sliding a whole width aside only a few percent of each is on screen.
+await page.setContent(`
+
+ `);
+await page.addScriptTag({ content: fs.readFileSync(path.join(root, 'dist/kineto.umd.js'), 'utf8') });
+
+const result = await page.evaluate(async () => {
+ const { Kineto } = window;
+ const frame = () => new Promise((resolve) => requestAnimationFrame(resolve));
+ const cards = ['right', 'left'].map((id) => document.getElementById(id));
+ Kineto.init(document);
+ // Reveal waits for the engine first; once it is known to be missing the two
+ // cards are created on the native path, still inside the closed panel.
+ const createdBy = performance.now() + 5000;
+ while (Kineto.instanceCount < 2 && performance.now() < createdBy) await frame();
+ const created = Kineto.instanceCount;
+ await frame();
+ await frame();
+ const whileClosed = cards.map((card) => getComputedStyle(card).opacity);
+ document.getElementById('panel').hidden = false;
+ const openedAt = performance.now();
+ const shown = () => cards.every((card) => getComputedStyle(card).opacity === '1');
+ while (!shown() && performance.now() - openedAt < 3000) await frame();
+ return {
+ gsap: Boolean(window.gsap),
+ created,
+ whileClosed,
+ opacity: cards.map((card) => getComputedStyle(card).opacity),
+ ms: Math.round(performance.now() - openedAt)
+ };
+});
+
+assert.deepEqual(errors, [], 'no page errors');
+assert.equal(result.gsap, false, 'the native path must run (GSAP blocked)');
+assert.equal(result.created, 2, 'both cards must be created while their panel is closed');
+assert.deepEqual(result.whileClosed, ['0', '0'], 'the cards wait hidden while the panel is closed');
+assert.deepEqual(result.opacity, ['1', '1'],
+ `slide-right and slide-left must play when the panel opens (opacity ${result.opacity.join(', ')} after ${result.ms}ms)`);
+
+await browser.close();
+console.log(`reveal-hidden-panel OK (${browserName}) — native slide-right/left created in a closed panel played ${result.ms}ms after it opened on a page that clips horizontal overflow.`);
diff --git a/tests/browser/squircle.mjs b/tests/browser/squircle.mjs
index b8d061f..cba9e4a 100644
--- a/tests/browser/squircle.mjs
+++ b/tests/browser/squircle.mjs
@@ -157,6 +157,58 @@ const result = await page.evaluate(() => {
return report;
});
+// Created inside a hidden panel (a closed tab, accordion or dialog) the box is
+// 0×0. The polyfill always redrew on resize, but the native path measured once
+// at create, found nothing, and never looked again: in Chromium a squircle in a
+// hidden tab stayed a plain rectangle after the tab opened. Both paths must
+// shape the element once it is shown, and a percentage radius must follow the
+// box as it resizes.
+const hiddenPanel = await page.evaluate(async () => {
+ const { Kineto } = window;
+ const main = document.querySelector('main');
+ main.innerHTML = '';
+ const frames = (count = 3) => new Promise((resolve) => {
+ const step = () => (--count <= 0 ? resolve() : requestAnimationFrame(step));
+ requestAnimationFrame(step);
+ });
+ const panel = document.createElement('div');
+ panel.style.display = 'none';
+ panel.innerHTML = '
'
+ + '
';
+ main.append(panel);
+ Kineto.init();
+ const native = document.getElementById('h-native');
+ const poly = document.getElementById('h-poly');
+ const whileHidden = { radius: native.style.borderRadius, clip: poly.style.clipPath };
+ panel.style.display = '';
+ await frames();
+ const shown = { radius: native.style.borderRadius, shape: native.style.cornerShape, clip: poly.style.clipPath };
+
+ const percent = document.createElement('div');
+ percent.className = 'cell';
+ percent.style.cssText = 'left:600px;width:200px;height:100px';
+ percent.setAttribute('data-kt-squircle', 'squircle');
+ percent.setAttribute('data-kt-corner-radius', '50%');
+ main.append(percent);
+ Kineto.init();
+ await frames();
+ const drawn = () => percent.style.clipPath && percent.style.clipPath !== 'none' ? percent.style.clipPath : percent.style.borderRadius;
+ const before = drawn();
+ percent.style.height = '160px';
+ await frames();
+ const after = drawn();
+ Kineto.destroy();
+ return { whileHidden, shown, before, after };
+});
+assert.equal(hiddenPanel.whileHidden.radius, '', 'nothing is drawn for a 0×0 box');
+assert.match(hiddenPanel.shown.clip, /^polygon\(/, 'the polyfill shapes an element created in a hidden panel once it is shown');
+if (result.supportsNative) {
+ assert.match(hiddenPanel.shown.radius, /^40px( 40px){0,3}$/, `the native path writes its radius once a hidden panel is shown ("${hiddenPanel.shown.radius}")`);
+ assert.ok(hiddenPanel.shown.shape, 'the native path writes corner-shape once a hidden panel is shown');
+}
+assert.ok(hiddenPanel.before && hiddenPanel.after && hiddenPanel.before !== hiddenPanel.after,
+ `a percentage radius follows the box as it resizes (${hiddenPanel.before} → ${hiddenPanel.after})`);
+
assert.deepEqual(errors, [], 'squircle must not raise page errors');
// Where the outline meets the 45° diagonal. On that line the two coordinates
diff --git a/tests/browser/stylize-patterns.mjs b/tests/browser/stylize-patterns.mjs
index dd974af..fad6a51 100644
--- a/tests/browser/stylize-patterns.mjs
+++ b/tests/browser/stylize-patterns.mjs
@@ -123,9 +123,21 @@ try {
const mounted = await Promise.all(types.map((type) => mount({ ...base, motion: type, motionSpeed: 1.6, motionAmount: 0.8, renderFps: 60 })));
const canvases = mounted.map(({ media }) => canvasOf(media));
const before = canvases.map((canvas) => canvas && readCanvas(canvas).slice());
- await wait(650);
+ // Three looks instead of one at 650ms: a periodic motion can come back to
+ // the first frame at a single sample point (`flow` repeats every cell of
+ // travel — about 390ms at this speed), and a Firefox CI run failed
+ // `dither-noise:flow` that way twice before passing. A motion must change
+ // the picture at one of them at least; `none` must hold still at all of
+ // them.
+ const most = types.map(() => 0);
+ for (const at of [250, 450, 650]) {
+ await wait(at === 250 ? 250 : 200);
+ canvases.forEach((canvas, index) => {
+ if (canvas) most[index] = Math.max(most[index], changedPixels(before[index], readCanvas(canvas)));
+ });
+ }
types.forEach((type, index) => {
- motion[`${label}:${type}`] = canvases[index] ? changedPixels(before[index], readCanvas(canvases[index])) : -1;
+ motion[`${label}:${type}`] = canvases[index] ? most[index] : -1;
});
mounted.forEach(teardown);
}
diff --git a/tests/ci-annotate.mjs b/tests/ci-annotate.mjs
index d4ac0be..355048d 100644
--- a/tests/ci-annotate.mjs
+++ b/tests/ci-annotate.mjs
@@ -12,13 +12,17 @@
// process printed and, if the process dies from an uncaught error or exits with
// a non-zero code, prints ONE `::error` line naming the script and the reason.
//
+// A child whose non-zero exit its caller expects (attw in
+// scripts/check-package-lint.mjs) is started with `expectedExitEnv()` from
+// scripts/gh-actions.mjs and stays silent here.
+//
// Safety: the text is our own test output. Newlines, `%`, and (in the title)
// `:` and `,` are escaped as the workflow-command format requires, so a message
// can never start a second workflow command. Nothing from the environment is
// printed.
import path from 'node:path';
import process from 'node:process';
-import { escapeData, escapeProperty, inGitHubActions } from '../scripts/gh-actions.mjs';
+import { escapeData, escapeProperty, failureAnnotationsEnabled } from '../scripts/gh-actions.mjs';
const entry = process.argv[1] || '';
// npm/npx only relay a child's exit code; the child reports for itself.
@@ -29,7 +33,7 @@ const isPackageManager = /(?:^|[\\/])(?:npm|npx)(?:-cli)?(?:\.js)?$/.test(entry)
const WRAPPERS = new Set(['scripts/run-lane.mjs', 'scripts/retry-command.mjs', 'tests/retry-browser-test.mjs']);
const isWrapper = WRAPPERS.has(path.relative(process.cwd(), entry).split(path.sep).join('/'));
-if (inGitHubActions() && !isPackageManager && !isWrapper) {
+if (failureAnnotationsEnabled() && !isPackageManager && !isWrapper) {
const script = path.relative(process.cwd(), entry) || 'node';
const MAX_LINES = 24;
const MAX_CHARS = 3000;
diff --git a/tests/consumer-bundles/package-lock.json b/tests/consumer-bundles/package-lock.json
index dd4403d..4dd6bf7 100644
--- a/tests/consumer-bundles/package-lock.json
+++ b/tests/consumer-bundles/package-lock.json
@@ -17,7 +17,7 @@
},
"../..": {
"name": "@dong-gri/kineto",
- "version": "0.13.0",
+ "version": "0.13.1",
"license": "MIT",
"devDependencies": {
"eslint": "^10.10.0",
diff --git a/tests/framework-qa/package-lock.json b/tests/framework-qa/package-lock.json
index 40cb228..0c81025 100644
--- a/tests/framework-qa/package-lock.json
+++ b/tests/framework-qa/package-lock.json
@@ -21,7 +21,7 @@
},
"../..": {
"name": "@dong-gri/kineto",
- "version": "0.13.0",
+ "version": "0.13.1",
"license": "MIT",
"devDependencies": {
"eslint": "^10.10.0",
diff --git a/tests/integrations/package-lock.json b/tests/integrations/package-lock.json
index dbed045..c4d632b 100644
--- a/tests/integrations/package-lock.json
+++ b/tests/integrations/package-lock.json
@@ -30,7 +30,7 @@
},
"../..": {
"name": "@dong-gri/kineto",
- "version": "0.13.0",
+ "version": "0.13.1",
"license": "MIT",
"devDependencies": {
"eslint": "^10.10.0",
diff --git a/tests/motion-regressions.mjs b/tests/motion-regressions.mjs
index 2fb86d0..04cf9a4 100644
--- a/tests/motion-regressions.mjs
+++ b/tests/motion-regressions.mjs
@@ -673,8 +673,10 @@ bounceHost.remove();
// Track settling is elapsed-time based rather than frame-count based: driving
// the same 100ms at 60Hz and 120Hz should land at practically the same point.
-// The first callback also establishes a normal frame interval, while a long
-// gap is capped inside the solver so a background-tab resume cannot teleport.
+// The first callback of a move counts as one normal (60Hz) frame at any
+// refresh rate — utils.frameClock(), like every other following loop — so both
+// runs start with a callback at 1000ms and then cover the same 100ms. A long gap
+// counts as one frame too, so a background-tab resume cannot teleport.
const realRequestAnimationFrame = globalThis.requestAnimationFrame;
const realCancelAnimationFrame = globalThis.cancelAnimationFrame;
let queuedFrame = null;
@@ -698,10 +700,10 @@ const driveSlider = (times) => {
host.remove();
return Number(transform.match(/translate3d\((-?[\d.]+)px/)?.[1] || 0);
};
-const at60Hz = driveSlider([16.667, 33.334, 50.001, 66.668, 83.335, 100.002]);
-const at120Hz = driveSlider(Array.from({ length: 12 }, (_, index) => (index + 1) * 8.333));
+const at60Hz = driveSlider([0, 16.667, 33.334, 50.001, 66.668, 83.335, 100.002].map((time) => 1000 + time));
+const at120Hz = driveSlider(Array.from({ length: 13 }, (_, index) => 1000 + index * 8.333));
assert.ok(Math.abs(at60Hz - at120Hz) < 0.02, `slider settling must be refresh-rate invariant — ${at60Hz} vs ${at120Hz}`);
-const cappedGap = driveSlider([16.667, 500]);
+const cappedGap = driveSlider([1000, 1500]);
assert.ok(cappedGap < 0.99, `a long frame gap must be capped instead of teleporting — ${cappedGap}`);
const springHost = document.createElement('div');
springHost.innerHTML = '';
diff --git a/tests/performance-runtime.mjs b/tests/performance-runtime.mjs
index a09438a..01532db 100644
--- a/tests/performance-runtime.mjs
+++ b/tests/performance-runtime.mjs
@@ -25,26 +25,47 @@ const { default: Kineto } = await import('../src/core.js');
const app = document.querySelector('main');
// scan() discovery cost must scale with engine tiers, not registered module
-// count. A hundred activation names still require one selector traversal for
-// the native tier; adding a GSAP-tier module makes that two, not 101+.
+// count: a hundred activation names share one selector per tier. jsdom 30+
+// rejects selectors over 2048 characters (browsers have no limit), so a very
+// large registry splits into a few selectors under that limit — never one
+// traversal per module, and never a RangeError in an app's unit tests.
+const SELECTOR_LIMIT = 2048;
+const created = [];
for (let i = 0; i < 100; i += 1) {
- Kineto.register(`bulkProbe${i}`, { create: (el) => ({ el, destroy() {} }) });
+ Kineto.register(`bulkProbe${i}`, { create: (el) => { created.push(`${i}:${el.id}`); return { el, destroy() {} }; } });
}
const discoveryRoot = document.createElement('section');
for (let i = 0; i < 100; i += 1) {
const el = document.createElement('div');
+ el.id = `b${i}`;
el.setAttribute(`data-kt-bulk-probe${i}`, '');
discoveryRoot.append(el);
}
+// One element carries a name from each end of the list (so from different
+// selectors) and sits after b99: it is found once, and created after b99.
+const both = document.createElement('div');
+both.id = 'both';
+both.setAttribute('data-kt-bulk-probe0', '');
+both.setAttribute('data-kt-bulk-probe99', '');
+discoveryRoot.append(both);
let selectorTraversals = 0;
-const realQuerySelectorAll = discoveryRoot.querySelectorAll.bind(discoveryRoot);
-discoveryRoot.querySelectorAll = (...args) => {
- selectorTraversals += 1;
- return realQuerySelectorAll(...args);
+const selectorLengths = [];
+const withinLimit = (selector) => {
+ selectorLengths.push(selector.length);
+ if (selector.length > SELECTOR_LIMIT) throw new RangeError(`Selector exceeds maximum allowed length of ${SELECTOR_LIMIT}.`);
};
+const realQuerySelectorAll = discoveryRoot.querySelectorAll.bind(discoveryRoot);
+const realMatches = discoveryRoot.matches.bind(discoveryRoot);
+discoveryRoot.querySelectorAll = (selector) => { selectorTraversals += 1; withinLimit(selector); return realQuerySelectorAll(selector); };
+discoveryRoot.matches = (selector) => { withinLimit(selector); return realMatches(selector); };
Kineto.scan(discoveryRoot);
-assert.equal(Kineto.instanceCount, 100);
-assert.equal(selectorTraversals, 1, '100 native modules must share one selector traversal');
+assert.equal(Kineto.instanceCount, 102);
+assert.ok(Math.max(...selectorLengths) <= SELECTOR_LIMIT, 'every discovery selector stays within jsdom\'s 2048-character limit');
+assert.equal(selectorTraversals, 2, '100 native modules (a 2.3 KB selector) share two traversals, not 100');
+assert.deepEqual(created.filter((entry) => entry.startsWith('99:')), ['99:b99', '99:both'],
+ 'each module creates its elements in document order across split selectors');
+assert.deepEqual(created.filter((entry) => entry.startsWith('0:')), ['0:b0', '0:both'],
+ 'an element found by two selectors is created once per module');
Kineto.destroy();
for (let i = 0; i < 100; i += 1) Kineto.unregister(`bulkProbe${i}`);
diff --git a/tests/release-automation.mjs b/tests/release-automation.mjs
index 88b5268..32985fc 100644
--- a/tests/release-automation.mjs
+++ b/tests/release-automation.mjs
@@ -6,7 +6,7 @@ import { fileURLToPath } from 'node:url';
import { assertPinnedAction } from './workflow-action-pins.mjs';
import { ciVerdict, githubRepo, readCiVerdict, waitForCi } from '../scripts/ci-status.mjs';
import { parseArgs, parseLane, parseShard, selectSteps } from '../scripts/run-lane.mjs';
-import { annotation, reportFlaky } from '../scripts/gh-actions.mjs';
+import { annotation, expectedExitEnv, reportFlaky } from '../scripts/gh-actions.mjs';
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const read = (relative) => fs.readFileSync(path.join(root, relative), 'utf8');
@@ -449,6 +449,14 @@ const waitAt = shipReleaseScript.indexOf('await waitForCi(');
const tagAt = shipReleaseScript.indexOf("run('git', ['tag', '-a'");
assert.ok(pushAt > 0 && waitAt > pushAt && tagAt > waitAt, 'release:ship must push main, then wait for CI, then create the tag');
assert.match(shipReleaseScript, /verdict !== 'success'[\s\S]*?fail\(/, 'release:ship must stop before tagging unless CI succeeded');
+// The release workflow's lockfile audit runs after the tag; an advisory that
+// appears after CI then burns the version (v0.13.0). release:ship runs the same
+// audit script last, before the tag, and stops if it fails.
+const auditAt = shipReleaseScript.indexOf("'scripts/audit-lockfiles.mjs'");
+assert.ok(auditAt > waitAt && auditAt < tagAt, 'release:ship must audit the lockfiles after CI and before creating the tag');
+assert.match(shipReleaseScript, /audit\.status !== 0\) \{\s*fail\(`the lockfile audit failed[\s\S]*?No tag was created/,
+ 'a failed audit must stop release:ship before the tag, saying the version is still available');
+assert.match(workflow, /npm run audit:lockfiles/, 'the release workflow audits the lockfiles release:ship audits');
assert.doesNotMatch(shipReleaseScript, /console\.log\([^)]*get-url/, 'the origin URL can hold credentials and must never be printed');
const ciStatusScript = read('scripts/ci-status.mjs');
assert.doesNotMatch(ciStatusScript, /process\.env/, 'the CI check never reads the environment; a caller passes a token explicitly');
@@ -521,6 +529,12 @@ for (const [name, source, steps] of [['ci.yml', ciWorkflow, ['Run Node tests', '
assert.match(inCi[0], /%0A::warning::injected/, 'text that looks like a workflow command stays escaped inside the annotation');
const local = run({ GITHUB_ACTIONS: '' }).split('\n').filter((line) => line.startsWith('::error title='));
assert.equal(local.length, 0, 'outside GitHub Actions the hook stays silent');
+ // A child whose non-zero exit the caller judges (attw) must not paint a green
+ // job with a failure annotation.
+ const expected = run(expectedExitEnv({ GITHUB_ACTIONS: 'true' })).split('\n').filter((line) => line.startsWith('::error title='));
+ assert.equal(expected.length, 0, 'a child started with expectedExitEnv() stays silent in CI');
+ const packageLint = fs.readFileSync(path.join(root, 'scripts/check-package-lint.mjs'), 'utf8');
+ assert.match(packageLint, /execFileSync\(bin\('attw'\), \[[^\]]*\], \{[^}]*env: expectedExitEnv\(\)/, 'attw runs with expectedExitEnv(): its exit 1 is judged from the JSON report');
}
console.log('release-automation OK — gated least-privilege publish, verified tarball reuse, rerun safety, pinned actions, sharded engine CI from one lane list, flaky-test reporting, the green-CI release gate, verify:push, CDN failure handling, the mcp-v release path, and release:ship waiting for green CI before tagging.');
diff --git a/tests/reveal-variant-browser.mjs b/tests/reveal-variant-browser.mjs
index 116b56b..0bc8c08 100644
--- a/tests/reveal-variant-browser.mjs
+++ b/tests/reveal-variant-browser.mjs
@@ -135,14 +135,18 @@ try {
window.ScrollTrigger?.refresh();
});
// Sample actual intermediate values via RAF; a jump from hidden to final
- // state must not satisfy these transition checks.
+ // state must not satisfy these transition checks. Sample for at least
+ // 450ms and on until every entrance has completed (at most 2.5s): a slow
+ // runner that starts an entrance late must not read as "no motion" or
+ // "never finished" (WebKit on CI started some a frame or two late).
const samples = await page.evaluate(async () => {
const { records, read } = window.__revealTest;
const seen = new Set();
const clips = {};
const premature = new Set();
- const until = window.performance.now() + 450;
- while (window.performance.now() < until) {
+ const started = window.performance.now();
+ const pending = () => records.some(({ preset, completed }) => preset !== 'class' && !completed);
+ while (window.performance.now() - started < 450 || (pending() && window.performance.now() - started < 2500)) {
for (const { preset, element, completed } of records) {
const current = read(element);
if (preset === 'class') continue;
@@ -195,6 +199,50 @@ try {
});
check(resumed.completed === 2 && resumed.opacity === '1', 'gsap/fade-up: resume must finish the paused replay once');
}
+ if (engine === 'native') {
+ // One long frame right after a native mask/wipe starts (a busy page load)
+ // must slow the entrance, not skip it: the old clock added the whole gap
+ // and a 300ms frame finished a .22s reveal with no motion on screen.
+ const hitch = await page.evaluate(async () => {
+ const { core } = window.__revealTest;
+ const holder = document.createElement('div');
+ holder.style.cssText = 'position:absolute;top:1600px;left:0;display:flex;gap:20px';
+ document.body.append(holder);
+ const records = ['mask', 'wipe'].map((preset) => {
+ const element = document.createElement('div');
+ element.className = 'probe';
+ element.textContent = preset;
+ holder.append(element);
+ return { preset, element, frames: 0, instance: core.create('reveal', element, { preset, duration: .22, ease: 'linear' }) };
+ });
+ await new Promise((resolve) => window.requestAnimationFrame(() => window.requestAnimationFrame(resolve)));
+ holder.style.top = '470px';
+ // The entrance starts when `is-inview` lands; its clock reads its first
+ // frame time on the next frame. Two 300ms frames from that moment put
+ // one long gap between the clock's first and second frames.
+ let longFrames = 0;
+ const until = window.performance.now() + 1200;
+ while (window.performance.now() < until) {
+ for (const record of records) {
+ const clip = window.getComputedStyle(record.element).clipPath;
+ if (clip !== 'none' && /[1-9]/.test(clip) && !clip.includes('100%')) record.frames += 1;
+ }
+ await new Promise(window.requestAnimationFrame);
+ if (longFrames < 2 && records.every(({ element }) => element.classList.contains('is-inview'))) {
+ longFrames += 1;
+ const start = window.performance.now();
+ while (window.performance.now() - start < 300) { /* a 300ms frame */ }
+ }
+ }
+ records.forEach(({ instance }) => instance.destroy());
+ holder.remove();
+ return { blocked: longFrames === 2, frames: Object.fromEntries(records.map(({ preset, frames }) => [preset, frames])) };
+ });
+ check(hitch.blocked, 'native hitch: the entrances must start so the long frames land inside them');
+ for (const [preset, frames] of Object.entries(hitch.frames)) {
+ check(frames >= 2, `native/${preset}: a 300ms frame after the start must not skip the motion (${frames} frame(s) in motion)`);
+ }
+ }
// Replay first, then destroy while work is still queued/running. Inspect
// after the original duration so stale RAFs/tweens cannot hide behind an
// immediate restoration assertion.
diff --git a/tests/slider-variant-browser.mjs b/tests/slider-variant-browser.mjs
index d07aaf3..6302161 100644
--- a/tests/slider-variant-browser.mjs
+++ b/tests/slider-variant-browser.mjs
@@ -67,6 +67,43 @@ try {
await page.evaluate(async (useSource) => {
window.__sourceSlider = (await import('/src/modules/slider.js')).default;
if (useSource) window.Kineto.register('slider', window.__sourceSlider);
+ // Waits count rendered frames as well as time, so a runner that stops
+ // rendering for a few seconds does not fail a wait that had no frames to
+ // work with: a wait gives up only once its time is up AND enough frames
+ // have run (half of 60 Hz by default); `hardMs` still ends a wait on a
+ // page that never renders. A failure says how many frames ran, the longest
+ // gap, how often the requestAnimationFrame timestamp repeated, and the
+ // page's errors — the evidence two WebKit CI failures lacked (a dissolve
+ // stopped at 98.1%, the same value both times; see src/modules/slider.js).
+ window.__pageErrors = [];
+ window.addEventListener('error', (event) => window.__pageErrors.push(String(event.message)));
+ window.addEventListener('unhandledrejection', (event) => window.__pageErrors.push(String(event.reason)));
+ window.__frameDeadline = (ms, { minFrames = Math.round(ms / 32), hardMs = ms * 4 } = {}) => {
+ const started = window.performance.now();
+ let frames = 0;
+ let last = started;
+ let longestGap = 0;
+ let lastStamp = null;
+ let repeatedStamps = 0;
+ return {
+ frame(stamp) {
+ const now = window.performance.now();
+ frames += 1;
+ longestGap = Math.max(longestGap, now - last);
+ last = now;
+ if (stamp === lastStamp) repeatedStamps += 1;
+ lastStamp = stamp;
+ },
+ expired(now = window.performance.now()) {
+ const elapsed = now - started;
+ return (elapsed > ms && frames >= minFrames) || elapsed > hardMs;
+ },
+ describe() {
+ const errors = window.__pageErrors.length ? `, page errors: ${window.__pageErrors.slice(0, 3).join(' | ')}` : '';
+ return `${frames} frames in ${Math.round(last - started)} ms, longest gap ${Math.round(longestGap)} ms, repeated rAF timestamps ${repeatedStamps}${errors}`;
+ }
+ };
+ };
window.__makeSliderFixture = (effect, source = false, overrides = {}) => {
const host = document.createElement('section');
host.className = 'fixture authored-slider';
@@ -118,28 +155,30 @@ try {
};
});
const waitForLanding = (index) => new Promise((resolve, reject) => {
- const started = window.performance.now();
+ const deadline = window.__frameDeadline(4000);
const reference = [...new window.DOMMatrixReadOnly(initial).toFloat64Array()];
- const tick = () => {
+ const tick = (stamp) => {
+ deadline.frame(stamp);
const state = capture();
const completeProgress = !Number.isFinite(state[index].progress) || state[index].progress === 1;
if (api.index === index && completeProgress && state[index].matrix.every((value, offset) => Math.abs(value - reference[offset]) < 0.001)) resolve(state);
- else if (window.performance.now() - started > 4000) reject(new Error(`${effect} did not settle at ${index}: ${JSON.stringify(state)}`));
+ else if (deadline.expired()) reject(new Error(`${effect} did not settle at ${index} (${deadline.describe()}): ${JSON.stringify(state)}`));
else window.requestAnimationFrame(tick);
};
window.requestAnimationFrame(tick);
});
const transition = (method) => new Promise((resolve, reject) => {
const beforeMotion = capture();
- const started = window.performance.now();
+ const deadline = window.__frameDeadline(1500);
api[method]();
- const tick = () => {
+ const tick = (stamp) => {
+ deadline.frame(stamp);
const state = capture();
const changed = state[0].matrix.some((value, offset) => Math.abs(value - beforeMotion[0].matrix[offset]) > 0.01)
|| Math.abs(state[0].opacity - beforeMotion[0].opacity) > 0.01
|| state[1].clip !== beforeMotion[1].clip;
if (changed && (effect === 'slide' || effect === 'coverflow' || effect === 'radial' || (state[1].progress > 0.03 && state[1].progress < 0.97))) resolve(state);
- else if (window.performance.now() - started > 1500) reject(new Error(`${effect} did not expose an intermediate animation frame`));
+ else if (deadline.expired()) reject(new Error(`${effect} did not expose an intermediate animation frame (${deadline.describe()})`));
else window.requestAnimationFrame(tick);
};
window.requestAnimationFrame(tick);