diff --git a/app/assets/builds/tailwind.css b/app/assets/builds/tailwind.css index 12a814a9..2d989f0b 100644 --- a/app/assets/builds/tailwind.css +++ b/app/assets/builds/tailwind.css @@ -2,5 +2,5 @@ --timeline-col-end,minmax(0,1fr) );grid-template-rows:var(--timeline-row-start,minmax(0,1fr)) auto var( --timeline-row-end,minmax(0,1fr) - );position:relative}.timeline>li>hr{border-width:0;width:100%}:where(.timeline>li>hr):first-child{grid-column-start:1;grid-row-start:2}:where(.timeline>li>hr):last-child{grid-column-end:none;grid-column-start:3;grid-row-end:auto;grid-row-start:2}.timeline-start{align-self:flex-end;grid-column-end:4;grid-column-start:1;grid-row-end:2;grid-row-start:1;justify-self:center;margin:.25rem}.timeline-middle{grid-column-start:2;grid-row-start:2}.timeline-end{align-self:flex-start;grid-column-end:4;grid-column-start:1;grid-row-end:4;grid-row-start:3;justify-self:center;margin:.25rem}.toast{display:flex;flex-direction:column;gap:.5rem;min-width:-moz-fit-content;min-width:fit-content;padding:1rem;position:fixed;white-space:nowrap}.toggle{flex-shrink:0;--tglbg:var(--fallback-b1,oklch(var(--b1)/1));--handleoffset:1.5rem;--handleoffsetcalculator:calc(var(--handleoffset)*-1);--togglehandleborder:0 0;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:currentColor;border-color:currentColor;border-radius:var(--rounded-badge,1.9rem);border-width:1px;box-shadow:var(--handleoffsetcalculator) 0 0 2px var(--tglbg) inset,0 0 0 2px var(--tglbg) inset,var(--togglehandleborder);color:var(--fallback-bc,oklch(var(--bc)/.5));cursor:pointer;height:1.5rem;transition:background,box-shadow var(--animation-input,.2s) ease-out;width:3rem}.alert-info{border-color:var(--fallback-in,oklch(var(--in)/.2));--tw-text-opacity:1;color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)));--alert-bg:var(--fallback-in,oklch(var(--in)/1));--alert-bg-mix:var(--fallback-b1,oklch(var(--b1)/1))}.alert-success{border-color:var(--fallback-su,oklch(var(--su)/.2));--tw-text-opacity:1;color:var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity)));--alert-bg:var(--fallback-su,oklch(var(--su)/1));--alert-bg-mix:var(--fallback-b1,oklch(var(--b1)/1))}.alert-warning{border-color:var(--fallback-wa,oklch(var(--wa)/.2));--tw-text-opacity:1;color:var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity)));--alert-bg:var(--fallback-wa,oklch(var(--wa)/1));--alert-bg-mix:var(--fallback-b1,oklch(var(--b1)/1))}.alert-error{border-color:var(--fallback-er,oklch(var(--er)/.2));--tw-text-opacity:1;color:var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)));--alert-bg:var(--fallback-er,oklch(var(--er)/1));--alert-bg-mix:var(--fallback-b1,oklch(var(--b1)/1))}.avatar-group :where(.avatar){border-radius:9999px;border-width:4px;overflow:hidden;--tw-border-opacity:1;border-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-border-opacity)))}.badge-neutral{background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));border-color:var(--fallback-n,oklch(var(--n)/var(--tw-border-opacity)));color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)))}.badge-neutral,.badge-primary{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1}.badge-primary{background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)));color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}.badge-secondary{background-color:var(--fallback-s,oklch(var(--s)/var(--tw-bg-opacity)));border-color:var(--fallback-s,oklch(var(--s)/var(--tw-border-opacity)));color:var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity)))}.badge-accent,.badge-secondary{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1}.badge-accent{background-color:var(--fallback-a,oklch(var(--a)/var(--tw-bg-opacity)));border-color:var(--fallback-a,oklch(var(--a)/var(--tw-border-opacity)));color:var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity)))}.badge-success{background-color:var(--fallback-su,oklch(var(--su)/var(--tw-bg-opacity)));color:var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity)))}.badge-success,.badge-warning{border-color:transparent;--tw-bg-opacity:1;--tw-text-opacity:1}.badge-warning{background-color:var(--fallback-wa,oklch(var(--wa)/var(--tw-bg-opacity)));color:var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity)))}.badge-error{border-color:transparent;--tw-bg-opacity:1;background-color:var(--fallback-er,oklch(var(--er)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)))}.badge-ghost{--tw-border-opacity:1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}.badge-outline{border-color:currentColor;--tw-border-opacity:0.5;background-color:transparent;color:currentColor}.badge-outline.badge-neutral{--tw-text-opacity:1;color:var(--fallback-n,oklch(var(--n)/var(--tw-text-opacity)))}.badge-outline.badge-primary{--tw-text-opacity:1;color:var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity)))}.badge-outline.badge-secondary{--tw-text-opacity:1;color:var(--fallback-s,oklch(var(--s)/var(--tw-text-opacity)))}.badge-outline.badge-accent{--tw-text-opacity:1;color:var(--fallback-a,oklch(var(--a)/var(--tw-text-opacity)))}.badge-outline.badge-info{--tw-text-opacity:1;color:var(--fallback-in,oklch(var(--in)/var(--tw-text-opacity)))}.badge-outline.badge-success{--tw-text-opacity:1;color:var(--fallback-su,oklch(var(--su)/var(--tw-text-opacity)))}.badge-outline.badge-warning{--tw-text-opacity:1;color:var(--fallback-wa,oklch(var(--wa)/var(--tw-text-opacity)))}.badge-outline.badge-error{--tw-text-opacity:1;color:var(--fallback-er,oklch(var(--er)/var(--tw-text-opacity)))}.btm-nav>:where(.active){border-top-width:2px;--tw-bg-opacity:1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)))}.btm-nav>.disabled,.btm-nav>[disabled]{pointer-events:none;--tw-border-opacity:0;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-bg-opacity:0.1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-text-opacity:0.2}.btm-nav>* .label{font-size:1rem;line-height:1.5rem}.btn:active:focus,.btn:active:hover{animation:button-pop 0s ease-out;transform:scale(var(--btn-focus-scale,.97))}@supports not (color:oklch(0 0 0)){.btn{background-color:var(--btn-color,var(--fallback-b2));border-color:var(--btn-color,var(--fallback-b2))}.btn-primary{--btn-color:var(--fallback-p)}.btn-neutral{--btn-color:var(--fallback-n)}.btn-info{--btn-color:var(--fallback-in)}.btn-success{--btn-color:var(--fallback-su)}.btn-warning{--btn-color:var(--fallback-wa)}.btn-error{--btn-color:var(--fallback-er)}}@supports (color:color-mix(in oklab,black,black)){.btn-active{background-color:color-mix(in oklab,oklch(var(--btn-color,var(--b3))/var(--tw-bg-opacity,1)) 90%,#000);border-color:color-mix(in oklab,oklch(var(--btn-color,var(--b3))/var(--tw-border-opacity,1)) 90%,#000)}.btn-outline.btn-primary.btn-active{background-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,#000)}.btn-outline.btn-secondary.btn-active{background-color:color-mix(in oklab,var(--fallback-s,oklch(var(--s)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-s,oklch(var(--s)/1)) 90%,#000)}.btn-outline.btn-accent.btn-active{background-color:color-mix(in oklab,var(--fallback-a,oklch(var(--a)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-a,oklch(var(--a)/1)) 90%,#000)}.btn-outline.btn-success.btn-active{background-color:color-mix(in oklab,var(--fallback-su,oklch(var(--su)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-su,oklch(var(--su)/1)) 90%,#000)}.btn-outline.btn-info.btn-active{background-color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 90%,#000)}.btn-outline.btn-warning.btn-active{background-color:color-mix(in oklab,var(--fallback-wa,oklch(var(--wa)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-wa,oklch(var(--wa)/1)) 90%,#000)}.btn-outline.btn-error.btn-active{background-color:color-mix(in oklab,var(--fallback-er,oklch(var(--er)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-er,oklch(var(--er)/1)) 90%,#000)}}.btn:focus-visible{outline-offset:2px;outline-style:solid;outline-width:2px}.btn-primary{--tw-text-opacity:1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)));outline-color:var(--fallback-p,oklch(var(--p)/1))}@supports (color:oklch(0 0 0)){.btn-primary{--btn-color:var(--p)}.btn-neutral{--btn-color:var(--n)}.btn-info{--btn-color:var(--in)}.btn-success{--btn-color:var(--su)}.btn-warning{--btn-color:var(--wa)}.btn-error{--btn-color:var(--er)}}.btn-neutral{--tw-text-opacity:1;color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)));outline-color:var(--fallback-n,oklch(var(--n)/1))}.btn-info{--tw-text-opacity:1;color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)));outline-color:var(--fallback-in,oklch(var(--in)/1))}.btn-success{--tw-text-opacity:1;color:var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity)));outline-color:var(--fallback-su,oklch(var(--su)/1))}.btn-warning{--tw-text-opacity:1;color:var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity)));outline-color:var(--fallback-wa,oklch(var(--wa)/1))}.btn-error{--tw-text-opacity:1;color:var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)));outline-color:var(--fallback-er,oklch(var(--er)/1))}.btn.glass{--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);outline-color:currentColor}.btn.glass.btn-active{--glass-opacity:25%;--glass-border-opacity:15%}.btn-ghost{background-color:transparent;border-color:transparent;border-width:1px;color:currentColor;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);outline-color:currentColor}.btn-ghost.btn-active{background-color:var(--fallback-bc,oklch(var(--bc)/.2));border-color:transparent}.btn-link.btn-active{background-color:transparent;border-color:transparent;text-decoration-line:underline}.btn-outline{background-color:transparent;border-color:currentColor;--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.btn-outline.btn-active{--tw-border-opacity:1;border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-b1,oklch(var(--b1)/var(--tw-text-opacity)))}.btn-outline.btn-primary{--tw-text-opacity:1;color:var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity)))}.btn-outline.btn-primary.btn-active{--tw-text-opacity:1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}.btn-outline.btn-secondary{--tw-text-opacity:1;color:var(--fallback-s,oklch(var(--s)/var(--tw-text-opacity)))}.btn-outline.btn-secondary.btn-active{--tw-text-opacity:1;color:var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity)))}.btn-outline.btn-accent{--tw-text-opacity:1;color:var(--fallback-a,oklch(var(--a)/var(--tw-text-opacity)))}.btn-outline.btn-accent.btn-active{--tw-text-opacity:1;color:var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity)))}.btn-outline.btn-success{--tw-text-opacity:1;color:var(--fallback-su,oklch(var(--su)/var(--tw-text-opacity)))}.btn-outline.btn-success.btn-active{--tw-text-opacity:1;color:var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity)))}.btn-outline.btn-info{--tw-text-opacity:1;color:var(--fallback-in,oklch(var(--in)/var(--tw-text-opacity)))}.btn-outline.btn-info.btn-active{--tw-text-opacity:1;color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)))}.btn-outline.btn-warning{--tw-text-opacity:1;color:var(--fallback-wa,oklch(var(--wa)/var(--tw-text-opacity)))}.btn-outline.btn-warning.btn-active{--tw-text-opacity:1;color:var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity)))}.btn-outline.btn-error{--tw-text-opacity:1;color:var(--fallback-er,oklch(var(--er)/var(--tw-text-opacity)))}.btn-outline.btn-error.btn-active{--tw-text-opacity:1;color:var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)))}.btn.btn-disabled,.btn:disabled,.btn[disabled]{--tw-border-opacity:0;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-bg-opacity:0.2;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-text-opacity:0.2}.btn:is(input[type=checkbox]:checked),.btn:is(input[type=radio]:checked){--tw-border-opacity:1;border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}.btn:is(input[type=checkbox]:checked):focus-visible,.btn:is(input[type=radio]:checked):focus-visible{outline-color:var(--fallback-p,oklch(var(--p)/1))}@keyframes button-pop{0%{transform:scale(var(--btn-focus-scale,.98))}40%{transform:scale(1.02)}to{transform:scale(1)}}.card :where(figure:first-child){border-end-end-radius:unset;border-end-start-radius:unset;border-start-end-radius:inherit;border-start-start-radius:inherit;overflow:hidden}.card :where(figure:last-child){border-end-end-radius:inherit;border-end-start-radius:inherit;border-start-end-radius:unset;border-start-start-radius:unset;overflow:hidden}.card:focus-visible{outline:2px solid currentColor;outline-offset:2px}.card.bordered{border-width:1px;--tw-border-opacity:1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)))}.card.compact .card-body{font-size:.875rem;line-height:1.25rem;padding:1rem}.card-title{align-items:center;display:flex;font-size:1.25rem;font-weight:600;gap:.5rem;line-height:1.75rem}.card.image-full :where(figure){border-radius:inherit;overflow:hidden}.checkbox:focus{box-shadow:none}.checkbox:focus-visible{outline-color:var(--fallback-bc,oklch(var(--bc)/1));outline-offset:2px;outline-style:solid;outline-width:2px}.checkbox:checked,.checkbox[aria-checked=true],.checkbox[checked=true]{animation:checkmark var(--animation-input,.2s) ease-out;background-color:var(--chkbg);background-image:linear-gradient(-45deg,transparent 65%,var(--chkbg) 65.99%),linear-gradient(45deg,transparent 75%,var(--chkbg) 75.99%),linear-gradient(-45deg,var(--chkbg) 40%,transparent 40.99%),linear-gradient(45deg,var(--chkbg) 30%,var(--chkfg) 30.99%,var(--chkfg) 40%,transparent 40.99%),linear-gradient(-45deg,var(--chkfg) 50%,var(--chkbg) 50.99%);background-repeat:no-repeat}.checkbox:indeterminate{--tw-bg-opacity:1;animation:checkmark var(--animation-input,.2s) ease-out;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));background-image:linear-gradient(90deg,transparent 80%,var(--chkbg) 80%),linear-gradient(-90deg,transparent 80%,var(--chkbg) 80%),linear-gradient(0deg,var(--chkbg) 43%,var(--chkfg) 43%,var(--chkfg) 57%,var(--chkbg) 57%);background-repeat:no-repeat}.checkbox:disabled{border-color:transparent;cursor:not-allowed;--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));opacity:.2}@keyframes checkmark{0%{background-position-y:5px}50%{background-position-y:-2px}to{background-position-y:0}}details.collapse{width:100%}details.collapse summary{display:block;outline:2px solid transparent;outline-offset:2px;position:relative}details.collapse summary::-webkit-details-marker{display:none}.collapse:focus-visible{outline-color:var(--fallback-bc,oklch(var(--bc)/1));outline-offset:2px;outline-style:solid;outline-width:2px}.collapse:has(.collapse-title:focus-visible),.collapse:has(>input[type=checkbox]:focus-visible),.collapse:has(>input[type=radio]:focus-visible){outline-color:var(--fallback-bc,oklch(var(--bc)/1));outline-offset:2px;outline-style:solid;outline-width:2px}.collapse-arrow>.collapse-title:after{--tw-translate-y:-100%;--tw-rotate:45deg;box-shadow:2px 2px;content:"";top:1.9rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));transform-origin:75% 75%;transition-duration:.15s;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-timing-function:cubic-bezier(0,0,.2,1)}.collapse-arrow>.collapse-title:after,.collapse-plus>.collapse-title:after{display:block;height:.5rem;inset-inline-end:1.4rem;pointer-events:none;position:absolute;transition-property:all;width:.5rem}.collapse-plus>.collapse-title:after{content:"+";top:.9rem;transition-duration:.3s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-timing-function:cubic-bezier(0,0,.2,1)}.collapse:not(.collapse-open):not(.collapse-close)>.collapse-title,.collapse:not(.collapse-open):not(.collapse-close)>input[type=checkbox],.collapse:not(.collapse-open):not(.collapse-close)>input[type=radio]:not(:checked){cursor:pointer}.collapse:focus:not(.collapse-open):not(.collapse-close):not(.collapse[open])>.collapse-title{cursor:unset}.collapse-title{position:relative}:where(.collapse>input[type=checkbox]),:where(.collapse>input[type=radio]){z-index:1}.collapse-title,:where(.collapse>input[type=checkbox]),:where(.collapse>input[type=radio]){min-height:3.75rem;padding:1rem;padding-inline-end:3rem;transition:background-color .2s ease-out;width:100%}.collapse-open>:where(.collapse-content),.collapse:focus:not(.collapse-close)>:where(.collapse-content),.collapse:not(.collapse-close)>:where(input[type=checkbox]:checked~.collapse-content),.collapse:not(.collapse-close)>:where(input[type=radio]:checked~.collapse-content),.collapse[open]>:where(.collapse-content){padding-bottom:1rem;transition:padding .2s ease-out,background-color .2s ease-out}.collapse-arrow:focus:not(.collapse-close)>.collapse-title:after,.collapse-arrow:not(.collapse-close)>input[type=checkbox]:checked~.collapse-title:after,.collapse-arrow:not(.collapse-close)>input[type=radio]:checked~.collapse-title:after,.collapse-open.collapse-arrow>.collapse-title:after,.collapse[open].collapse-arrow>.collapse-title:after{--tw-translate-y:-50%;--tw-rotate:225deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.collapse-open.collapse-plus>.collapse-title:after,.collapse-plus:focus:not(.collapse-close)>.collapse-title:after,.collapse-plus:not(.collapse-close)>input[type=checkbox]:checked~.collapse-title:after,.collapse-plus:not(.collapse-close)>input[type=radio]:checked~.collapse-title:after,.collapse[open].collapse-plus>.collapse-title:after{content:"−"}.divider:not(:empty){gap:1rem}.drawer-toggle:focus-visible~.drawer-content label.drawer-button{outline-offset:2px;outline-style:solid;outline-width:2px}.dropdown.dropdown-open .dropdown-content,.dropdown:focus .dropdown-content,.dropdown:focus-within .dropdown-content{--tw-scale-x:1;--tw-scale-y:1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.file-input-bordered{--tw-border-opacity:0.2}.file-input:focus{outline-color:var(--fallback-bc,oklch(var(--bc)/.2));outline-offset:2px;outline-style:solid;outline-width:2px}.file-input-disabled,.file-input[disabled]{cursor:not-allowed;--tw-border-opacity:1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));--tw-text-opacity:0.2}.file-input-disabled::-moz-placeholder,.file-input[disabled]::-moz-placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.file-input-disabled::placeholder,.file-input[disabled]::placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.file-input-disabled::file-selector-button,.file-input[disabled]::file-selector-button{--tw-border-opacity:0;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-bg-opacity:0.2;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-text-opacity:0.2}.footer-title{font-weight:700;margin-bottom:.5rem;opacity:.6;text-transform:uppercase}.label-text{font-size:.875rem;line-height:1.25rem}.label-text,.label-text-alt{--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}.label-text-alt{font-size:.75rem;line-height:1rem}.\!input input{--tw-bg-opacity:1!important;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)))!important;background-color:transparent!important}.input input{--tw-bg-opacity:1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));background-color:transparent}.\!input input:focus{outline:2px solid transparent!important;outline-offset:2px!important}.input input:focus{outline:2px solid transparent;outline-offset:2px}.\!input[list]::-webkit-calendar-picker-indicator{line-height:1em!important}.input[list]::-webkit-calendar-picker-indicator{line-height:1em}.input-bordered{border-color:var(--fallback-bc,oklch(var(--bc)/.2))}.input:focus,.input:focus-within{border-color:var(--fallback-bc,oklch(var(--bc)/.2));box-shadow:none;outline-color:var(--fallback-bc,oklch(var(--bc)/.2));outline-offset:2px;outline-style:solid;outline-width:2px}.\!input:focus,.\!input:focus-within{border-color:var(--fallback-bc,oklch(var(--bc)/.2))!important;box-shadow:none!important;outline-color:var(--fallback-bc,oklch(var(--bc)/.2))!important;outline-offset:2px!important;outline-style:solid!important;outline-width:2px!important}.input-primary{--tw-border-opacity:1;border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)))}.input-primary:focus,.input-primary:focus-within{--tw-border-opacity:1;border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)));outline-color:var(--fallback-p,oklch(var(--p)/1))}.input-error{--tw-border-opacity:1;border-color:var(--fallback-er,oklch(var(--er)/var(--tw-border-opacity)))}.input-error:focus,.input-error:focus-within{--tw-border-opacity:1;border-color:var(--fallback-er,oklch(var(--er)/var(--tw-border-opacity)));outline-color:var(--fallback-er,oklch(var(--er)/1))}.input-disabled,.input:disabled,.input[disabled]{cursor:not-allowed;--tw-border-opacity:1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));color:var(--fallback-bc,oklch(var(--bc)/.4))}.\!input:disabled,.\!input[disabled]{cursor:not-allowed!important;--tw-border-opacity:1!important;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)))!important;--tw-bg-opacity:1!important;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)))!important;color:var(--fallback-bc,oklch(var(--bc)/.4))!important}.input-disabled::-moz-placeholder,.input:disabled::-moz-placeholder,.input[disabled]::-moz-placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.input-disabled::placeholder,.input:disabled::placeholder,.input[disabled]::placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.\!input:disabled::-moz-placeholder,.\!input[disabled]::-moz-placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)))!important;--tw-placeholder-opacity:0.2!important}.\!input:disabled::placeholder,.\!input[disabled]::placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)))!important;--tw-placeholder-opacity:0.2!important}.\!input::-webkit-date-and-time-value{text-align:inherit!important}.input::-webkit-date-and-time-value{text-align:inherit}.join>:where(:not(:first-child)){margin-bottom:0;margin-top:0;margin-inline-start:-1px}.join-item:focus{isolation:isolate}.link-primary{--tw-text-opacity:1;color:var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity)))}@supports (color:color-mix(in oklab,black,black)){@media (hover:hover){.link-primary:hover{color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 80%,#000)}.link-info:hover{color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 80%,#000)}}}.link-info{--tw-text-opacity:1;color:var(--fallback-in,oklch(var(--in)/var(--tw-text-opacity)))}.link:focus{outline:2px solid transparent;outline-offset:2px}.link:focus-visible{outline:2px solid currentColor;outline-offset:2px}.loading{aspect-ratio:1/1;background-color:currentColor;display:inline-block;-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:100%;mask-size:100%;pointer-events:none;width:1.5rem}.loading,.loading-spinner{-webkit-mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' stroke='%23000'%3E%3Cstyle%3E@keyframes spinner_zKoa{to{transform:rotate(360deg)}}@keyframes spinner_YpZS{0%25{stroke-dasharray:0 150;stroke-dashoffset:0}47.5%25{stroke-dasharray:42 150;stroke-dashoffset:-16}95%25,to{stroke-dasharray:42 150;stroke-dashoffset:-59}}%3C/style%3E%3Cg style='transform-origin:center;animation:spinner_zKoa 2s linear infinite'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' class='spinner_V8m1' style='stroke-linecap:round;animation:spinner_YpZS 1.5s ease-out infinite'/%3E%3C/g%3E%3C/svg%3E");mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' stroke='%23000'%3E%3Cstyle%3E@keyframes spinner_zKoa{to{transform:rotate(360deg)}}@keyframes spinner_YpZS{0%25{stroke-dasharray:0 150;stroke-dashoffset:0}47.5%25{stroke-dasharray:42 150;stroke-dashoffset:-16}95%25,to{stroke-dasharray:42 150;stroke-dashoffset:-59}}%3C/style%3E%3Cg style='transform-origin:center;animation:spinner_zKoa 2s linear infinite'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' class='spinner_V8m1' style='stroke-linecap:round;animation:spinner_YpZS 1.5s ease-out infinite'/%3E%3C/g%3E%3C/svg%3E")}.loading-dots{-webkit-mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cstyle%3E@keyframes spinner_8HQG{0%25,57.14%25{animation-timing-function:cubic-bezier(.33,.66,.66,1);transform:translate(0)}28.57%25{animation-timing-function:cubic-bezier(.33,0,.66,.33);transform:translateY(-6px)}to{transform:translate(0)}}.spinner_qM83{animation:spinner_8HQG 1.05s infinite}%3C/style%3E%3Ccircle cx='4' cy='12' r='3' class='spinner_qM83'/%3E%3Ccircle cx='12' cy='12' r='3' class='spinner_qM83' style='animation-delay:.1s'/%3E%3Ccircle cx='20' cy='12' r='3' class='spinner_qM83' style='animation-delay:.2s'/%3E%3C/svg%3E");mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cstyle%3E@keyframes spinner_8HQG{0%25,57.14%25{animation-timing-function:cubic-bezier(.33,.66,.66,1);transform:translate(0)}28.57%25{animation-timing-function:cubic-bezier(.33,0,.66,.33);transform:translateY(-6px)}to{transform:translate(0)}}.spinner_qM83{animation:spinner_8HQG 1.05s infinite}%3C/style%3E%3Ccircle cx='4' cy='12' r='3' class='spinner_qM83'/%3E%3Ccircle cx='12' cy='12' r='3' class='spinner_qM83' style='animation-delay:.1s'/%3E%3Ccircle cx='20' cy='12' r='3' class='spinner_qM83' style='animation-delay:.2s'/%3E%3C/svg%3E")}.loading-sm{width:1.25rem}.loading-md{width:1.5rem}.loading-lg{width:2.5rem}:where(.menu li:empty){--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));height:1px;margin:.5rem 1rem;opacity:.1}.menu :where(li ul):before{bottom:.75rem;inset-inline-start:0;position:absolute;top:.75rem;width:1px;--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));content:"";opacity:.1}.menu :where(li:not(.menu-title)>:not(ul,details,.menu-title,.btn)),.menu :where(li:not(.menu-title)>details>summary:not(.menu-title)){border-radius:var(--rounded-btn,.5rem);padding:.5rem 1rem;text-align:start;text-wrap:balance;transition-duration:.2s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-timing-function:cubic-bezier(0,0,.2,1)}:where(.menu li:not(.menu-title,.disabled)>:not(ul,details,.menu-title)):is(summary):not(.active,.btn):focus-visible,:where(.menu li:not(.menu-title,.disabled)>:not(ul,details,.menu-title)):not(summary,.active,.btn).focus,:where(.menu li:not(.menu-title,.disabled)>:not(ul,details,.menu-title)):not(summary,.active,.btn):focus,:where(.menu li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):is(summary):not(.active,.btn):focus-visible,:where(.menu li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(summary,.active,.btn).focus,:where(.menu li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(summary,.active,.btn):focus{background-color:var(--fallback-bc,oklch(var(--bc)/.1));cursor:pointer;--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));outline:2px solid transparent;outline-offset:2px}.menu li>:not(ul,.menu-title,details,.btn).active,.menu li>:not(ul,.menu-title,details,.btn):active,.menu li>details>summary:active{--tw-bg-opacity:1;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)))}.menu :where(li>details>summary)::-webkit-details-marker{display:none}.menu :where(li>.menu-dropdown-toggle):after,.menu :where(li>details>summary):after{box-shadow:2px 2px;content:"";display:block;height:.5rem;justify-self:end;margin-top:-.5rem;pointer-events:none;transform:rotate(45deg);transform-origin:75% 75%;transition-duration:.3s;transition-property:transform,margin-top;transition-timing-function:cubic-bezier(.4,0,.2,1);width:.5rem}.menu :where(li>.menu-dropdown-toggle.menu-dropdown-show):after,.menu :where(li>details[open]>summary):after{margin-top:0;transform:rotate(225deg)}.mockup-phone .camera{background:#000;border-bottom-left-radius:17px;border-bottom-right-radius:17px;height:25px;left:0;margin:0 auto;position:relative;top:0;width:150px;z-index:11}.mockup-phone .camera:before{background-color:#0c0b0e;border-radius:5px;content:"";height:4px;left:50%;position:absolute;top:35%;transform:translate(-50%,-50%);width:50px}.mockup-phone .camera:after{background-color:#0f0b25;border-radius:5px;content:"";height:8px;left:70%;position:absolute;top:20%;width:8px}.mockup-phone .display{border-radius:40px;margin-top:-25px;overflow:hidden}.mockup-browser .mockup-browser-toolbar .\!input{display:block!important;height:1.75rem!important;margin-left:auto!important;margin-right:auto!important;overflow:hidden!important;position:relative!important;text-overflow:ellipsis!important;white-space:nowrap!important;width:24rem!important;--tw-bg-opacity:1!important;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)))!important;direction:ltr!important;padding-left:2rem!important}.mockup-browser .mockup-browser-toolbar .input{display:block;height:1.75rem;margin-left:auto;margin-right:auto;overflow:hidden;position:relative;text-overflow:ellipsis;white-space:nowrap;width:24rem;--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));direction:ltr;padding-left:2rem}.mockup-browser .mockup-browser-toolbar .\!input:before{aspect-ratio:1/1!important;content:""!important;height:.75rem!important;left:.5rem!important;position:absolute!important;top:50%!important;--tw-translate-y:-50%!important;border-color:currentColor!important;border-radius:9999px!important;border-width:2px!important;opacity:.6!important;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))!important}.mockup-browser .mockup-browser-toolbar .input:before{aspect-ratio:1/1;content:"";height:.75rem;left:.5rem;position:absolute;top:50%;--tw-translate-y:-50%;border-color:currentColor;border-radius:9999px;border-width:2px;opacity:.6;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.mockup-browser .mockup-browser-toolbar .\!input:after{content:""!important;height:.5rem!important;left:1.25rem!important;position:absolute!important;top:50%!important;--tw-translate-y:25%!important;--tw-rotate:-45deg!important;border-color:currentColor!important;border-radius:9999px!important;border-width:1px!important;opacity:.6!important;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))!important}.mockup-browser .mockup-browser-toolbar .input:after{content:"";height:.5rem;left:1.25rem;position:absolute;top:50%;--tw-translate-y:25%;--tw-rotate:-45deg;border-color:currentColor;border-radius:9999px;border-width:1px;opacity:.6;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.modal::backdrop,.modal:not(dialog:not(.modal-open)){animation:modal-pop .2s ease-out;background-color:#0006}.modal-backdrop{align-self:stretch;color:transparent;display:grid;grid-column-start:1;grid-row-start:1;justify-self:stretch;z-index:-1}.modal-open .modal-box,.modal-toggle:checked+.modal .modal-box,.modal:target .modal-box,.modal[open] .modal-box{--tw-translate-y:0px;--tw-scale-x:1;--tw-scale-y:1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.modal-action>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.5rem*var(--tw-space-x-reverse))}@keyframes modal-pop{0%{opacity:0}}.progress::-moz-progress-bar{border-radius:var(--rounded-box,1rem);--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)))}.progress-primary::-moz-progress-bar{border-radius:var(--rounded-box,1rem);--tw-bg-opacity:1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)))}.progress-secondary::-moz-progress-bar{border-radius:var(--rounded-box,1rem);--tw-bg-opacity:1;background-color:var(--fallback-s,oklch(var(--s)/var(--tw-bg-opacity)))}.progress-accent::-moz-progress-bar{border-radius:var(--rounded-box,1rem);--tw-bg-opacity:1;background-color:var(--fallback-a,oklch(var(--a)/var(--tw-bg-opacity)))}.progress-info::-moz-progress-bar{border-radius:var(--rounded-box,1rem);--tw-bg-opacity:1;background-color:var(--fallback-in,oklch(var(--in)/var(--tw-bg-opacity)))}.progress-success::-moz-progress-bar{border-radius:var(--rounded-box,1rem);--tw-bg-opacity:1;background-color:var(--fallback-su,oklch(var(--su)/var(--tw-bg-opacity)))}.progress-warning::-moz-progress-bar{border-radius:var(--rounded-box,1rem);--tw-bg-opacity:1;background-color:var(--fallback-wa,oklch(var(--wa)/var(--tw-bg-opacity)))}.progress:indeterminate{--progress-color:var(--fallback-bc,oklch(var(--bc)/1));animation:progress-loading 5s ease-in-out infinite;background-image:repeating-linear-gradient(90deg,var(--progress-color) -1%,var(--progress-color) 10%,transparent 10%,transparent 90%);background-position-x:15%;background-size:200%}.progress-primary:indeterminate{--progress-color:var(--fallback-p,oklch(var(--p)/1))}.progress-secondary:indeterminate{--progress-color:var(--fallback-s,oklch(var(--s)/1))}.progress-accent:indeterminate{--progress-color:var(--fallback-a,oklch(var(--a)/1))}.progress-info:indeterminate{--progress-color:var(--fallback-in,oklch(var(--in)/1))}.progress-success:indeterminate{--progress-color:var(--fallback-su,oklch(var(--su)/1))}.progress-warning:indeterminate{--progress-color:var(--fallback-wa,oklch(var(--wa)/1))}.progress::-webkit-progress-bar{background-color:transparent;border-radius:var(--rounded-box,1rem)}.progress::-webkit-progress-value{border-radius:var(--rounded-box,1rem);--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)))}.progress-primary::-webkit-progress-value{--tw-bg-opacity:1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)))}.progress-secondary::-webkit-progress-value{--tw-bg-opacity:1;background-color:var(--fallback-s,oklch(var(--s)/var(--tw-bg-opacity)))}.progress-accent::-webkit-progress-value{--tw-bg-opacity:1;background-color:var(--fallback-a,oklch(var(--a)/var(--tw-bg-opacity)))}.progress-info::-webkit-progress-value{--tw-bg-opacity:1;background-color:var(--fallback-in,oklch(var(--in)/var(--tw-bg-opacity)))}.progress-success::-webkit-progress-value{--tw-bg-opacity:1;background-color:var(--fallback-su,oklch(var(--su)/var(--tw-bg-opacity)))}.progress-warning::-webkit-progress-value{--tw-bg-opacity:1;background-color:var(--fallback-wa,oklch(var(--wa)/var(--tw-bg-opacity)))}.progress:indeterminate::-moz-progress-bar{animation:progress-loading 5s ease-in-out infinite;background-color:transparent;background-image:repeating-linear-gradient(90deg,var(--progress-color) -1%,var(--progress-color) 10%,transparent 10%,transparent 90%);background-position-x:15%;background-size:200%}@keyframes progress-loading{50%{background-position-x:-115%}}.radio:focus{box-shadow:none}.radio:focus-visible{outline-color:var(--fallback-bc,oklch(var(--bc)/1));outline-offset:2px;outline-style:solid;outline-width:2px}.radio:checked,.radio[aria-checked=true]{--tw-bg-opacity:1;animation:radiomark var(--animation-input,.2s) ease-out;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));background-image:none;box-shadow:0 0 0 4px var(--fallback-b1,oklch(var(--b1)/1)) inset,0 0 0 4px var(--fallback-b1,oklch(var(--b1)/1)) inset}.radio-primary{--chkbg:var(--p);--tw-border-opacity:1;border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)))}.radio-primary:focus-visible{outline-color:var(--fallback-p,oklch(var(--p)/1))}.radio-primary:checked,.radio-primary[aria-checked=true]{--tw-border-opacity:1;border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}.radio:disabled{cursor:not-allowed;opacity:.2}@keyframes radiomark{0%{box-shadow:0 0 0 12px var(--fallback-b1,oklch(var(--b1)/1)) inset,0 0 0 12px var(--fallback-b1,oklch(var(--b1)/1)) inset}50%{box-shadow:0 0 0 3px var(--fallback-b1,oklch(var(--b1)/1)) inset,0 0 0 3px var(--fallback-b1,oklch(var(--b1)/1)) inset}to{box-shadow:0 0 0 4px var(--fallback-b1,oklch(var(--b1)/1)) inset,0 0 0 4px var(--fallback-b1,oklch(var(--b1)/1)) inset}}.range:focus-visible::-webkit-slider-thumb{--focus-shadow:0 0 0 6px var(--fallback-b1,oklch(var(--b1)/1)) inset,0 0 0 2rem var(--range-shdw) inset}.range:focus-visible::-moz-range-thumb{--focus-shadow:0 0 0 6px var(--fallback-b1,oklch(var(--b1)/1)) inset,0 0 0 2rem var(--range-shdw) inset}.range::-webkit-slider-runnable-track{background-color:var(--fallback-bc,oklch(var(--bc)/.1));border-radius:var(--rounded-box,1rem);height:.5rem;width:100%}.range::-moz-range-track{background-color:var(--fallback-bc,oklch(var(--bc)/.1));border-radius:var(--rounded-box,1rem);height:.5rem;width:100%}.range::-webkit-slider-thumb{border-radius:var(--rounded-box,1rem);border-style:none;height:1.5rem;position:relative;width:1.5rem;--tw-bg-opacity:1;appearance:none;-webkit-appearance:none;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));color:var(--range-shdw);top:50%;transform:translateY(-50%);--filler-size:100rem;--filler-offset:0.6rem;box-shadow:0 0 0 3px var(--range-shdw) inset,var(--focus-shadow,0 0),calc(var(--filler-size)*-1 - var(--filler-offset)) 0 0 var(--filler-size)}.range::-moz-range-thumb{border-radius:var(--rounded-box,1rem);border-style:none;height:1.5rem;position:relative;width:1.5rem;--tw-bg-opacity:1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));color:var(--range-shdw);top:50%;--filler-size:100rem;--filler-offset:0.5rem;box-shadow:0 0 0 3px var(--range-shdw) inset,var(--focus-shadow,0 0),calc(var(--filler-size)*-1 - var(--filler-offset)) 0 0 var(--filler-size)}.range-error{--range-shdw:var(--fallback-er,oklch(var(--er)/1))}@keyframes rating-pop{0%{transform:translateY(-.125em)}40%{transform:translateY(-.125em)}to{transform:translateY(0)}}.select-bordered,.select:focus{border-color:var(--fallback-bc,oklch(var(--bc)/.2))}.select:focus{box-shadow:none;outline-color:var(--fallback-bc,oklch(var(--bc)/.2));outline-offset:2px;outline-style:solid;outline-width:2px}.select-disabled,.select:disabled,.select[disabled]{cursor:not-allowed;--tw-border-opacity:1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-text-opacity:0.2}.select-disabled::-moz-placeholder,.select:disabled::-moz-placeholder,.select[disabled]::-moz-placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.select-disabled::placeholder,.select:disabled::placeholder,.select[disabled]::placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.select-multiple,.select[multiple],.select[size].select:not([size="1"]){background-image:none;padding-right:1rem}[dir=rtl] .select{background-position:12px calc(1px + 50%),16px calc(1px + 50%)}@keyframes skeleton{0%{background-position:150%}to{background-position:-50%}}:where(.stats)>:not([hidden])~:not([hidden]){--tw-divide-x-reverse:0;--tw-divide-y-reverse:0;border-width:calc(0px*(1 - var(--tw-divide-y-reverse))) calc(1px*var(--tw-divide-x-reverse)) calc(0px*var(--tw-divide-y-reverse)) calc(1px*(1 - var(--tw-divide-x-reverse)))}:is([dir=rtl] .stats>:not([hidden])~:not([hidden])){--tw-divide-x-reverse:1}.steps .step:before{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));content:"";height:.5rem;margin-inline-start:-100%;top:0;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));width:100%}.steps .step:after,.steps .step:before{grid-column-start:1;grid-row-start:1;--tw-bg-opacity:1;background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)));--tw-text-opacity:1}.steps .step:after{border-radius:9999px;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));content:counter(step);counter-increment:step;display:grid;height:2rem;place-items:center;place-self:center;position:relative;width:2rem;z-index:1}.steps .step:first-child:before{content:none}.steps .step[data-content]:after{content:attr(data-content)}.steps .step-neutral+.step-neutral:before,.steps .step-neutral:after{--tw-bg-opacity:1;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)))}.steps .step-primary+.step-primary:before,.steps .step-primary:after{--tw-bg-opacity:1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}.steps .step-secondary+.step-secondary:before,.steps .step-secondary:after{--tw-bg-opacity:1;background-color:var(--fallback-s,oklch(var(--s)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity)))}.steps .step-accent+.step-accent:before,.steps .step-accent:after{--tw-bg-opacity:1;background-color:var(--fallback-a,oklch(var(--a)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity)))}.steps .step-info+.step-info:before,.steps .step-info:after{--tw-bg-opacity:1;background-color:var(--fallback-in,oklch(var(--in)/var(--tw-bg-opacity)))}.steps .step-info:after{--tw-text-opacity:1;color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)))}.steps .step-success+.step-success:before,.steps .step-success:after{--tw-bg-opacity:1;background-color:var(--fallback-su,oklch(var(--su)/var(--tw-bg-opacity)))}.steps .step-success:after{--tw-text-opacity:1;color:var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity)))}.steps .step-warning+.step-warning:before,.steps .step-warning:after{--tw-bg-opacity:1;background-color:var(--fallback-wa,oklch(var(--wa)/var(--tw-bg-opacity)))}.steps .step-warning:after{--tw-text-opacity:1;color:var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity)))}.steps .step-error+.step-error:before,.steps .step-error:after{--tw-bg-opacity:1;background-color:var(--fallback-er,oklch(var(--er)/var(--tw-bg-opacity)))}.steps .step-error:after{--tw-text-opacity:1;color:var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)))}.tabs-lifted>.tab:focus-visible{border-end-end-radius:0;border-end-start-radius:0}.tab.tab-active:not(.tab-disabled):not([disabled]),.tab:is(input:checked){border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));--tw-border-opacity:1;--tw-text-opacity:1}.tab:focus{outline:2px solid transparent;outline-offset:2px}.tab:focus-visible{outline:2px solid currentColor;outline-offset:-5px}.tab-disabled,.tab[disabled]{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));cursor:not-allowed;--tw-text-opacity:0.2}.tabs-bordered>.tab{border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));--tw-border-opacity:0.2;border-bottom-width:calc(var(--tab-border, 1px) + 1px);border-style:solid}.tabs-lifted>.tab{border:var(--tab-border,1px) solid transparent;border-bottom-color:var(--tab-border-color);border-start-end-radius:var(--tab-radius,.5rem);border-start-start-radius:var(--tab-radius,.5rem);border-width:0 0 var(--tab-border,1px) 0;padding-inline-end:var(--tab-padding,1rem);padding-inline-start:var(--tab-padding,1rem);padding-top:var(--tab-border,1px)}.tabs-lifted>.tab.tab-active:not(.tab-disabled):not([disabled]),.tabs-lifted>.tab:is(input:checked){background-color:var(--tab-bg);border-inline-end-color:var(--tab-border-color);border-inline-start-color:var(--tab-border-color);border-top-color:var(--tab-border-color);border-width:var(--tab-border,1px) var(--tab-border,1px) 0 var(--tab-border,1px);padding-inline-end:calc(var(--tab-padding, 1rem) - var(--tab-border, 1px));padding-bottom:var(--tab-border,1px);padding-inline-start:calc(var(--tab-padding, 1rem) - var(--tab-border, 1px));padding-top:0}.tabs-lifted>.tab.tab-active:not(.tab-disabled):not([disabled]):before,.tabs-lifted>.tab:is(input:checked):before{background-position:0 0,100% 0;background-repeat:no-repeat;background-size:var(--tab-radius,.5rem);bottom:0;content:"";display:block;height:var(--tab-radius,.5rem);position:absolute;width:calc(100% + var(--tab-radius, .5rem)*2);z-index:1;--tab-grad:calc(69% - var(--tab-border, 1px));--radius-start:radial-gradient(circle at top left,transparent var(--tab-grad),var(--tab-border-color) calc(var(--tab-grad) + 0.25px),var(--tab-border-color) calc(var(--tab-grad) + var(--tab-border, 1px)),var(--tab-bg) calc(var(--tab-grad) + var(--tab-border, 1px) + 0.25px));--radius-end:radial-gradient(circle at top right,transparent var(--tab-grad),var(--tab-border-color) calc(var(--tab-grad) + 0.25px),var(--tab-border-color) calc(var(--tab-grad) + var(--tab-border, 1px)),var(--tab-bg) calc(var(--tab-grad) + var(--tab-border, 1px) + 0.25px));background-image:var(--radius-start),var(--radius-end)}.tabs-lifted>.tab.tab-active:not(.tab-disabled):not([disabled]):first-child:before,.tabs-lifted>.tab:is(input:checked):first-child:before{background-image:var(--radius-end);background-position:100% 0}[dir=rtl] .tabs-lifted>.tab.tab-active:not(.tab-disabled):not([disabled]):first-child:before,[dir=rtl] .tabs-lifted>.tab:is(input:checked):first-child:before{background-image:var(--radius-start);background-position:0 0}.tabs-lifted>.tab.tab-active:not(.tab-disabled):not([disabled]):last-child:before,.tabs-lifted>.tab:is(input:checked):last-child:before{background-image:var(--radius-start);background-position:0 0}[dir=rtl] .tabs-lifted>.tab.tab-active:not(.tab-disabled):not([disabled]):last-child:before,[dir=rtl] .tabs-lifted>.tab:is(input:checked):last-child:before{background-image:var(--radius-end);background-position:100% 0}.tabs-lifted>.tab-active:not(.tab-disabled):not([disabled])+.tabs-lifted .tab-active:not(.tab-disabled):not([disabled]):before,.tabs-lifted>.tab:is(input:checked)+.tabs-lifted .tab:is(input:checked):before{background-image:var(--radius-end);background-position:100% 0}.tabs-boxed{--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));padding:.25rem}.tabs-boxed,.tabs-boxed .tab{border-radius:var(--rounded-btn,.5rem)}.tabs-boxed .tab-active:not(.tab-disabled):not([disabled]),.tabs-boxed :is(input:checked){--tw-bg-opacity:1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}:is([dir=rtl] .table){text-align:right}.table :where(th,td){padding:.75rem 1rem;vertical-align:middle}.table tr.active,.table tr.active:nth-child(2n),.table-zebra tbody tr:nth-child(2n){--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)))}.table-zebra tr.active,.table-zebra tr.active:nth-child(2n),.table-zebra-zebra tbody tr:nth-child(2n){--tw-bg-opacity:1;background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)))}.table :where(thead,tbody) :where(tr:first-child:last-child),.table :where(thead,tbody) :where(tr:not(:last-child)){border-bottom-width:1px;--tw-border-opacity:1;border-bottom-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)))}.table :where(thead,tfoot){color:var(--fallback-bc,oklch(var(--bc)/.6));font-size:.75rem;font-weight:700;line-height:1rem;white-space:nowrap}.textarea-bordered,.textarea:focus{border-color:var(--fallback-bc,oklch(var(--bc)/.2))}.textarea:focus{box-shadow:none;outline-color:var(--fallback-bc,oklch(var(--bc)/.2));outline-offset:2px;outline-style:solid;outline-width:2px}.textarea-disabled,.textarea:disabled,.textarea[disabled]{cursor:not-allowed;--tw-border-opacity:1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));--tw-text-opacity:0.2}.textarea-disabled::-moz-placeholder,.textarea:disabled::-moz-placeholder,.textarea[disabled]::-moz-placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.textarea-disabled::placeholder,.textarea:disabled::placeholder,.textarea[disabled]::placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.timeline hr{height:.25rem}:where(.timeline hr){--tw-bg-opacity:1;background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)))}:where(.timeline:has(.timeline-middle) hr):first-child{border-end-end-radius:var(--rounded-badge,1.9rem);border-end-start-radius:0;border-start-end-radius:var(--rounded-badge,1.9rem);border-start-start-radius:0}:where(.timeline:has(.timeline-middle) hr):last-child{border-end-end-radius:0;border-end-start-radius:var(--rounded-badge,1.9rem);border-start-end-radius:0;border-start-start-radius:var(--rounded-badge,1.9rem)}:where(.timeline:not(:has(.timeline-middle)) :first-child hr:last-child){border-end-end-radius:0;border-end-start-radius:var(--rounded-badge,1.9rem);border-start-end-radius:0;border-start-start-radius:var(--rounded-badge,1.9rem)}:where(.timeline:not(:has(.timeline-middle)) :last-child hr:first-child){border-end-end-radius:var(--rounded-badge,1.9rem);border-end-start-radius:0;border-start-end-radius:var(--rounded-badge,1.9rem);border-start-start-radius:0}.timeline-box{border-radius:var(--rounded-box,1rem);border-width:1px;--tw-border-opacity:1;border-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));padding:.5rem 1rem;--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.toast>*{animation:toast-pop .25s ease-out}@keyframes toast-pop{0%{opacity:0;transform:scale(.9)}to{opacity:1;transform:scale(1)}}[dir=rtl] .toggle{--handleoffsetcalculator:calc(var(--handleoffset)*1)}.toggle:focus-visible{outline-color:var(--fallback-bc,oklch(var(--bc)/.2));outline-offset:2px;outline-style:solid;outline-width:2px}.toggle:hover{background-color:currentColor}.toggle:checked,.toggle[aria-checked=true],.toggle[checked=true]{background-image:none;--handleoffsetcalculator:var(--handleoffset);--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}[dir=rtl] .toggle:checked,[dir=rtl] .toggle[aria-checked=true],[dir=rtl] .toggle[checked=true]{--handleoffsetcalculator:calc(var(--handleoffset)*-1)}.toggle:indeterminate{--tw-text-opacity:1;box-shadow:calc(var(--handleoffset)/2) 0 0 2px var(--tglbg) inset,calc(var(--handleoffset)/-2) 0 0 2px var(--tglbg) inset,0 0 0 2px var(--tglbg) inset;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}[dir=rtl] .toggle:indeterminate{box-shadow:calc(var(--handleoffset)/2) 0 0 2px var(--tglbg) inset,calc(var(--handleoffset)/-2) 0 0 2px var(--tglbg) inset,0 0 0 2px var(--tglbg) inset}.toggle-primary:focus-visible{outline-color:var(--fallback-p,oklch(var(--p)/1))}.toggle-primary:checked,.toggle-primary[aria-checked=true],.toggle-primary[checked=true]{border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)));--tw-border-opacity:0.1;--tw-bg-opacity:1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}.toggle-error:focus-visible{outline-color:var(--fallback-er,oklch(var(--er)/1))}.toggle-error:checked,.toggle-error[aria-checked=true],.toggle-error[checked=true]{border-color:var(--fallback-er,oklch(var(--er)/var(--tw-border-opacity)));--tw-border-opacity:0.1;--tw-bg-opacity:1;background-color:var(--fallback-er,oklch(var(--er)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)))}.toggle:disabled{cursor:not-allowed;--tw-border-opacity:1;background-color:transparent;border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));opacity:.3;--togglehandleborder:0 0 0 3px var(--fallback-bc,oklch(var(--bc)/1)) inset,var(--handleoffsetcalculator) 0 0 3px var(--fallback-bc,oklch(var(--bc)/1)) inset}.glass,.glass.btn-active{-webkit-backdrop-filter:blur(var(--glass-blur,40px));backdrop-filter:blur(var(--glass-blur,40px));background-color:transparent;background-image:linear-gradient(135deg,rgb(255 255 255/var(--glass-opacity,30%)) 0,transparent 100%),linear-gradient(var(--glass-reflex-degree,100deg),rgb(255 255 255/var(--glass-reflex-opacity,10%)) 25%,transparent 25%);border:none;box-shadow:0 0 0 1px rgb(255 255 255/var(--glass-border-opacity,10%)) inset,0 0 0 2px rgb(0 0 0/5%);text-shadow:0 1px rgb(0 0 0/var(--glass-text-shadow-opacity,5%))}@media (hover:hover){.glass.btn-active{-webkit-backdrop-filter:blur(var(--glass-blur,40px));backdrop-filter:blur(var(--glass-blur,40px));background-color:transparent;background-image:linear-gradient(135deg,rgb(255 255 255/var(--glass-opacity,30%)) 0,transparent 100%),linear-gradient(var(--glass-reflex-degree,100deg),rgb(255 255 255/var(--glass-reflex-opacity,10%)) 25%,transparent 25%);border:none;box-shadow:0 0 0 1px rgb(255 255 255/var(--glass-border-opacity,10%)) inset,0 0 0 2px rgb(0 0 0/5%);text-shadow:0 1px rgb(0 0 0/var(--glass-text-shadow-opacity,5%))}}.badge-xs{font-size:.75rem;height:.75rem;line-height:.75rem;padding-left:.313rem;padding-right:.313rem}.badge-sm{font-size:.75rem;height:1rem;line-height:1rem;padding-left:.438rem;padding-right:.438rem}.badge-lg{font-size:1rem;height:1.5rem;line-height:1.5rem;padding-left:.688rem;padding-right:.688rem}.btm-nav-xs>:where(.active){border-top-width:1px}.btm-nav-sm>:where(.active){border-top-width:2px}.btm-nav-md>:where(.active){border-top-width:2px}.btm-nav-lg>:where(.active){border-top-width:4px}.btn-xs{font-size:.75rem;height:1.5rem;min-height:1.5rem;padding-left:.5rem;padding-right:.5rem}.btn-sm{font-size:.875rem;height:2rem;min-height:2rem;padding-left:.75rem;padding-right:.75rem}.btn-lg{font-size:1.125rem;height:4rem;min-height:4rem;padding-left:1.5rem;padding-right:1.5rem}.btn-wide{width:16rem}.btn-block{width:100%}.btn-square:where(.btn-xs){height:1.5rem;padding:0;width:1.5rem}.btn-square:where(.btn-sm){height:2rem;padding:0;width:2rem}.btn-square:where(.btn-lg){height:4rem;padding:0;width:4rem}.btn-circle:where(.btn-xs){border-radius:9999px;height:1.5rem;padding:0;width:1.5rem}.btn-circle:where(.btn-sm){border-radius:9999px;height:2rem;padding:0;width:2rem}.btn-circle:where(.btn-md){border-radius:9999px;height:3rem;padding:0;width:3rem}.btn-circle:where(.btn-lg){border-radius:9999px;height:4rem;padding:0;width:4rem}[type=checkbox].checkbox-sm{height:1.25rem;width:1.25rem}.indicator :where(.indicator-item){bottom:auto;inset-inline-end:0;inset-inline-start:auto;top:0;--tw-translate-y:-50%;--tw-translate-x:50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}:is([dir=rtl] .indicator :where(.indicator-item)){--tw-translate-x:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.indicator :where(.indicator-item.indicator-start){inset-inline-end:auto;inset-inline-start:0;--tw-translate-x:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}:is([dir=rtl] .indicator :where(.indicator-item.indicator-start)){--tw-translate-x:50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.indicator :where(.indicator-item.indicator-center){inset-inline-end:50%;inset-inline-start:50%;--tw-translate-x:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}:is([dir=rtl] .indicator :where(.indicator-item.indicator-center)){--tw-translate-x:50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.indicator :where(.indicator-item.indicator-end){inset-inline-end:0;inset-inline-start:auto;--tw-translate-x:50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}:is([dir=rtl] .indicator :where(.indicator-item.indicator-end)){--tw-translate-x:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.indicator :where(.indicator-item.indicator-bottom){bottom:0;top:auto;--tw-translate-y:50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.indicator :where(.indicator-item.indicator-middle){bottom:50%;top:50%;--tw-translate-y:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.indicator :where(.indicator-item.indicator-top){bottom:auto;top:0;--tw-translate-y:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.input-sm{font-size:.875rem;height:2rem;line-height:2rem;padding-left:.75rem;padding-right:.75rem}.join.join-vertical{flex-direction:column}.join.join-vertical .join-item:first-child:not(:last-child),.join.join-vertical :first-child:not(:last-child) .join-item{border-end-end-radius:0;border-end-start-radius:0;border-start-end-radius:inherit;border-start-start-radius:inherit}.join.join-vertical .join-item:last-child:not(:first-child),.join.join-vertical :last-child:not(:first-child) .join-item{border-end-end-radius:inherit;border-end-start-radius:inherit;border-start-end-radius:0;border-start-start-radius:0}.join.join-horizontal{flex-direction:row}.join.join-horizontal .join-item:first-child:not(:last-child),.join.join-horizontal :first-child:not(:last-child) .join-item{border-end-end-radius:0;border-end-start-radius:inherit;border-start-end-radius:0;border-start-start-radius:inherit}.join.join-horizontal .join-item:last-child:not(:first-child),.join.join-horizontal :last-child:not(:first-child) .join-item{border-end-end-radius:inherit;border-end-start-radius:0;border-start-end-radius:inherit;border-start-start-radius:0}.menu-horizontal{display:inline-flex;flex-direction:row}.menu-horizontal>li:not(.menu-title)>details>ul{position:absolute}[type=radio].radio-sm{height:1.25rem;width:1.25rem}.select-sm{font-size:.875rem;height:2rem;line-height:2rem;min-height:2rem;padding-left:.75rem;padding-right:2rem}[dir=rtl] .select-sm{padding-left:2rem;padding-right:.75rem}.select-xs{font-size:.75rem;height:1.5rem;line-height:1rem;line-height:1.625;min-height:1.5rem;padding-left:.5rem;padding-right:2rem}[dir=rtl] .select-xs{padding-left:2rem;padding-right:.5rem}.stats-vertical{grid-auto-flow:row}.steps-horizontal .step{display:grid;grid-template-columns:repeat(1,minmax(0,1fr));grid-template-rows:repeat(2,minmax(0,1fr));place-items:center;text-align:center}.steps-vertical .step{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));grid-template-rows:repeat(1,minmax(0,1fr))}.tabs-md :where(.tab){font-size:.875rem;height:2rem;line-height:1.25rem;line-height:2;--tab-padding:1rem}.tabs-lg :where(.tab){font-size:1.125rem;height:3rem;line-height:1.75rem;line-height:2;--tab-padding:1.25rem}.tabs-sm :where(.tab){font-size:.875rem;height:1.5rem;line-height:.75rem;--tab-padding:0.75rem}.tabs-xs :where(.tab){font-size:.75rem;height:1.25rem;line-height:.75rem;--tab-padding:0.5rem}.timeline-vertical{flex-direction:column}.timeline-compact .timeline-start,.timeline-horizontal.timeline-compact .timeline-start{align-self:flex-start;grid-column-end:4;grid-column-start:1;grid-row-end:4;grid-row-start:3;justify-self:center;margin:.25rem}.timeline-compact li:has(.timeline-start) .timeline-end,.timeline-horizontal.timeline-compact li:has(.timeline-start) .timeline-end{grid-column-start:none;grid-row-start:auto}.timeline-vertical.timeline-compact>li{--timeline-col-start:0}.timeline-vertical.timeline-compact .timeline-start{align-self:center;grid-column-end:4;grid-column-start:3;grid-row-end:4;grid-row-start:1;justify-self:start}.timeline-vertical.timeline-compact li:has(.timeline-start) .timeline-end{grid-column-start:auto;grid-row-start:none}:where(.timeline-vertical>li){--timeline-row-start:minmax(0,1fr);--timeline-row-end:minmax(0,1fr);justify-items:center}.timeline-vertical>li>hr{height:100%}:where(.timeline-vertical>li>hr):first-child{grid-column-start:2;grid-row-start:1}:where(.timeline-vertical>li>hr):last-child{grid-column-end:auto;grid-column-start:2;grid-row-end:none;grid-row-start:3}.timeline-vertical .timeline-start{align-self:center;grid-column-end:2;grid-column-start:1;grid-row-end:4;grid-row-start:1;justify-self:end}.timeline-vertical .timeline-end{align-self:center;grid-column-end:4;grid-column-start:3;grid-row-end:4;grid-row-start:1;justify-self:start}.timeline-vertical:where(.timeline-snap-icon)>li{--timeline-col-start:minmax(0,1fr);--timeline-row-start:0.5rem}.timeline-horizontal .timeline-start{align-self:flex-end;grid-column-end:4;grid-column-start:1;grid-row-end:2;grid-row-start:1;justify-self:center}.timeline-horizontal .timeline-end{align-self:flex-start;grid-column-end:4;grid-column-start:1;grid-row-end:4;grid-row-start:3;justify-self:center}.timeline-horizontal:where(.timeline-snap-icon)>li,:where(.timeline-snap-icon)>li{--timeline-col-start:0.5rem;--timeline-row-start:minmax(0,1fr)}:where(.toast){bottom:0;inset-inline-end:0;inset-inline-start:auto;top:auto;--tw-translate-x:0px;--tw-translate-y:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.toast:where(.toast-start){inset-inline-end:auto;inset-inline-start:0;--tw-translate-x:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.toast:where(.toast-center){inset-inline-end:50%;inset-inline-start:50%;--tw-translate-x:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}:is([dir=rtl] .toast:where(.toast-center)){--tw-translate-x:50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.toast:where(.toast-end){inset-inline-end:0;inset-inline-start:auto;--tw-translate-x:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.toast:where(.toast-bottom){bottom:0;top:auto;--tw-translate-y:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.toast:where(.toast-middle){bottom:auto;top:50%;--tw-translate-y:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.toast:where(.toast-top){bottom:auto;top:0;--tw-translate-y:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}[type=checkbox].toggle-sm{--handleoffset:0.75rem;height:1.25rem;width:2rem}.tooltip{--tooltip-offset:calc(100% + 1px + var(--tooltip-tail, 0px))}.tooltip:before{content:var(--tw-content);pointer-events:none;position:absolute;z-index:1;--tw-content:attr(data-tip)}.tooltip-top:before,.tooltip:before{bottom:var(--tooltip-offset);left:50%;right:auto;top:auto;transform:translateX(-50%)}.tooltip-bottom:before{bottom:auto;left:50%;right:auto;top:var(--tooltip-offset);transform:translateX(-50%)}.tooltip-left:before{left:auto;right:var(--tooltip-offset)}.tooltip-left:before,.tooltip-right:before{bottom:auto;top:50%;transform:translateY(-50%)}.tooltip-right:before{left:var(--tooltip-offset);right:auto}.avatar.online:before{background-color:var(--fallback-su,oklch(var(--su)/var(--tw-bg-opacity)))}.avatar.offline:before,.avatar.online:before{border-radius:9999px;content:"";display:block;position:absolute;z-index:10;--tw-bg-opacity:1;height:15%;outline-color:var(--fallback-b1,oklch(var(--b1)/1));outline-style:solid;outline-width:2px;right:7%;top:7%;width:15%}.avatar.offline:before{background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)))}.card-compact .card-body{font-size:.875rem;line-height:1.25rem;padding:1rem}.card-compact .card-title{margin-bottom:.25rem}.card-normal .card-body{font-size:1rem;line-height:1.5rem;padding:var(--padding-card,2rem)}.card-normal .card-title{margin-bottom:.75rem}.join.join-vertical>:where(:not(:first-child)){margin-left:0;margin-right:0;margin-top:-1px}.join.join-horizontal>:where(:not(:first-child)){margin-bottom:0;margin-top:0;margin-inline-start:-1px}.menu-horizontal>li:not(.menu-title)>details>ul{margin-inline-start:0;margin-top:1rem;padding-bottom:.5rem;padding-inline-end:.5rem;padding-top:.5rem}.menu-horizontal>li>details>ul:before{content:none}:where(.menu-horizontal>li:not(.menu-title)>details>ul){border-radius:var(--rounded-box,1rem);--tw-bg-opacity:1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.menu-sm :where(li:not(.menu-title)>:not(ul,details,.menu-title)),.menu-sm :where(li:not(.menu-title)>details>summary:not(.menu-title)){border-radius:var(--rounded-btn,.5rem);font-size:.875rem;line-height:1.25rem;padding:.25rem .75rem}.menu-sm .menu-title{padding:.5rem .75rem}.modal-top :where(.modal-box){max-width:none;width:100%;--tw-translate-y:-2.5rem;--tw-scale-x:1;--tw-scale-y:1;border-bottom-left-radius:var(--rounded-box,1rem);border-bottom-right-radius:var(--rounded-box,1rem);border-top-left-radius:0;border-top-right-radius:0;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.modal-middle :where(.modal-box){max-width:32rem;width:91.666667%;--tw-translate-y:0px;--tw-scale-x:.9;--tw-scale-y:.9;border-bottom-left-radius:var(--rounded-box,1rem);border-bottom-right-radius:var(--rounded-box,1rem);border-top-left-radius:var(--rounded-box,1rem);border-top-right-radius:var(--rounded-box,1rem);transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.modal-bottom :where(.modal-box){max-width:none;width:100%;--tw-translate-y:2.5rem;--tw-scale-x:1;--tw-scale-y:1;border-bottom-left-radius:0;border-bottom-right-radius:0;border-top-left-radius:var(--rounded-box,1rem);border-top-right-radius:var(--rounded-box,1rem);transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.stats-vertical>:not([hidden])~:not([hidden]){--tw-divide-x-reverse:0;--tw-divide-y-reverse:0;border-width:calc(1px*(1 - var(--tw-divide-y-reverse))) calc(0px*var(--tw-divide-x-reverse)) calc(1px*var(--tw-divide-y-reverse)) calc(0px*(1 - var(--tw-divide-x-reverse)))}.stats-vertical{overflow-y:auto}.steps-horizontal .step{grid-template-columns:auto;grid-template-rows:40px 1fr;min-width:4rem}.steps-horizontal .step:before{height:.5rem;width:100%;--tw-translate-x:0px;--tw-translate-y:0px;content:"";margin-inline-start:-100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}:is([dir=rtl] .steps-horizontal .step):before{--tw-translate-x:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.steps-vertical .step{gap:.5rem;grid-template-columns:40px 1fr;grid-template-rows:auto;justify-items:start;min-height:4rem}.steps-vertical .step:before{height:100%;width:.5rem;--tw-translate-x:-50%;--tw-translate-y:-50%;margin-inline-start:50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}:is([dir=rtl] .steps-vertical .step):before{--tw-translate-x:50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.timeline-vertical>li>hr{width:.25rem}:where(.timeline-vertical:has(.timeline-middle)>li>hr):first-child{border-bottom-left-radius:var(--rounded-badge,1.9rem);border-bottom-right-radius:var(--rounded-badge,1.9rem);border-top-left-radius:0;border-top-right-radius:0}:where(.timeline-vertical:has(.timeline-middle)>li>hr):last-child{border-bottom-left-radius:0;border-bottom-right-radius:0;border-top-left-radius:var(--rounded-badge,1.9rem);border-top-right-radius:var(--rounded-badge,1.9rem)}:where(.timeline-vertical:not(:has(.timeline-middle)) :first-child>hr:last-child){border-bottom-left-radius:0;border-bottom-right-radius:0;border-top-left-radius:var(--rounded-badge,1.9rem);border-top-right-radius:var(--rounded-badge,1.9rem)}:where(.timeline-vertical:not(:has(.timeline-middle)) :last-child>hr:first-child){border-bottom-left-radius:var(--rounded-badge,1.9rem);border-bottom-right-radius:var(--rounded-badge,1.9rem);border-top-left-radius:0;border-top-right-radius:0}:where(.timeline-horizontal:has(.timeline-middle)>li>hr):first-child{border-end-end-radius:var(--rounded-badge,1.9rem);border-end-start-radius:0;border-start-end-radius:var(--rounded-badge,1.9rem);border-start-start-radius:0}:where(.timeline-horizontal:has(.timeline-middle)>li>hr):last-child{border-end-end-radius:0;border-end-start-radius:var(--rounded-badge,1.9rem);border-start-end-radius:0;border-start-start-radius:var(--rounded-badge,1.9rem)}.tooltip{display:inline-block;position:relative;text-align:center;--tooltip-tail:0.1875rem;--tooltip-color:var(--fallback-n,oklch(var(--n)/1));--tooltip-text-color:var(--fallback-nc,oklch(var(--nc)/1));--tooltip-tail-offset:calc(100% + 0.0625rem - var(--tooltip-tail))}.tooltip:after,.tooltip:before{opacity:0;transition-delay:.1s;transition-duration:.2s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.tooltip:after{border-style:solid;border-width:var(--tooltip-tail,0);content:"";display:block;height:0;position:absolute;width:0}.tooltip:before{background-color:var(--tooltip-color);border-radius:.25rem;color:var(--tooltip-text-color);font-size:.875rem;line-height:1.25rem;max-width:20rem;padding:.25rem .5rem;width:-moz-max-content;width:max-content}.tooltip.tooltip-open:after,.tooltip.tooltip-open:before,.tooltip:hover:after,.tooltip:hover:before{opacity:1;transition-delay:75ms}.tooltip:has(:focus-visible):after,.tooltip:has(:focus-visible):before{opacity:1;transition-delay:75ms}.tooltip:not([data-tip]):hover:after,.tooltip:not([data-tip]):hover:before{opacity:0;visibility:hidden}.tooltip-top:after,.tooltip:after{border-color:var(--tooltip-color) transparent transparent transparent;bottom:var(--tooltip-tail-offset);left:50%;right:auto;top:auto;transform:translateX(-50%)}.tooltip-bottom:after{border-color:transparent transparent var(--tooltip-color) transparent;bottom:auto;left:50%;right:auto;top:var(--tooltip-tail-offset);transform:translateX(-50%)}.tooltip-left:after{border-color:transparent transparent transparent var(--tooltip-color);left:auto;right:calc(var(--tooltip-tail-offset) + .0625rem)}.tooltip-left:after,.tooltip-right:after{bottom:auto;top:50%;transform:translateY(-50%)}.tooltip-right:after{border-color:transparent var(--tooltip-color) transparent transparent;left:calc(var(--tooltip-tail-offset) + .0625rem);right:auto}.fade-out{opacity:0;transition:opacity .15s ease-in-out}.visible{visibility:visible}.invisible{visibility:hidden}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.bottom-0{bottom:0}.left-0{left:0}.left-2{left:.5rem}.left-4{left:1rem}.right-0{right:0}.right-2{right:.5rem}.right-5{right:1.25rem}.top-0{top:0}.top-16{top:4rem}.top-2{top:.5rem}.top-4{top:1rem}.top-5{top:1.25rem}.z-0{z-index:0}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-\[1\]{z-index:1}.z-\[5000\]{z-index:5000}.z-\[6000\]{z-index:6000}.col-span-2{grid-column:span 2/span 2}.m-0{margin:0}.m-5{margin:1.25rem}.m-auto{margin:auto}.mx-1{margin-left:.25rem;margin-right:.25rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-5{margin-left:1.25rem;margin-right:1.25rem}.mx-auto{margin-left:auto;margin-right:auto}.my-10{margin-bottom:2.5rem;margin-top:2.5rem}.my-2{margin-bottom:.5rem;margin-top:.5rem}.my-3{margin-bottom:.75rem;margin-top:.75rem}.my-4{margin-bottom:1rem;margin-top:1rem}.my-5{margin-bottom:1.25rem;margin-top:1.25rem}.mb-1{margin-bottom:.25rem}.mb-12{margin-bottom:3rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-14{margin-left:3.5rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mr-4{margin-right:1rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-10{height:2.5rem}.h-12{height:3rem}.h-16{height:4rem}.h-2{height:.5rem}.h-24{height:6rem}.h-3{height:.75rem}.h-4{height:1rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-96{height:24rem}.h-\[250px\]{height:250px}.h-\[25rem\]{height:25rem}.h-fit{height:-moz-fit-content;height:fit-content}.h-full{height:100%}.h-screen{height:100vh}.max-h-48{max-height:12rem}.max-h-96{max-height:24rem}.min-h-80{min-height:20rem}.min-h-\[4rem\]{min-height:4rem}.min-h-screen{min-height:100vh}.w-1\/2{width:50%}.w-10{width:2.5rem}.w-10\/12{width:83.333333%}.w-12{width:3rem}.w-2{width:.5rem}.w-24{width:6rem}.w-28{width:7rem}.w-3{width:.75rem}.w-4{width:1rem}.w-4\/12{width:33.333333%}.w-5{width:1.25rem}.w-52{width:13rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-80{width:20rem}.w-96{width:24rem}.w-auto{width:auto}.w-full{width:100%}.min-w-52{min-width:13rem}.min-w-full{min-width:100%}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-5xl{max-width:64rem}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.flex-shrink-0,.shrink-0{flex-shrink:0}.flex-grow{flex-grow:1}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes bounce{0%,to{animation-timing-function:cubic-bezier(.8,0,1,1);transform:translateY(-25%)}50%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:none}}.animate-bounce{animation:bounce 1s infinite}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize{resize:both}.list-inside{list-style-position:inside}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.gap-6{gap:1.5rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-y-2{row-gap:.5rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.5rem*var(--tw-space-x-reverse))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.75rem*var(--tw-space-x-reverse))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1rem*var(--tw-space-x-reverse))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.25rem*var(--tw-space-y-reverse));margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.5rem*var(--tw-space-y-reverse));margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.75rem*var(--tw-space-y-reverse));margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1rem*var(--tw-space-y-reverse));margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.5rem*var(--tw-space-y-reverse));margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(2rem*var(--tw-space-y-reverse));margin-top:calc(2rem*(1 - var(--tw-space-y-reverse)))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-bottom-width:calc(1px*var(--tw-divide-y-reverse));border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)))}.divide-base-300>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-divide-opacity,1)))}.justify-self-end{justify-self:end}.justify-self-center{justify-self:center}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;white-space:nowrap}.text-ellipsis,.truncate{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-box{border-radius:var(--rounded-box,1rem)}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-b{border-bottom-left-radius:.25rem;border-bottom-right-radius:.25rem}.rounded-t-none{border-top-left-radius:0;border-top-right-radius:0}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-base-300{--tw-border-opacity:1;border-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-border-opacity,1)))}.border-blue-300{--tw-border-opacity:1;border-color:rgb(147 197 253/var(--tw-border-opacity,1))}.border-blue-500{--tw-border-opacity:1;border-color:rgb(59 130 246/var(--tw-border-opacity,1))}.border-error{--tw-border-opacity:1;border-color:var(--fallback-er,oklch(var(--er)/var(--tw-border-opacity,1)))}.border-gray-100{--tw-border-opacity:1;border-color:rgb(243 244 246/var(--tw-border-opacity,1))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity,1))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity,1))}.border-info\/20{border-color:var(--fallback-in,oklch(var(--in)/.2))}.border-neutral{--tw-border-opacity:1;border-color:var(--fallback-n,oklch(var(--n)/var(--tw-border-opacity,1)))}.border-red-300{--tw-border-opacity:1;border-color:rgb(252 165 165/var(--tw-border-opacity,1))}.border-secondary\/20{border-color:var(--fallback-s,oklch(var(--s)/.2))}.border-sky-500{--tw-border-opacity:1;border-color:rgb(14 165 233/var(--tw-border-opacity,1))}.border-success\/20{border-color:var(--fallback-su,oklch(var(--su)/.2))}.border-transparent{border-color:transparent}.border-warning\/20{border-color:var(--fallback-wa,oklch(var(--wa)/.2))}.border-white{--tw-border-opacity:1;border-color:rgb(255 255 255/var(--tw-border-opacity,1))}.border-opacity-20{--tw-border-opacity:0.2}.bg-base-100{--tw-bg-opacity:1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity,1)))}.bg-base-200{--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity,1)))}.bg-base-300{--tw-bg-opacity:1;background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity,1)))}.bg-blue-100{--tw-bg-opacity:1;background-color:rgb(219 234 254/var(--tw-bg-opacity,1))}.bg-blue-50{--tw-bg-opacity:1;background-color:rgb(239 246 255/var(--tw-bg-opacity,1))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity,1))}.bg-blue-600{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity,1))}.bg-blue-900{--tw-bg-opacity:1;background-color:rgb(30 58 138/var(--tw-bg-opacity,1))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity,1))}.bg-gray-400{--tw-bg-opacity:1;background-color:rgb(156 163 175/var(--tw-bg-opacity,1))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity,1))}.bg-green-50{--tw-bg-opacity:1;background-color:rgb(240 253 244/var(--tw-bg-opacity,1))}.bg-green-500{--tw-bg-opacity:1;background-color:rgb(34 197 94/var(--tw-bg-opacity,1))}.bg-info{--tw-bg-opacity:1;background-color:var(--fallback-in,oklch(var(--in)/var(--tw-bg-opacity,1)))}.bg-info\/10{background-color:var(--fallback-in,oklch(var(--in)/.1))}.bg-neutral{--tw-bg-opacity:1;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity,1)))}.bg-primary{--tw-bg-opacity:1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity,1)))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity,1))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity,1))}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}.bg-secondary{--tw-bg-opacity:1;background-color:var(--fallback-s,oklch(var(--s)/var(--tw-bg-opacity,1)))}.bg-secondary-content{--tw-bg-opacity:1;background-color:var(--fallback-sc,oklch(var(--sc)/var(--tw-bg-opacity,1)))}.bg-secondary\/10{background-color:var(--fallback-s,oklch(var(--s)/.1))}.bg-success{--tw-bg-opacity:1;background-color:var(--fallback-su,oklch(var(--su)/var(--tw-bg-opacity,1)))}.bg-success\/10{background-color:var(--fallback-su,oklch(var(--su)/.1))}.bg-warning{--tw-bg-opacity:1;background-color:var(--fallback-wa,oklch(var(--wa)/var(--tw-bg-opacity,1)))}.bg-warning\/10{background-color:var(--fallback-wa,oklch(var(--wa)/.1))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.bg-opacity-10{--tw-bg-opacity:0.1}.bg-opacity-60{--tw-bg-opacity:0.6}.bg-opacity-80{--tw-bg-opacity:0.8}.bg-gradient-to-bl{background-image:linear-gradient(to bottom left,var(--tw-gradient-stops))}.bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.bg-gradient-to-tl{background-image:linear-gradient(to top left,var(--tw-gradient-stops))}.bg-gradient-to-tr{background-image:linear-gradient(to top right,var(--tw-gradient-stops))}.from-base-100{--tw-gradient-from:var(--fallback-b1,oklch(var(--b1)/1)) var(--tw-gradient-from-position);--tw-gradient-to:var(--fallback-b1,oklch(var(--b1)/0)) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-blue-500{--tw-gradient-from:#3b82f6 var(--tw-gradient-from-position);--tw-gradient-to:rgba(59,130,246,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-blue-600{--tw-gradient-from:#2563eb var(--tw-gradient-from-position);--tw-gradient-to:rgba(37,99,235,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-green-400{--tw-gradient-from:#4ade80 var(--tw-gradient-from-position);--tw-gradient-to:rgba(74,222,128,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-green-500{--tw-gradient-from:#22c55e var(--tw-gradient-from-position);--tw-gradient-to:rgba(34,197,94,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-orange-400{--tw-gradient-from:#fb923c var(--tw-gradient-from-position);--tw-gradient-to:rgba(251,146,60,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-orange-600{--tw-gradient-from:#ea580c var(--tw-gradient-from-position);--tw-gradient-to:rgba(234,88,12,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-primary{--tw-gradient-from:var(--fallback-p,oklch(var(--p)/1)) var(--tw-gradient-from-position);--tw-gradient-to:var(--fallback-p,oklch(var(--p)/0)) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-red-400{--tw-gradient-from:#f87171 var(--tw-gradient-from-position);--tw-gradient-to:hsla(0,91%,71%,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-red-800{--tw-gradient-from:#991b1b var(--tw-gradient-from-position);--tw-gradient-to:rgba(153,27,27,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-yellow-400{--tw-gradient-from:#facc15 var(--tw-gradient-from-position);--tw-gradient-to:rgba(250,204,21,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-yellow-700{--tw-gradient-from:#a16207 var(--tw-gradient-from-position);--tw-gradient-to:rgba(161,98,7,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.to-base-200{--tw-gradient-to:var(--fallback-b2,oklch(var(--b2)/1)) var(--tw-gradient-to-position)}.to-blue-700{--tw-gradient-to:#1d4ed8 var(--tw-gradient-to-position)}.to-blue-800{--tw-gradient-to:#1e40af var(--tw-gradient-to-position)}.to-green-700{--tw-gradient-to:#15803d var(--tw-gradient-to-position)}.to-orange-600{--tw-gradient-to:#ea580c var(--tw-gradient-to-position)}.to-orange-700{--tw-gradient-to:#c2410c var(--tw-gradient-to-position)}.to-purple-600{--tw-gradient-to:#9333ea var(--tw-gradient-to-position)}.to-red-400{--tw-gradient-to:#f87171 var(--tw-gradient-to-position)}.to-red-600{--tw-gradient-to:#dc2626 var(--tw-gradient-to-position)}.to-red-900{--tw-gradient-to:#7f1d1d var(--tw-gradient-to-position)}.to-secondary{--tw-gradient-to:var(--fallback-s,oklch(var(--s)/1)) var(--tw-gradient-to-position)}.to-yellow-400{--tw-gradient-to:#facc15 var(--tw-gradient-to-position)}.to-yellow-600{--tw-gradient-to:#ca8a04 var(--tw-gradient-to-position)}.stroke-current{stroke:currentColor}.stroke-info{stroke:var(--fallback-in,oklch(var(--in)/1))}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.py-12{padding-bottom:3rem;padding-top:3rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.py-20{padding-bottom:5rem;padding-top:5rem}.py-3{padding-bottom:.75rem;padding-top:.75rem}.py-4{padding-bottom:1rem;padding-top:1rem}.py-5{padding-bottom:1.25rem;padding-top:1.25rem}.py-6{padding-bottom:1.5rem;padding-top:1.5rem}.py-8{padding-bottom:2rem;padding-top:2rem}.pb-2{padding-bottom:.5rem}.pl-4{padding-left:1rem}.pl-5{padding-left:1.25rem}.pl-6{padding-left:1.5rem}.pr-10{padding-right:2.5rem}.pt-2{padding-top:.5rem}.pt-4{padding-top:1rem}.pt-6{padding-top:1.5rem}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-5xl{font-size:3rem;line-height:1}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-black{font-weight:900}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.normal-case{text-transform:none}.italic{font-style:italic}.text-accent{--tw-text-opacity:1;color:var(--fallback-a,oklch(var(--a)/var(--tw-text-opacity,1)))}.text-accent-content{--tw-text-opacity:1;color:var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity,1)))}.text-base-content{--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity,1)))}.text-base-content\/60{color:var(--fallback-bc,oklch(var(--bc)/.6))}.text-base-content\/70{color:var(--fallback-bc,oklch(var(--bc)/.7))}.text-base-content\/80{color:var(--fallback-bc,oklch(var(--bc)/.8))}.text-blue-600{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity,1))}.text-blue-700{--tw-text-opacity:1;color:rgb(29 78 216/var(--tw-text-opacity,1))}.text-error{--tw-text-opacity:1;color:var(--fallback-er,oklch(var(--er)/var(--tw-text-opacity,1)))}.text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity,1))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity,1))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity,1))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}.text-gray-800{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity,1))}.text-green-500{--tw-text-opacity:1;color:rgb(34 197 94/var(--tw-text-opacity,1))}.text-green-600{--tw-text-opacity:1;color:rgb(22 163 74/var(--tw-text-opacity,1))}.text-info{--tw-text-opacity:1;color:var(--fallback-in,oklch(var(--in)/var(--tw-text-opacity,1)))}.text-info-content{--tw-text-opacity:1;color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity,1)))}.text-neutral{--tw-text-opacity:1;color:var(--fallback-n,oklch(var(--n)/var(--tw-text-opacity,1)))}.text-neutral-content{--tw-text-opacity:1;color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity,1)))}.text-orange-600{--tw-text-opacity:1;color:rgb(234 88 12/var(--tw-text-opacity,1))}.text-primary{--tw-text-opacity:1;color:var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity,1)))}.text-primary-content{--tw-text-opacity:1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity,1)))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity,1))}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity,1))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity,1))}.text-secondary{--tw-text-opacity:1;color:var(--fallback-s,oklch(var(--s)/var(--tw-text-opacity,1)))}.text-secondary-content{--tw-text-opacity:1;color:var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity,1)))}.text-success{--tw-text-opacity:1;color:var(--fallback-su,oklch(var(--su)/var(--tw-text-opacity,1)))}.text-success-content{--tw-text-opacity:1;color:var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity,1)))}.text-warning{--tw-text-opacity:1;color:var(--fallback-wa,oklch(var(--wa)/var(--tw-text-opacity,1)))}.text-warning-content{--tw-text-opacity:1;color:var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity,1)))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.underline{text-decoration-line:underline}.decoration-dotted{text-decoration-style:dotted}.placeholder-base-content\/70::-moz-placeholder{color:var(--fallback-bc,oklch(var(--bc)/.7))}.placeholder-base-content\/70::placeholder{color:var(--fallback-bc,oklch(var(--bc)/.7))}.opacity-0{opacity:0}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-80{opacity:.8}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-2xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgba(0,0,0,.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 rgba(0,0,0,.05);--tw-shadow-colored:inset 0 2px 4px 0 var(--tw-shadow-color)}.shadow-inner,.shadow-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-md,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.outline{outline-style:solid}.ring-2{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-primary{--tw-ring-opacity:1;--tw-ring-color:var(--fallback-p,oklch(var(--p)/var(--tw-ring-opacity,1)))}.ring-offset-2{--tw-ring-offset-width:2px}.blur{--tw-blur:blur(8px)}.blur,.grayscale{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.grayscale{--tw-grayscale:grayscale(100%)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-all{transition-duration:.15s;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-colors{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-opacity{transition-duration:.15s;transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-shadow{transition-duration:.15s;transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-transform{transition-duration:.15s;transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}@tailwind daisyui;.leaflet-right-panel{background:#fff;border-radius:4px;box-shadow:0 1px 4px rgba(0,0,0,.3);margin-right:10px;margin-top:80px;transform:none;transition:right .3s ease-in-out;z-index:400}.add-visit-marker{align-items:center;animation:pulse-visit 2s infinite;background:#fff;border:2px solid #007bff;border-radius:50%;box-shadow:0 2px 8px rgba(0,123,255,.3);display:flex!important;font-size:20px;justify-content:center}@keyframes pulse-visit{0%{box-shadow:0 2px 8px rgba(0,123,255,.3);transform:scale(1)}50%{box-shadow:0 4px 12px rgba(0,123,255,.5);transform:scale(1.1)}to{box-shadow:0 2px 8px rgba(0,123,255,.3);transform:scale(1)}}.visit-form-popup .leaflet-popup-content-wrapper{border-radius:8px;box-shadow:0 4px 20px rgba(0,0,0,.15)}.leaflet-right-panel.controls-shifted{right:310px}.leaflet-drawer{background:hsla(0,0%,100%,.5);border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,.15);cursor:default;height:auto;max-height:calc(100% - 20px);opacity:0;position:absolute;right:70px;top:10px;transform:scale(.95);transition:opacity .2s ease-in-out,transform .2s ease-in-out,visibility .2s;visibility:hidden;width:24rem;z-index:450}.leaflet-drawer *{cursor:default}.leaflet-drawer .btn,.leaflet-drawer a,.leaflet-drawer button,.leaflet-drawer input[type=checkbox]{cursor:pointer}.leaflet-drawer.open{opacity:1;transform:scale(1);visibility:visible}.leaflet-control-button,.leaflet-control-layers,.toggle-panel-button{z-index:500}.leaflet-control-custom{align-items:center;background-color:#fff;border-radius:4px;box-shadow:0 1px 4px rgba(0,0,0,.3);cursor:pointer;display:flex;height:30px;justify-content:center;width:30px}.leaflet-control-custom:hover{background-color:#f3f4f6}#selection-tool-button.active{background-color:#60a5fa;color:#fff}#cancel-selection-button{width:100%}em-emoji-picker{--color-border-over:rgba(0,0,0,.1);--color-border:rgba(0,0,0,.05);--font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;--rgb-accent:96,165,250;border-radius:8px;box-shadow:0 4px 20px rgba(0,0,0,.15);max-width:400px;min-width:318px;overflow:auto;position:absolute;resize:horizontal;z-index:1000}[data-theme=dark] em-emoji-picker,html.dark em-emoji-picker{--color-border-over:hsla(0,0%,100%,.1);--color-border:hsla(0,0%,100%,.05);--rgb-accent:96,165,250}@media (max-width:768px){em-emoji-picker{max-width:90vw;min-width:280px}}.color-input{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:none;padding:0}.color-input::-webkit-color-swatch-wrapper{padding:0}.color-input::-webkit-color-swatch{border:none;border-radius:.5rem}.color-input::-moz-color-swatch{border:none;border-radius:.5rem}@media (hover:hover){.hover\:btn-ghost:hover:hover{border-color:transparent}@supports (color:oklch(0 0 0)){.hover\:btn-ghost:hover:hover{background-color:var(--fallback-bc,oklch(var(--bc)/.2))}}.hover\:btn-info:hover.btn-outline:hover{--tw-text-opacity:1;color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)))}@supports (color:color-mix(in oklab,black,black)){.hover\:btn-info:hover.btn-outline:hover{background-color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 90%,#000)}}}@supports not (color:oklch(0 0 0)){.hover\:btn-info:hover{--btn-color:var(--fallback-in)}}@supports (color:color-mix(in oklab,black,black)){.hover\:btn-info:hover.btn-outline.btn-active{background-color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 90%,#000)}}@supports (color:oklch(0 0 0)){.hover\:btn-info:hover{--btn-color:var(--in)}}.hover\:btn-info:hover{--tw-text-opacity:1;color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)));outline-color:var(--fallback-in,oklch(var(--in)/1))}.hover\:btn-ghost:hover{background-color:transparent;border-color:transparent;border-width:1px;color:currentColor;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);outline-color:currentColor}.hover\:btn-ghost:hover.btn-active{background-color:var(--fallback-bc,oklch(var(--bc)/.2));border-color:transparent}.hover\:btn-info:hover.btn-outline{--tw-text-opacity:1;color:var(--fallback-in,oklch(var(--in)/var(--tw-text-opacity)))}.hover\:btn-info:hover.btn-outline.btn-active{--tw-text-opacity:1;color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)))}.hover\:input-primary:hover{--tw-border-opacity:1;border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)))}.hover\:input-primary:hover:focus,.hover\:input-primary:hover:focus-within{--tw-border-opacity:1;border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)));outline-color:var(--fallback-p,oklch(var(--p)/1))}@media not all and (min-width:768px){.max-md\:timeline-compact,.max-md\:timeline-compact + );position:relative}.timeline>li>hr{border-width:0;width:100%}:where(.timeline>li>hr):first-child{grid-column-start:1;grid-row-start:2}:where(.timeline>li>hr):last-child{grid-column-end:none;grid-column-start:3;grid-row-end:auto;grid-row-start:2}.timeline-start{align-self:flex-end;grid-column-end:4;grid-column-start:1;grid-row-end:2;grid-row-start:1;justify-self:center;margin:.25rem}.timeline-middle{grid-column-start:2;grid-row-start:2}.timeline-end{align-self:flex-start;grid-column-end:4;grid-column-start:1;grid-row-end:4;grid-row-start:3;justify-self:center;margin:.25rem}.toast{display:flex;flex-direction:column;gap:.5rem;min-width:-moz-fit-content;min-width:fit-content;padding:1rem;position:fixed;white-space:nowrap}.toggle{flex-shrink:0;--tglbg:var(--fallback-b1,oklch(var(--b1)/1));--handleoffset:1.5rem;--handleoffsetcalculator:calc(var(--handleoffset)*-1);--togglehandleborder:0 0;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:currentColor;border-color:currentColor;border-radius:var(--rounded-badge,1.9rem);border-width:1px;box-shadow:var(--handleoffsetcalculator) 0 0 2px var(--tglbg) inset,0 0 0 2px var(--tglbg) inset,var(--togglehandleborder);color:var(--fallback-bc,oklch(var(--bc)/.5));cursor:pointer;height:1.5rem;transition:background,box-shadow var(--animation-input,.2s) ease-out;width:3rem}.alert-info{border-color:var(--fallback-in,oklch(var(--in)/.2));--tw-text-opacity:1;color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)));--alert-bg:var(--fallback-in,oklch(var(--in)/1));--alert-bg-mix:var(--fallback-b1,oklch(var(--b1)/1))}.alert-success{border-color:var(--fallback-su,oklch(var(--su)/.2));--tw-text-opacity:1;color:var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity)));--alert-bg:var(--fallback-su,oklch(var(--su)/1));--alert-bg-mix:var(--fallback-b1,oklch(var(--b1)/1))}.alert-warning{border-color:var(--fallback-wa,oklch(var(--wa)/.2));--tw-text-opacity:1;color:var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity)));--alert-bg:var(--fallback-wa,oklch(var(--wa)/1));--alert-bg-mix:var(--fallback-b1,oklch(var(--b1)/1))}.alert-error{border-color:var(--fallback-er,oklch(var(--er)/.2));--tw-text-opacity:1;color:var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)));--alert-bg:var(--fallback-er,oklch(var(--er)/1));--alert-bg-mix:var(--fallback-b1,oklch(var(--b1)/1))}.avatar-group :where(.avatar){border-radius:9999px;border-width:4px;overflow:hidden;--tw-border-opacity:1;border-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-border-opacity)))}.badge-neutral{background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));border-color:var(--fallback-n,oklch(var(--n)/var(--tw-border-opacity)));color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)))}.badge-neutral,.badge-primary{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1}.badge-primary{background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)));color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}.badge-secondary{background-color:var(--fallback-s,oklch(var(--s)/var(--tw-bg-opacity)));border-color:var(--fallback-s,oklch(var(--s)/var(--tw-border-opacity)));color:var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity)))}.badge-accent,.badge-secondary{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1}.badge-accent{background-color:var(--fallback-a,oklch(var(--a)/var(--tw-bg-opacity)));border-color:var(--fallback-a,oklch(var(--a)/var(--tw-border-opacity)));color:var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity)))}.badge-success{background-color:var(--fallback-su,oklch(var(--su)/var(--tw-bg-opacity)));color:var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity)))}.badge-success,.badge-warning{border-color:transparent;--tw-bg-opacity:1;--tw-text-opacity:1}.badge-warning{background-color:var(--fallback-wa,oklch(var(--wa)/var(--tw-bg-opacity)));color:var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity)))}.badge-error{border-color:transparent;--tw-bg-opacity:1;background-color:var(--fallback-er,oklch(var(--er)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)))}.badge-ghost{--tw-border-opacity:1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}.badge-outline{border-color:currentColor;--tw-border-opacity:0.5;background-color:transparent;color:currentColor}.badge-outline.badge-neutral{--tw-text-opacity:1;color:var(--fallback-n,oklch(var(--n)/var(--tw-text-opacity)))}.badge-outline.badge-primary{--tw-text-opacity:1;color:var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity)))}.badge-outline.badge-secondary{--tw-text-opacity:1;color:var(--fallback-s,oklch(var(--s)/var(--tw-text-opacity)))}.badge-outline.badge-accent{--tw-text-opacity:1;color:var(--fallback-a,oklch(var(--a)/var(--tw-text-opacity)))}.badge-outline.badge-info{--tw-text-opacity:1;color:var(--fallback-in,oklch(var(--in)/var(--tw-text-opacity)))}.badge-outline.badge-success{--tw-text-opacity:1;color:var(--fallback-su,oklch(var(--su)/var(--tw-text-opacity)))}.badge-outline.badge-warning{--tw-text-opacity:1;color:var(--fallback-wa,oklch(var(--wa)/var(--tw-text-opacity)))}.badge-outline.badge-error{--tw-text-opacity:1;color:var(--fallback-er,oklch(var(--er)/var(--tw-text-opacity)))}.btm-nav>:where(.active){border-top-width:2px;--tw-bg-opacity:1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)))}.btm-nav>.disabled,.btm-nav>[disabled]{pointer-events:none;--tw-border-opacity:0;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-bg-opacity:0.1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-text-opacity:0.2}.btm-nav>* .label{font-size:1rem;line-height:1.5rem}.btn:active:focus,.btn:active:hover{animation:button-pop 0s ease-out;transform:scale(var(--btn-focus-scale,.97))}@supports not (color:oklch(0 0 0)){.btn{background-color:var(--btn-color,var(--fallback-b2));border-color:var(--btn-color,var(--fallback-b2))}.btn-primary{--btn-color:var(--fallback-p)}.btn-neutral{--btn-color:var(--fallback-n)}.btn-info{--btn-color:var(--fallback-in)}.btn-success{--btn-color:var(--fallback-su)}.btn-warning{--btn-color:var(--fallback-wa)}.btn-error{--btn-color:var(--fallback-er)}}@supports (color:color-mix(in oklab,black,black)){.btn-active{background-color:color-mix(in oklab,oklch(var(--btn-color,var(--b3))/var(--tw-bg-opacity,1)) 90%,#000);border-color:color-mix(in oklab,oklch(var(--btn-color,var(--b3))/var(--tw-border-opacity,1)) 90%,#000)}.btn-outline.btn-primary.btn-active{background-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 90%,#000)}.btn-outline.btn-secondary.btn-active{background-color:color-mix(in oklab,var(--fallback-s,oklch(var(--s)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-s,oklch(var(--s)/1)) 90%,#000)}.btn-outline.btn-accent.btn-active{background-color:color-mix(in oklab,var(--fallback-a,oklch(var(--a)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-a,oklch(var(--a)/1)) 90%,#000)}.btn-outline.btn-success.btn-active{background-color:color-mix(in oklab,var(--fallback-su,oklch(var(--su)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-su,oklch(var(--su)/1)) 90%,#000)}.btn-outline.btn-info.btn-active{background-color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 90%,#000)}.btn-outline.btn-warning.btn-active{background-color:color-mix(in oklab,var(--fallback-wa,oklch(var(--wa)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-wa,oklch(var(--wa)/1)) 90%,#000)}.btn-outline.btn-error.btn-active{background-color:color-mix(in oklab,var(--fallback-er,oklch(var(--er)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-er,oklch(var(--er)/1)) 90%,#000)}}.btn:focus-visible{outline-offset:2px;outline-style:solid;outline-width:2px}.btn-primary{--tw-text-opacity:1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)));outline-color:var(--fallback-p,oklch(var(--p)/1))}@supports (color:oklch(0 0 0)){.btn-primary{--btn-color:var(--p)}.btn-neutral{--btn-color:var(--n)}.btn-info{--btn-color:var(--in)}.btn-success{--btn-color:var(--su)}.btn-warning{--btn-color:var(--wa)}.btn-error{--btn-color:var(--er)}}.btn-neutral{--tw-text-opacity:1;color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)));outline-color:var(--fallback-n,oklch(var(--n)/1))}.btn-info{--tw-text-opacity:1;color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)));outline-color:var(--fallback-in,oklch(var(--in)/1))}.btn-success{--tw-text-opacity:1;color:var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity)));outline-color:var(--fallback-su,oklch(var(--su)/1))}.btn-warning{--tw-text-opacity:1;color:var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity)));outline-color:var(--fallback-wa,oklch(var(--wa)/1))}.btn-error{--tw-text-opacity:1;color:var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)));outline-color:var(--fallback-er,oklch(var(--er)/1))}.btn.glass{--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);outline-color:currentColor}.btn.glass.btn-active{--glass-opacity:25%;--glass-border-opacity:15%}.btn-ghost{background-color:transparent;border-color:transparent;border-width:1px;color:currentColor;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);outline-color:currentColor}.btn-ghost.btn-active{background-color:var(--fallback-bc,oklch(var(--bc)/.2));border-color:transparent}.btn-link.btn-active{background-color:transparent;border-color:transparent;text-decoration-line:underline}.btn-outline{background-color:transparent;border-color:currentColor;--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.btn-outline.btn-active{--tw-border-opacity:1;border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-b1,oklch(var(--b1)/var(--tw-text-opacity)))}.btn-outline.btn-primary{--tw-text-opacity:1;color:var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity)))}.btn-outline.btn-primary.btn-active{--tw-text-opacity:1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}.btn-outline.btn-secondary{--tw-text-opacity:1;color:var(--fallback-s,oklch(var(--s)/var(--tw-text-opacity)))}.btn-outline.btn-secondary.btn-active{--tw-text-opacity:1;color:var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity)))}.btn-outline.btn-accent{--tw-text-opacity:1;color:var(--fallback-a,oklch(var(--a)/var(--tw-text-opacity)))}.btn-outline.btn-accent.btn-active{--tw-text-opacity:1;color:var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity)))}.btn-outline.btn-success{--tw-text-opacity:1;color:var(--fallback-su,oklch(var(--su)/var(--tw-text-opacity)))}.btn-outline.btn-success.btn-active{--tw-text-opacity:1;color:var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity)))}.btn-outline.btn-info{--tw-text-opacity:1;color:var(--fallback-in,oklch(var(--in)/var(--tw-text-opacity)))}.btn-outline.btn-info.btn-active{--tw-text-opacity:1;color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)))}.btn-outline.btn-warning{--tw-text-opacity:1;color:var(--fallback-wa,oklch(var(--wa)/var(--tw-text-opacity)))}.btn-outline.btn-warning.btn-active{--tw-text-opacity:1;color:var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity)))}.btn-outline.btn-error{--tw-text-opacity:1;color:var(--fallback-er,oklch(var(--er)/var(--tw-text-opacity)))}.btn-outline.btn-error.btn-active{--tw-text-opacity:1;color:var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)))}.btn.btn-disabled,.btn:disabled,.btn[disabled]{--tw-border-opacity:0;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-bg-opacity:0.2;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-text-opacity:0.2}.btn:is(input[type=checkbox]:checked),.btn:is(input[type=radio]:checked){--tw-border-opacity:1;border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}.btn:is(input[type=checkbox]:checked):focus-visible,.btn:is(input[type=radio]:checked):focus-visible{outline-color:var(--fallback-p,oklch(var(--p)/1))}@keyframes button-pop{0%{transform:scale(var(--btn-focus-scale,.98))}40%{transform:scale(1.02)}to{transform:scale(1)}}.card :where(figure:first-child){border-end-end-radius:unset;border-end-start-radius:unset;border-start-end-radius:inherit;border-start-start-radius:inherit;overflow:hidden}.card :where(figure:last-child){border-end-end-radius:inherit;border-end-start-radius:inherit;border-start-end-radius:unset;border-start-start-radius:unset;overflow:hidden}.card:focus-visible{outline:2px solid currentColor;outline-offset:2px}.card.bordered{border-width:1px;--tw-border-opacity:1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)))}.card.compact .card-body{font-size:.875rem;line-height:1.25rem;padding:1rem}.card-title{align-items:center;display:flex;font-size:1.25rem;font-weight:600;gap:.5rem;line-height:1.75rem}.card.image-full :where(figure){border-radius:inherit;overflow:hidden}.checkbox:focus{box-shadow:none}.checkbox:focus-visible{outline-color:var(--fallback-bc,oklch(var(--bc)/1));outline-offset:2px;outline-style:solid;outline-width:2px}.checkbox:checked,.checkbox[aria-checked=true],.checkbox[checked=true]{animation:checkmark var(--animation-input,.2s) ease-out;background-color:var(--chkbg);background-image:linear-gradient(-45deg,transparent 65%,var(--chkbg) 65.99%),linear-gradient(45deg,transparent 75%,var(--chkbg) 75.99%),linear-gradient(-45deg,var(--chkbg) 40%,transparent 40.99%),linear-gradient(45deg,var(--chkbg) 30%,var(--chkfg) 30.99%,var(--chkfg) 40%,transparent 40.99%),linear-gradient(-45deg,var(--chkfg) 50%,var(--chkbg) 50.99%);background-repeat:no-repeat}.checkbox:indeterminate{--tw-bg-opacity:1;animation:checkmark var(--animation-input,.2s) ease-out;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));background-image:linear-gradient(90deg,transparent 80%,var(--chkbg) 80%),linear-gradient(-90deg,transparent 80%,var(--chkbg) 80%),linear-gradient(0deg,var(--chkbg) 43%,var(--chkfg) 43%,var(--chkfg) 57%,var(--chkbg) 57%);background-repeat:no-repeat}.checkbox:disabled{border-color:transparent;cursor:not-allowed;--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));opacity:.2}@keyframes checkmark{0%{background-position-y:5px}50%{background-position-y:-2px}to{background-position-y:0}}details.collapse{width:100%}details.collapse summary{display:block;outline:2px solid transparent;outline-offset:2px;position:relative}details.collapse summary::-webkit-details-marker{display:none}.collapse:focus-visible{outline-color:var(--fallback-bc,oklch(var(--bc)/1));outline-offset:2px;outline-style:solid;outline-width:2px}.collapse:has(.collapse-title:focus-visible),.collapse:has(>input[type=checkbox]:focus-visible),.collapse:has(>input[type=radio]:focus-visible){outline-color:var(--fallback-bc,oklch(var(--bc)/1));outline-offset:2px;outline-style:solid;outline-width:2px}.collapse-arrow>.collapse-title:after{--tw-translate-y:-100%;--tw-rotate:45deg;box-shadow:2px 2px;content:"";top:1.9rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));transform-origin:75% 75%;transition-duration:.15s;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-timing-function:cubic-bezier(0,0,.2,1)}.collapse-arrow>.collapse-title:after,.collapse-plus>.collapse-title:after{display:block;height:.5rem;inset-inline-end:1.4rem;pointer-events:none;position:absolute;transition-property:all;width:.5rem}.collapse-plus>.collapse-title:after{content:"+";top:.9rem;transition-duration:.3s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-timing-function:cubic-bezier(0,0,.2,1)}.collapse:not(.collapse-open):not(.collapse-close)>.collapse-title,.collapse:not(.collapse-open):not(.collapse-close)>input[type=checkbox],.collapse:not(.collapse-open):not(.collapse-close)>input[type=radio]:not(:checked){cursor:pointer}.collapse:focus:not(.collapse-open):not(.collapse-close):not(.collapse[open])>.collapse-title{cursor:unset}.collapse-title{position:relative}:where(.collapse>input[type=checkbox]),:where(.collapse>input[type=radio]){z-index:1}.collapse-title,:where(.collapse>input[type=checkbox]),:where(.collapse>input[type=radio]){min-height:3.75rem;padding:1rem;padding-inline-end:3rem;transition:background-color .2s ease-out;width:100%}.collapse-open>:where(.collapse-content),.collapse:focus:not(.collapse-close)>:where(.collapse-content),.collapse:not(.collapse-close)>:where(input[type=checkbox]:checked~.collapse-content),.collapse:not(.collapse-close)>:where(input[type=radio]:checked~.collapse-content),.collapse[open]>:where(.collapse-content){padding-bottom:1rem;transition:padding .2s ease-out,background-color .2s ease-out}.collapse-arrow:focus:not(.collapse-close)>.collapse-title:after,.collapse-arrow:not(.collapse-close)>input[type=checkbox]:checked~.collapse-title:after,.collapse-arrow:not(.collapse-close)>input[type=radio]:checked~.collapse-title:after,.collapse-open.collapse-arrow>.collapse-title:after,.collapse[open].collapse-arrow>.collapse-title:after{--tw-translate-y:-50%;--tw-rotate:225deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.collapse-open.collapse-plus>.collapse-title:after,.collapse-plus:focus:not(.collapse-close)>.collapse-title:after,.collapse-plus:not(.collapse-close)>input[type=checkbox]:checked~.collapse-title:after,.collapse-plus:not(.collapse-close)>input[type=radio]:checked~.collapse-title:after,.collapse[open].collapse-plus>.collapse-title:after{content:"−"}.divider:not(:empty){gap:1rem}.drawer-toggle:focus-visible~.drawer-content label.drawer-button{outline-offset:2px;outline-style:solid;outline-width:2px}.dropdown.dropdown-open .dropdown-content,.dropdown:focus .dropdown-content,.dropdown:focus-within .dropdown-content{--tw-scale-x:1;--tw-scale-y:1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.file-input-bordered{--tw-border-opacity:0.2}.file-input:focus{outline-color:var(--fallback-bc,oklch(var(--bc)/.2));outline-offset:2px;outline-style:solid;outline-width:2px}.file-input-disabled,.file-input[disabled]{cursor:not-allowed;--tw-border-opacity:1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));--tw-text-opacity:0.2}.file-input-disabled::-moz-placeholder,.file-input[disabled]::-moz-placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.file-input-disabled::placeholder,.file-input[disabled]::placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.file-input-disabled::file-selector-button,.file-input[disabled]::file-selector-button{--tw-border-opacity:0;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-bg-opacity:0.2;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-text-opacity:0.2}.footer-title{font-weight:700;margin-bottom:.5rem;opacity:.6;text-transform:uppercase}.label-text{font-size:.875rem;line-height:1.25rem}.label-text,.label-text-alt{--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}.label-text-alt{font-size:.75rem;line-height:1rem}.\!input input{--tw-bg-opacity:1!important;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)))!important;background-color:transparent!important}.input input{--tw-bg-opacity:1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));background-color:transparent}.\!input input:focus{outline:2px solid transparent!important;outline-offset:2px!important}.input input:focus{outline:2px solid transparent;outline-offset:2px}.\!input[list]::-webkit-calendar-picker-indicator{line-height:1em!important}.input[list]::-webkit-calendar-picker-indicator{line-height:1em}.input-bordered{border-color:var(--fallback-bc,oklch(var(--bc)/.2))}.input:focus,.input:focus-within{border-color:var(--fallback-bc,oklch(var(--bc)/.2));box-shadow:none;outline-color:var(--fallback-bc,oklch(var(--bc)/.2));outline-offset:2px;outline-style:solid;outline-width:2px}.\!input:focus,.\!input:focus-within{border-color:var(--fallback-bc,oklch(var(--bc)/.2))!important;box-shadow:none!important;outline-color:var(--fallback-bc,oklch(var(--bc)/.2))!important;outline-offset:2px!important;outline-style:solid!important;outline-width:2px!important}.input-primary{--tw-border-opacity:1;border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)))}.input-primary:focus,.input-primary:focus-within{--tw-border-opacity:1;border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)));outline-color:var(--fallback-p,oklch(var(--p)/1))}.input-error{--tw-border-opacity:1;border-color:var(--fallback-er,oklch(var(--er)/var(--tw-border-opacity)))}.input-error:focus,.input-error:focus-within{--tw-border-opacity:1;border-color:var(--fallback-er,oklch(var(--er)/var(--tw-border-opacity)));outline-color:var(--fallback-er,oklch(var(--er)/1))}.input-disabled,.input:disabled,.input[disabled]{cursor:not-allowed;--tw-border-opacity:1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));color:var(--fallback-bc,oklch(var(--bc)/.4))}.\!input:disabled,.\!input[disabled]{cursor:not-allowed!important;--tw-border-opacity:1!important;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)))!important;--tw-bg-opacity:1!important;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)))!important;color:var(--fallback-bc,oklch(var(--bc)/.4))!important}.input-disabled::-moz-placeholder,.input:disabled::-moz-placeholder,.input[disabled]::-moz-placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.input-disabled::placeholder,.input:disabled::placeholder,.input[disabled]::placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.\!input:disabled::-moz-placeholder,.\!input[disabled]::-moz-placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)))!important;--tw-placeholder-opacity:0.2!important}.\!input:disabled::placeholder,.\!input[disabled]::placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)))!important;--tw-placeholder-opacity:0.2!important}.\!input::-webkit-date-and-time-value{text-align:inherit!important}.input::-webkit-date-and-time-value{text-align:inherit}.join>:where(:not(:first-child)){margin-bottom:0;margin-top:0;margin-inline-start:-1px}.join-item:focus{isolation:isolate}.link-primary{--tw-text-opacity:1;color:var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity)))}@supports (color:color-mix(in oklab,black,black)){@media (hover:hover){.link-primary:hover{color:color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 80%,#000)}.link-info:hover{color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 80%,#000)}}}.link-info{--tw-text-opacity:1;color:var(--fallback-in,oklch(var(--in)/var(--tw-text-opacity)))}.link:focus{outline:2px solid transparent;outline-offset:2px}.link:focus-visible{outline:2px solid currentColor;outline-offset:2px}.loading{aspect-ratio:1/1;background-color:currentColor;display:inline-block;-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:100%;mask-size:100%;pointer-events:none;width:1.5rem}.loading,.loading-spinner{-webkit-mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' stroke='%23000'%3E%3Cstyle%3E@keyframes spinner_zKoa{to{transform:rotate(360deg)}}@keyframes spinner_YpZS{0%25{stroke-dasharray:0 150;stroke-dashoffset:0}47.5%25{stroke-dasharray:42 150;stroke-dashoffset:-16}95%25,to{stroke-dasharray:42 150;stroke-dashoffset:-59}}%3C/style%3E%3Cg style='transform-origin:center;animation:spinner_zKoa 2s linear infinite'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' class='spinner_V8m1' style='stroke-linecap:round;animation:spinner_YpZS 1.5s ease-out infinite'/%3E%3C/g%3E%3C/svg%3E");mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' stroke='%23000'%3E%3Cstyle%3E@keyframes spinner_zKoa{to{transform:rotate(360deg)}}@keyframes spinner_YpZS{0%25{stroke-dasharray:0 150;stroke-dashoffset:0}47.5%25{stroke-dasharray:42 150;stroke-dashoffset:-16}95%25,to{stroke-dasharray:42 150;stroke-dashoffset:-59}}%3C/style%3E%3Cg style='transform-origin:center;animation:spinner_zKoa 2s linear infinite'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' class='spinner_V8m1' style='stroke-linecap:round;animation:spinner_YpZS 1.5s ease-out infinite'/%3E%3C/g%3E%3C/svg%3E")}.loading-dots{-webkit-mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cstyle%3E@keyframes spinner_8HQG{0%25,57.14%25{animation-timing-function:cubic-bezier(.33,.66,.66,1);transform:translate(0)}28.57%25{animation-timing-function:cubic-bezier(.33,0,.66,.33);transform:translateY(-6px)}to{transform:translate(0)}}.spinner_qM83{animation:spinner_8HQG 1.05s infinite}%3C/style%3E%3Ccircle cx='4' cy='12' r='3' class='spinner_qM83'/%3E%3Ccircle cx='12' cy='12' r='3' class='spinner_qM83' style='animation-delay:.1s'/%3E%3Ccircle cx='20' cy='12' r='3' class='spinner_qM83' style='animation-delay:.2s'/%3E%3C/svg%3E");mask-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cstyle%3E@keyframes spinner_8HQG{0%25,57.14%25{animation-timing-function:cubic-bezier(.33,.66,.66,1);transform:translate(0)}28.57%25{animation-timing-function:cubic-bezier(.33,0,.66,.33);transform:translateY(-6px)}to{transform:translate(0)}}.spinner_qM83{animation:spinner_8HQG 1.05s infinite}%3C/style%3E%3Ccircle cx='4' cy='12' r='3' class='spinner_qM83'/%3E%3Ccircle cx='12' cy='12' r='3' class='spinner_qM83' style='animation-delay:.1s'/%3E%3Ccircle cx='20' cy='12' r='3' class='spinner_qM83' style='animation-delay:.2s'/%3E%3C/svg%3E")}.loading-sm{width:1.25rem}.loading-md{width:1.5rem}.loading-lg{width:2.5rem}:where(.menu li:empty){--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));height:1px;margin:.5rem 1rem;opacity:.1}.menu :where(li ul):before{bottom:.75rem;inset-inline-start:0;position:absolute;top:.75rem;width:1px;--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));content:"";opacity:.1}.menu :where(li:not(.menu-title)>:not(ul,details,.menu-title,.btn)),.menu :where(li:not(.menu-title)>details>summary:not(.menu-title)){border-radius:var(--rounded-btn,.5rem);padding:.5rem 1rem;text-align:start;text-wrap:balance;transition-duration:.2s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-timing-function:cubic-bezier(0,0,.2,1)}:where(.menu li:not(.menu-title,.disabled)>:not(ul,details,.menu-title)):is(summary):not(.active,.btn):focus-visible,:where(.menu li:not(.menu-title,.disabled)>:not(ul,details,.menu-title)):not(summary,.active,.btn).focus,:where(.menu li:not(.menu-title,.disabled)>:not(ul,details,.menu-title)):not(summary,.active,.btn):focus,:where(.menu li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):is(summary):not(.active,.btn):focus-visible,:where(.menu li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(summary,.active,.btn).focus,:where(.menu li:not(.menu-title,.disabled)>details>summary:not(.menu-title)):not(summary,.active,.btn):focus{background-color:var(--fallback-bc,oklch(var(--bc)/.1));cursor:pointer;--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));outline:2px solid transparent;outline-offset:2px}.menu li>:not(ul,.menu-title,details,.btn).active,.menu li>:not(ul,.menu-title,details,.btn):active,.menu li>details>summary:active{--tw-bg-opacity:1;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)))}.menu :where(li>details>summary)::-webkit-details-marker{display:none}.menu :where(li>.menu-dropdown-toggle):after,.menu :where(li>details>summary):after{box-shadow:2px 2px;content:"";display:block;height:.5rem;justify-self:end;margin-top:-.5rem;pointer-events:none;transform:rotate(45deg);transform-origin:75% 75%;transition-duration:.3s;transition-property:transform,margin-top;transition-timing-function:cubic-bezier(.4,0,.2,1);width:.5rem}.menu :where(li>.menu-dropdown-toggle.menu-dropdown-show):after,.menu :where(li>details[open]>summary):after{margin-top:0;transform:rotate(225deg)}.mockup-phone .camera{background:#000;border-bottom-left-radius:17px;border-bottom-right-radius:17px;height:25px;left:0;margin:0 auto;position:relative;top:0;width:150px;z-index:11}.mockup-phone .camera:before{background-color:#0c0b0e;border-radius:5px;content:"";height:4px;left:50%;position:absolute;top:35%;transform:translate(-50%,-50%);width:50px}.mockup-phone .camera:after{background-color:#0f0b25;border-radius:5px;content:"";height:8px;left:70%;position:absolute;top:20%;width:8px}.mockup-phone .display{border-radius:40px;margin-top:-25px;overflow:hidden}.mockup-browser .mockup-browser-toolbar .\!input{display:block!important;height:1.75rem!important;margin-left:auto!important;margin-right:auto!important;overflow:hidden!important;position:relative!important;text-overflow:ellipsis!important;white-space:nowrap!important;width:24rem!important;--tw-bg-opacity:1!important;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)))!important;direction:ltr!important;padding-left:2rem!important}.mockup-browser .mockup-browser-toolbar .input{display:block;height:1.75rem;margin-left:auto;margin-right:auto;overflow:hidden;position:relative;text-overflow:ellipsis;white-space:nowrap;width:24rem;--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));direction:ltr;padding-left:2rem}.mockup-browser .mockup-browser-toolbar .\!input:before{aspect-ratio:1/1!important;content:""!important;height:.75rem!important;left:.5rem!important;position:absolute!important;top:50%!important;--tw-translate-y:-50%!important;border-color:currentColor!important;border-radius:9999px!important;border-width:2px!important;opacity:.6!important;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))!important}.mockup-browser .mockup-browser-toolbar .input:before{aspect-ratio:1/1;content:"";height:.75rem;left:.5rem;position:absolute;top:50%;--tw-translate-y:-50%;border-color:currentColor;border-radius:9999px;border-width:2px;opacity:.6;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.mockup-browser .mockup-browser-toolbar .\!input:after{content:""!important;height:.5rem!important;left:1.25rem!important;position:absolute!important;top:50%!important;--tw-translate-y:25%!important;--tw-rotate:-45deg!important;border-color:currentColor!important;border-radius:9999px!important;border-width:1px!important;opacity:.6!important;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))!important}.mockup-browser .mockup-browser-toolbar .input:after{content:"";height:.5rem;left:1.25rem;position:absolute;top:50%;--tw-translate-y:25%;--tw-rotate:-45deg;border-color:currentColor;border-radius:9999px;border-width:1px;opacity:.6;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.modal::backdrop,.modal:not(dialog:not(.modal-open)){animation:modal-pop .2s ease-out;background-color:#0006}.modal-backdrop{align-self:stretch;color:transparent;display:grid;grid-column-start:1;grid-row-start:1;justify-self:stretch;z-index:-1}.modal-open .modal-box,.modal-toggle:checked+.modal .modal-box,.modal:target .modal-box,.modal[open] .modal-box{--tw-translate-y:0px;--tw-scale-x:1;--tw-scale-y:1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.modal-action>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.5rem*var(--tw-space-x-reverse))}@keyframes modal-pop{0%{opacity:0}}.progress::-moz-progress-bar{border-radius:var(--rounded-box,1rem);--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)))}.progress-primary::-moz-progress-bar{border-radius:var(--rounded-box,1rem);--tw-bg-opacity:1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)))}.progress-secondary::-moz-progress-bar{border-radius:var(--rounded-box,1rem);--tw-bg-opacity:1;background-color:var(--fallback-s,oklch(var(--s)/var(--tw-bg-opacity)))}.progress-accent::-moz-progress-bar{border-radius:var(--rounded-box,1rem);--tw-bg-opacity:1;background-color:var(--fallback-a,oklch(var(--a)/var(--tw-bg-opacity)))}.progress-info::-moz-progress-bar{border-radius:var(--rounded-box,1rem);--tw-bg-opacity:1;background-color:var(--fallback-in,oklch(var(--in)/var(--tw-bg-opacity)))}.progress-success::-moz-progress-bar{border-radius:var(--rounded-box,1rem);--tw-bg-opacity:1;background-color:var(--fallback-su,oklch(var(--su)/var(--tw-bg-opacity)))}.progress-warning::-moz-progress-bar{border-radius:var(--rounded-box,1rem);--tw-bg-opacity:1;background-color:var(--fallback-wa,oklch(var(--wa)/var(--tw-bg-opacity)))}.progress:indeterminate{--progress-color:var(--fallback-bc,oklch(var(--bc)/1));animation:progress-loading 5s ease-in-out infinite;background-image:repeating-linear-gradient(90deg,var(--progress-color) -1%,var(--progress-color) 10%,transparent 10%,transparent 90%);background-position-x:15%;background-size:200%}.progress-primary:indeterminate{--progress-color:var(--fallback-p,oklch(var(--p)/1))}.progress-secondary:indeterminate{--progress-color:var(--fallback-s,oklch(var(--s)/1))}.progress-accent:indeterminate{--progress-color:var(--fallback-a,oklch(var(--a)/1))}.progress-info:indeterminate{--progress-color:var(--fallback-in,oklch(var(--in)/1))}.progress-success:indeterminate{--progress-color:var(--fallback-su,oklch(var(--su)/1))}.progress-warning:indeterminate{--progress-color:var(--fallback-wa,oklch(var(--wa)/1))}.progress::-webkit-progress-bar{background-color:transparent;border-radius:var(--rounded-box,1rem)}.progress::-webkit-progress-value{border-radius:var(--rounded-box,1rem);--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)))}.progress-primary::-webkit-progress-value{--tw-bg-opacity:1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)))}.progress-secondary::-webkit-progress-value{--tw-bg-opacity:1;background-color:var(--fallback-s,oklch(var(--s)/var(--tw-bg-opacity)))}.progress-accent::-webkit-progress-value{--tw-bg-opacity:1;background-color:var(--fallback-a,oklch(var(--a)/var(--tw-bg-opacity)))}.progress-info::-webkit-progress-value{--tw-bg-opacity:1;background-color:var(--fallback-in,oklch(var(--in)/var(--tw-bg-opacity)))}.progress-success::-webkit-progress-value{--tw-bg-opacity:1;background-color:var(--fallback-su,oklch(var(--su)/var(--tw-bg-opacity)))}.progress-warning::-webkit-progress-value{--tw-bg-opacity:1;background-color:var(--fallback-wa,oklch(var(--wa)/var(--tw-bg-opacity)))}.progress:indeterminate::-moz-progress-bar{animation:progress-loading 5s ease-in-out infinite;background-color:transparent;background-image:repeating-linear-gradient(90deg,var(--progress-color) -1%,var(--progress-color) 10%,transparent 10%,transparent 90%);background-position-x:15%;background-size:200%}@keyframes progress-loading{50%{background-position-x:-115%}}.radio:focus{box-shadow:none}.radio:focus-visible{outline-color:var(--fallback-bc,oklch(var(--bc)/1));outline-offset:2px;outline-style:solid;outline-width:2px}.radio:checked,.radio[aria-checked=true]{--tw-bg-opacity:1;animation:radiomark var(--animation-input,.2s) ease-out;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));background-image:none;box-shadow:0 0 0 4px var(--fallback-b1,oklch(var(--b1)/1)) inset,0 0 0 4px var(--fallback-b1,oklch(var(--b1)/1)) inset}.radio-primary{--chkbg:var(--p);--tw-border-opacity:1;border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)))}.radio-primary:focus-visible{outline-color:var(--fallback-p,oklch(var(--p)/1))}.radio-primary:checked,.radio-primary[aria-checked=true]{--tw-border-opacity:1;border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}.radio:disabled{cursor:not-allowed;opacity:.2}@keyframes radiomark{0%{box-shadow:0 0 0 12px var(--fallback-b1,oklch(var(--b1)/1)) inset,0 0 0 12px var(--fallback-b1,oklch(var(--b1)/1)) inset}50%{box-shadow:0 0 0 3px var(--fallback-b1,oklch(var(--b1)/1)) inset,0 0 0 3px var(--fallback-b1,oklch(var(--b1)/1)) inset}to{box-shadow:0 0 0 4px var(--fallback-b1,oklch(var(--b1)/1)) inset,0 0 0 4px var(--fallback-b1,oklch(var(--b1)/1)) inset}}.range:focus-visible::-webkit-slider-thumb{--focus-shadow:0 0 0 6px var(--fallback-b1,oklch(var(--b1)/1)) inset,0 0 0 2rem var(--range-shdw) inset}.range:focus-visible::-moz-range-thumb{--focus-shadow:0 0 0 6px var(--fallback-b1,oklch(var(--b1)/1)) inset,0 0 0 2rem var(--range-shdw) inset}.range::-webkit-slider-runnable-track{background-color:var(--fallback-bc,oklch(var(--bc)/.1));border-radius:var(--rounded-box,1rem);height:.5rem;width:100%}.range::-moz-range-track{background-color:var(--fallback-bc,oklch(var(--bc)/.1));border-radius:var(--rounded-box,1rem);height:.5rem;width:100%}.range::-webkit-slider-thumb{border-radius:var(--rounded-box,1rem);border-style:none;height:1.5rem;position:relative;width:1.5rem;--tw-bg-opacity:1;appearance:none;-webkit-appearance:none;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));color:var(--range-shdw);top:50%;transform:translateY(-50%);--filler-size:100rem;--filler-offset:0.6rem;box-shadow:0 0 0 3px var(--range-shdw) inset,var(--focus-shadow,0 0),calc(var(--filler-size)*-1 - var(--filler-offset)) 0 0 var(--filler-size)}.range::-moz-range-thumb{border-radius:var(--rounded-box,1rem);border-style:none;height:1.5rem;position:relative;width:1.5rem;--tw-bg-opacity:1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));color:var(--range-shdw);top:50%;--filler-size:100rem;--filler-offset:0.5rem;box-shadow:0 0 0 3px var(--range-shdw) inset,var(--focus-shadow,0 0),calc(var(--filler-size)*-1 - var(--filler-offset)) 0 0 var(--filler-size)}.range-primary{--range-shdw:var(--fallback-p,oklch(var(--p)/1))}.range-error{--range-shdw:var(--fallback-er,oklch(var(--er)/1))}@keyframes rating-pop{0%{transform:translateY(-.125em)}40%{transform:translateY(-.125em)}to{transform:translateY(0)}}.select-bordered,.select:focus{border-color:var(--fallback-bc,oklch(var(--bc)/.2))}.select:focus{box-shadow:none;outline-color:var(--fallback-bc,oklch(var(--bc)/.2));outline-offset:2px;outline-style:solid;outline-width:2px}.select-disabled,.select:disabled,.select[disabled]{cursor:not-allowed;--tw-border-opacity:1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));--tw-text-opacity:0.2}.select-disabled::-moz-placeholder,.select:disabled::-moz-placeholder,.select[disabled]::-moz-placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.select-disabled::placeholder,.select:disabled::placeholder,.select[disabled]::placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.select-multiple,.select[multiple],.select[size].select:not([size="1"]){background-image:none;padding-right:1rem}[dir=rtl] .select{background-position:12px calc(1px + 50%),16px calc(1px + 50%)}@keyframes skeleton{0%{background-position:150%}to{background-position:-50%}}:where(.stats)>:not([hidden])~:not([hidden]){--tw-divide-x-reverse:0;--tw-divide-y-reverse:0;border-width:calc(0px*(1 - var(--tw-divide-y-reverse))) calc(1px*var(--tw-divide-x-reverse)) calc(0px*var(--tw-divide-y-reverse)) calc(1px*(1 - var(--tw-divide-x-reverse)))}:is([dir=rtl] .stats>:not([hidden])~:not([hidden])){--tw-divide-x-reverse:1}.steps .step:before{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));content:"";height:.5rem;margin-inline-start:-100%;top:0;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));width:100%}.steps .step:after,.steps .step:before{grid-column-start:1;grid-row-start:1;--tw-bg-opacity:1;background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)));--tw-text-opacity:1}.steps .step:after{border-radius:9999px;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));content:counter(step);counter-increment:step;display:grid;height:2rem;place-items:center;place-self:center;position:relative;width:2rem;z-index:1}.steps .step:first-child:before{content:none}.steps .step[data-content]:after{content:attr(data-content)}.steps .step-neutral+.step-neutral:before,.steps .step-neutral:after{--tw-bg-opacity:1;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)))}.steps .step-primary+.step-primary:before,.steps .step-primary:after{--tw-bg-opacity:1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}.steps .step-secondary+.step-secondary:before,.steps .step-secondary:after{--tw-bg-opacity:1;background-color:var(--fallback-s,oklch(var(--s)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity)))}.steps .step-accent+.step-accent:before,.steps .step-accent:after{--tw-bg-opacity:1;background-color:var(--fallback-a,oklch(var(--a)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity)))}.steps .step-info+.step-info:before,.steps .step-info:after{--tw-bg-opacity:1;background-color:var(--fallback-in,oklch(var(--in)/var(--tw-bg-opacity)))}.steps .step-info:after{--tw-text-opacity:1;color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)))}.steps .step-success+.step-success:before,.steps .step-success:after{--tw-bg-opacity:1;background-color:var(--fallback-su,oklch(var(--su)/var(--tw-bg-opacity)))}.steps .step-success:after{--tw-text-opacity:1;color:var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity)))}.steps .step-warning+.step-warning:before,.steps .step-warning:after{--tw-bg-opacity:1;background-color:var(--fallback-wa,oklch(var(--wa)/var(--tw-bg-opacity)))}.steps .step-warning:after{--tw-text-opacity:1;color:var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity)))}.steps .step-error+.step-error:before,.steps .step-error:after{--tw-bg-opacity:1;background-color:var(--fallback-er,oklch(var(--er)/var(--tw-bg-opacity)))}.steps .step-error:after{--tw-text-opacity:1;color:var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)))}.tabs-lifted>.tab:focus-visible{border-end-end-radius:0;border-end-start-radius:0}.tab.tab-active:not(.tab-disabled):not([disabled]),.tab:is(input:checked){border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));--tw-border-opacity:1;--tw-text-opacity:1}.tab:focus{outline:2px solid transparent;outline-offset:2px}.tab:focus-visible{outline:2px solid currentColor;outline-offset:-5px}.tab-disabled,.tab[disabled]{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));cursor:not-allowed;--tw-text-opacity:0.2}.tabs-bordered>.tab{border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));--tw-border-opacity:0.2;border-bottom-width:calc(var(--tab-border, 1px) + 1px);border-style:solid}.tabs-lifted>.tab{border:var(--tab-border,1px) solid transparent;border-bottom-color:var(--tab-border-color);border-start-end-radius:var(--tab-radius,.5rem);border-start-start-radius:var(--tab-radius,.5rem);border-width:0 0 var(--tab-border,1px) 0;padding-inline-end:var(--tab-padding,1rem);padding-inline-start:var(--tab-padding,1rem);padding-top:var(--tab-border,1px)}.tabs-lifted>.tab.tab-active:not(.tab-disabled):not([disabled]),.tabs-lifted>.tab:is(input:checked){background-color:var(--tab-bg);border-inline-end-color:var(--tab-border-color);border-inline-start-color:var(--tab-border-color);border-top-color:var(--tab-border-color);border-width:var(--tab-border,1px) var(--tab-border,1px) 0 var(--tab-border,1px);padding-inline-end:calc(var(--tab-padding, 1rem) - var(--tab-border, 1px));padding-bottom:var(--tab-border,1px);padding-inline-start:calc(var(--tab-padding, 1rem) - var(--tab-border, 1px));padding-top:0}.tabs-lifted>.tab.tab-active:not(.tab-disabled):not([disabled]):before,.tabs-lifted>.tab:is(input:checked):before{background-position:0 0,100% 0;background-repeat:no-repeat;background-size:var(--tab-radius,.5rem);bottom:0;content:"";display:block;height:var(--tab-radius,.5rem);position:absolute;width:calc(100% + var(--tab-radius, .5rem)*2);z-index:1;--tab-grad:calc(69% - var(--tab-border, 1px));--radius-start:radial-gradient(circle at top left,transparent var(--tab-grad),var(--tab-border-color) calc(var(--tab-grad) + 0.25px),var(--tab-border-color) calc(var(--tab-grad) + var(--tab-border, 1px)),var(--tab-bg) calc(var(--tab-grad) + var(--tab-border, 1px) + 0.25px));--radius-end:radial-gradient(circle at top right,transparent var(--tab-grad),var(--tab-border-color) calc(var(--tab-grad) + 0.25px),var(--tab-border-color) calc(var(--tab-grad) + var(--tab-border, 1px)),var(--tab-bg) calc(var(--tab-grad) + var(--tab-border, 1px) + 0.25px));background-image:var(--radius-start),var(--radius-end)}.tabs-lifted>.tab.tab-active:not(.tab-disabled):not([disabled]):first-child:before,.tabs-lifted>.tab:is(input:checked):first-child:before{background-image:var(--radius-end);background-position:100% 0}[dir=rtl] .tabs-lifted>.tab.tab-active:not(.tab-disabled):not([disabled]):first-child:before,[dir=rtl] .tabs-lifted>.tab:is(input:checked):first-child:before{background-image:var(--radius-start);background-position:0 0}.tabs-lifted>.tab.tab-active:not(.tab-disabled):not([disabled]):last-child:before,.tabs-lifted>.tab:is(input:checked):last-child:before{background-image:var(--radius-start);background-position:0 0}[dir=rtl] .tabs-lifted>.tab.tab-active:not(.tab-disabled):not([disabled]):last-child:before,[dir=rtl] .tabs-lifted>.tab:is(input:checked):last-child:before{background-image:var(--radius-end);background-position:100% 0}.tabs-lifted>.tab-active:not(.tab-disabled):not([disabled])+.tabs-lifted .tab-active:not(.tab-disabled):not([disabled]):before,.tabs-lifted>.tab:is(input:checked)+.tabs-lifted .tab:is(input:checked):before{background-image:var(--radius-end);background-position:100% 0}.tabs-boxed{--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));padding:.25rem}.tabs-boxed,.tabs-boxed .tab{border-radius:var(--rounded-btn,.5rem)}.tabs-boxed .tab-active:not(.tab-disabled):not([disabled]),.tabs-boxed :is(input:checked){--tw-bg-opacity:1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}:is([dir=rtl] .table){text-align:right}.table :where(th,td){padding:.75rem 1rem;vertical-align:middle}.table tr.active,.table tr.active:nth-child(2n),.table-zebra tbody tr:nth-child(2n){--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)))}.table-zebra tr.active,.table-zebra tr.active:nth-child(2n),.table-zebra-zebra tbody tr:nth-child(2n){--tw-bg-opacity:1;background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)))}.table :where(thead,tbody) :where(tr:first-child:last-child),.table :where(thead,tbody) :where(tr:not(:last-child)){border-bottom-width:1px;--tw-border-opacity:1;border-bottom-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)))}.table :where(thead,tfoot){color:var(--fallback-bc,oklch(var(--bc)/.6));font-size:.75rem;font-weight:700;line-height:1rem;white-space:nowrap}.textarea-bordered,.textarea:focus{border-color:var(--fallback-bc,oklch(var(--bc)/.2))}.textarea:focus{box-shadow:none;outline-color:var(--fallback-bc,oklch(var(--bc)/.2));outline-offset:2px;outline-style:solid;outline-width:2px}.textarea-disabled,.textarea:disabled,.textarea[disabled]{cursor:not-allowed;--tw-border-opacity:1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));--tw-text-opacity:0.2}.textarea-disabled::-moz-placeholder,.textarea:disabled::-moz-placeholder,.textarea[disabled]::-moz-placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.textarea-disabled::placeholder,.textarea:disabled::placeholder,.textarea[disabled]::placeholder{color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)));--tw-placeholder-opacity:0.2}.timeline hr{height:.25rem}:where(.timeline hr){--tw-bg-opacity:1;background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)))}:where(.timeline:has(.timeline-middle) hr):first-child{border-end-end-radius:var(--rounded-badge,1.9rem);border-end-start-radius:0;border-start-end-radius:var(--rounded-badge,1.9rem);border-start-start-radius:0}:where(.timeline:has(.timeline-middle) hr):last-child{border-end-end-radius:0;border-end-start-radius:var(--rounded-badge,1.9rem);border-start-end-radius:0;border-start-start-radius:var(--rounded-badge,1.9rem)}:where(.timeline:not(:has(.timeline-middle)) :first-child hr:last-child){border-end-end-radius:0;border-end-start-radius:var(--rounded-badge,1.9rem);border-start-end-radius:0;border-start-start-radius:var(--rounded-badge,1.9rem)}:where(.timeline:not(:has(.timeline-middle)) :last-child hr:first-child){border-end-end-radius:var(--rounded-badge,1.9rem);border-end-start-radius:0;border-start-end-radius:var(--rounded-badge,1.9rem);border-start-start-radius:0}.timeline-box{border-radius:var(--rounded-box,1rem);border-width:1px;--tw-border-opacity:1;border-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-border-opacity)));--tw-bg-opacity:1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));padding:.5rem 1rem;--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.toast>*{animation:toast-pop .25s ease-out}@keyframes toast-pop{0%{opacity:0;transform:scale(.9)}to{opacity:1;transform:scale(1)}}[dir=rtl] .toggle{--handleoffsetcalculator:calc(var(--handleoffset)*1)}.toggle:focus-visible{outline-color:var(--fallback-bc,oklch(var(--bc)/.2));outline-offset:2px;outline-style:solid;outline-width:2px}.toggle:hover{background-color:currentColor}.toggle:checked,.toggle[aria-checked=true],.toggle[checked=true]{background-image:none;--handleoffsetcalculator:var(--handleoffset);--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}[dir=rtl] .toggle:checked,[dir=rtl] .toggle[aria-checked=true],[dir=rtl] .toggle[checked=true]{--handleoffsetcalculator:calc(var(--handleoffset)*-1)}.toggle:indeterminate{--tw-text-opacity:1;box-shadow:calc(var(--handleoffset)/2) 0 0 2px var(--tglbg) inset,calc(var(--handleoffset)/-2) 0 0 2px var(--tglbg) inset,0 0 0 2px var(--tglbg) inset;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}[dir=rtl] .toggle:indeterminate{box-shadow:calc(var(--handleoffset)/2) 0 0 2px var(--tglbg) inset,calc(var(--handleoffset)/-2) 0 0 2px var(--tglbg) inset,0 0 0 2px var(--tglbg) inset}.toggle-primary:focus-visible{outline-color:var(--fallback-p,oklch(var(--p)/1))}.toggle-primary:checked,.toggle-primary[aria-checked=true],.toggle-primary[checked=true]{border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)));--tw-border-opacity:0.1;--tw-bg-opacity:1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)))}.toggle-error:focus-visible{outline-color:var(--fallback-er,oklch(var(--er)/1))}.toggle-error:checked,.toggle-error[aria-checked=true],.toggle-error[checked=true]{border-color:var(--fallback-er,oklch(var(--er)/var(--tw-border-opacity)));--tw-border-opacity:0.1;--tw-bg-opacity:1;background-color:var(--fallback-er,oklch(var(--er)/var(--tw-bg-opacity)));--tw-text-opacity:1;color:var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)))}.toggle:disabled{cursor:not-allowed;--tw-border-opacity:1;background-color:transparent;border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));opacity:.3;--togglehandleborder:0 0 0 3px var(--fallback-bc,oklch(var(--bc)/1)) inset,var(--handleoffsetcalculator) 0 0 3px var(--fallback-bc,oklch(var(--bc)/1)) inset}.glass,.glass.btn-active{-webkit-backdrop-filter:blur(var(--glass-blur,40px));backdrop-filter:blur(var(--glass-blur,40px));background-color:transparent;background-image:linear-gradient(135deg,rgb(255 255 255/var(--glass-opacity,30%)) 0,transparent 100%),linear-gradient(var(--glass-reflex-degree,100deg),rgb(255 255 255/var(--glass-reflex-opacity,10%)) 25%,transparent 25%);border:none;box-shadow:0 0 0 1px rgb(255 255 255/var(--glass-border-opacity,10%)) inset,0 0 0 2px rgb(0 0 0/5%);text-shadow:0 1px rgb(0 0 0/var(--glass-text-shadow-opacity,5%))}@media (hover:hover){.glass.btn-active{-webkit-backdrop-filter:blur(var(--glass-blur,40px));backdrop-filter:blur(var(--glass-blur,40px));background-color:transparent;background-image:linear-gradient(135deg,rgb(255 255 255/var(--glass-opacity,30%)) 0,transparent 100%),linear-gradient(var(--glass-reflex-degree,100deg),rgb(255 255 255/var(--glass-reflex-opacity,10%)) 25%,transparent 25%);border:none;box-shadow:0 0 0 1px rgb(255 255 255/var(--glass-border-opacity,10%)) inset,0 0 0 2px rgb(0 0 0/5%);text-shadow:0 1px rgb(0 0 0/var(--glass-text-shadow-opacity,5%))}}.badge-xs{font-size:.75rem;height:.75rem;line-height:.75rem;padding-left:.313rem;padding-right:.313rem}.badge-sm{font-size:.75rem;height:1rem;line-height:1rem;padding-left:.438rem;padding-right:.438rem}.badge-lg{font-size:1rem;height:1.5rem;line-height:1.5rem;padding-left:.688rem;padding-right:.688rem}.btm-nav-xs>:where(.active){border-top-width:1px}.btm-nav-sm>:where(.active){border-top-width:2px}.btm-nav-md>:where(.active){border-top-width:2px}.btm-nav-lg>:where(.active){border-top-width:4px}.btn-xs{font-size:.75rem;height:1.5rem;min-height:1.5rem;padding-left:.5rem;padding-right:.5rem}.btn-sm{font-size:.875rem;height:2rem;min-height:2rem;padding-left:.75rem;padding-right:.75rem}.btn-lg{font-size:1.125rem;height:4rem;min-height:4rem;padding-left:1.5rem;padding-right:1.5rem}.btn-wide{width:16rem}.btn-block{width:100%}.btn-square:where(.btn-xs){height:1.5rem;padding:0;width:1.5rem}.btn-square:where(.btn-sm){height:2rem;padding:0;width:2rem}.btn-square:where(.btn-lg){height:4rem;padding:0;width:4rem}.btn-circle:where(.btn-xs){border-radius:9999px;height:1.5rem;padding:0;width:1.5rem}.btn-circle:where(.btn-sm){border-radius:9999px;height:2rem;padding:0;width:2rem}.btn-circle:where(.btn-md){border-radius:9999px;height:3rem;padding:0;width:3rem}.btn-circle:where(.btn-lg){border-radius:9999px;height:4rem;padding:0;width:4rem}[type=checkbox].checkbox-sm{height:1.25rem;width:1.25rem}.indicator :where(.indicator-item){bottom:auto;inset-inline-end:0;inset-inline-start:auto;top:0;--tw-translate-y:-50%;--tw-translate-x:50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}:is([dir=rtl] .indicator :where(.indicator-item)){--tw-translate-x:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.indicator :where(.indicator-item.indicator-start){inset-inline-end:auto;inset-inline-start:0;--tw-translate-x:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}:is([dir=rtl] .indicator :where(.indicator-item.indicator-start)){--tw-translate-x:50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.indicator :where(.indicator-item.indicator-center){inset-inline-end:50%;inset-inline-start:50%;--tw-translate-x:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}:is([dir=rtl] .indicator :where(.indicator-item.indicator-center)){--tw-translate-x:50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.indicator :where(.indicator-item.indicator-end){inset-inline-end:0;inset-inline-start:auto;--tw-translate-x:50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}:is([dir=rtl] .indicator :where(.indicator-item.indicator-end)){--tw-translate-x:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.indicator :where(.indicator-item.indicator-bottom){bottom:0;top:auto;--tw-translate-y:50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.indicator :where(.indicator-item.indicator-middle){bottom:50%;top:50%;--tw-translate-y:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.indicator :where(.indicator-item.indicator-top){bottom:auto;top:0;--tw-translate-y:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.input-sm{font-size:.875rem;height:2rem;line-height:2rem;padding-left:.75rem;padding-right:.75rem}.join.join-vertical{flex-direction:column}.join.join-vertical .join-item:first-child:not(:last-child),.join.join-vertical :first-child:not(:last-child) .join-item{border-end-end-radius:0;border-end-start-radius:0;border-start-end-radius:inherit;border-start-start-radius:inherit}.join.join-vertical .join-item:last-child:not(:first-child),.join.join-vertical :last-child:not(:first-child) .join-item{border-end-end-radius:inherit;border-end-start-radius:inherit;border-start-end-radius:0;border-start-start-radius:0}.join.join-horizontal{flex-direction:row}.join.join-horizontal .join-item:first-child:not(:last-child),.join.join-horizontal :first-child:not(:last-child) .join-item{border-end-end-radius:0;border-end-start-radius:inherit;border-start-end-radius:0;border-start-start-radius:inherit}.join.join-horizontal .join-item:last-child:not(:first-child),.join.join-horizontal :last-child:not(:first-child) .join-item{border-end-end-radius:inherit;border-end-start-radius:0;border-start-end-radius:inherit;border-start-start-radius:0}.menu-horizontal{display:inline-flex;flex-direction:row}.menu-horizontal>li:not(.menu-title)>details>ul{position:absolute}[type=radio].radio-sm{height:1.25rem;width:1.25rem}.range-sm{height:1.25rem}.range-sm::-webkit-slider-runnable-track{height:.25rem}.range-sm::-moz-range-track{height:.25rem}.range-sm::-webkit-slider-thumb{height:1.25rem;width:1.25rem;--filler-offset:0.5rem}.range-sm::-moz-range-thumb{height:1.25rem;width:1.25rem;--filler-offset:0.5rem}.select-xs{font-size:.75rem;height:1.5rem;line-height:1rem;line-height:1.625;min-height:1.5rem;padding-left:.5rem;padding-right:2rem}[dir=rtl] .select-xs{padding-left:2rem;padding-right:.5rem}.stats-vertical{grid-auto-flow:row}.steps-horizontal .step{display:grid;grid-template-columns:repeat(1,minmax(0,1fr));grid-template-rows:repeat(2,minmax(0,1fr));place-items:center;text-align:center}.steps-vertical .step{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));grid-template-rows:repeat(1,minmax(0,1fr))}.tabs-md :where(.tab){font-size:.875rem;height:2rem;line-height:1.25rem;line-height:2;--tab-padding:1rem}.tabs-lg :where(.tab){font-size:1.125rem;height:3rem;line-height:1.75rem;line-height:2;--tab-padding:1.25rem}.tabs-sm :where(.tab){font-size:.875rem;height:1.5rem;line-height:.75rem;--tab-padding:0.75rem}.tabs-xs :where(.tab){font-size:.75rem;height:1.25rem;line-height:.75rem;--tab-padding:0.5rem}.timeline-vertical{flex-direction:column}.timeline-compact .timeline-start,.timeline-horizontal.timeline-compact .timeline-start{align-self:flex-start;grid-column-end:4;grid-column-start:1;grid-row-end:4;grid-row-start:3;justify-self:center;margin:.25rem}.timeline-compact li:has(.timeline-start) .timeline-end,.timeline-horizontal.timeline-compact li:has(.timeline-start) .timeline-end{grid-column-start:none;grid-row-start:auto}.timeline-vertical.timeline-compact>li{--timeline-col-start:0}.timeline-vertical.timeline-compact .timeline-start{align-self:center;grid-column-end:4;grid-column-start:3;grid-row-end:4;grid-row-start:1;justify-self:start}.timeline-vertical.timeline-compact li:has(.timeline-start) .timeline-end{grid-column-start:auto;grid-row-start:none}:where(.timeline-vertical>li){--timeline-row-start:minmax(0,1fr);--timeline-row-end:minmax(0,1fr);justify-items:center}.timeline-vertical>li>hr{height:100%}:where(.timeline-vertical>li>hr):first-child{grid-column-start:2;grid-row-start:1}:where(.timeline-vertical>li>hr):last-child{grid-column-end:auto;grid-column-start:2;grid-row-end:none;grid-row-start:3}.timeline-vertical .timeline-start{align-self:center;grid-column-end:2;grid-column-start:1;grid-row-end:4;grid-row-start:1;justify-self:end}.timeline-vertical .timeline-end{align-self:center;grid-column-end:4;grid-column-start:3;grid-row-end:4;grid-row-start:1;justify-self:start}.timeline-vertical:where(.timeline-snap-icon)>li{--timeline-col-start:minmax(0,1fr);--timeline-row-start:0.5rem}.timeline-horizontal .timeline-start{align-self:flex-end;grid-column-end:4;grid-column-start:1;grid-row-end:2;grid-row-start:1;justify-self:center}.timeline-horizontal .timeline-end{align-self:flex-start;grid-column-end:4;grid-column-start:1;grid-row-end:4;grid-row-start:3;justify-self:center}.timeline-horizontal:where(.timeline-snap-icon)>li,:where(.timeline-snap-icon)>li{--timeline-col-start:0.5rem;--timeline-row-start:minmax(0,1fr)}:where(.toast){bottom:0;inset-inline-end:0;inset-inline-start:auto;top:auto;--tw-translate-x:0px;--tw-translate-y:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.toast:where(.toast-start){inset-inline-end:auto;inset-inline-start:0;--tw-translate-x:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.toast:where(.toast-center){inset-inline-end:50%;inset-inline-start:50%;--tw-translate-x:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}:is([dir=rtl] .toast:where(.toast-center)){--tw-translate-x:50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.toast:where(.toast-end){inset-inline-end:0;inset-inline-start:auto;--tw-translate-x:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.toast:where(.toast-bottom){bottom:0;top:auto;--tw-translate-y:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.toast:where(.toast-middle){bottom:auto;top:50%;--tw-translate-y:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.toast:where(.toast-top){bottom:auto;top:0;--tw-translate-y:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}[type=checkbox].toggle-sm{--handleoffset:0.75rem;height:1.25rem;width:2rem}.tooltip{--tooltip-offset:calc(100% + 1px + var(--tooltip-tail, 0px))}.tooltip:before{content:var(--tw-content);pointer-events:none;position:absolute;z-index:1;--tw-content:attr(data-tip)}.tooltip-top:before,.tooltip:before{bottom:var(--tooltip-offset);left:50%;right:auto;top:auto;transform:translateX(-50%)}.tooltip-bottom:before{bottom:auto;left:50%;right:auto;top:var(--tooltip-offset);transform:translateX(-50%)}.tooltip-left:before{left:auto;right:var(--tooltip-offset)}.tooltip-left:before,.tooltip-right:before{bottom:auto;top:50%;transform:translateY(-50%)}.tooltip-right:before{left:var(--tooltip-offset);right:auto}.avatar.online:before{background-color:var(--fallback-su,oklch(var(--su)/var(--tw-bg-opacity)))}.avatar.offline:before,.avatar.online:before{border-radius:9999px;content:"";display:block;position:absolute;z-index:10;--tw-bg-opacity:1;height:15%;outline-color:var(--fallback-b1,oklch(var(--b1)/1));outline-style:solid;outline-width:2px;right:7%;top:7%;width:15%}.avatar.offline:before{background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)))}.card-compact .card-body{font-size:.875rem;line-height:1.25rem;padding:1rem}.card-compact .card-title{margin-bottom:.25rem}.card-normal .card-body{font-size:1rem;line-height:1.5rem;padding:var(--padding-card,2rem)}.card-normal .card-title{margin-bottom:.75rem}.join.join-vertical>:where(:not(:first-child)){margin-left:0;margin-right:0;margin-top:-1px}.join.join-horizontal>:where(:not(:first-child)){margin-bottom:0;margin-top:0;margin-inline-start:-1px}.menu-horizontal>li:not(.menu-title)>details>ul{margin-inline-start:0;margin-top:1rem;padding-bottom:.5rem;padding-inline-end:.5rem;padding-top:.5rem}.menu-horizontal>li>details>ul:before{content:none}:where(.menu-horizontal>li:not(.menu-title)>details>ul){border-radius:var(--rounded-box,1rem);--tw-bg-opacity:1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.menu-sm :where(li:not(.menu-title)>:not(ul,details,.menu-title)),.menu-sm :where(li:not(.menu-title)>details>summary:not(.menu-title)){border-radius:var(--rounded-btn,.5rem);font-size:.875rem;line-height:1.25rem;padding:.25rem .75rem}.menu-sm .menu-title{padding:.5rem .75rem}.modal-top :where(.modal-box){max-width:none;width:100%;--tw-translate-y:-2.5rem;--tw-scale-x:1;--tw-scale-y:1;border-bottom-left-radius:var(--rounded-box,1rem);border-bottom-right-radius:var(--rounded-box,1rem);border-top-left-radius:0;border-top-right-radius:0;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.modal-middle :where(.modal-box){max-width:32rem;width:91.666667%;--tw-translate-y:0px;--tw-scale-x:.9;--tw-scale-y:.9;border-bottom-left-radius:var(--rounded-box,1rem);border-bottom-right-radius:var(--rounded-box,1rem);border-top-left-radius:var(--rounded-box,1rem);border-top-right-radius:var(--rounded-box,1rem);transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.modal-bottom :where(.modal-box){max-width:none;width:100%;--tw-translate-y:2.5rem;--tw-scale-x:1;--tw-scale-y:1;border-bottom-left-radius:0;border-bottom-right-radius:0;border-top-left-radius:var(--rounded-box,1rem);border-top-right-radius:var(--rounded-box,1rem);transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.stats-vertical>:not([hidden])~:not([hidden]){--tw-divide-x-reverse:0;--tw-divide-y-reverse:0;border-width:calc(1px*(1 - var(--tw-divide-y-reverse))) calc(0px*var(--tw-divide-x-reverse)) calc(1px*var(--tw-divide-y-reverse)) calc(0px*(1 - var(--tw-divide-x-reverse)))}.stats-vertical{overflow-y:auto}.steps-horizontal .step{grid-template-columns:auto;grid-template-rows:40px 1fr;min-width:4rem}.steps-horizontal .step:before{height:.5rem;width:100%;--tw-translate-x:0px;--tw-translate-y:0px;content:"";margin-inline-start:-100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}:is([dir=rtl] .steps-horizontal .step):before{--tw-translate-x:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.steps-vertical .step{gap:.5rem;grid-template-columns:40px 1fr;grid-template-rows:auto;justify-items:start;min-height:4rem}.steps-vertical .step:before{height:100%;width:.5rem;--tw-translate-x:-50%;--tw-translate-y:-50%;margin-inline-start:50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}:is([dir=rtl] .steps-vertical .step):before{--tw-translate-x:50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.timeline-vertical>li>hr{width:.25rem}:where(.timeline-vertical:has(.timeline-middle)>li>hr):first-child{border-bottom-left-radius:var(--rounded-badge,1.9rem);border-bottom-right-radius:var(--rounded-badge,1.9rem);border-top-left-radius:0;border-top-right-radius:0}:where(.timeline-vertical:has(.timeline-middle)>li>hr):last-child{border-bottom-left-radius:0;border-bottom-right-radius:0;border-top-left-radius:var(--rounded-badge,1.9rem);border-top-right-radius:var(--rounded-badge,1.9rem)}:where(.timeline-vertical:not(:has(.timeline-middle)) :first-child>hr:last-child){border-bottom-left-radius:0;border-bottom-right-radius:0;border-top-left-radius:var(--rounded-badge,1.9rem);border-top-right-radius:var(--rounded-badge,1.9rem)}:where(.timeline-vertical:not(:has(.timeline-middle)) :last-child>hr:first-child){border-bottom-left-radius:var(--rounded-badge,1.9rem);border-bottom-right-radius:var(--rounded-badge,1.9rem);border-top-left-radius:0;border-top-right-radius:0}:where(.timeline-horizontal:has(.timeline-middle)>li>hr):first-child{border-end-end-radius:var(--rounded-badge,1.9rem);border-end-start-radius:0;border-start-end-radius:var(--rounded-badge,1.9rem);border-start-start-radius:0}:where(.timeline-horizontal:has(.timeline-middle)>li>hr):last-child{border-end-end-radius:0;border-end-start-radius:var(--rounded-badge,1.9rem);border-start-end-radius:0;border-start-start-radius:var(--rounded-badge,1.9rem)}.tooltip{display:inline-block;position:relative;text-align:center;--tooltip-tail:0.1875rem;--tooltip-color:var(--fallback-n,oklch(var(--n)/1));--tooltip-text-color:var(--fallback-nc,oklch(var(--nc)/1));--tooltip-tail-offset:calc(100% + 0.0625rem - var(--tooltip-tail))}.tooltip:after,.tooltip:before{opacity:0;transition-delay:.1s;transition-duration:.2s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.tooltip:after{border-style:solid;border-width:var(--tooltip-tail,0);content:"";display:block;height:0;position:absolute;width:0}.tooltip:before{background-color:var(--tooltip-color);border-radius:.25rem;color:var(--tooltip-text-color);font-size:.875rem;line-height:1.25rem;max-width:20rem;padding:.25rem .5rem;width:-moz-max-content;width:max-content}.tooltip.tooltip-open:after,.tooltip.tooltip-open:before,.tooltip:hover:after,.tooltip:hover:before{opacity:1;transition-delay:75ms}.tooltip:has(:focus-visible):after,.tooltip:has(:focus-visible):before{opacity:1;transition-delay:75ms}.tooltip:not([data-tip]):hover:after,.tooltip:not([data-tip]):hover:before{opacity:0;visibility:hidden}.tooltip-top:after,.tooltip:after{border-color:var(--tooltip-color) transparent transparent transparent;bottom:var(--tooltip-tail-offset);left:50%;right:auto;top:auto;transform:translateX(-50%)}.tooltip-bottom:after{border-color:transparent transparent var(--tooltip-color) transparent;bottom:auto;left:50%;right:auto;top:var(--tooltip-tail-offset);transform:translateX(-50%)}.tooltip-left:after{border-color:transparent transparent transparent var(--tooltip-color);left:auto;right:calc(var(--tooltip-tail-offset) + .0625rem)}.tooltip-left:after,.tooltip-right:after{bottom:auto;top:50%;transform:translateY(-50%)}.tooltip-right:after{border-color:transparent var(--tooltip-color) transparent transparent;left:calc(var(--tooltip-tail-offset) + .0625rem);right:auto}.fade-out{opacity:0;transition:opacity .15s ease-in-out}.visible{visibility:visible}.invisible{visibility:hidden}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.bottom-0{bottom:0}.left-0{left:0}.left-2{left:.5rem}.left-4{left:1rem}.right-0{right:0}.right-2{right:.5rem}.right-5{right:1.25rem}.top-0{top:0}.top-16{top:4rem}.top-2{top:.5rem}.top-4{top:1rem}.top-5{top:1.25rem}.z-0{z-index:0}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-\[1\]{z-index:1}.z-\[5000\]{z-index:5000}.z-\[6000\]{z-index:6000}.col-span-2{grid-column:span 2/span 2}.m-0{margin:0}.m-5{margin:1.25rem}.m-auto{margin:auto}.mx-1{margin-left:.25rem;margin-right:.25rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-5{margin-left:1.25rem;margin-right:1.25rem}.mx-auto{margin-left:auto;margin-right:auto}.my-10{margin-bottom:2.5rem;margin-top:2.5rem}.my-2{margin-bottom:.5rem;margin-top:.5rem}.my-3{margin-bottom:.75rem;margin-top:.75rem}.my-4{margin-bottom:1rem;margin-top:1rem}.my-5{margin-bottom:1.25rem;margin-top:1.25rem}.mb-1{margin-bottom:.25rem}.mb-12{margin-bottom:3rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-14{margin-left:3.5rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mr-4{margin-right:1rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-10{height:2.5rem}.h-12{height:3rem}.h-16{height:4rem}.h-2{height:.5rem}.h-24{height:6rem}.h-3{height:.75rem}.h-4{height:1rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-96{height:24rem}.h-\[250px\]{height:250px}.h-\[25rem\]{height:25rem}.h-fit{height:-moz-fit-content;height:fit-content}.h-full{height:100%}.h-screen{height:100vh}.max-h-48{max-height:12rem}.max-h-96{max-height:24rem}.min-h-80{min-height:20rem}.min-h-\[4rem\]{min-height:4rem}.min-h-screen{min-height:100vh}.w-1\/2{width:50%}.w-10{width:2.5rem}.w-10\/12{width:83.333333%}.w-12{width:3rem}.w-2{width:.5rem}.w-24{width:6rem}.w-28{width:7rem}.w-3{width:.75rem}.w-4{width:1rem}.w-4\/12{width:33.333333%}.w-5{width:1.25rem}.w-52{width:13rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-80{width:20rem}.w-96{width:24rem}.w-auto{width:auto}.w-full{width:100%}.min-w-52{min-width:13rem}.min-w-full{min-width:100%}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-5xl{max-width:64rem}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.flex-shrink{flex-shrink:1}.flex-shrink-0,.shrink-0{flex-shrink:0}.flex-grow{flex-grow:1}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes bounce{0%,to{animation-timing-function:cubic-bezier(.8,0,1,1);transform:translateY(-25%)}50%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:none}}.animate-bounce{animation:bounce 1s infinite}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize{resize:both}.list-inside{list-style-position:inside}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.gap-6{gap:1.5rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-y-2{row-gap:.5rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.5rem*var(--tw-space-x-reverse))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.75rem*var(--tw-space-x-reverse))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1rem*var(--tw-space-x-reverse))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.25rem*var(--tw-space-y-reverse));margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.5rem*var(--tw-space-y-reverse));margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.75rem*var(--tw-space-y-reverse));margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1rem*var(--tw-space-y-reverse));margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.5rem*var(--tw-space-y-reverse));margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(2rem*var(--tw-space-y-reverse));margin-top:calc(2rem*(1 - var(--tw-space-y-reverse)))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-bottom-width:calc(1px*var(--tw-divide-y-reverse));border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)))}.divide-base-300>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-divide-opacity,1)))}.justify-self-end{justify-self:end}.justify-self-center{justify-self:center}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;white-space:nowrap}.text-ellipsis,.truncate{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-box{border-radius:var(--rounded-box,1rem)}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-b{border-bottom-left-radius:.25rem;border-bottom-right-radius:.25rem}.rounded-t-none{border-top-left-radius:0;border-top-right-radius:0}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-base-300{--tw-border-opacity:1;border-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-border-opacity,1)))}.border-blue-300{--tw-border-opacity:1;border-color:rgb(147 197 253/var(--tw-border-opacity,1))}.border-blue-500{--tw-border-opacity:1;border-color:rgb(59 130 246/var(--tw-border-opacity,1))}.border-error{--tw-border-opacity:1;border-color:var(--fallback-er,oklch(var(--er)/var(--tw-border-opacity,1)))}.border-gray-100{--tw-border-opacity:1;border-color:rgb(243 244 246/var(--tw-border-opacity,1))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity,1))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity,1))}.border-info\/20{border-color:var(--fallback-in,oklch(var(--in)/.2))}.border-neutral{--tw-border-opacity:1;border-color:var(--fallback-n,oklch(var(--n)/var(--tw-border-opacity,1)))}.border-red-300{--tw-border-opacity:1;border-color:rgb(252 165 165/var(--tw-border-opacity,1))}.border-secondary\/20{border-color:var(--fallback-s,oklch(var(--s)/.2))}.border-sky-500{--tw-border-opacity:1;border-color:rgb(14 165 233/var(--tw-border-opacity,1))}.border-success\/20{border-color:var(--fallback-su,oklch(var(--su)/.2))}.border-transparent{border-color:transparent}.border-warning\/20{border-color:var(--fallback-wa,oklch(var(--wa)/.2))}.border-white{--tw-border-opacity:1;border-color:rgb(255 255 255/var(--tw-border-opacity,1))}.border-opacity-20{--tw-border-opacity:0.2}.bg-base-100{--tw-bg-opacity:1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity,1)))}.bg-base-200{--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity,1)))}.bg-base-300{--tw-bg-opacity:1;background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity,1)))}.bg-blue-100{--tw-bg-opacity:1;background-color:rgb(219 234 254/var(--tw-bg-opacity,1))}.bg-blue-50{--tw-bg-opacity:1;background-color:rgb(239 246 255/var(--tw-bg-opacity,1))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity,1))}.bg-blue-600{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity,1))}.bg-blue-900{--tw-bg-opacity:1;background-color:rgb(30 58 138/var(--tw-bg-opacity,1))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity,1))}.bg-gray-400{--tw-bg-opacity:1;background-color:rgb(156 163 175/var(--tw-bg-opacity,1))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity,1))}.bg-green-50{--tw-bg-opacity:1;background-color:rgb(240 253 244/var(--tw-bg-opacity,1))}.bg-green-500{--tw-bg-opacity:1;background-color:rgb(34 197 94/var(--tw-bg-opacity,1))}.bg-info{--tw-bg-opacity:1;background-color:var(--fallback-in,oklch(var(--in)/var(--tw-bg-opacity,1)))}.bg-info\/10{background-color:var(--fallback-in,oklch(var(--in)/.1))}.bg-neutral{--tw-bg-opacity:1;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity,1)))}.bg-primary{--tw-bg-opacity:1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity,1)))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity,1))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity,1))}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}.bg-secondary{--tw-bg-opacity:1;background-color:var(--fallback-s,oklch(var(--s)/var(--tw-bg-opacity,1)))}.bg-secondary-content{--tw-bg-opacity:1;background-color:var(--fallback-sc,oklch(var(--sc)/var(--tw-bg-opacity,1)))}.bg-secondary\/10{background-color:var(--fallback-s,oklch(var(--s)/.1))}.bg-success{--tw-bg-opacity:1;background-color:var(--fallback-su,oklch(var(--su)/var(--tw-bg-opacity,1)))}.bg-success\/10{background-color:var(--fallback-su,oklch(var(--su)/.1))}.bg-warning{--tw-bg-opacity:1;background-color:var(--fallback-wa,oklch(var(--wa)/var(--tw-bg-opacity,1)))}.bg-warning\/10{background-color:var(--fallback-wa,oklch(var(--wa)/.1))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.bg-opacity-10{--tw-bg-opacity:0.1}.bg-opacity-60{--tw-bg-opacity:0.6}.bg-opacity-80{--tw-bg-opacity:0.8}.bg-gradient-to-bl{background-image:linear-gradient(to bottom left,var(--tw-gradient-stops))}.bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.bg-gradient-to-tl{background-image:linear-gradient(to top left,var(--tw-gradient-stops))}.bg-gradient-to-tr{background-image:linear-gradient(to top right,var(--tw-gradient-stops))}.from-base-100{--tw-gradient-from:var(--fallback-b1,oklch(var(--b1)/1)) var(--tw-gradient-from-position);--tw-gradient-to:var(--fallback-b1,oklch(var(--b1)/0)) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-blue-500{--tw-gradient-from:#3b82f6 var(--tw-gradient-from-position);--tw-gradient-to:rgba(59,130,246,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-blue-600{--tw-gradient-from:#2563eb var(--tw-gradient-from-position);--tw-gradient-to:rgba(37,99,235,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-green-400{--tw-gradient-from:#4ade80 var(--tw-gradient-from-position);--tw-gradient-to:rgba(74,222,128,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-green-500{--tw-gradient-from:#22c55e var(--tw-gradient-from-position);--tw-gradient-to:rgba(34,197,94,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-orange-400{--tw-gradient-from:#fb923c var(--tw-gradient-from-position);--tw-gradient-to:rgba(251,146,60,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-orange-600{--tw-gradient-from:#ea580c var(--tw-gradient-from-position);--tw-gradient-to:rgba(234,88,12,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-primary{--tw-gradient-from:var(--fallback-p,oklch(var(--p)/1)) var(--tw-gradient-from-position);--tw-gradient-to:var(--fallback-p,oklch(var(--p)/0)) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-red-400{--tw-gradient-from:#f87171 var(--tw-gradient-from-position);--tw-gradient-to:hsla(0,91%,71%,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-red-800{--tw-gradient-from:#991b1b var(--tw-gradient-from-position);--tw-gradient-to:rgba(153,27,27,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-yellow-400{--tw-gradient-from:#facc15 var(--tw-gradient-from-position);--tw-gradient-to:rgba(250,204,21,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-yellow-700{--tw-gradient-from:#a16207 var(--tw-gradient-from-position);--tw-gradient-to:rgba(161,98,7,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.to-base-200{--tw-gradient-to:var(--fallback-b2,oklch(var(--b2)/1)) var(--tw-gradient-to-position)}.to-blue-700{--tw-gradient-to:#1d4ed8 var(--tw-gradient-to-position)}.to-blue-800{--tw-gradient-to:#1e40af var(--tw-gradient-to-position)}.to-green-700{--tw-gradient-to:#15803d var(--tw-gradient-to-position)}.to-orange-600{--tw-gradient-to:#ea580c var(--tw-gradient-to-position)}.to-orange-700{--tw-gradient-to:#c2410c var(--tw-gradient-to-position)}.to-purple-600{--tw-gradient-to:#9333ea var(--tw-gradient-to-position)}.to-red-400{--tw-gradient-to:#f87171 var(--tw-gradient-to-position)}.to-red-600{--tw-gradient-to:#dc2626 var(--tw-gradient-to-position)}.to-red-900{--tw-gradient-to:#7f1d1d var(--tw-gradient-to-position)}.to-secondary{--tw-gradient-to:var(--fallback-s,oklch(var(--s)/1)) var(--tw-gradient-to-position)}.to-yellow-400{--tw-gradient-to:#facc15 var(--tw-gradient-to-position)}.to-yellow-600{--tw-gradient-to:#ca8a04 var(--tw-gradient-to-position)}.stroke-current{stroke:currentColor}.stroke-info{stroke:var(--fallback-in,oklch(var(--in)/1))}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.py-12{padding-bottom:3rem;padding-top:3rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.py-20{padding-bottom:5rem;padding-top:5rem}.py-3{padding-bottom:.75rem;padding-top:.75rem}.py-4{padding-bottom:1rem;padding-top:1rem}.py-5{padding-bottom:1.25rem;padding-top:1.25rem}.py-6{padding-bottom:1.5rem;padding-top:1.5rem}.py-8{padding-bottom:2rem;padding-top:2rem}.pb-2{padding-bottom:.5rem}.pl-4{padding-left:1rem}.pl-5{padding-left:1.25rem}.pl-6{padding-left:1.5rem}.pr-10{padding-right:2.5rem}.pt-2{padding-top:.5rem}.pt-4{padding-top:1rem}.pt-6{padding-top:1.5rem}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-5xl{font-size:3rem;line-height:1}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-black{font-weight:900}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.normal-case{text-transform:none}.italic{font-style:italic}.text-accent{--tw-text-opacity:1;color:var(--fallback-a,oklch(var(--a)/var(--tw-text-opacity,1)))}.text-accent-content{--tw-text-opacity:1;color:var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity,1)))}.text-base-content{--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity,1)))}.text-base-content\/60{color:var(--fallback-bc,oklch(var(--bc)/.6))}.text-base-content\/70{color:var(--fallback-bc,oklch(var(--bc)/.7))}.text-base-content\/80{color:var(--fallback-bc,oklch(var(--bc)/.8))}.text-blue-600{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity,1))}.text-blue-700{--tw-text-opacity:1;color:rgb(29 78 216/var(--tw-text-opacity,1))}.text-error{--tw-text-opacity:1;color:var(--fallback-er,oklch(var(--er)/var(--tw-text-opacity,1)))}.text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity,1))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity,1))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity,1))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}.text-gray-800{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity,1))}.text-green-500{--tw-text-opacity:1;color:rgb(34 197 94/var(--tw-text-opacity,1))}.text-green-600{--tw-text-opacity:1;color:rgb(22 163 74/var(--tw-text-opacity,1))}.text-info{--tw-text-opacity:1;color:var(--fallback-in,oklch(var(--in)/var(--tw-text-opacity,1)))}.text-info-content{--tw-text-opacity:1;color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity,1)))}.text-neutral{--tw-text-opacity:1;color:var(--fallback-n,oklch(var(--n)/var(--tw-text-opacity,1)))}.text-neutral-content{--tw-text-opacity:1;color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity,1)))}.text-orange-600{--tw-text-opacity:1;color:rgb(234 88 12/var(--tw-text-opacity,1))}.text-primary{--tw-text-opacity:1;color:var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity,1)))}.text-primary-content{--tw-text-opacity:1;color:var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity,1)))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity,1))}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity,1))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity,1))}.text-secondary{--tw-text-opacity:1;color:var(--fallback-s,oklch(var(--s)/var(--tw-text-opacity,1)))}.text-secondary-content{--tw-text-opacity:1;color:var(--fallback-sc,oklch(var(--sc)/var(--tw-text-opacity,1)))}.text-success{--tw-text-opacity:1;color:var(--fallback-su,oklch(var(--su)/var(--tw-text-opacity,1)))}.text-success-content{--tw-text-opacity:1;color:var(--fallback-suc,oklch(var(--suc)/var(--tw-text-opacity,1)))}.text-warning{--tw-text-opacity:1;color:var(--fallback-wa,oklch(var(--wa)/var(--tw-text-opacity,1)))}.text-warning-content{--tw-text-opacity:1;color:var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity,1)))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.underline{text-decoration-line:underline}.decoration-dotted{text-decoration-style:dotted}.placeholder-base-content\/70::-moz-placeholder{color:var(--fallback-bc,oklch(var(--bc)/.7))}.placeholder-base-content\/70::placeholder{color:var(--fallback-bc,oklch(var(--bc)/.7))}.opacity-0{opacity:0}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-80{opacity:.8}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-2xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgba(0,0,0,.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 rgba(0,0,0,.05);--tw-shadow-colored:inset 0 2px 4px 0 var(--tw-shadow-color)}.shadow-inner,.shadow-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-md,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.outline{outline-style:solid}.ring-2{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-primary{--tw-ring-opacity:1;--tw-ring-color:var(--fallback-p,oklch(var(--p)/var(--tw-ring-opacity,1)))}.ring-offset-2{--tw-ring-offset-width:2px}.blur{--tw-blur:blur(8px)}.blur,.grayscale{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.grayscale{--tw-grayscale:grayscale(100%)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-all{transition-duration:.15s;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-colors{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-opacity{transition-duration:.15s;transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-shadow{transition-duration:.15s;transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-transform{transition-duration:.15s;transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}@tailwind daisyui;.leaflet-right-panel{background:#fff;border-radius:4px;box-shadow:0 1px 4px rgba(0,0,0,.3);margin-right:10px;margin-top:80px;transform:none;transition:right .3s ease-in-out;z-index:400}.add-visit-marker{align-items:center;animation:pulse-visit 2s infinite;background:#fff;border:2px solid #007bff;border-radius:50%;box-shadow:0 2px 8px rgba(0,123,255,.3);display:flex!important;font-size:20px;justify-content:center}@keyframes pulse-visit{0%{box-shadow:0 2px 8px rgba(0,123,255,.3);transform:scale(1)}50%{box-shadow:0 4px 12px rgba(0,123,255,.5);transform:scale(1.1)}to{box-shadow:0 2px 8px rgba(0,123,255,.3);transform:scale(1)}}.visit-form-popup .leaflet-popup-content-wrapper{border-radius:8px;box-shadow:0 4px 20px rgba(0,0,0,.15)}.leaflet-right-panel.controls-shifted{right:310px}.leaflet-drawer{background:hsla(0,0%,100%,.5);border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,.15);cursor:default;height:auto;max-height:calc(100% - 20px);opacity:0;position:absolute;right:70px;top:10px;transform:scale(.95);transition:opacity .2s ease-in-out,transform .2s ease-in-out,visibility .2s;visibility:hidden;width:24rem;z-index:450}.leaflet-drawer *{cursor:default}.leaflet-drawer .btn,.leaflet-drawer a,.leaflet-drawer button,.leaflet-drawer input[type=checkbox]{cursor:pointer}.leaflet-drawer.open{opacity:1;transform:scale(1);visibility:visible}.leaflet-control-button,.leaflet-control-layers,.toggle-panel-button{z-index:500}.leaflet-control-custom{align-items:center;background-color:#fff;border-radius:4px;box-shadow:0 1px 4px rgba(0,0,0,.3);cursor:pointer;display:flex;height:30px;justify-content:center;width:30px}.leaflet-control-custom:hover{background-color:#f3f4f6}#selection-tool-button.active{background-color:#60a5fa;color:#fff}#cancel-selection-button{width:100%}em-emoji-picker{--color-border-over:rgba(0,0,0,.1);--color-border:rgba(0,0,0,.05);--font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;--rgb-accent:96,165,250;border-radius:8px;box-shadow:0 4px 20px rgba(0,0,0,.15);max-width:400px;min-width:318px;overflow:auto;position:absolute;resize:horizontal;z-index:1000}[data-theme=dark] em-emoji-picker,html.dark em-emoji-picker{--color-border-over:hsla(0,0%,100%,.1);--color-border:hsla(0,0%,100%,.05);--rgb-accent:96,165,250}@media (max-width:768px){em-emoji-picker{max-width:90vw;min-width:280px}}.color-input{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:none;padding:0}.color-input::-webkit-color-swatch-wrapper{padding:0}.color-input::-webkit-color-swatch{border:none;border-radius:.5rem}.color-input::-moz-color-swatch{border:none;border-radius:.5rem}@media (hover:hover){.hover\:btn-ghost:hover:hover{border-color:transparent}@supports (color:oklch(0 0 0)){.hover\:btn-ghost:hover:hover{background-color:var(--fallback-bc,oklch(var(--bc)/.2))}}.hover\:btn-info:hover.btn-outline:hover{--tw-text-opacity:1;color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)))}@supports (color:color-mix(in oklab,black,black)){.hover\:btn-info:hover.btn-outline:hover{background-color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 90%,#000)}}}@supports not (color:oklch(0 0 0)){.hover\:btn-info:hover{--btn-color:var(--fallback-in)}}@supports (color:color-mix(in oklab,black,black)){.hover\:btn-info:hover.btn-outline.btn-active{background-color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 90%,#000);border-color:color-mix(in oklab,var(--fallback-in,oklch(var(--in)/1)) 90%,#000)}}@supports (color:oklch(0 0 0)){.hover\:btn-info:hover{--btn-color:var(--in)}}.hover\:btn-info:hover{--tw-text-opacity:1;color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)));outline-color:var(--fallback-in,oklch(var(--in)/1))}.hover\:btn-ghost:hover{background-color:transparent;border-color:transparent;border-width:1px;color:currentColor;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);outline-color:currentColor}.hover\:btn-ghost:hover.btn-active{background-color:var(--fallback-bc,oklch(var(--bc)/.2));border-color:transparent}.hover\:btn-info:hover.btn-outline{--tw-text-opacity:1;color:var(--fallback-in,oklch(var(--in)/var(--tw-text-opacity)))}.hover\:btn-info:hover.btn-outline.btn-active{--tw-text-opacity:1;color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)))}.hover\:input-primary:hover{--tw-border-opacity:1;border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)))}.hover\:input-primary:hover:focus,.hover\:input-primary:hover:focus-within{--tw-border-opacity:1;border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)));outline-color:var(--fallback-p,oklch(var(--p)/1))}@media not all and (min-width:768px){.max-md\:timeline-compact,.max-md\:timeline-compact .timeline-horizontal{--timeline-row-start:0}.max-md\:timeline-compact .timeline-horizontal .timeline-start,.max-md\:timeline-compact .timeline-start{align-self:flex-start;grid-column-end:4;grid-column-start:1;grid-row-end:4;grid-row-start:3;justify-self:center;margin:.25rem}.max-md\:timeline-compact .timeline-horizontal li:has(.timeline-start) .timeline-end,.max-md\:timeline-compact li:has(.timeline-start) .timeline-end{grid-column-start:none;grid-row-start:auto}.max-md\:timeline-compact.timeline-vertical>li{--timeline-col-start:0}.max-md\:timeline-compact.timeline-vertical .timeline-start{align-self:center;grid-column-end:4;grid-column-start:3;grid-row-end:4;grid-row-start:1;justify-self:start}.max-md\:timeline-compact.timeline-vertical li:has(.timeline-start) .timeline-end{grid-column-start:auto;grid-row-start:none}}@media (min-width:1024px){.lg\:stats-horizontal{grid-auto-flow:column}.lg\:stats-horizontal>:not([hidden])~:not([hidden]){--tw-divide-x-reverse:0;--tw-divide-y-reverse:0;border-width:calc(0px*(1 - var(--tw-divide-y-reverse))) calc(1px*var(--tw-divide-x-reverse)) calc(0px*var(--tw-divide-y-reverse)) calc(1px*(1 - var(--tw-divide-x-reverse)))}.lg\:stats-horizontal{overflow-x:auto}:is([dir=rtl] .lg\:stats-horizontal){--tw-divide-x-reverse:1}}.last\:border-0:last-child{border-width:0}.hover\:scale-105:hover{--tw-scale-x:1.05;--tw-scale-y:1.05}.hover\:scale-105:hover,.hover\:scale-110:hover{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:scale-110:hover{--tw-scale-x:1.1;--tw-scale-y:1.1}.hover\:scale-\[1\.02\]:hover{--tw-scale-x:1.02;--tw-scale-y:1.02;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:cursor-pointer:hover{cursor:pointer}.hover\:border-primary:hover{--tw-border-opacity:1;border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity,1)))}.hover\:border-primary\/40:hover{border-color:var(--fallback-p,oklch(var(--p)/.4))}.hover\:bg-accent:hover{--tw-bg-opacity:1;background-color:var(--fallback-a,oklch(var(--a)/var(--tw-bg-opacity,1)))}.hover\:bg-base-200:hover{--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity,1)))}.hover\:bg-base-200\/50:hover{background-color:var(--fallback-b2,oklch(var(--b2)/.5))}.hover\:bg-base-300:hover{--tw-bg-opacity:1;background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity,1)))}.hover\:bg-blue-50:hover{--tw-bg-opacity:1;background-color:rgb(239 246 255/var(--tw-bg-opacity,1))}.hover\:bg-blue-700:hover{--tw-bg-opacity:1;background-color:rgb(29 78 216/var(--tw-bg-opacity,1))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity,1))}.hover\:bg-white:hover{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.hover\:text-accent-content:hover{--tw-text-opacity:1;color:var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity,1)))}.hover\:text-blue-800:hover{--tw-text-opacity:1;color:rgb(30 64 175/var(--tw-text-opacity,1))}.hover\:text-gray-600:hover{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity,1))}.hover\:text-primary:hover{--tw-text-opacity:1;color:var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity,1)))}.hover\:underline:hover{text-decoration-line:underline}.hover\:no-underline:hover{text-decoration-line:none}.hover\:shadow-2xl:hover{--tw-shadow:0 25px 50px -12px rgba(0,0,0,.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.hover\:shadow-2xl:hover,.hover\:shadow-lg:hover{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.hover\:shadow-primary\/20:hover{--tw-shadow-color:var(--fallback-p,oklch(var(--p)/0.2));--tw-shadow:var(--tw-shadow-colored)}.focus\:border-primary:focus{--tw-border-opacity:1;border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity,1)))}.focus\:border-transparent:focus{border-color:transparent}.focus\:bg-base-100:focus{--tw-bg-opacity:1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity,1)))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-blue-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(59 130 246/var(--tw-ring-opacity,1))}.group:hover .group-hover\:text-primary{--tw-text-opacity:1;color:var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity,1)))}.group:hover .group-hover\:opacity-100{opacity:1}.peer:checked~.peer-checked\:scale-105{--tw-scale-x:1.05;--tw-scale-y:1.05;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@media (min-width:640px){.sm\:inline{display:inline}.sm\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}}@media (min-width:768px){.md\:h-64{height:16rem}.md\:min-h-64{min-height:16rem}.md\:w-1\/12{width:8.333333%}.md\:w-2\/12{width:16.666667%}.md\:w-2\/3{width:66.666667%}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:items-end{align-items:flex-end}.md\:space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1rem*var(--tw-space-x-reverse))}.md\:text-end{text-align:end}}@media (min-width:1024px){.lg\:mt-0{margin-top:0}.lg\:\!block{display:block!important}.lg\:flex{display:flex}.lg\:hidden{display:none}.lg\:w-1\/12{width:8.333333%}.lg\:w-1\/2{width:50%}.lg\:w-2\/12{width:16.666667%}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:flex-row-reverse{flex-direction:row-reverse}.lg\:items-end{align-items:flex-end}.lg\:space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1rem*var(--tw-space-x-reverse))}.lg\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(0px*var(--tw-space-y-reverse));margin-top:calc(0px*(1 - var(--tw-space-y-reverse)))}.lg\:text-left{text-align:left}} \ No newline at end of file diff --git a/app/controllers/api/v1/settings_controller.rb b/app/controllers/api/v1/settings_controller.rb index ab7e33c1..2bbcfea7 100644 --- a/app/controllers/api/v1/settings_controller.rb +++ b/app/controllers/api/v1/settings_controller.rb @@ -5,7 +5,7 @@ class Api::V1::SettingsController < ApiController def index render json: { - settings: current_api_user.safe_settings, + settings: current_api_user.settings, status: 'success' }, status: :ok end @@ -31,9 +31,7 @@ class Api::V1::SettingsController < ApiController :preferred_map_layer, :points_rendering_mode, :live_map_enabled, :immich_url, :immich_api_key, :photoprism_url, :photoprism_api_key, :speed_colored_routes, :speed_color_scale, :fog_of_war_threshold, - :maps_v2_style, :maps_v2_heatmap, :maps_v2_visits, :maps_v2_photos, - :maps_v2_areas, :maps_v2_tracks, :maps_v2_fog, :maps_v2_scratch, - :maps_v2_clustering, :maps_v2_cluster_radius, + :maps_v2_style, enabled_map_layers: [] ) end diff --git a/app/javascript/controllers/maps_v2/layer_manager.js b/app/javascript/controllers/maps_v2/layer_manager.js index d0dd8e0b..cf338887 100644 --- a/app/javascript/controllers/maps_v2/layer_manager.js +++ b/app/javascript/controllers/maps_v2/layer_manager.js @@ -160,7 +160,9 @@ export class LayerManager { _addRoutesLayer(routesGeoJSON) { if (!this.layers.routesLayer) { - this.layers.routesLayer = new RoutesLayer(this.map) + this.layers.routesLayer = new RoutesLayer(this.map, { + visible: this.settings.routesVisible !== false // Default true unless explicitly false + }) this.layers.routesLayer.add(routesGeoJSON) } else { this.layers.routesLayer.update(routesGeoJSON) @@ -205,7 +207,9 @@ export class LayerManager { _addPointsLayer(pointsGeoJSON) { if (!this.layers.pointsLayer) { - this.layers.pointsLayer = new PointsLayer(this.map) + this.layers.pointsLayer = new PointsLayer(this.map, { + visible: this.settings.pointsVisible !== false // Default true unless explicitly false + }) this.layers.pointsLayer.add(pointsGeoJSON) } else { this.layers.pointsLayer.update(pointsGeoJSON) diff --git a/app/javascript/controllers/maps_v2_controller.js b/app/javascript/controllers/maps_v2_controller.js index 47e564d2..a531c845 100644 --- a/app/javascript/controllers/maps_v2_controller.js +++ b/app/javascript/controllers/maps_v2_controller.js @@ -24,7 +24,30 @@ export default class extends Controller { endDate: String } - static targets = ['container', 'loading', 'loadingText', 'monthSelect', 'clusterToggle', 'settingsPanel', 'visitsSearch'] + static targets = [ + 'container', + 'loading', + 'loadingText', + 'monthSelect', + 'clusterToggle', + 'settingsPanel', + 'visitsSearch', + 'routeOpacityRange', + 'fogRadiusValue', + 'fogThresholdValue', + 'metersBetweenValue', + 'minutesBetweenValue', + // Layer toggles + 'pointsToggle', + 'routesToggle', + 'heatmapToggle', + 'visitsToggle', + 'photosToggle', + 'areasToggle', + // 'tracksToggle', + 'fogToggle', + 'scratchToggle' + ] async connect() { this.cleanup = new CleanupHelper() @@ -35,6 +58,9 @@ export default class extends Controller { // Sync settings from backend (will fall back to localStorage if needed) await this.loadSettings() + // Sync toggle states with loaded settings + this.syncToggleStates() + await this.initializeMap() this.initializeAPI() @@ -66,6 +92,90 @@ export default class extends Controller { console.log('[Maps V2] Settings loaded:', this.settings) } + /** + * Sync UI controls with loaded settings + */ + syncToggleStates() { + // Sync layer toggles + const toggleMap = { + pointsToggle: 'pointsVisible', + routesToggle: 'routesVisible', + heatmapToggle: 'heatmapEnabled', + visitsToggle: 'visitsEnabled', + photosToggle: 'photosEnabled', + areasToggle: 'areasEnabled', + // tracksToggle: 'tracksEnabled', + fogToggle: 'fogEnabled', + scratchToggle: 'scratchEnabled' + } + + Object.entries(toggleMap).forEach(([targetName, settingKey]) => { + const target = `${targetName}Target` + if (this[target]) { + this[target].checked = this.settings[settingKey] + } + }) + + // Sync route opacity slider + if (this.hasRouteOpacityRangeTarget) { + this.routeOpacityRangeTarget.value = (this.settings.routeOpacity || 1.0) * 100 + } + + // Sync map style dropdown + const mapStyleSelect = this.element.querySelector('select[name="mapStyle"]') + if (mapStyleSelect) { + mapStyleSelect.value = this.settings.mapStyle || 'light' + } + + // Sync fog of war settings + const fogRadiusInput = this.element.querySelector('input[name="fogOfWarRadius"]') + if (fogRadiusInput) { + fogRadiusInput.value = this.settings.fogOfWarRadius || 1000 + if (this.hasFogRadiusValueTarget) { + this.fogRadiusValueTarget.textContent = `${fogRadiusInput.value}m` + } + } + + const fogThresholdInput = this.element.querySelector('input[name="fogOfWarThreshold"]') + if (fogThresholdInput) { + fogThresholdInput.value = this.settings.fogOfWarThreshold || 1 + if (this.hasFogThresholdValueTarget) { + this.fogThresholdValueTarget.textContent = fogThresholdInput.value + } + } + + // Sync route generation settings + const metersBetweenInput = this.element.querySelector('input[name="metersBetweenRoutes"]') + if (metersBetweenInput) { + metersBetweenInput.value = this.settings.metersBetweenRoutes || 500 + if (this.hasMetersBetweenValueTarget) { + this.metersBetweenValueTarget.textContent = `${metersBetweenInput.value}m` + } + } + + const minutesBetweenInput = this.element.querySelector('input[name="minutesBetweenRoutes"]') + if (minutesBetweenInput) { + minutesBetweenInput.value = this.settings.minutesBetweenRoutes || 60 + if (this.hasMinutesBetweenValueTarget) { + this.minutesBetweenValueTarget.textContent = `${minutesBetweenInput.value}min` + } + } + + // Sync points rendering mode radio buttons + const pointsRenderingRadios = this.element.querySelectorAll('input[name="pointsRenderingMode"]') + pointsRenderingRadios.forEach(radio => { + radio.checked = radio.value === (this.settings.pointsRenderingMode || 'raw') + }) + + // Sync speed-colored routes toggle + const speedColoredRoutesToggle = this.element.querySelector('input[name="speedColoredRoutes"]') + if (speedColoredRoutesToggle) { + speedColoredRoutesToggle.checked = this.settings.speedColoredRoutes || false + } + + console.log('[Maps V2] UI controls synced with settings') + } + /** * Initialize MapLibre map */ @@ -213,46 +323,59 @@ export default class extends Controller { * Toggle layer visibility */ toggleLayer(event) { - const button = event.currentTarget - const layerName = button.dataset.layer + const element = event.currentTarget + const layerName = element.dataset.layer || event.params?.layer const visible = this.layerManager.toggleLayer(layerName) if (visible === null) return - // Update button style - if (visible) { - button.classList.add('btn-primary') - button.classList.remove('btn-outline') - } else { - button.classList.remove('btn-primary') - button.classList.add('btn-outline') + // Update button style (for button-based toggles) + if (element.tagName === 'BUTTON') { + if (visible) { + element.classList.add('btn-primary') + element.classList.remove('btn-outline') + } else { + element.classList.remove('btn-primary') + element.classList.add('btn-outline') + } + } + + // Update checkbox state (for checkbox-based toggles) + if (element.tagName === 'INPUT' && element.type === 'checkbox') { + element.checked = visible } } /** - * Toggle point clustering + * Toggle points layer visibility */ - toggleClustering(event) { + togglePoints(event) { + const element = event.currentTarget + const visible = element.checked + const pointsLayer = this.layerManager.getLayer('points') - if (!pointsLayer) return - - const button = event.currentTarget - - // Toggle clustering state - const newClusteringState = !pointsLayer.clusteringEnabled - pointsLayer.toggleClustering(newClusteringState) - - // Update button style to reflect state - if (newClusteringState) { - button.classList.add('btn-primary') - button.classList.remove('btn-outline') - } else { - button.classList.remove('btn-primary') - button.classList.add('btn-outline') + if (pointsLayer) { + pointsLayer.toggle(visible) } // Save setting - SettingsManager.updateSetting('clustering', newClusteringState) + SettingsManager.updateSetting('pointsVisible', visible) + } + + /** + * Toggle routes layer visibility + */ + toggleRoutes(event) { + const element = event.currentTarget + const visible = element.checked + + const routesLayer = this.layerManager.getLayer('routes') + if (routesLayer) { + routesLayer.toggle(visible) + } + + // Save setting + SettingsManager.updateSetting('routesVisible', visible) } /** @@ -312,6 +435,119 @@ export default class extends Controller { } } + /** + * Update route opacity in real-time + */ + updateRouteOpacity(event) { + const opacity = parseInt(event.target.value) / 100 + + const routesLayer = this.layerManager.getLayer('routes') + if (routesLayer && this.map.getLayer('routes')) { + this.map.setPaintProperty('routes', 'line-opacity', opacity) + } + + // Save setting + SettingsManager.updateSetting('routeOpacity', opacity) + } + + /** + * Update fog radius display value + */ + updateFogRadiusDisplay(event) { + if (this.hasFogRadiusValueTarget) { + this.fogRadiusValueTarget.textContent = `${event.target.value}m` + } + } + + /** + * Update fog threshold display value + */ + updateFogThresholdDisplay(event) { + if (this.hasFogThresholdValueTarget) { + this.fogThresholdValueTarget.textContent = event.target.value + } + } + + /** + * Update meters between routes display value + */ + updateMetersBetweenDisplay(event) { + if (this.hasMetersBetweenValueTarget) { + this.metersBetweenValueTarget.textContent = `${event.target.value}m` + } + } + + /** + * Update minutes between routes display value + */ + updateMinutesBetweenDisplay(event) { + if (this.hasMinutesBetweenValueTarget) { + this.minutesBetweenValueTarget.textContent = `${event.target.value}min` + } + } + + /** + * Update advanced settings from form submission + */ + async updateAdvancedSettings(event) { + event.preventDefault() + + const formData = new FormData(event.target) + const settings = { + routeOpacity: parseFloat(formData.get('routeOpacity')) / 100, + fogOfWarRadius: parseInt(formData.get('fogOfWarRadius')), + fogOfWarThreshold: parseInt(formData.get('fogOfWarThreshold')), + metersBetweenRoutes: parseInt(formData.get('metersBetweenRoutes')), + minutesBetweenRoutes: parseInt(formData.get('minutesBetweenRoutes')), + pointsRenderingMode: formData.get('pointsRenderingMode'), + speedColoredRoutes: formData.get('speedColoredRoutes') === 'on' + } + + // Apply settings to current map + await this.applySettingsToMap(settings) + + // Save to backend and localStorage + for (const [key, value] of Object.entries(settings)) { + await SettingsManager.updateSetting(key, value) + } + + Toast.success('Settings updated successfully') + } + + /** + * Apply settings to map without reload + */ + async applySettingsToMap(settings) { + // Update route opacity + if (settings.routeOpacity !== undefined) { + const routesLayer = this.layerManager.getLayer('routes') + if (routesLayer && this.map.getLayer('routes')) { + this.map.setPaintProperty('routes', 'line-opacity', settings.routeOpacity) + } + } + + // Update fog of war settings + if (settings.fogOfWarRadius !== undefined || settings.fogOfWarThreshold !== undefined) { + const fogLayer = this.layerManager.getLayer('fog') + if (fogLayer) { + if (settings.fogOfWarRadius) { + fogLayer.clearRadius = settings.fogOfWarRadius + } + // Redraw fog layer + if (fogLayer.visible) { + await fogLayer.update(fogLayer.data) + } + } + } + + // For settings that require data reload (points rendering mode, speed-colored routes, etc) + // we need to reload the map data + if (settings.pointsRenderingMode || settings.speedColoredRoutes !== undefined) { + Toast.info('Reloading map data with new settings...') + await this.loadMapData() + } + } + /** * Toggle visits layer */ diff --git a/app/javascript/maps_v2/SETTINGS_PERSISTENCE.md b/app/javascript/maps_v2/SETTINGS_PERSISTENCE.md index 70040524..1a14f563 100644 --- a/app/javascript/maps_v2/SETTINGS_PERSISTENCE.md +++ b/app/javascript/maps_v2/SETTINGS_PERSISTENCE.md @@ -1,6 +1,6 @@ # Maps V2 Settings Persistence -Maps V2 now persists user settings across sessions and devices using a hybrid approach with backend API storage and localStorage fallback. +Maps V2 persists user settings across sessions and devices using a hybrid approach with backend API storage and localStorage fallback. **Settings are shared with Maps V1** for seamless migration. ## Architecture @@ -10,6 +10,7 @@ Maps V2 now persists user settings across sessions and devices using a hybrid ap - Settings stored in User's `settings` JSONB column - Syncs across all devices/browsers - Requires authentication via API key + - **Compatible with v1 map settings** 2. **Fallback: localStorage** - Instant save/load without network @@ -18,22 +19,27 @@ Maps V2 now persists user settings across sessions and devices using a hybrid ap ## Settings Stored -All Maps V2 user preferences are persisted: +Maps V2 shares layer visibility settings with v1 using the `enabled_map_layers` array: | Frontend Setting | Backend Key | Type | Default | |-----------------|-------------|------|---------| | `mapStyle` | `maps_v2_style` | string | `'light'` | -| `clustering` | `maps_v2_clustering` | boolean | `true` | -| `clusterRadius` | `maps_v2_cluster_radius` | number | `50` | -| `heatmapEnabled` | `maps_v2_heatmap` | boolean | `false` | -| `pointsVisible` | `maps_v2_points` | boolean | `true` | -| `routesVisible` | `maps_v2_routes` | boolean | `true` | -| `visitsEnabled` | `maps_v2_visits` | boolean | `false` | -| `photosEnabled` | `maps_v2_photos` | boolean | `false` | -| `areasEnabled` | `maps_v2_areas` | boolean | `false` | -| `tracksEnabled` | `maps_v2_tracks` | boolean | `false` | -| `fogEnabled` | `maps_v2_fog` | boolean | `false` | -| `scratchEnabled` | `maps_v2_scratch` | boolean | `false` | +| `enabledMapLayers` | `enabled_map_layers` | array | `['Points', 'Routes']` | + +### Layer Names + +The `enabled_map_layers` array contains layer names as strings: +- `'Points'` - Individual location points +- `'Routes'` - Connected route lines +- `'Heatmap'` - Density heatmap +- `'Visits'` - Detected area visits +- `'Photos'` - Geotagged photos +- `'Areas'` - Defined areas +- `'Tracks'` - Saved tracks +- `'Fog of War'` - Explored areas +- `'Scratch map'` - Scratched countries + +Internally, v2 converts these to boolean flags (e.g., `pointsVisible`, `routesVisible`) for easier state management, but always saves back to the shared array format. ## How It Works @@ -58,18 +64,52 @@ All Maps V2 user preferences are persisted: ### Update Flow ``` -User changes setting (e.g., enables heatmap) +User toggles Heatmap layer ↓ SettingsManager.updateSetting('heatmapEnabled', true) ↓ +Convert booleans → array: ['Points', 'Routes', 'Heatmap'] + ↓ ┌──────────────────┬──────────────────┐ │ Save to │ Save to │ │ localStorage │ Backend API │ │ (instant) │ (async) │ └──────────────────┴──────────────────┘ ↓ ↓ -UI updates Backend stores -immediately (non-blocking) +UI updates Backend stores: +immediately { enabled_map_layers: [...] } +``` + +### Format Conversion + +v2 internally uses boolean flags for state management but saves/loads using v1's array format: + +**Loading (Array → Booleans)**: +```javascript +// Backend returns +{ enabled_map_layers: ['Points', 'Routes', 'Heatmap'] } + +// Converted to +{ + pointsVisible: true, + routesVisible: true, + heatmapEnabled: true, + visitsEnabled: false, + // ... etc +} +``` + +**Saving (Booleans → Array)**: +```javascript +// v2 state +{ + pointsVisible: true, + routesVisible: false, + heatmapEnabled: true +} + +// Saved as +{ enabled_map_layers: ['Points', 'Heatmap'] } ``` ## API Integration @@ -242,16 +282,14 @@ Existing users with localStorage settings will seamlessly migrate: Settings stored in `users.settings` JSONB column: ```sql --- Example user settings +-- Example user settings (shared between v1 and v2) { "maps_v2_style": "dark", - "maps_v2_heatmap": true, - "maps_v2_clustering": true, - "maps_v2_cluster_radius": 50, - // ... other Maps V2 settings - // ... Maps V1 settings (coexist) - "preferred_map_layer": "Light", - "enabled_map_layers": ["Routes", "Heatmap"] + "enabled_map_layers": ["Points", "Routes", "Heatmap", "Visits"], + // ... other settings shared by both versions + "preferred_map_layer": "OpenStreetMap", + "fog_of_war_meters": "100", + "route_opacity": 60 } ``` diff --git a/app/javascript/maps_v2/layers/points_layer.js b/app/javascript/maps_v2/layers/points_layer.js index 3c97b2fb..8a7f9d33 100644 --- a/app/javascript/maps_v2/layers/points_layer.js +++ b/app/javascript/maps_v2/layers/points_layer.js @@ -1,14 +1,11 @@ import { BaseLayer } from './base_layer' /** - * Points layer with toggleable clustering + * Points layer for displaying individual location points */ export class PointsLayer extends BaseLayer { constructor(map, options = {}) { super(map, { id: 'points', ...options }) - this.clusterRadius = options.clusterRadius || 50 - this.clusterMaxZoom = options.clusterMaxZoom || 14 - this.clusteringEnabled = options.clustering !== false // Default to enabled } getSourceConfig() { @@ -17,63 +14,17 @@ export class PointsLayer extends BaseLayer { data: this.data || { type: 'FeatureCollection', features: [] - }, - cluster: this.clusteringEnabled, - clusterMaxZoom: this.clusterMaxZoom, - clusterRadius: this.clusterRadius + } } } getLayerConfigs() { return [ - // Cluster circles - { - id: `${this.id}-clusters`, - type: 'circle', - source: this.sourceId, - filter: ['has', 'point_count'], - paint: { - 'circle-color': [ - 'step', - ['get', 'point_count'], - '#51bbd6', 10, - '#f1f075', 50, - '#f28cb1', 100, - '#ff6b6b' - ], - 'circle-radius': [ - 'step', - ['get', 'point_count'], - 20, 10, - 30, 50, - 40, 100, - 50 - ] - } - }, - - // Cluster count labels - { - id: `${this.id}-count`, - type: 'symbol', - source: this.sourceId, - filter: ['has', 'point_count'], - layout: { - 'text-field': '{point_count_abbreviated}', - 'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'], - 'text-size': 12 - }, - paint: { - 'text-color': '#ffffff' - } - }, - // Individual points { id: this.id, type: 'circle', source: this.sourceId, - filter: ['!', ['has', 'point_count']], paint: { 'circle-color': '#3b82f6', 'circle-radius': 6, @@ -83,56 +34,4 @@ export class PointsLayer extends BaseLayer { } ] } - - /** - * Toggle clustering on/off - * @param {boolean} enabled - Whether to enable clustering - */ - toggleClustering(enabled) { - if (!this.data) { - console.warn('Cannot toggle clustering: no data loaded') - return - } - - this.clusteringEnabled = enabled - - // Need to recreate the source with new clustering setting - // MapLibre doesn't support changing cluster setting dynamically - // So we remove and re-add the source - const currentData = this.data - const wasVisible = this.visible - - // Remove all layers first - this.getLayerIds().forEach(layerId => { - if (this.map.getLayer(layerId)) { - this.map.removeLayer(layerId) - } - }) - - // Remove source - if (this.map.getSource(this.sourceId)) { - this.map.removeSource(this.sourceId) - } - - // Re-add source with new clustering setting - this.map.addSource(this.sourceId, this.getSourceConfig()) - - // Re-add layers - const layers = this.getLayerConfigs() - layers.forEach(layerConfig => { - this.map.addLayer(layerConfig) - }) - - // Restore visibility state - this.visible = wasVisible - this.setVisibility(wasVisible) - - // Update data - this.data = currentData - const source = this.map.getSource(this.sourceId) - if (source && source.setData) { - source.setData(currentData) - } - - } } diff --git a/app/javascript/maps_v2/utils/settings_manager.js b/app/javascript/maps_v2/utils/settings_manager.js index 1b02308b..f0d7eb41 100644 --- a/app/javascript/maps_v2/utils/settings_manager.js +++ b/app/javascript/maps_v2/utils/settings_manager.js @@ -7,31 +7,34 @@ const STORAGE_KEY = 'dawarich-maps-v2-settings' const DEFAULT_SETTINGS = { mapStyle: 'light', - clustering: true, - clusterRadius: 50, - heatmapEnabled: false, - pointsVisible: true, - routesVisible: true, - visitsEnabled: false, - photosEnabled: false, - areasEnabled: false, - tracksEnabled: false, - fogEnabled: false, - scratchEnabled: false + enabledMapLayers: ['Points', 'Routes'], // Compatible with v1 map + // Advanced settings + routeOpacity: 1.0, + fogOfWarRadius: 1000, + fogOfWarThreshold: 1, + metersBetweenRoutes: 500, + minutesBetweenRoutes: 60, + pointsRenderingMode: 'raw', + speedColoredRoutes: false +} + +// Mapping between v2 layer names and v1 layer names in enabled_map_layers array +const LAYER_NAME_MAP = { + 'Points': 'pointsVisible', + 'Routes': 'routesVisible', + 'Heatmap': 'heatmapEnabled', + 'Visits': 'visitsEnabled', + 'Photos': 'photosEnabled', + 'Areas': 'areasEnabled', + 'Tracks': 'tracksEnabled', + 'Fog of War': 'fogEnabled', + 'Scratch map': 'scratchEnabled' } // Mapping between frontend settings and backend API keys const BACKEND_SETTINGS_MAP = { mapStyle: 'maps_v2_style', - heatmapEnabled: 'maps_v2_heatmap', - visitsEnabled: 'maps_v2_visits', - photosEnabled: 'maps_v2_photos', - areasEnabled: 'maps_v2_areas', - tracksEnabled: 'maps_v2_tracks', - fogEnabled: 'maps_v2_fog', - scratchEnabled: 'maps_v2_scratch', - clustering: 'maps_v2_clustering', - clusterRadius: 'maps_v2_cluster_radius' + enabledMapLayers: 'enabled_map_layers' } export class SettingsManager { @@ -47,18 +50,55 @@ export class SettingsManager { /** * Get all settings (localStorage first, then merge with defaults) + * Converts enabled_map_layers array to individual boolean flags * @returns {Object} Settings object */ static getSettings() { try { const stored = localStorage.getItem(STORAGE_KEY) - return stored ? { ...DEFAULT_SETTINGS, ...JSON.parse(stored) } : DEFAULT_SETTINGS + const settings = stored ? { ...DEFAULT_SETTINGS, ...JSON.parse(stored) } : DEFAULT_SETTINGS + + // Convert enabled_map_layers array to individual boolean flags + return this._expandLayerSettings(settings) } catch (error) { console.error('Failed to load settings:', error) return DEFAULT_SETTINGS } } + /** + * Convert enabled_map_layers array to individual boolean flags + * @param {Object} settings - Settings with enabledMapLayers array + * @returns {Object} Settings with individual layer booleans + */ + static _expandLayerSettings(settings) { + const enabledLayers = settings.enabledMapLayers || [] + + // Set boolean flags based on array contents + Object.entries(LAYER_NAME_MAP).forEach(([layerName, settingKey]) => { + settings[settingKey] = enabledLayers.includes(layerName) + }) + + return settings + } + + /** + * Convert individual boolean flags to enabled_map_layers array + * @param {Object} settings - Settings with individual layer booleans + * @returns {Array} Array of enabled layer names + */ + static _collapseLayerSettings(settings) { + const enabledLayers = [] + + Object.entries(LAYER_NAME_MAP).forEach(([layerName, settingKey]) => { + if (settings[settingKey] === true) { + enabledLayers.push(layerName) + } + }) + + return enabledLayers + } + /** * Load settings from backend API * @returns {Promise} Settings object from backend @@ -92,11 +132,21 @@ export class SettingsManager { } }) - // Merge with defaults and save to localStorage + // Merge with defaults, but prioritize backend's enabled_map_layers completely const mergedSettings = { ...DEFAULT_SETTINGS, ...frontendSettings } - this.saveToLocalStorage(mergedSettings) - return mergedSettings + // If backend has enabled_map_layers, use it as-is (don't merge with defaults) + if (backendSettings.enabled_map_layers) { + mergedSettings.enabledMapLayers = backendSettings.enabled_map_layers + } + + // Convert enabled_map_layers array to individual boolean flags + const expandedSettings = this._expandLayerSettings(mergedSettings) + + // Save to localStorage + this.saveToLocalStorage(expandedSettings) + + return expandedSettings } catch (error) { console.error('[Settings] Failed to load from backend:', error) return null @@ -127,10 +177,16 @@ export class SettingsManager { } try { + // Convert individual layer booleans to enabled_map_layers array + const enabledMapLayers = this._collapseLayerSettings(settings) + // Convert frontend settings to backend format const backendSettings = {} Object.entries(BACKEND_SETTINGS_MAP).forEach(([frontendKey, backendKey]) => { - if (frontendKey in settings) { + if (frontendKey === 'enabledMapLayers') { + // Use the collapsed array + backendSettings[backendKey] = enabledMapLayers + } else if (frontendKey in settings) { backendSettings[backendKey] = settings[frontendKey] } }) @@ -148,7 +204,7 @@ export class SettingsManager { throw new Error(`Failed to save settings: ${response.status}`) } - console.log('[Settings] Saved to backend successfully') + console.log('[Settings] Saved to backend successfully:', backendSettings) return true } catch (error) { console.error('[Settings] Failed to save to backend:', error) diff --git a/app/views/maps_v2/_settings_panel.html.erb b/app/views/maps_v2/_settings_panel.html.erb index d558aa62..a16163db 100644 --- a/app/views/maps_v2/_settings_panel.html.erb +++ b/app/views/maps_v2/_settings_panel.html.erb @@ -73,8 +73,8 @@

Show individual location points

@@ -87,8 +87,8 @@

Show connected route lines

@@ -101,6 +101,7 @@ @@ -114,6 +115,7 @@ @@ -128,7 +130,7 @@ class="input input-sm input-bordered w-full" data-action="input->maps-v2#searchVisits" /> - Photos @@ -156,6 +159,7 @@ @@ -165,23 +169,25 @@
-
+ <%#

Show saved tracks

-
+
%> -
+ <%#
%>
@@ -195,23 +201,26 @@

Show scratched countries

+
-
+
-
+
- + +
+ + +
+ 10% + 50% + 100% +
+
+ +
+ + +
+ + +
+ 5m + 1000m + 2000m +
+

Clear radius around visited points

+
+ +
+ + +
+ 1 + 5 + 10 +
+

Minimum points to clear fog

+
+ +
+ + +
+ + +
+ 100m + 2500m + 5000m +
+

Distance threshold for route splitting

+
+ +
+ + +
+ 1min + 90min + 180min +
+

Time threshold for route splitting

+
+ +
+ + +
+ +
+ + +
+
+ +
+ +
-

Group nearby points together

+

Color routes by speed

-
+
@@ -249,10 +397,20 @@

Show new points in real-time

-
+
+ + + - -
+
@@ -272,8 +430,8 @@ .map-control-panel { position: absolute; top: 0; - right: -420px; /* Hidden by default */ - width: 420px; + right: -480px; /* Hidden by default */ + width: 480px; height: 100%; background: oklch(var(--b1)); box-shadow: -4px 0 24px rgba(0, 0, 0, 0.15); @@ -297,6 +455,7 @@ align-items: center; padding: 16px 0; gap: 8px; + flex-shrink: 0; } .tab-btn { @@ -347,6 +506,7 @@ display: flex; flex-direction: column; overflow: hidden; + min-width: 0; } .panel-header { @@ -356,6 +516,7 @@ padding: 20px 24px; border-bottom: 1px solid oklch(var(--bc) / 0.1); background: oklch(var(--b1)); + flex-shrink: 0; } .panel-title { @@ -398,11 +559,156 @@ background: oklch(var(--bc) / 0.3); } - /* Responsive */ + /* Toggle Focus State - Remove all focus indicators */ + .toggle:focus, + .toggle:focus-visible, + .toggle:focus-within { + outline: none !important; + box-shadow: none !important; + border-color: inherit !important; + } + + /* Override DaisyUI toggle focus styles */ + .toggle:focus-visible:checked, + .toggle:checked:focus, + .toggle:checked:focus-visible { + outline: none !important; + box-shadow: none !important; + } + + /* Ensure no outline on the toggle container */ + .form-control .toggle:focus { + outline: none !important; + } + + /* Prevent indeterminate visual state on toggles */ + .toggle:indeterminate { + opacity: 1; + } + + /* Ensure smooth toggle transitions without intermediate states */ + .toggle { + transition: background-color 0.2s ease, border-color 0.2s ease; + } + + .toggle:checked { + transition: background-color 0.2s ease, border-color 0.2s ease; + } + + /* Remove any active/pressed state that might cause intermediate appearance */ + .toggle:active, + .toggle:active:focus { + outline: none !important; + box-shadow: none !important; + } + + /* Responsive Breakpoints */ + + /* Large tablets and smaller desktops (1024px - 1280px) */ + @media (max-width: 1280px) { + .map-control-panel { + width: 420px; + right: -420px; + } + } + + /* Tablets (768px - 1024px) */ + @media (max-width: 1024px) { + .map-control-panel { + width: 380px; + right: -380px; + } + + .panel-body { + padding: 20px; + } + } + + /* Small tablets and large phones (640px - 768px) */ + @media (max-width: 768px) { + .map-control-panel { + width: 90%; + right: -90%; + max-width: 400px; + } + + .panel-header { + padding: 16px 20px; + } + + .panel-title { + font-size: 1.125rem; + } + + .panel-body { + padding: 16px 20px; + } + } + + /* Mobile phones (< 640px) */ @media (max-width: 640px) { .map-control-panel { width: 100%; right: -100%; + max-width: none; + } + + .panel-tabs { + width: 56px; + padding: 12px 0; + gap: 6px; + } + + .tab-btn { + width: 44px; + height: 44px; + } + + .tab-icon { + width: 20px; + height: 20px; + } + + .panel-header { + padding: 14px 16px; + } + + .panel-title { + font-size: 1rem; + } + + .panel-body { + padding: 16px; + } + + /* Reduce spacing on mobile */ + .space-y-4 > * + * { + margin-top: 0.75rem; + } + + .space-y-6 > * + * { + margin-top: 1rem; + } + } + + /* Very small phones (< 375px) */ + @media (max-width: 375px) { + .panel-tabs { + width: 52px; + padding: 10px 0; + } + + .tab-btn { + width: 40px; + height: 40px; + } + + .panel-header { + padding: 12px; + } + + .panel-body { + padding: 12px; } } diff --git a/app/views/maps_v2/index.html.erb b/app/views/maps_v2/index.html.erb index f77e7347..5d866de7 100644 --- a/app/views/maps_v2/index.html.erb +++ b/app/views/maps_v2/index.html.erb @@ -21,35 +21,11 @@ - -
- - - - - - - - + +
diff --git a/e2e/README.md b/e2e/README.md index ee328ffd..f9d19fa9 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -8,8 +8,14 @@ End-to-end tests for Dawarich using Playwright. # Run all tests npx playwright test +# Run V1 map tests (Leaflet-based) +npx playwright test e2e/map/ + +# Run V2 map tests (MapLibre-based) +npx playwright test e2e/v2/map/ + # Run specific test file -npx playwright test e2e/map/map-controls.spec.js +npx playwright test e2e/v2/map/settings.spec.js # Run tests in headed mode (watch browser) npx playwright test --headed @@ -27,11 +33,73 @@ npx playwright test --grep-invert @destructive npx playwright test --grep @destructive ``` +## Test Structure + +``` +e2e/ +├── setup/ # Test setup and authentication +├── helpers/ # Shared helper functions +├── map/ # V1 Map tests (Leaflet) - 81 tests +├── v2/ # V2 Map tests (MapLibre) - 52 tests +│ ├── helpers/ # V2-specific helpers +│ ├── map/ # V2 core map tests +│ │ └── layers/ # V2 layer-specific tests +│ └── realtime/ # V2 real-time features +└── temp/ # Playwright artifacts (screenshots, videos) +``` + +## V1 Map Tests (Leaflet-based) - 81 tests + +**Map Tests** +- `map-controls.spec.js` - Basic map controls, zoom, tile layers (5 tests) +- `map-layers.spec.js` - Layer toggles: Routes, Heatmap, Fog, etc. (8 tests) +- `map-points.spec.js` - Point interactions and deletion (4 tests, 1 destructive) +- `map-visits.spec.js` - Confirmed visit interactions and management (5 tests, 3 destructive) +- `map-suggested-visits.spec.js` - Suggested visit interactions (6 tests, 3 destructive) +- `map-add-visit.spec.js` - Add visit control and form (8 tests) +- `map-selection-tool.spec.js` - Selection tool functionality (4 tests) +- `map-calendar-panel.spec.js` - Calendar panel navigation (9 tests) +- `map-side-panel.spec.js` - Side panel (visits drawer) functionality (13 tests)* +- `map-bulk-delete.spec.js` - Bulk point deletion (12 tests, all destructive) +- `map-places-creation.spec.js` - Creating new places on map (9 tests, 2 destructive) +- `map-places-layers.spec.js` - Places layer visibility and filtering (10 tests) + +\* Some side panel tests may be skipped if demo data doesn't contain visits + +## V2 Map Tests (MapLibre-based) - 52 tests + +**Organized by feature domain:** + +### Core Map Tests +- `v2/map/core.spec.js` - Map initialization, lifecycle, loading states (8 tests) +- `v2/map/navigation.spec.js` - Zoom controls, date picker navigation (4 tests) +- `v2/map/interactions.spec.js` - Point clicks, hover effects, popups (2 tests) +- `v2/map/settings.spec.js` - Settings panel, layer toggles, persistence (10 tests) +- `v2/map/performance.spec.js` - Load time benchmarks, efficiency (2 tests) + +### Layer Tests +- `v2/map/layers/points.spec.js` - Points display, GeoJSON data (3 tests) +- `v2/map/layers/routes.spec.js` - Routes geometry, styling, ordering (8 tests) +- `v2/map/layers/heatmap.spec.js` - Heatmap creation, toggle, persistence (3 tests) +- `v2/map/layers/visits.spec.js` - Visits layer toggle and display (2 tests) +- `v2/map/layers/photos.spec.js` - Photos layer toggle and display (2 tests) +- `v2/map/layers/areas.spec.js` - Areas layer toggle and display (2 tests) +- `v2/map/layers/advanced.spec.js` - Fog of war, scratch map (3 tests) + +### Real-time Features +- `v2/realtime/family.spec.js` - Family tracking, ActionCable (2 tests, skipped) + +### V2 Test Organization Benefits +- ✅ **Feature-based hierarchy** - Clear organization by domain +- ✅ **Zero duplication** - All settings tests consolidated +- ✅ **Easy to navigate** - Obvious file naming +- ✅ **Better maintainability** - One feature = one file + ## Test Tags Tests are tagged to enable selective execution: -- **@destructive** (22 tests) - Tests that delete or modify data: +- **@destructive** (22 tests in V1) - Tests that delete or modify data: - Bulk delete operations (12 tests) - Point deletion (1 test) - Visit modification/deletion (3 tests) @@ -51,47 +119,34 @@ npx playwright test --grep @destructive npx playwright test e2e/map/map-bulk-delete.spec.js ``` -## Structure - -``` -e2e/ -├── setup/ # Test setup and authentication -├── helpers/ # Shared helper functions -├── map/ # Map-related tests (40 tests total) -└── temp/ # Playwright artifacts (screenshots, videos) -``` - -### Test Files - -**Map Tests (81 tests)** -- `map-controls.spec.js` - Basic map controls, zoom, tile layers (5 tests) -- `map-layers.spec.js` - Layer toggles: Routes, Heatmap, Fog, etc. (8 tests) -- `map-points.spec.js` - Point interactions and deletion (4 tests, 1 destructive) -- `map-visits.spec.js` - Confirmed visit interactions and management (5 tests, 3 destructive) -- `map-suggested-visits.spec.js` - Suggested visit interactions (6 tests, 3 destructive) -- `map-add-visit.spec.js` - Add visit control and form (8 tests) -- `map-selection-tool.spec.js` - Selection tool functionality (4 tests) -- `map-calendar-panel.spec.js` - Calendar panel navigation (9 tests) -- `map-side-panel.spec.js` - Side panel (visits drawer) functionality (13 tests)* -- `map-bulk-delete.spec.js` - Bulk point deletion (12 tests, all destructive) -- `map-places-creation.spec.js` - Creating new places on map (9 tests, 2 destructive) -- `map-places-layers.spec.js` - Places layer visibility and filtering (10 tests) - -\* Some side panel tests may be skipped if demo data doesn't contain visits - ## Helper Functions -### Map Helpers (`helpers/map.js`) +### V1 Map Helpers (`helpers/map.js`) - `waitForMap(page)` - Wait for Leaflet map initialization - `enableLayer(page, layerName)` - Enable a map layer by name - `clickConfirmedVisit(page)` - Click first confirmed visit circle - `clickSuggestedVisit(page)` - Click first suggested visit circle - `getMapZoom(page)` - Get current map zoom level +### V2 Map Helpers (`v2/helpers/setup.js`) +- `navigateToMapsV2(page)` - Navigate to MapLibre map +- `navigateToMapsV2WithDate(page, startDate, endDate)` - Navigate with date range +- `waitForMapLibre(page)` - Wait for MapLibre initialization +- `waitForLoadingComplete(page)` - Wait for data loading +- `hasMapInstance(page)` - Check if map is initialized +- `getMapZoom(page)` - Get current zoom level +- `getMapCenter(page)` - Get map center coordinates +- `hasLayer(page, layerId)` - Check if layer exists +- `getLayerVisibility(page, layerId)` - Get layer visibility state +- `getPointsSourceData(page)` - Get points source data +- `getRoutesSourceData(page)` - Get routes source data +- `clickMapAt(page, x, y)` - Click at specific coordinates +- `hasPopup(page)` - Check if popup is visible + ### Navigation Helpers (`helpers/navigation.js`) - `closeOnboardingModal(page)` - Close getting started modal - `navigateToDate(page, startDate, endDate)` - Navigate to specific date range -- `navigateToMap(page)` - Navigate to map page with setup +- `navigateToMap(page)` - Navigate to V1 map with setup ### Selection Helpers (`helpers/selection.js`) - `drawSelectionRectangle(page, options)` - Draw selection on map @@ -99,7 +154,7 @@ e2e/ ## Common Patterns -### Basic Test Template +### V1 Basic Test Template (Leaflet) ```javascript import { test, expect } from '@playwright/test'; import { navigateToMap } from '../helpers/navigation.js'; @@ -112,12 +167,60 @@ test('my test', async ({ page }) => { }); ``` -### Testing Map Layers +### V2 Basic Test Template (MapLibre) ```javascript -import { enableLayer } from '../helpers/map.js'; +import { test, expect } from '@playwright/test'; +import { closeOnboardingModal } from '../../helpers/navigation.js'; +import { + navigateToMapsV2, + waitForMapLibre, + waitForLoadingComplete +} from '../helpers/setup.js'; -await enableLayer(page, 'Routes'); -await enableLayer(page, 'Heatmap'); +test.describe('My Feature', () => { + test.beforeEach(async ({ page }) => { + await navigateToMapsV2(page); + await closeOnboardingModal(page); + await waitForMapLibre(page); + await waitForLoadingComplete(page); + }); + + test('my test', async ({ page }) => { + // Your test logic + }); +}); +``` + +### V2 Testing Layer Visibility +```javascript +import { getLayerVisibility } from '../helpers/setup.js'; + +// Check if layer is visible +const isVisible = await getLayerVisibility(page, 'points'); +expect(isVisible).toBe(true); + +// Wait for layer to exist +await page.waitForFunction(() => { + const element = document.querySelector('[data-controller="maps-v2"]'); + const app = window.Stimulus || window.Application; + const controller = app?.getControllerForElementAndIdentifier(element, 'maps-v2'); + return controller?.map?.getLayer('routes') !== undefined; +}, { timeout: 5000 }); +``` + +### V2 Testing Settings Panel +```javascript +// Open settings +await page.click('button[title="Open map settings"]'); +await page.waitForTimeout(400); + +// Switch to layers tab +await page.click('button[data-tab="layers"]'); +await page.waitForTimeout(300); + +// Check toggle state +const toggle = page.locator('label:has-text("Points")').first().locator('input.toggle'); +const isChecked = await toggle.isChecked(); ``` ## Debugging @@ -132,10 +235,25 @@ test-results/ ``` ### Common Issues + +#### V1 Tests - **Flaky tests**: Run with `--workers=1` to avoid parallel interference - **Timeout errors**: Increase timeout in test or use `page.waitForTimeout()` - **Map not loading**: Ensure `waitForMap()` is called after navigation +#### V2 Tests +- **Layer not ready**: Use `page.waitForFunction()` to wait for layer existence +- **Settings panel timing**: Add `waitForTimeout()` after opening/closing +- **Parallel test failures**: Some tests pass individually but fail in parallel - run with `--workers=3` or `--workers=1` +- **Source data not available**: Wait for source to be defined before accessing data + +### V2 Test Tips +1. Always wait for MapLibre to initialize with `waitForMapLibre(page)` +2. Wait for data loading with `waitForLoadingComplete(page)` +3. Add layer existence checks before testing layer properties +4. Use proper waits for settings panel animations +5. Consider timing when testing layer toggles + ## CI/CD Tests run with: @@ -146,6 +264,20 @@ Tests run with: See `playwright.config.js` for full configuration. -## Important considerations +## Important Considerations -- We're using Rails 8 with Turbo, which might not cause full page reloads. +- We're using Rails 8 with Turbo, which might not cause full page reloads +- V2 map uses MapLibre GL JS with Stimulus controllers +- V2 settings are persisted to localStorage +- V2 layer visibility is based on user settings (no hardcoded defaults) +- Some V2 layers (routes, heatmap) are created dynamically based on data + +## Test Migration Notes + +V2 tests were refactored from phase-based to feature-based organization: +- **Before**: 9 phase files, 96 tests (many duplicates) +- **After**: 13 feature files, 52 focused tests (zero duplication) +- **Code reduction**: 56% (2,314 lines → 1,018 lines) +- **Pass rate**: 94% (49/52 tests passing, 1 flaky, 2 skipped) + +See `E2E_REFACTORING_SUCCESS.md` for complete migration details. diff --git a/e2e/v2/helpers/setup.js b/e2e/v2/helpers/setup.js index d09ad613..1bc5d6ef 100644 --- a/e2e/v2/helpers/setup.js +++ b/e2e/v2/helpers/setup.js @@ -126,21 +126,22 @@ export async function getMapCenter(page) { export async function getPointsSourceData(page) { return await page.evaluate(() => { const element = document.querySelector('[data-controller="maps-v2"]'); - if (!element) return { hasSource: false, featureCount: 0 }; + if (!element) return { hasSource: false, featureCount: 0, features: [] }; const app = window.Stimulus || window.Application; - if (!app) return { hasSource: false, featureCount: 0 }; + if (!app) return { hasSource: false, featureCount: 0, features: [] }; const controller = app.getControllerForElementAndIdentifier(element, 'maps-v2'); - if (!controller?.map) return { hasSource: false, featureCount: 0 }; + if (!controller?.map) return { hasSource: false, featureCount: 0, features: [] }; const source = controller.map.getSource('points-source'); - if (!source) return { hasSource: false, featureCount: 0 }; + if (!source) return { hasSource: false, featureCount: 0, features: [] }; const data = source._data; return { hasSource: true, - featureCount: data?.features?.length || 0 + featureCount: data?.features?.length || 0, + features: data?.features || [] }; }); } diff --git a/e2e/v2/map/core.spec.js b/e2e/v2/map/core.spec.js new file mode 100644 index 00000000..e0ebee96 --- /dev/null +++ b/e2e/v2/map/core.spec.js @@ -0,0 +1,137 @@ +import { test, expect } from '@playwright/test' +import { closeOnboardingModal } from '../../helpers/navigation.js' +import { + navigateToMapsV2, + navigateToMapsV2WithDate, + waitForMapLibre, + waitForLoadingComplete, + hasMapInstance, + getMapZoom, + getMapCenter +} from '../helpers/setup.js' + +test.describe('Map Core', () => { + test.beforeEach(async ({ page }) => { + await navigateToMapsV2(page) + await closeOnboardingModal(page) + }) + + test.describe('Initialization', () => { + test('loads map container', async ({ page }) => { + const mapContainer = page.locator('[data-maps-v2-target="container"]') + await expect(mapContainer).toBeVisible() + }) + + test('initializes MapLibre instance', async ({ page }) => { + await waitForMapLibre(page) + + const canvas = page.locator('.maplibregl-canvas') + await expect(canvas).toBeVisible() + + const hasMap = await hasMapInstance(page) + expect(hasMap).toBe(true) + }) + + test('has valid initial center and zoom', async ({ page }) => { + await page.goto('/maps_v2?start_at=2025-10-15T00:00&end_at=2025-10-15T23:59') + await closeOnboardingModal(page) + await waitForMapLibre(page) + await waitForLoadingComplete(page) + await page.waitForTimeout(1000) + + const center = await getMapCenter(page) + const zoom = await getMapZoom(page) + + expect(center).not.toBeNull() + expect(center.lng).toBeGreaterThan(-180) + expect(center.lng).toBeLessThan(180) + expect(center.lat).toBeGreaterThan(-90) + expect(center.lat).toBeLessThan(90) + + expect(zoom).toBeGreaterThan(0) + expect(zoom).toBeLessThan(20) + }) + }) + + test.describe('Loading States', () => { + test('shows loading indicator during data fetch', async ({ page }) => { + const loading = page.locator('[data-maps-v2-target="loading"]') + + const navigationPromise = page.reload({ waitUntil: 'domcontentloaded' }) + + const loadingVisible = await loading.evaluate((el) => !el.classList.contains('hidden')) + .catch(() => false) + + await navigationPromise + await closeOnboardingModal(page) + + await waitForLoadingComplete(page) + await expect(loading).toHaveClass(/hidden/) + }) + + test('handles empty data gracefully', async ({ page }) => { + await navigateToMapsV2WithDate(page, '2020-01-01T00:00', '2020-01-01T23:59') + await closeOnboardingModal(page) + + await waitForLoadingComplete(page) + await page.waitForTimeout(500) + + const hasMap = await hasMapInstance(page) + expect(hasMap).toBe(true) + }) + }) + + test.describe('Data Bounds', () => { + test('fits map bounds to loaded data', async ({ page }) => { + await page.goto('/maps_v2?start_at=2025-10-15T00:00&end_at=2025-10-15T23:59') + await closeOnboardingModal(page) + await waitForMapLibre(page) + await waitForLoadingComplete(page) + await page.waitForTimeout(1000) + + const zoom = await getMapZoom(page) + expect(zoom).toBeGreaterThan(2) + }) + }) + + test.describe('Lifecycle', () => { + test('cleans up and reinitializes on navigation', async ({ page }) => { + await page.goto('/maps_v2?start_at=2025-10-15T00:00&end_at=2025-10-15T23:59') + await closeOnboardingModal(page) + await waitForLoadingComplete(page) + + // Navigate away + await page.goto('/') + await page.waitForTimeout(500) + + // Navigate back + await navigateToMapsV2(page) + await closeOnboardingModal(page) + + await waitForMapLibre(page) + const hasMap = await hasMapInstance(page) + expect(hasMap).toBe(true) + }) + + test('reloads data when changing date range', async ({ page }) => { + await page.goto('/maps_v2?start_at=2025-10-15T00:00&end_at=2025-10-15T23:59') + await closeOnboardingModal(page) + await waitForLoadingComplete(page) + + const startInput = page.locator('input[type="datetime-local"][name="start_at"]') + const initialStartDate = await startInput.inputValue() + + await navigateToMapsV2WithDate(page, '2024-10-14T00:00', '2024-10-14T23:59') + await closeOnboardingModal(page) + + await waitForMapLibre(page) + await waitForLoadingComplete(page) + + const newStartDate = await startInput.inputValue() + expect(newStartDate).not.toBe(initialStartDate) + + const hasMap = await hasMapInstance(page) + expect(hasMap).toBe(true) + }) + }) +}) diff --git a/e2e/v2/map/interactions.spec.js b/e2e/v2/map/interactions.spec.js new file mode 100644 index 00000000..18761a1f --- /dev/null +++ b/e2e/v2/map/interactions.spec.js @@ -0,0 +1,64 @@ +import { test, expect } from '@playwright/test' +import { closeOnboardingModal } from '../../helpers/navigation.js' +import { + navigateToMapsV2WithDate, + waitForLoadingComplete, + clickMapAt, + hasPopup +} from '../helpers/setup.js' + +test.describe('Map Interactions', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/maps_v2?start_at=2025-10-15T00:00&end_at=2025-10-15T23:59') + await closeOnboardingModal(page) + await waitForLoadingComplete(page) + await page.waitForTimeout(500) + }) + + test.describe('Point Clicks', () => { + test('shows popup when clicking on point', async ({ page }) => { + await page.waitForTimeout(1000) + + // Try clicking at different positions to find a point + const positions = [ + { x: 400, y: 300 }, + { x: 500, y: 300 }, + { x: 600, y: 400 }, + { x: 350, y: 250 } + ] + + let popupFound = false + for (const pos of positions) { + try { + await clickMapAt(page, pos.x, pos.y) + await page.waitForTimeout(500) + + if (await hasPopup(page)) { + popupFound = true + break + } + } catch (error) { + // Click might fail if map is still loading + console.log(`Click at ${pos.x},${pos.y} failed: ${error.message}`) + } + } + + if (popupFound) { + const popup = page.locator('.maplibregl-popup') + await expect(popup).toBeVisible() + + const popupContent = page.locator('.point-popup') + await expect(popupContent).toBeVisible() + } else { + console.log('No point clicked (points might be clustered or sparse)') + } + }) + }) + + test.describe('Hover Effects', () => { + test('map container is interactive', async ({ page }) => { + const mapContainer = page.locator('[data-maps-v2-target="container"]') + await expect(mapContainer).toBeVisible() + }) + }) +}) diff --git a/e2e/v2/map/layers/advanced.spec.js b/e2e/v2/map/layers/advanced.spec.js new file mode 100644 index 00000000..6de68ea8 --- /dev/null +++ b/e2e/v2/map/layers/advanced.spec.js @@ -0,0 +1,54 @@ +import { test, expect } from '@playwright/test' +import { closeOnboardingModal } from '../../../helpers/navigation.js' + +test.describe('Advanced Layers', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/maps_v2') + await page.evaluate(() => { + localStorage.removeItem('dawarich-maps-v2-settings') + }) + + await page.goto('/maps_v2?start_at=2025-10-15T00:00&end_at=2025-10-15T23:59') + await closeOnboardingModal(page) + await page.waitForTimeout(2000) + }) + + test.describe('Fog of War', () => { + test('fog layer is disabled by default', async ({ page }) => { + const fogEnabled = await page.evaluate(() => { + const settings = JSON.parse(localStorage.getItem('dawarich-maps-v2-settings') || '{}') + return settings.fogEnabled + }) + + expect(fogEnabled).toBeFalsy() + }) + + test('can toggle fog layer', async ({ page }) => { + await page.click('button[title="Open map settings"]') + await page.waitForTimeout(400) + await page.click('button[data-tab="layers"]') + await page.waitForTimeout(300) + + const fogToggle = page.locator('label:has-text("Fog of War")').first().locator('input.toggle') + await fogToggle.check() + await page.waitForTimeout(500) + + expect(await fogToggle.isChecked()).toBe(true) + }) + }) + + test.describe('Scratch Map', () => { + test('can toggle scratch map layer', async ({ page }) => { + await page.click('button[title="Open map settings"]') + await page.waitForTimeout(400) + await page.click('button[data-tab="layers"]') + await page.waitForTimeout(300) + + const scratchToggle = page.locator('label:has-text("Scratch map")').first().locator('input.toggle') + await scratchToggle.check() + await page.waitForTimeout(500) + + expect(await scratchToggle.isChecked()).toBe(true) + }) + }) +}) diff --git a/e2e/v2/map/layers/areas.spec.js b/e2e/v2/map/layers/areas.spec.js new file mode 100644 index 00000000..7a14b94d --- /dev/null +++ b/e2e/v2/map/layers/areas.spec.js @@ -0,0 +1,39 @@ +import { test, expect } from '@playwright/test' +import { closeOnboardingModal } from '../../../helpers/navigation.js' +import { navigateToMapsV2, waitForMapLibre, waitForLoadingComplete } from '../../helpers/setup.js' + +test.describe('Areas Layer', () => { + test.beforeEach(async ({ page }) => { + await navigateToMapsV2(page) + await closeOnboardingModal(page) + await waitForMapLibre(page) + await waitForLoadingComplete(page) + await page.waitForTimeout(1500) + }) + + test.describe('Toggle', () => { + test('areas layer toggle exists', async ({ page }) => { + await page.click('button[title="Open map settings"]') + await page.waitForTimeout(400) + await page.click('button[data-tab="layers"]') + await page.waitForTimeout(300) + + const areasToggle = page.locator('label:has-text("Areas")').first().locator('input.toggle') + await expect(areasToggle).toBeVisible() + }) + + test('can toggle areas layer', async ({ page }) => { + await page.click('button[title="Open map settings"]') + await page.waitForTimeout(400) + await page.click('button[data-tab="layers"]') + await page.waitForTimeout(300) + + const areasToggle = page.locator('label:has-text("Areas")').first().locator('input.toggle') + await areasToggle.check() + await page.waitForTimeout(500) + + const isChecked = await areasToggle.isChecked() + expect(isChecked).toBe(true) + }) + }) +}) diff --git a/e2e/v2/map/layers/heatmap.spec.js b/e2e/v2/map/layers/heatmap.spec.js new file mode 100644 index 00000000..6fd6a4d6 --- /dev/null +++ b/e2e/v2/map/layers/heatmap.spec.js @@ -0,0 +1,80 @@ +import { test, expect } from '@playwright/test' +import { closeOnboardingModal } from '../../../helpers/navigation.js' + +test.describe('Heatmap Layer', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/maps_v2?start_at=2025-10-15T00:00&end_at=2025-10-15T23:59') + await closeOnboardingModal(page) + await page.waitForTimeout(2000) + }) + + test.describe('Creation', () => { + test('heatmap layer can be enabled', async ({ page }) => { + await page.click('button[title="Open map settings"]') + await page.waitForTimeout(500) + + await page.click('button[data-tab="layers"]') + await page.waitForTimeout(300) + + const heatmapLabel = page.locator('label:has-text("Heatmap")').first() + const heatmapToggle = heatmapLabel.locator('input.toggle') + await heatmapToggle.check() + + // Wait for heatmap layer to be created + await page.waitForFunction(() => { + const element = document.querySelector('[data-controller="maps-v2"]') + if (!element) return false + const app = window.Stimulus || window.Application + const controller = app?.getControllerForElementAndIdentifier(element, 'maps-v2') + return controller?.map?.getLayer('heatmap') !== undefined + }, { timeout: 3000 }).catch(() => false) + + const hasHeatmap = await page.evaluate(() => { + const element = document.querySelector('[data-controller="maps-v2"]') + if (!element) return false + const app = window.Stimulus || window.Application + const controller = app?.getControllerForElementAndIdentifier(element, 'maps-v2') + return controller?.map?.getLayer('heatmap') !== undefined + }) + + expect(hasHeatmap).toBe(true) + }) + + test('heatmap can be toggled', async ({ page }) => { + await page.click('button[title="Open map settings"]') + await page.waitForTimeout(500) + await page.click('button[data-tab="layers"]') + await page.waitForTimeout(300) + + const heatmapToggle = page.locator('label:has-text("Heatmap")').first().locator('input.toggle') + + await heatmapToggle.check() + await page.waitForTimeout(500) + expect(await heatmapToggle.isChecked()).toBe(true) + + await heatmapToggle.uncheck() + await page.waitForTimeout(500) + expect(await heatmapToggle.isChecked()).toBe(false) + }) + }) + + test.describe('Persistence', () => { + test('heatmap setting persists', async ({ page }) => { + await page.click('button[title="Open map settings"]') + await page.waitForTimeout(500) + await page.click('button[data-tab="layers"]') + await page.waitForTimeout(300) + + const heatmapToggle = page.locator('label:has-text("Heatmap")').first().locator('input.toggle') + await heatmapToggle.check() + await page.waitForTimeout(500) + + const settings = await page.evaluate(() => { + return localStorage.getItem('dawarich-maps-v2-settings') + }) + + const parsed = JSON.parse(settings) + expect(parsed.heatmapEnabled).toBe(true) + }) + }) +}) diff --git a/e2e/v2/map/layers/photos.spec.js b/e2e/v2/map/layers/photos.spec.js new file mode 100644 index 00000000..f7e13c1f --- /dev/null +++ b/e2e/v2/map/layers/photos.spec.js @@ -0,0 +1,39 @@ +import { test, expect } from '@playwright/test' +import { closeOnboardingModal } from '../../../helpers/navigation.js' +import { navigateToMapsV2, waitForMapLibre, waitForLoadingComplete } from '../../helpers/setup.js' + +test.describe('Photos Layer', () => { + test.beforeEach(async ({ page }) => { + await navigateToMapsV2(page) + await closeOnboardingModal(page) + await waitForMapLibre(page) + await waitForLoadingComplete(page) + await page.waitForTimeout(1500) + }) + + test.describe('Toggle', () => { + test('photos layer toggle exists', async ({ page }) => { + await page.click('button[title="Open map settings"]') + await page.waitForTimeout(400) + await page.click('button[data-tab="layers"]') + await page.waitForTimeout(300) + + const photosToggle = page.locator('label:has-text("Photos")').first().locator('input.toggle') + await expect(photosToggle).toBeVisible() + }) + + test('can toggle photos layer', async ({ page }) => { + await page.click('button[title="Open map settings"]') + await page.waitForTimeout(400) + await page.click('button[data-tab="layers"]') + await page.waitForTimeout(300) + + const photosToggle = page.locator('label:has-text("Photos")').first().locator('input.toggle') + await photosToggle.check() + await page.waitForTimeout(500) + + const isChecked = await photosToggle.isChecked() + expect(isChecked).toBe(true) + }) + }) +}) diff --git a/e2e/v2/map/layers/points.spec.js b/e2e/v2/map/layers/points.spec.js new file mode 100644 index 00000000..c4f95d1c --- /dev/null +++ b/e2e/v2/map/layers/points.spec.js @@ -0,0 +1,71 @@ +import { test, expect } from '@playwright/test' +import { closeOnboardingModal } from '../../../helpers/navigation.js' +import { + navigateToMapsV2WithDate, + waitForLoadingComplete, + hasLayer, + getPointsSourceData +} from '../../helpers/setup.js' + +test.describe('Points Layer', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/maps_v2?start_at=2025-10-15T00:00&end_at=2025-10-15T23:59') + await closeOnboardingModal(page) + await waitForLoadingComplete(page) + await page.waitForTimeout(1500) + }) + + test.describe('Display', () => { + test('displays points layer', async ({ page }) => { + // Wait for points layer to be added + await page.waitForFunction(() => { + const element = document.querySelector('[data-controller="maps-v2"]') + const app = window.Stimulus || window.Application + const controller = app?.getControllerForElementAndIdentifier(element, 'maps-v2') + return controller?.map?.getLayer('points') !== undefined + }, { timeout: 10000 }).catch(() => false) + + const hasPoints = await hasLayer(page, 'points') + expect(hasPoints).toBe(true) + }) + + test('loads and displays point data', async ({ page }) => { + await page.waitForFunction(() => { + const element = document.querySelector('[data-controller="maps-v2"]') + const app = window.Stimulus || window.Application + const controller = app?.getControllerForElementAndIdentifier(element, 'maps-v2') + return controller?.map?.getSource('points-source') !== undefined + }, { timeout: 15000 }).catch(() => false) + + const sourceData = await getPointsSourceData(page) + expect(sourceData.hasSource).toBe(true) + expect(sourceData.featureCount).toBeGreaterThan(0) + }) + }) + + test.describe('Data Source', () => { + test('points source contains valid GeoJSON features', async ({ page }) => { + // Wait for source to be added + await page.waitForFunction(() => { + const element = document.querySelector('[data-controller="maps-v2"]') + const app = window.Stimulus || window.Application + const controller = app?.getControllerForElementAndIdentifier(element, 'maps-v2') + return controller?.map?.getSource('points-source') !== undefined + }, { timeout: 10000 }).catch(() => false) + + const sourceData = await getPointsSourceData(page) + + expect(sourceData.hasSource).toBe(true) + expect(sourceData.features).toBeDefined() + expect(Array.isArray(sourceData.features)).toBe(true) + + if (sourceData.features.length > 0) { + const firstFeature = sourceData.features[0] + expect(firstFeature.type).toBe('Feature') + expect(firstFeature.geometry).toBeDefined() + expect(firstFeature.geometry.type).toBe('Point') + expect(firstFeature.geometry.coordinates).toHaveLength(2) + } + }) + }) +}) diff --git a/e2e/v2/map/layers/routes.spec.js b/e2e/v2/map/layers/routes.spec.js new file mode 100644 index 00000000..e9f6ed41 --- /dev/null +++ b/e2e/v2/map/layers/routes.spec.js @@ -0,0 +1,186 @@ +import { test, expect } from '@playwright/test' +import { closeOnboardingModal } from '../../../helpers/navigation.js' +import { + navigateToMapsV2WithDate, + waitForMapLibre, + waitForLoadingComplete, + hasLayer, + getLayerVisibility, + getRoutesSourceData +} from '../../helpers/setup.js' + +test.describe('Routes Layer', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/maps_v2?start_at=2025-10-15T00:00&end_at=2025-10-15T23:59') + await closeOnboardingModal(page) + await waitForMapLibre(page) + await waitForLoadingComplete(page) + await page.waitForTimeout(1500) + }) + + test.describe('Layer Existence', () => { + test('routes layer exists on map', async ({ page }) => { + await page.waitForFunction(() => { + const element = document.querySelector('[data-controller="maps-v2"]') + if (!element) return false + const app = window.Stimulus || window.Application + if (!app) return false + const controller = app.getControllerForElementAndIdentifier(element, 'maps-v2') + return controller?.map?.getLayer('routes') !== undefined + }, { timeout: 10000 }).catch(() => false) + + const hasRoutesLayer = await hasLayer(page, 'routes') + expect(hasRoutesLayer).toBe(true) + }) + }) + + test.describe('Data Source', () => { + test('routes source has data', async ({ page }) => { + await page.waitForFunction(() => { + const element = document.querySelector('[data-controller="maps-v2"]') + if (!element) return false + const app = window.Stimulus || window.Application + if (!app) return false + const controller = app.getControllerForElementAndIdentifier(element, 'maps-v2') + return controller?.map?.getSource('routes-source') !== undefined + }, { timeout: 20000 }) + + const { hasSource, featureCount } = await getRoutesSourceData(page) + + expect(hasSource).toBe(true) + expect(featureCount).toBeGreaterThanOrEqual(0) + }) + + test('routes have LineString geometry', async ({ page }) => { + const { features } = await getRoutesSourceData(page) + + if (features.length > 0) { + features.forEach(feature => { + expect(feature.geometry.type).toBe('LineString') + expect(feature.geometry.coordinates.length).toBeGreaterThan(1) + }) + } + }) + + test('routes have distance properties', async ({ page }) => { + const { features } = await getRoutesSourceData(page) + + if (features.length > 0) { + features.forEach(feature => { + expect(feature.properties).toHaveProperty('distance') + expect(typeof feature.properties.distance).toBe('number') + expect(feature.properties.distance).toBeGreaterThanOrEqual(0) + }) + } + }) + + test('routes connect points chronologically', async ({ page }) => { + const { features } = await getRoutesSourceData(page) + + if (features.length > 0) { + features.forEach(feature => { + expect(feature.properties).toHaveProperty('startTime') + expect(feature.properties).toHaveProperty('endTime') + expect(feature.properties.endTime).toBeGreaterThanOrEqual(feature.properties.startTime) + expect(feature.properties).toHaveProperty('pointCount') + expect(feature.properties.pointCount).toBeGreaterThan(1) + }) + } + }) + }) + + test.describe('Styling', () => { + test('routes have solid color (not speed-based)', async ({ page }) => { + await page.waitForFunction(() => { + const element = document.querySelector('[data-controller="maps-v2"]') + if (!element) return false + const app = window.Stimulus || window.Application + if (!app) return false + const controller = app.getControllerForElementAndIdentifier(element, 'maps-v2') + return controller?.map?.getLayer('routes') !== undefined + }, { timeout: 20000 }) + + const routeLayerInfo = await page.evaluate(() => { + const element = document.querySelector('[data-controller="maps-v2"]') + if (!element) return null + + const app = window.Stimulus || window.Application + if (!app) return null + + const controller = app.getControllerForElementAndIdentifier(element, 'maps-v2') + if (!controller?.map) return null + + const layer = controller.map.getLayer('routes') + if (!layer) return null + + const lineColor = controller.map.getPaintProperty('routes', 'line-color') + + return { + exists: !!lineColor, + isArray: Array.isArray(lineColor), + value: lineColor + } + }) + + expect(routeLayerInfo).toBeTruthy() + expect(routeLayerInfo.exists).toBe(true) + expect(routeLayerInfo.isArray).toBe(false) + expect(routeLayerInfo.value).toBe('#f97316') + }) + }) + + test.describe('Layer Order', () => { + test('routes layer renders below points layer', async ({ page }) => { + await page.waitForFunction(() => { + const element = document.querySelector('[data-controller="maps-v2"]') + const app = window.Stimulus || window.Application + const controller = app?.getControllerForElementAndIdentifier(element, 'maps-v2') + return controller?.map?.getLayer('routes') !== undefined && + controller?.map?.getLayer('points') !== undefined + }, { timeout: 10000 }) + + const layerOrder = await page.evaluate(() => { + const element = document.querySelector('[data-controller="maps-v2"]') + if (!element) return null + + const app = window.Stimulus || window.Application + if (!app) return null + + const controller = app.getControllerForElementAndIdentifier(element, 'maps-v2') + if (!controller?.map) return null + + const style = controller.map.getStyle() + const layers = style.layers || [] + + const routesIndex = layers.findIndex(l => l.id === 'routes') + const pointsIndex = layers.findIndex(l => l.id === 'points') + + return { routesIndex, pointsIndex } + }) + + expect(layerOrder).toBeTruthy() + if (layerOrder.routesIndex >= 0 && layerOrder.pointsIndex >= 0) { + expect(layerOrder.routesIndex).toBeLessThan(layerOrder.pointsIndex) + } + }) + }) + + test.describe('Persistence', () => { + test('date navigation preserves routes layer', async ({ page }) => { + await page.waitForTimeout(1000) + + const initialRoutes = await hasLayer(page, 'routes') + expect(initialRoutes).toBe(true) + + await navigateToMapsV2WithDate(page, '2025-10-16T00:00', '2025-10-16T23:59') + await closeOnboardingModal(page) + + await waitForMapLibre(page) + await waitForLoadingComplete(page) + await page.waitForTimeout(1500) + + const hasRoutesLayer = await hasLayer(page, 'routes') + expect(hasRoutesLayer).toBe(true) + }) + }) +}) diff --git a/e2e/v2/map/layers/visits.spec.js b/e2e/v2/map/layers/visits.spec.js new file mode 100644 index 00000000..c9ae6b04 --- /dev/null +++ b/e2e/v2/map/layers/visits.spec.js @@ -0,0 +1,39 @@ +import { test, expect } from '@playwright/test' +import { closeOnboardingModal } from '../../../helpers/navigation.js' +import { navigateToMapsV2, waitForMapLibre, waitForLoadingComplete } from '../../helpers/setup.js' + +test.describe('Visits Layer', () => { + test.beforeEach(async ({ page }) => { + await navigateToMapsV2(page) + await closeOnboardingModal(page) + await waitForMapLibre(page) + await waitForLoadingComplete(page) + await page.waitForTimeout(1500) + }) + + test.describe('Toggle', () => { + test('visits layer toggle exists', async ({ page }) => { + await page.click('button[title="Open map settings"]') + await page.waitForTimeout(400) + await page.click('button[data-tab="layers"]') + await page.waitForTimeout(300) + + const visitsToggle = page.locator('label:has-text("Visits")').first().locator('input.toggle') + await expect(visitsToggle).toBeVisible() + }) + + test('can toggle visits layer', async ({ page }) => { + await page.click('button[title="Open map settings"]') + await page.waitForTimeout(400) + await page.click('button[data-tab="layers"]') + await page.waitForTimeout(300) + + const visitsToggle = page.locator('label:has-text("Visits")').first().locator('input.toggle') + await visitsToggle.check() + await page.waitForTimeout(500) + + const isChecked = await visitsToggle.isChecked() + expect(isChecked).toBe(true) + }) + }) +}) diff --git a/e2e/v2/map/navigation.spec.js b/e2e/v2/map/navigation.spec.js new file mode 100644 index 00000000..3ec659a4 --- /dev/null +++ b/e2e/v2/map/navigation.spec.js @@ -0,0 +1,66 @@ +import { test, expect } from '@playwright/test' +import { closeOnboardingModal } from '../../helpers/navigation.js' +import { + navigateToMapsV2, + waitForMapLibre, + getMapZoom +} from '../helpers/setup.js' + +test.describe('Map Navigation', () => { + test.beforeEach(async ({ page }) => { + await navigateToMapsV2(page) + await closeOnboardingModal(page) + }) + + test.describe('Controls', () => { + test('displays navigation controls', async ({ page }) => { + await waitForMapLibre(page) + + const navControls = page.locator('.maplibregl-ctrl-top-right') + await expect(navControls).toBeVisible() + + const zoomIn = page.locator('.maplibregl-ctrl-zoom-in') + const zoomOut = page.locator('.maplibregl-ctrl-zoom-out') + await expect(zoomIn).toBeVisible() + await expect(zoomOut).toBeVisible() + }) + + test('zooms in when clicking zoom in button', async ({ page }) => { + await waitForMapLibre(page) + + const initialZoom = await getMapZoom(page) + await page.locator('.maplibregl-ctrl-zoom-in').click() + await page.waitForTimeout(500) + const newZoom = await getMapZoom(page) + + expect(newZoom).toBeGreaterThan(initialZoom) + }) + + test('zooms out when clicking zoom out button', async ({ page }) => { + await waitForMapLibre(page) + + // First zoom in to ensure we can zoom out + await page.locator('.maplibregl-ctrl-zoom-in').click() + await page.waitForTimeout(500) + + const initialZoom = await getMapZoom(page) + await page.locator('.maplibregl-ctrl-zoom-out').click() + await page.waitForTimeout(500) + const newZoom = await getMapZoom(page) + + expect(newZoom).toBeLessThan(initialZoom) + }) + }) + + test.describe('Date Picker', () => { + test('displays date navigation inputs', async ({ page }) => { + const startInput = page.locator('input[type="datetime-local"][name="start_at"]') + const endInput = page.locator('input[type="datetime-local"][name="end_at"]') + const searchButton = page.locator('input[type="submit"][value="Search"]') + + await expect(startInput).toBeVisible() + await expect(endInput).toBeVisible() + await expect(searchButton).toBeVisible() + }) + }) +}) diff --git a/e2e/v2/map/performance.spec.js b/e2e/v2/map/performance.spec.js new file mode 100644 index 00000000..af67bdac --- /dev/null +++ b/e2e/v2/map/performance.spec.js @@ -0,0 +1,34 @@ +import { test, expect } from '@playwright/test' +import { closeOnboardingModal } from '../../helpers/navigation.js' +import { waitForMapLibre, waitForLoadingComplete } from '../helpers/setup.js' + +test.describe('Map Performance', () => { + test('map loads within acceptable time', async ({ page }) => { + const startTime = Date.now() + + await page.goto('/maps_v2?start_at=2025-10-15T00:00&end_at=2025-10-15T23:59') + await closeOnboardingModal(page) + await waitForMapLibre(page) + await waitForLoadingComplete(page) + + const loadTime = Date.now() - startTime + console.log(`Map loaded in ${loadTime}ms`) + + // Should load in less than 15 seconds (including modal, map init, data fetch) + expect(loadTime).toBeLessThan(15000) + }) + + test('handles large datasets efficiently', async ({ page }) => { + await page.goto('/maps_v2?start_at=2025-10-01T00:00&end_at=2025-10-31T23:59') + await closeOnboardingModal(page) + + const startTime = Date.now() + await waitForLoadingComplete(page) + const loadTime = Date.now() - startTime + + console.log(`Large dataset loaded in ${loadTime}ms`) + + // Should still complete reasonably quickly + expect(loadTime).toBeLessThan(15000) + }) +}) diff --git a/e2e/v2/map/settings.spec.js b/e2e/v2/map/settings.spec.js new file mode 100644 index 00000000..61052ba1 --- /dev/null +++ b/e2e/v2/map/settings.spec.js @@ -0,0 +1,202 @@ +import { test, expect } from '@playwright/test' +import { closeOnboardingModal } from '../../helpers/navigation.js' +import { getLayerVisibility } from '../helpers/setup.js' + +test.describe('Map Settings', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/maps_v2?start_at=2025-10-15T00:00&end_at=2025-10-15T23:59') + await closeOnboardingModal(page) + await page.waitForTimeout(2000) + }) + + test.describe('Settings Panel', () => { + test('opens and closes settings panel', async ({ page }) => { + const settingsButton = page.locator('button[title="Open map settings"]') + await settingsButton.waitFor({ state: 'visible', timeout: 5000 }) + await settingsButton.click() + await page.waitForTimeout(500) + + const panel = page.locator('[data-maps-v2-target="settingsPanel"]') + await expect(panel).toHaveClass(/open/) + + const closeButton = page.locator('button[title="Close panel"]') + await closeButton.click() + await page.waitForTimeout(500) + await expect(panel).not.toHaveClass(/open/) + }) + + test('displays layer controls in settings', async ({ page }) => { + await page.click('button[title="Open map settings"]') + await page.waitForTimeout(400) + + await page.click('button[data-tab="layers"]') + await page.waitForTimeout(300) + + const pointsToggle = page.locator('label:has-text("Points")').first().locator('input.toggle') + const routesToggle = page.locator('label:has-text("Routes")').first().locator('input.toggle') + + await expect(pointsToggle).toBeVisible() + await expect(routesToggle).toBeVisible() + }) + + test('has tabs for different settings sections', async ({ page }) => { + await page.click('button[title="Open map settings"]') + await page.waitForTimeout(400) + + const searchTab = page.locator('button[data-tab="search"]') + const layersTab = page.locator('button[data-tab="layers"]') + const settingsTab = page.locator('button[data-tab="settings"]') + + await expect(searchTab).toBeVisible() + await expect(layersTab).toBeVisible() + await expect(settingsTab).toBeVisible() + }) + }) + + test.describe('Layer Toggles', () => { + test('points layer visibility matches toggle state', async ({ page }) => { + // Wait for points layer to exist + await page.waitForFunction(() => { + const element = document.querySelector('[data-controller="maps-v2"]') + const app = window.Stimulus || window.Application + const controller = app?.getControllerForElementAndIdentifier(element, 'maps-v2') + return controller?.map?.getLayer('points') !== undefined + }, { timeout: 5000 }).catch(() => false) + + const isVisible = await getLayerVisibility(page, 'points') + + await page.click('button[title="Open map settings"]') + await page.waitForTimeout(400) + await page.click('button[data-tab="layers"]') + await page.waitForTimeout(300) + + const pointsToggle = page.locator('label:has-text("Points")').first().locator('input.toggle') + const toggleState = await pointsToggle.isChecked() + + expect(isVisible).toBe(toggleState) + }) + + test('routes layer visibility matches toggle state', async ({ page }) => { + // Wait for routes layer to exist + await page.waitForFunction(() => { + const element = document.querySelector('[data-controller="maps-v2"]') + const app = window.Stimulus || window.Application + const controller = app?.getControllerForElementAndIdentifier(element, 'maps-v2') + return controller?.map?.getLayer('routes') !== undefined + }, { timeout: 5000 }).catch(() => false) + + const isVisible = await getLayerVisibility(page, 'routes') + + await page.click('button[title="Open map settings"]') + await page.waitForTimeout(400) + await page.click('button[data-tab="layers"]') + await page.waitForTimeout(300) + + const routesToggle = page.locator('label:has-text("Routes")').first().locator('input.toggle') + const toggleState = await routesToggle.isChecked() + + expect(isVisible).toBe(toggleState) + }) + + test('can toggle points layer', async ({ page }) => { + await page.click('button[title="Open map settings"]') + await page.waitForTimeout(400) + await page.click('button[data-tab="layers"]') + await page.waitForTimeout(300) + + const pointsLabel = page.locator('label:has-text("Points")').first() + const pointsToggle = pointsLabel.locator('input.toggle') + + const initialState = await pointsToggle.isChecked() + + await pointsLabel.click() + await page.waitForTimeout(500) + + const newState = await pointsToggle.isChecked() + expect(newState).toBe(!initialState) + + await pointsLabel.click() + await page.waitForTimeout(500) + + const finalState = await pointsToggle.isChecked() + expect(finalState).toBe(initialState) + }) + + test('can toggle routes layer', async ({ page }) => { + await page.click('button[title="Open map settings"]') + await page.waitForTimeout(400) + await page.click('button[data-tab="layers"]') + await page.waitForTimeout(300) + + const routesLabel = page.locator('label:has-text("Routes")').first() + const routesToggle = routesLabel.locator('input.toggle') + + const initialState = await routesToggle.isChecked() + + await routesLabel.click() + await page.waitForTimeout(500) + + const newState = await routesToggle.isChecked() + expect(newState).toBe(!initialState) + }) + + test('multiple layers can be toggled simultaneously', async ({ page }) => { + await page.click('button[title="Open map settings"]') + await page.waitForTimeout(400) + await page.click('button[data-tab="layers"]') + await page.waitForTimeout(300) + + const pointsToggle = page.locator('label:has-text("Points")').first().locator('input.toggle') + const routesToggle = page.locator('label:has-text("Routes")').first().locator('input.toggle') + + if (!(await pointsToggle.isChecked())) { + await pointsToggle.check() + await page.waitForTimeout(500) + } + if (!(await routesToggle.isChecked())) { + await routesToggle.check() + await page.waitForTimeout(500) + } + + const pointsVisible = await getLayerVisibility(page, 'points') + const routesVisible = await getLayerVisibility(page, 'routes') + + expect(pointsVisible).toBe(true) + expect(routesVisible).toBe(true) + }) + }) + + test.describe('Settings Persistence', () => { + test('layer toggle state persists in localStorage', async ({ page }) => { + await page.click('button[title="Open map settings"]') + await page.waitForTimeout(400) + await page.click('button[data-tab="layers"]') + await page.waitForTimeout(300) + + const pointsToggle = page.locator('label:has-text("Points")').first().locator('input.toggle') + const initialState = await pointsToggle.isChecked() + + const settings = await page.evaluate(() => { + return localStorage.getItem('dawarich-maps-v2-settings') + }) + + expect(settings).toBeTruthy() + + const parsed = JSON.parse(settings) + expect(parsed).toHaveProperty('pointsVisible') + expect(parsed.pointsVisible).toBe(initialState) + }) + }) + + test.describe('Advanced Settings', () => { + test('displays advanced settings options', async ({ page }) => { + await page.click('button[title="Open map settings"]') + await page.waitForTimeout(400) + await page.click('button[data-tab="settings"]') + await page.waitForTimeout(300) + + const panel = page.locator('[data-tab-content="settings"]') + await expect(panel).toBeVisible() + }) + }) +}) diff --git a/e2e/v2/phase-1-mvp.spec.js b/e2e/v2/phase-1-mvp.spec.js deleted file mode 100644 index 59021dcb..00000000 --- a/e2e/v2/phase-1-mvp.spec.js +++ /dev/null @@ -1,314 +0,0 @@ -import { test, expect } from '@playwright/test'; -import { closeOnboardingModal } from '../helpers/navigation.js'; -import { - navigateToMapsV2, - navigateToMapsV2WithDate, - waitForMapLibre, - waitForLoadingComplete, - hasMapInstance, - getMapZoom, - getMapCenter, - getPointsSourceData, - hasLayer, - clickMapAt, - hasPopup -} from './helpers/setup.js'; - -test.describe('Phase 1: MVP - Basic Map with Points', () => { - test.beforeEach(async ({ page }) => { - // Navigate to Maps V2 page - await navigateToMapsV2(page); - await closeOnboardingModal(page); - }); - - test('should load map container', async ({ page }) => { - const mapContainer = page.locator('[data-maps-v2-target="container"]'); - await expect(mapContainer).toBeVisible(); - }); - - test('should initialize MapLibre map', async ({ page }) => { - // Wait for map to load - await waitForMapLibre(page); - - // Verify MapLibre canvas is present - const canvas = page.locator('.maplibregl-canvas'); - await expect(canvas).toBeVisible(); - - // Verify map instance exists - const hasMap = await hasMapInstance(page); - expect(hasMap).toBe(true); - }); - - test('should display navigation controls', async ({ page }) => { - await waitForMapLibre(page); - - // Verify navigation controls are present - const navControls = page.locator('.maplibregl-ctrl-top-right'); - await expect(navControls).toBeVisible(); - - // Verify zoom controls - const zoomIn = page.locator('.maplibregl-ctrl-zoom-in'); - const zoomOut = page.locator('.maplibregl-ctrl-zoom-out'); - await expect(zoomIn).toBeVisible(); - await expect(zoomOut).toBeVisible(); - }); - - test('should display date navigation', async ({ page }) => { - // Verify date inputs are present - const startInput = page.locator('input[type="datetime-local"][name="start_at"]'); - const endInput = page.locator('input[type="datetime-local"][name="end_at"]'); - const searchButton = page.locator('input[type="submit"][value="Search"]'); - - await expect(startInput).toBeVisible(); - await expect(endInput).toBeVisible(); - await expect(searchButton).toBeVisible(); - }); - - test('should show loading indicator during data fetch', async ({ page }) => { - const loading = page.locator('[data-maps-v2-target="loading"]'); - - // Start navigation without waiting - const navigationPromise = page.reload({ waitUntil: 'domcontentloaded' }); - - // Check that loading appears (it should be visible during data fetch) - // We wait up to 1 second for it to appear - if data loads too fast, we skip this check - const loadingVisible = await loading.evaluate((el) => !el.classList.contains('hidden')) - .catch(() => false); - - // Wait for navigation to complete - await navigationPromise; - await closeOnboardingModal(page); - - // Wait for loading to hide - await waitForLoadingComplete(page); - await expect(loading).toHaveClass(/hidden/); - }); - - test('should load and display points on map', async ({ page }) => { - // Navigate to specific date with known data (same as existing map tests) - await navigateToMapsV2WithDate(page, '2025-10-15T00:00', '2025-10-15T23:59'); - - // navigateToMapsV2WithDate already waits for loading to complete - // Wait for style to load and layers to be added - await page.waitForFunction(() => { - const element = document.querySelector('[data-controller="maps-v2"]'); - const app = window.Stimulus || window.Application; - const controller = app?.getControllerForElementAndIdentifier(element, 'maps-v2'); - return controller?.map?.getSource('points-source') !== undefined; - }, { timeout: 15000 }).catch(() => { - console.log('Timeout waiting for points source'); - return false; - }); - - // Check if points source exists and has data - const sourceData = await getPointsSourceData(page); - expect(sourceData.hasSource).toBe(true); - expect(sourceData.featureCount).toBeGreaterThan(0); - - console.log(`Loaded ${sourceData.featureCount} points on map`); - }); - - test('should display points layers (clusters, counts, individual points)', async ({ page }) => { - await navigateToMapsV2WithDate(page, '2025-10-15T00:00', '2025-10-15T23:59'); - await waitForLoadingComplete(page); - - // Check for all three point layers - const hasClusters = await hasLayer(page, 'points-clusters'); - const hasCount = await hasLayer(page, 'points-count'); - const hasPoints = await hasLayer(page, 'points'); - - expect(hasClusters).toBe(true); - expect(hasCount).toBe(true); - expect(hasPoints).toBe(true); - }); - - test('should zoom in when clicking zoom in button', async ({ page }) => { - await waitForMapLibre(page); - - const initialZoom = await getMapZoom(page); - await page.locator('.maplibregl-ctrl-zoom-in').click(); - await page.waitForTimeout(500); - const newZoom = await getMapZoom(page); - - expect(newZoom).toBeGreaterThan(initialZoom); - }); - - test('should zoom out when clicking zoom out button', async ({ page }) => { - await waitForMapLibre(page); - - // First zoom in to make sure we can zoom out - await page.locator('.maplibregl-ctrl-zoom-in').click(); - await page.waitForTimeout(500); - - const initialZoom = await getMapZoom(page); - await page.locator('.maplibregl-ctrl-zoom-out').click(); - await page.waitForTimeout(500); - const newZoom = await getMapZoom(page); - - expect(newZoom).toBeLessThan(initialZoom); - }); - - test('should fit map bounds to data', async ({ page }) => { - await navigateToMapsV2WithDate(page, '2025-10-15T00:00', '2025-10-15T23:59'); - - // navigateToMapsV2WithDate already waits for loading - // Give a bit more time for fitBounds to complete - await page.waitForTimeout(500); - - // Get map zoom level (should be > 2 if fitBounds worked) - const zoom = await getMapZoom(page); - expect(zoom).toBeGreaterThan(2); - }); - - test('should show popup when clicking on point', async ({ page }) => { - await navigateToMapsV2WithDate(page, '2025-10-15T00:00', '2025-10-15T23:59'); - await waitForLoadingComplete(page); - - // Wait a bit for points to render - await page.waitForTimeout(1000); - - // Try clicking at different positions to find a point - const positions = [ - { x: 400, y: 300 }, - { x: 500, y: 300 }, - { x: 600, y: 400 }, - { x: 350, y: 250 } - ]; - - let popupFound = false; - for (const pos of positions) { - try { - await clickMapAt(page, pos.x, pos.y); - await page.waitForTimeout(500); - - if (await hasPopup(page)) { - popupFound = true; - break; - } - } catch (error) { - // Click might fail if map is still loading or covered - console.log(`Click at ${pos.x},${pos.y} failed: ${error.message}`); - } - } - - // If we found a popup, verify its content - if (popupFound) { - const popup = page.locator('.maplibregl-popup'); - await expect(popup).toBeVisible(); - - // Verify popup has point information - const popupContent = page.locator('.point-popup'); - await expect(popupContent).toBeVisible(); - - console.log('Successfully clicked a point and showed popup'); - } else { - console.log('No point clicked (might be expected if points are clustered or sparse)'); - // Don't fail the test - points might be clustered or not at exact positions - } - }); - - test('should change cursor on hover over points', async ({ page }) => { - await navigateToMapsV2WithDate(page, '2025-10-15T00:00', '2025-10-15T23:59'); - await waitForLoadingComplete(page); - - // Check if cursor changes when hovering over map - // Note: This is a basic check; actual cursor change happens on point hover - const mapContainer = page.locator('[data-maps-v2-target="container"]'); - await expect(mapContainer).toBeVisible(); - }); - - test('should reload data when changing date range', async ({ page }) => { - await navigateToMapsV2WithDate(page, '2025-10-15T00:00', '2025-10-15T23:59'); - await closeOnboardingModal(page); - await waitForLoadingComplete(page); - - // Verify initial data loaded - const initialData = await getPointsSourceData(page); - expect(initialData.hasSource).toBe(true); - const initialCount = initialData.featureCount; - - // Get initial date inputs - const startInput = page.locator('input[type="datetime-local"][name="start_at"]'); - const initialStartDate = await startInput.inputValue(); - - // Change date range - with Turbo this might not cause full page reload - await navigateToMapsV2WithDate(page, '2024-10-14T00:00', '2024-10-14T23:59'); - await closeOnboardingModal(page); - - // Wait for map to reload/reinitialize - await waitForMapLibre(page); - await waitForLoadingComplete(page); - - // Verify date input changed (proving form submission worked) - const newStartDate = await startInput.inputValue(); - expect(newStartDate).not.toBe(initialStartDate); - - // Verify map still works - const hasMap = await hasMapInstance(page); - expect(hasMap).toBe(true); - - console.log(`Date changed from ${initialStartDate} to ${newStartDate}`); - }); - - test('should handle empty data gracefully', async ({ page }) => { - // Navigate to a date range with likely no data - await navigateToMapsV2WithDate(page, '2020-01-01T00:00', '2020-01-01T23:59'); - await closeOnboardingModal(page); - - // Wait for loading to complete - await waitForLoadingComplete(page); - await page.waitForTimeout(500); // Give sources time to initialize - - // Map should still work with empty data - const hasMap = await hasMapInstance(page); - expect(hasMap).toBe(true); - - // Check if source exists - it may or may not depending on timing - const sourceData = await getPointsSourceData(page); - // If source exists, it should have 0 features for this date range - if (sourceData.hasSource) { - expect(sourceData.featureCount).toBeGreaterThanOrEqual(0); - } - }); - - test('should have valid map center and zoom', async ({ page }) => { - await navigateToMapsV2WithDate(page, '2025-10-15T00:00', '2025-10-15T23:59'); - await waitForLoadingComplete(page); - - const center = await getMapCenter(page); - const zoom = await getMapZoom(page); - - // Verify valid coordinates - expect(center).not.toBeNull(); - expect(center.lng).toBeGreaterThan(-180); - expect(center.lng).toBeLessThan(180); - expect(center.lat).toBeGreaterThan(-90); - expect(center.lat).toBeLessThan(90); - - // Verify valid zoom level - expect(zoom).toBeGreaterThan(0); - expect(zoom).toBeLessThan(20); - - console.log(`Map center: ${center.lat}, ${center.lng}, zoom: ${zoom}`); - }); - - test('should cleanup map on disconnect', async ({ page }) => { - await navigateToMapsV2WithDate(page, '2025-10-15T00:00', '2025-10-15T23:59'); - await waitForLoadingComplete(page); - - // Navigate away - await page.goto('/'); - - // Wait a bit for cleanup - await page.waitForTimeout(500); - - // Navigate back - await navigateToMapsV2(page); - await closeOnboardingModal(page); - - // Map should reinitialize properly - await waitForMapLibre(page); - const hasMap = await hasMapInstance(page); - expect(hasMap).toBe(true); - }); -}); diff --git a/e2e/v2/phase-2-routes.spec.js b/e2e/v2/phase-2-routes.spec.js deleted file mode 100644 index 4acad517..00000000 --- a/e2e/v2/phase-2-routes.spec.js +++ /dev/null @@ -1,354 +0,0 @@ -import { test, expect } from '@playwright/test'; -import { closeOnboardingModal } from '../helpers/navigation.js'; -import { - navigateToMapsV2, - navigateToMapsV2WithDate, - waitForMapLibre, - waitForLoadingComplete, - hasLayer, - getLayerVisibility, - getRoutesSourceData -} from './helpers/setup.js'; - -test.describe('Phase 2: Routes + Layer Controls', () => { - test.beforeEach(async ({ page }) => { - // Navigate directly with URL parameters to date range with data - await page.goto('/maps_v2?start_at=2025-10-15T00:00&end_at=2025-10-15T23:59'); - await closeOnboardingModal(page); - await waitForMapLibre(page); - await waitForLoadingComplete(page); - // Give extra time for routes layer to be added after points (needs time for style.load event) - await page.waitForTimeout(1500); - }); - - test('routes layer exists on map', async ({ page }) => { - // Wait for routes layer to be added (it's added after points layer) - await page.waitForFunction(() => { - const element = document.querySelector('[data-controller="maps-v2"]'); - if (!element) return false; - const app = window.Stimulus || window.Application; - if (!app) return false; - const controller = app.getControllerForElementAndIdentifier(element, 'maps-v2'); - return controller?.map?.getLayer('routes') !== undefined; - }, { timeout: 10000 }).catch(() => false); - - // Check if routes layer exists - const hasRoutesLayer = await hasLayer(page, 'routes'); - expect(hasRoutesLayer).toBe(true); - }); - - test('routes source has data', async ({ page }) => { - // Wait for routes layer to be added with longer timeout - await page.waitForFunction(() => { - const element = document.querySelector('[data-controller="maps-v2"]'); - if (!element) return false; - const app = window.Stimulus || window.Application; - if (!app) return false; - const controller = app.getControllerForElementAndIdentifier(element, 'maps-v2'); - return controller?.map?.getSource('routes-source') !== undefined; - }, { timeout: 20000 }); - - const { hasSource, featureCount } = await getRoutesSourceData(page); - - expect(hasSource).toBe(true); - // Should have at least one route if there are points - expect(featureCount).toBeGreaterThanOrEqual(0); - }); - - test('routes have LineString geometry', async ({ page }) => { - const { features } = await getRoutesSourceData(page); - - if (features.length > 0) { - features.forEach(feature => { - expect(feature.geometry.type).toBe('LineString'); - expect(feature.geometry.coordinates.length).toBeGreaterThan(1); - }); - } - }); - - test('routes have distance properties', async ({ page }) => { - const { features } = await getRoutesSourceData(page); - - if (features.length > 0) { - features.forEach(feature => { - expect(feature.properties).toHaveProperty('distance'); - expect(typeof feature.properties.distance).toBe('number'); - expect(feature.properties.distance).toBeGreaterThanOrEqual(0); - }); - } - }); - - test('routes have solid color (not speed-based)', async ({ page }) => { - // Wait for routes layer to be added with longer timeout - await page.waitForFunction(() => { - const element = document.querySelector('[data-controller="maps-v2"]'); - if (!element) return false; - const app = window.Stimulus || window.Application; - if (!app) return false; - const controller = app.getControllerForElementAndIdentifier(element, 'maps-v2'); - return controller?.map?.getLayer('routes') !== undefined; - }, { timeout: 20000 }); - - const routeLayerInfo = await page.evaluate(() => { - const element = document.querySelector('[data-controller="maps-v2"]'); - if (!element) return null; - - const app = window.Stimulus || window.Application; - if (!app) return null; - - const controller = app.getControllerForElementAndIdentifier(element, 'maps-v2'); - if (!controller?.map) return null; - - const layer = controller.map.getLayer('routes'); - if (!layer) return null; - - // Get paint property using MapLibre's getPaintProperty method - const lineColor = controller.map.getPaintProperty('routes', 'line-color'); - - return { - exists: !!lineColor, - isArray: Array.isArray(lineColor), - value: lineColor - }; - }); - - expect(routeLayerInfo).toBeTruthy(); - expect(routeLayerInfo.exists).toBe(true); - // Should NOT be a speed-based interpolation array - expect(routeLayerInfo.isArray).toBe(false); - // Should be orange color - expect(routeLayerInfo.value).toBe('#f97316'); - }); - - test('layer controls are visible', async ({ page }) => { - const pointsButton = page.locator('button[data-layer="points"]'); - const routesButton = page.locator('button[data-layer="routes"]'); - - await expect(pointsButton).toBeVisible(); - await expect(routesButton).toBeVisible(); - }); - - test('points layer starts visible', async ({ page }) => { - const isVisible = await getLayerVisibility(page, 'points'); - expect(isVisible).toBe(true); - }); - - test('routes layer starts visible', async ({ page }) => { - const isVisible = await getLayerVisibility(page, 'routes'); - expect(isVisible).toBe(true); - }); - - test('can toggle points layer off and on', async ({ page }) => { - const pointsButton = page.locator('button[data-layer="points"]'); - - // Initially visible - let isVisible = await getLayerVisibility(page, 'points'); - expect(isVisible).toBe(true); - - // Toggle off and wait for visibility to change - await pointsButton.click(); - await page.waitForFunction(() => { - const element = document.querySelector('[data-controller="maps-v2"]'); - const app = window.Stimulus || window.Application; - const controller = app?.getControllerForElementAndIdentifier(element, 'maps-v2'); - const visibility = controller?.map?.getLayoutProperty('points', 'visibility'); - return visibility === 'none'; - }, { timeout: 2000 }).catch(() => {}); - - isVisible = await getLayerVisibility(page, 'points'); - expect(isVisible).toBe(false); - - // Toggle back on - await pointsButton.click(); - await page.waitForFunction(() => { - const element = document.querySelector('[data-controller="maps-v2"]'); - const app = window.Stimulus || window.Application; - const controller = app?.getControllerForElementAndIdentifier(element, 'maps-v2'); - const visibility = controller?.map?.getLayoutProperty('points', 'visibility'); - return visibility === 'visible' || visibility === undefined; - }, { timeout: 2000 }).catch(() => {}); - - isVisible = await getLayerVisibility(page, 'points'); - expect(isVisible).toBe(true); - }); - - test('can toggle routes layer off and on', async ({ page }) => { - // Wait for routes layer to exist first - await page.waitForFunction(() => { - const element = document.querySelector('[data-controller="maps-v2"]'); - const app = window.Stimulus || window.Application; - const controller = app?.getControllerForElementAndIdentifier(element, 'maps-v2'); - return controller?.map?.getLayer('routes') !== undefined; - }, { timeout: 10000 }).catch(() => false); - - const routesButton = page.locator('button[data-layer="routes"]'); - - // Initially visible - let isVisible = await getLayerVisibility(page, 'routes'); - expect(isVisible).toBe(true); - - // Toggle off and wait for visibility to change - await routesButton.click(); - await page.waitForFunction(() => { - const element = document.querySelector('[data-controller="maps-v2"]'); - const app = window.Stimulus || window.Application; - const controller = app?.getControllerForElementAndIdentifier(element, 'maps-v2'); - const visibility = controller?.map?.getLayoutProperty('routes', 'visibility'); - return visibility === 'none'; - }, { timeout: 2000 }).catch(() => {}); - - isVisible = await getLayerVisibility(page, 'routes'); - expect(isVisible).toBe(false); - - // Toggle back on - await routesButton.click(); - await page.waitForFunction(() => { - const element = document.querySelector('[data-controller="maps-v2"]'); - const app = window.Stimulus || window.Application; - const controller = app?.getControllerForElementAndIdentifier(element, 'maps-v2'); - const visibility = controller?.map?.getLayoutProperty('routes', 'visibility'); - return visibility === 'visible' || visibility === undefined; - }, { timeout: 2000 }).catch(() => {}); - - isVisible = await getLayerVisibility(page, 'routes'); - expect(isVisible).toBe(true); - }); - - test('layer toggle button styles change with visibility', async ({ page }) => { - const pointsButton = page.locator('button[data-layer="points"]'); - - // Initially should have btn-primary - await expect(pointsButton).toHaveClass(/btn-primary/); - - // Click to toggle off - await pointsButton.click(); - await page.waitForTimeout(100); - - // Should now have btn-outline - await expect(pointsButton).toHaveClass(/btn-outline/); - - // Click to toggle back on - await pointsButton.click(); - await page.waitForTimeout(100); - - // Should have btn-primary again - await expect(pointsButton).toHaveClass(/btn-primary/); - }); - - test('both layers can be visible simultaneously', async ({ page }) => { - const pointsVisible = await getLayerVisibility(page, 'points'); - const routesVisible = await getLayerVisibility(page, 'routes'); - - expect(pointsVisible).toBe(true); - expect(routesVisible).toBe(true); - }); - - test('both layers can be hidden simultaneously', async ({ page }) => { - const pointsButton = page.locator('button[data-layer="points"]'); - const routesButton = page.locator('button[data-layer="routes"]'); - - // Toggle points off and wait - await pointsButton.click(); - await page.waitForFunction(() => { - const element = document.querySelector('[data-controller="maps-v2"]'); - const app = window.Stimulus || window.Application; - const controller = app?.getControllerForElementAndIdentifier(element, 'maps-v2'); - const visibility = controller?.map?.getLayoutProperty('points', 'visibility'); - return visibility === 'none'; - }, { timeout: 2000 }).catch(() => {}); - - // Toggle routes off and wait - await routesButton.click(); - await page.waitForFunction(() => { - const element = document.querySelector('[data-controller="maps-v2"]'); - const app = window.Stimulus || window.Application; - const controller = app?.getControllerForElementAndIdentifier(element, 'maps-v2'); - const visibility = controller?.map?.getLayoutProperty('routes', 'visibility'); - return visibility === 'none'; - }, { timeout: 2000 }).catch(() => {}); - - const pointsVisible = await getLayerVisibility(page, 'points'); - const routesVisible = await getLayerVisibility(page, 'routes'); - - expect(pointsVisible).toBe(false); - expect(routesVisible).toBe(false); - }); - - test('date navigation preserves routes layer', async ({ page }) => { - // Wait for routes layer to be added first - await page.waitForTimeout(1000); - - // Verify routes exist initially - const initialRoutes = await hasLayer(page, 'routes'); - expect(initialRoutes).toBe(true); - - // Navigate to a different date with known data (Oct 16 instead of Oct 15) - await navigateToMapsV2WithDate(page, '2025-10-16T00:00', '2025-10-16T23:59'); - await closeOnboardingModal(page); - - // Wait for map to fully reload - await waitForMapLibre(page); - await waitForLoadingComplete(page); - await page.waitForTimeout(1500); - - // Verify routes layer still exists after navigation - const hasRoutesLayer = await hasLayer(page, 'routes'); - expect(hasRoutesLayer).toBe(true); - }); - - test('routes connect points chronologically', async ({ page }) => { - const { features } = await getRoutesSourceData(page); - - if (features.length > 0) { - features.forEach(feature => { - // Each route should have start and end times - expect(feature.properties).toHaveProperty('startTime'); - expect(feature.properties).toHaveProperty('endTime'); - - // End time should be after start time - expect(feature.properties.endTime).toBeGreaterThanOrEqual(feature.properties.startTime); - - // Should have point count - expect(feature.properties).toHaveProperty('pointCount'); - expect(feature.properties.pointCount).toBeGreaterThan(1); - }); - } - }); - - test('routes layer renders below points layer', async ({ page }) => { - // Wait for both layers to exist - await page.waitForFunction(() => { - const element = document.querySelector('[data-controller="maps-v2"]'); - const app = window.Stimulus || window.Application; - const controller = app?.getControllerForElementAndIdentifier(element, 'maps-v2'); - return controller?.map?.getLayer('routes') !== undefined && - controller?.map?.getLayer('points') !== undefined; - }, { timeout: 10000 }); - - // Get layer order - routes should be added before points - const layerOrder = await page.evaluate(() => { - const element = document.querySelector('[data-controller="maps-v2"]'); - if (!element) return null; - - const app = window.Stimulus || window.Application; - if (!app) return null; - - const controller = app.getControllerForElementAndIdentifier(element, 'maps-v2'); - if (!controller?.map) return null; - - const style = controller.map.getStyle(); - const layers = style.layers || []; - - const routesIndex = layers.findIndex(l => l.id === 'routes'); - const pointsIndex = layers.findIndex(l => l.id === 'points'); - - return { routesIndex, pointsIndex }; - }); - - expect(layerOrder).toBeTruthy(); - // Routes should come before points in layer order (lower index = rendered first/below) - if (layerOrder.routesIndex >= 0 && layerOrder.pointsIndex >= 0) { - expect(layerOrder.routesIndex).toBeLessThan(layerOrder.pointsIndex); - } - }); -}); diff --git a/e2e/v2/phase-3-heatmap.spec.js b/e2e/v2/phase-3-heatmap.spec.js deleted file mode 100644 index d977536d..00000000 --- a/e2e/v2/phase-3-heatmap.spec.js +++ /dev/null @@ -1,305 +0,0 @@ -import { test, expect } from '@playwright/test' -import { navigateToMapsV2, navigateToMapsV2WithDate, waitForMapLibre, waitForLoadingComplete } from './helpers/setup' -import { closeOnboardingModal } from '../helpers/navigation' - -test.describe('Phase 3: Heatmap + Settings', () => { - // Use serial mode to avoid overwhelming the system with parallel requests - test.describe.configure({ mode: 'serial' }) - - test.beforeEach(async ({ page }) => { - // Navigate with a date that has data - await page.goto('/maps_v2?start_at=2025-10-15T00:00&end_at=2025-10-15T23:59') - await closeOnboardingModal(page) - - // Wait for map with retry logic - try { - await waitForMapLibre(page) - await waitForLoadingComplete(page) - } catch (error) { - console.log('Map loading timeout, waiting and retrying...') - await page.waitForTimeout(2000) - // Try one more time - await waitForLoadingComplete(page).catch(() => { - console.log('Second attempt also timed out, continuing anyway...') - }) - } - - await page.waitForTimeout(1000) // Give layers time to initialize - }) - - test.describe('Heatmap Layer', () => { - test('heatmap layer can be created', async ({ page }) => { - // Heatmap layer might not exist by default, but should be creatable - // Open settings panel - await page.click('button[title="Settings"]') - await page.waitForTimeout(500) - - // Switch to Layers tab - await page.click('button[data-tab="layers"]') - await page.waitForTimeout(300) - - // Find and toggle heatmap using DaisyUI toggle - const heatmapLabel = page.locator('label:has-text("Heatmap")').first() - const heatmapToggle = heatmapLabel.locator('input.toggle') - await heatmapToggle.check() - await page.waitForTimeout(500) - - // Check if heatmap layer now exists - const hasHeatmap = await page.evaluate(() => { - const element = document.querySelector('[data-controller="maps-v2"]') - if (!element) return false - const app = window.Stimulus || window.Application - if (!app) return false - const controller = app.getControllerForElementAndIdentifier(element, 'maps-v2') - return controller?.map?.getLayer('heatmap') !== undefined - }) - - expect(hasHeatmap).toBe(true) - }) - - test('heatmap can be toggled', async ({ page }) => { - // Open settings panel - await page.click('button[title="Settings"]') - await page.waitForTimeout(500) - - // Switch to Layers tab - await page.click('button[data-tab="layers"]') - await page.waitForTimeout(300) - - // Toggle heatmap on - find toggle by its label text - const heatmapLabel = page.locator('label:has-text("Heatmap")').first() - const heatmapToggle = heatmapLabel.locator('input.toggle') - await heatmapToggle.check() - await page.waitForTimeout(500) - - const isVisible = await page.evaluate(() => { - const element = document.querySelector('[data-controller="maps-v2"]') - const app = window.Stimulus || window.Application - const controller = app?.getControllerForElementAndIdentifier(element, 'maps-v2') - const visibility = controller?.map?.getLayoutProperty('heatmap', 'visibility') - return visibility === 'visible' || visibility === undefined - }) - - expect(isVisible).toBe(true) - }) - - test('heatmap setting persists', async ({ page }) => { - await page.click('button[title="Settings"]') - await page.waitForTimeout(300) - - // Switch to Layers tab - await page.click('button[data-tab="layers"]') - await page.waitForTimeout(300) - - const heatmapToggle = page.locator('label:has-text("Heatmap")').first().locator('input.toggle') - await heatmapToggle.check() - await page.waitForTimeout(300) - - // Check localStorage - const savedSetting = await page.evaluate(() => { - const settings = JSON.parse(localStorage.getItem('dawarich-maps-v2-settings') || '{}') - return settings.heatmapEnabled - }) - - expect(savedSetting).toBe(true) - }) - }) - - test.describe('Settings Panel', () => { - test('settings panel opens and closes', async ({ page }) => { - const settingsBtn = page.locator('button[title="Settings"]') - await settingsBtn.click() - - // Wait for panel to open (animation takes 300ms) - const panel = page.locator('.map-control-panel') - await page.waitForTimeout(400) - await expect(panel).toHaveClass(/open/) - - // Close the panel using the close button - await page.click('.panel-header button[title="Close panel"]') - - // Wait for panel close animation - await page.waitForTimeout(400) - await expect(panel).not.toHaveClass(/open/) - }) - - test('tab switching works', async ({ page }) => { - await page.click('button[title="Settings"]') - await page.waitForTimeout(400) - - // Check default tab is Search - const searchTab = page.locator('[data-tab-content="search"]') - await expect(searchTab).toHaveClass(/active/) - - // Switch to Layers tab - await page.click('button[data-tab="layers"]') - await page.waitForTimeout(300) - - const layersTab = page.locator('[data-tab-content="layers"]') - await expect(layersTab).toHaveClass(/active/) - await expect(searchTab).not.toHaveClass(/active/) - - // Switch to Settings tab - await page.click('button[data-tab="settings"]') - await page.waitForTimeout(300) - - const settingsTab = page.locator('[data-tab-content="settings"]') - await expect(settingsTab).toHaveClass(/active/) - await expect(layersTab).not.toHaveClass(/active/) - }) - - test('map style can be changed', async ({ page }) => { - await page.click('button[title="Settings"]') - await page.waitForTimeout(300) - - // Switch to Settings tab - await page.click('button[data-tab="settings"]') - await page.waitForTimeout(300) - - const styleSelect = page.locator('select.select-bordered').first() - await styleSelect.selectOption('dark') - - // Wait for style to load - await page.waitForTimeout(1000) - - const savedStyle = await page.evaluate(() => { - const settings = JSON.parse(localStorage.getItem('dawarich-maps-v2-settings') || '{}') - return settings.mapStyle - }) - - expect(savedStyle).toBe('dark') - }) - - test('settings persist across page loads', async ({ page }) => { - // Change a setting - await page.click('button[title="Settings"]') - await page.waitForTimeout(300) - - // Switch to Layers tab - await page.click('button[data-tab="layers"]') - await page.waitForTimeout(300) - - const heatmapToggle = page.locator('label:has-text("Heatmap")').first().locator('input.toggle') - await heatmapToggle.check() - await page.waitForTimeout(300) - - // Reload page - await page.reload() - await closeOnboardingModal(page) - await waitForMapLibre(page) - - // Check if setting persisted - const savedSetting = await page.evaluate(() => { - const settings = JSON.parse(localStorage.getItem('dawarich-maps-v2-settings') || '{}') - return settings.heatmapEnabled - }) - - expect(savedSetting).toBe(true) - }) - - test('reset to defaults works', async ({ page }) => { - // Change settings - await page.click('button[title="Settings"]') - await page.waitForTimeout(300) - - // Switch to Settings tab - await page.click('button[data-tab="settings"]') - await page.waitForTimeout(300) - - await page.locator('select.select-bordered').first().selectOption('dark') - await page.waitForTimeout(300) - - // Switch to Layers tab to enable heatmap - await page.click('button[data-tab="layers"]') - await page.waitForTimeout(300) - - const heatmapToggle = page.locator('label:has-text("Heatmap")').first().locator('input.toggle') - await heatmapToggle.check() - await page.waitForTimeout(300) - - // Switch back to Settings tab - await page.click('button[data-tab="settings"]') - await page.waitForTimeout(300) - - // Setup dialog handler before clicking reset - page.on('dialog', dialog => dialog.accept()) - - // Reset - this will reload the page - await page.click('.btn-outline:has-text("Reset to Defaults")') - - // Wait for page reload - await closeOnboardingModal(page) - await waitForMapLibre(page) - - // Check defaults restored (localStorage should be empty) - const settings = await page.evaluate(() => { - const stored = localStorage.getItem('dawarich-maps-v2-settings') - return stored ? JSON.parse(stored) : null - }) - - // After reset, localStorage should be null or empty - expect(settings).toBeNull() - }) - }) - - test.describe('Regression Tests', () => { - test('points layer still works', async ({ page }) => { - // Wait for points source to be available - await page.waitForFunction(() => { - const element = document.querySelector('[data-controller="maps-v2"]') - const app = window.Stimulus || window.Application - const controller = app?.getControllerForElementAndIdentifier(element, 'maps-v2') - return controller?.map?.getSource('points-source') !== undefined - }, { timeout: 10000 }) - - const hasPoints = await page.evaluate(() => { - const element = document.querySelector('[data-controller="maps-v2"]') - const app = window.Stimulus || window.Application - const controller = app?.getControllerForElementAndIdentifier(element, 'maps-v2') - const source = controller?.map?.getSource('points-source') - return source && source._data?.features?.length > 0 - }) - - expect(hasPoints).toBe(true) - }) - - test('routes layer still works', async ({ page }) => { - // Wait for routes source to be available - await page.waitForFunction(() => { - const element = document.querySelector('[data-controller="maps-v2"]') - const app = window.Stimulus || window.Application - const controller = app?.getControllerForElementAndIdentifier(element, 'maps-v2') - return controller?.map?.getSource('routes-source') !== undefined - }, { timeout: 10000 }) - - const hasRoutes = await page.evaluate(() => { - const element = document.querySelector('[data-controller="maps-v2"]') - const app = window.Stimulus || window.Application - const controller = app?.getControllerForElementAndIdentifier(element, 'maps-v2') - const source = controller?.map?.getSource('routes-source') - return source && source._data?.features?.length > 0 - }) - - expect(hasRoutes).toBe(true) - }) - - test('layer toggle still works', async ({ page }) => { - // Just verify settings panel has layer toggles - await page.click('button[title="Settings"]') - await page.waitForTimeout(400) - - // Switch to Layers tab - await page.click('button[data-tab="layers"]') - await page.waitForTimeout(300) - - // Check that settings panel is open - const settingsPanel = page.locator('.map-control-panel.open') - await expect(settingsPanel).toBeVisible() - - // Check that DaisyUI toggles exist (any layer toggle) - const toggles = page.locator('input.toggle') - const count = await toggles.count() - expect(count).toBeGreaterThan(0) - }) - }) -}) diff --git a/e2e/v2/phase-4-visits.spec.js b/e2e/v2/phase-4-visits.spec.js deleted file mode 100644 index c396300f..00000000 --- a/e2e/v2/phase-4-visits.spec.js +++ /dev/null @@ -1,131 +0,0 @@ -import { test, expect } from '@playwright/test' -import { closeOnboardingModal } from '../helpers/navigation' -import { - navigateToMapsV2, - waitForMapLibre, - waitForLoadingComplete, - hasLayer -} from './helpers/setup' - -test.describe('Phase 4: Visits + Photos', () => { - test.beforeEach(async ({ page }) => { - await navigateToMapsV2(page) - await closeOnboardingModal(page) - await waitForMapLibre(page) - await waitForLoadingComplete(page) - await page.waitForTimeout(1500) - }) - - test.describe('Visits Layer', () => { - test('visits layer toggle exists', async ({ page }) => { - // Open settings - await page.click('button[title="Settings"]') - await page.waitForTimeout(400) - - const visitsToggle = page.locator('label.setting-checkbox:has-text("Show Visits")') - await expect(visitsToggle).toBeVisible() - }) - - test('can toggle visits layer in settings', async ({ page }) => { - // Open settings - await page.click('button[title="Settings"]') - await page.waitForTimeout(400) - - // Toggle visits - const visitsCheckbox = page.locator('label.setting-checkbox:has-text("Show Visits")').locator('input[type="checkbox"]') - await visitsCheckbox.check() - await page.waitForTimeout(500) - - // Verify checkbox is checked - const isChecked = await visitsCheckbox.isChecked() - expect(isChecked).toBe(true) - }) - }) - - test.describe('Photos Layer', () => { - test('photos layer toggle exists', async ({ page }) => { - // Photos now use HTML markers, not MapLibre layers - // Just check the settings toggle exists - await page.click('button[title="Settings"]') - await page.waitForTimeout(400) - - const photosToggle = page.locator('label.setting-checkbox:has-text("Show Photos")') - await expect(photosToggle).toBeVisible() - }) - - test('photos layer starts hidden', async ({ page }) => { - // Photos use HTML markers - check if they are hidden - const photoMarkers = page.locator('.photo-marker') - const count = await photoMarkers.count() - - if (count > 0) { - // If markers exist, check they're hidden - const firstMarker = photoMarkers.first() - const isHidden = await firstMarker.evaluate(el => - el.parentElement.style.display === 'none' - ) - expect(isHidden).toBe(true) - } else { - // If no markers, that's also fine (no photos in test data) - expect(count).toBe(0) - } - }) - - test('can toggle photos layer in settings', async ({ page }) => { - // Open settings - await page.click('button[title="Settings"]') - await page.waitForTimeout(400) - - // Toggle photos - const photosCheckbox = page.locator('label.setting-checkbox:has-text("Show Photos")').locator('input[type="checkbox"]') - await photosCheckbox.check() - await page.waitForTimeout(500) - - // Verify checkbox is checked - const isChecked = await photosCheckbox.isChecked() - expect(isChecked).toBe(true) - }) - }) - - test.describe('Visits Search', () => { - test('visits search input exists', async ({ page }) => { - // Just check the search input exists in DOM - const searchInput = page.locator('#visits-search') - await expect(searchInput).toBeAttached() - }) - - test('can search visits', async ({ page }) => { - // Open settings and enable visits - await page.click('button[title="Settings"]') - await page.waitForTimeout(400) - - const visitsCheckbox = page.locator('label.setting-checkbox:has-text("Show Visits")').locator('input[type="checkbox"]') - await visitsCheckbox.check() - await page.waitForTimeout(500) - - // Wait for search input to be visible - const searchInput = page.locator('#visits-search') - await expect(searchInput).toBeVisible({ timeout: 5000 }) - - // Search - await searchInput.fill('test') - await page.waitForTimeout(300) - - // Verify search was applied (filter should have run) - const searchValue = await searchInput.inputValue() - expect(searchValue).toBe('test') - }) - }) - - test.describe('Regression Tests', () => { - test('all previous layers still work', async ({ page }) => { - // Just verify the settings panel opens - await page.click('button[title="Settings"]') - await page.waitForTimeout(400) - - // Check settings panel is open - const settingsPanel = page.locator('.settings-panel.open') - await expect(settingsPanel).toBeVisible() - }) - }) -}) diff --git a/e2e/v2/phase-5-areas.spec.js b/e2e/v2/phase-5-areas.spec.js deleted file mode 100644 index a129f7c4..00000000 --- a/e2e/v2/phase-5-areas.spec.js +++ /dev/null @@ -1,156 +0,0 @@ -import { test, expect } from '@playwright/test' -import { closeOnboardingModal } from '../helpers/navigation' -import { - navigateToMapsV2, - waitForMapLibre, - waitForLoadingComplete, - hasLayer -} from './helpers/setup' - -test.describe('Phase 5: Areas + Drawing Tools', () => { - test.beforeEach(async ({ page }) => { - await navigateToMapsV2(page) - await closeOnboardingModal(page) - await waitForMapLibre(page) - await waitForLoadingComplete(page) - await page.waitForTimeout(1500) - }) - - test.describe('Areas Layer', () => { - test.skip('areas layer exists on map (requires test data)', async ({ page }) => { - // NOTE: This test requires areas to be created in the test database - // Layer is only added when areas data is available - const hasAreasLayer = await hasLayer(page, 'areas-fill') - expect(hasAreasLayer).toBe(true) - }) - - test('areas layer starts hidden', async ({ page }) => { - const isVisible = await page.evaluate(() => { - const element = document.querySelector('[data-controller="maps-v2"]') - const app = window.Stimulus || window.Application - const controller = app?.getControllerForElementAndIdentifier(element, 'maps-v2') - const visibility = controller?.map?.getLayoutProperty('areas-fill', 'visibility') - return visibility === 'visible' - }) - - expect(isVisible).toBe(false) - }) - - test('can toggle areas layer in settings', async ({ page }) => { - // Open settings - await page.click('button[title="Settings"]') - await page.waitForTimeout(400) - - // Toggle areas - const areasCheckbox = page.locator('label.setting-checkbox:has-text("Show Areas")').locator('input[type="checkbox"]') - await areasCheckbox.check() - await page.waitForTimeout(300) - - // Check visibility - const isVisible = await page.evaluate(() => { - const element = document.querySelector('[data-controller="maps-v2"]') - const app = window.Stimulus || window.Application - const controller = app?.getControllerForElementAndIdentifier(element, 'maps-v2') - const visibility = controller?.map?.getLayoutProperty('areas-fill', 'visibility') - return visibility === 'visible' || visibility === undefined - }) - - expect(isVisible).toBe(true) - }) - }) - - test.describe('Tracks Layer', () => { - test.skip('tracks layer exists on map (requires backend API)', async ({ page }) => { - // NOTE: Tracks API endpoint (/api/v1/tracks) doesn't exist yet - // This is a future enhancement - const hasTracksLayer = await hasLayer(page, 'tracks') - expect(hasTracksLayer).toBe(true) - }) - - test('tracks layer starts hidden', async ({ page }) => { - const isVisible = await page.evaluate(() => { - const element = document.querySelector('[data-controller="maps-v2"]') - const app = window.Stimulus || window.Application - const controller = app?.getControllerForElementAndIdentifier(element, 'maps-v2') - const visibility = controller?.map?.getLayoutProperty('tracks', 'visibility') - return visibility === 'visible' - }) - - expect(isVisible).toBe(false) - }) - - test('can toggle tracks layer in settings', async ({ page }) => { - // Open settings - await page.click('button[title="Settings"]') - await page.waitForTimeout(400) - - // Toggle tracks - const tracksCheckbox = page.locator('label.setting-checkbox:has-text("Show Tracks")').locator('input[type="checkbox"]') - await tracksCheckbox.check() - await page.waitForTimeout(300) - - // Check visibility - const isVisible = await page.evaluate(() => { - const element = document.querySelector('[data-controller="maps-v2"]') - const app = window.Stimulus || window.Application - const controller = app?.getControllerForElementAndIdentifier(element, 'maps-v2') - const visibility = controller?.map?.getLayoutProperty('tracks', 'visibility') - return visibility === 'visible' || visibility === undefined - }) - - expect(isVisible).toBe(true) - }) - }) - - test.describe('Layer Order', () => { - test.skip('areas render below tracks (requires both layers with data)', async ({ page }) => { - const layerOrder = await page.evaluate(() => { - const element = document.querySelector('[data-controller="maps-v2"]') - const app = window.Stimulus || window.Application - const controller = app?.getControllerForElementAndIdentifier(element, 'maps-v2') - const layers = controller?.map?.getStyle()?.layers || [] - - const areasIndex = layers.findIndex(l => l.id === 'areas-fill') - const tracksIndex = layers.findIndex(l => l.id === 'tracks') - - return { areasIndex, tracksIndex } - }) - - // Areas should render before (below) tracks - expect(layerOrder.areasIndex).toBeLessThan(layerOrder.tracksIndex) - }) - }) - - test.describe('Regression Tests', () => { - test('all previous layers still work', async ({ page }) => { - // Check that map loads successfully - const hasMap = await page.evaluate(() => { - const element = document.querySelector('[data-controller="maps-v2"]') - const app = window.Stimulus || window.Application - const controller = app?.getControllerForElementAndIdentifier(element, 'maps-v2') - return !!controller?.map - }) - expect(hasMap).toBe(true) - }) - - test('settings panel has all toggles', async ({ page }) => { - // Open settings - await page.click('button[title="Settings"]') - await page.waitForTimeout(400) - - // Check all toggles exist - const toggles = [ - 'Show Heatmap', - 'Show Visits', - 'Show Photos', - 'Show Areas', - 'Show Tracks' - ] - - for (const toggleText of toggles) { - const toggle = page.locator(`label.setting-checkbox:has-text("${toggleText}")`) - await expect(toggle).toBeVisible() - } - }) - }) -}) diff --git a/e2e/v2/phase-6-advanced.spec.js b/e2e/v2/phase-6-advanced.spec.js deleted file mode 100644 index bb95c9e1..00000000 --- a/e2e/v2/phase-6-advanced.spec.js +++ /dev/null @@ -1,200 +0,0 @@ -import { test, expect } from '@playwright/test' -import { closeOnboardingModal } from '../helpers/navigation' -import { - navigateToMapsV2WithDate, - waitForMapLibre, - waitForLoadingComplete -} from './helpers/setup' - -test.describe('Phase 6: Advanced Features (Fog + Scratch + Toast)', () => { - test.beforeEach(async ({ page }) => { - // Clear settings BEFORE navigation to ensure clean state - await page.goto('/maps_v2') - await page.evaluate(() => { - localStorage.removeItem('maps_v2_settings') - }) - - // Now navigate to a date range with data - await navigateToMapsV2WithDate(page, '2025-10-15T00:00', '2025-10-15T23:59') - await closeOnboardingModal(page) - - await waitForMapLibre(page) - await waitForLoadingComplete(page) - await page.waitForTimeout(1500) - }) - - test.describe('Fog of War Layer', () => { - test('fog layer is disabled by default in settings', async ({ page }) => { - // Check that fog is disabled in settings by default - const fogEnabled = await page.evaluate(() => { - const settings = JSON.parse(localStorage.getItem('maps_v2_settings') || '{}') - return settings.fogEnabled - }) - - // undefined or false both mean disabled - expect(fogEnabled).toBeFalsy() - }) - - test('can toggle fog layer in settings', async ({ page }) => { - // Open settings - await page.click('button[title="Settings"]') - await page.waitForTimeout(400) - - // Toggle fog - const fogCheckbox = page.locator('label.setting-checkbox:has-text("Show Fog of War")').locator('input[type="checkbox"]') - await fogCheckbox.check() - await page.waitForTimeout(300) - - // Check if visible - const fogCanvas = await page.locator('.fog-canvas') - await fogCanvas.waitFor({ state: 'attached', timeout: 5000 }) - const isVisible = await fogCanvas.evaluate(el => el.style.display !== 'none') - expect(isVisible).toBe(true) - }) - - // Note: Fog canvas is created lazily, so we test it through the toggle test above - }) - - test.describe('Scratch Map Layer', () => { - test('scratch layer settings toggle exists', async ({ page }) => { - // Open settings - await page.click('button[title="Settings"]') - await page.waitForTimeout(400) - - const scratchToggle = page.locator('label.setting-checkbox:has-text("Show Scratch Map")') - await expect(scratchToggle).toBeVisible() - }) - - test('can toggle scratch map in settings', async ({ page }) => { - // Open settings - await page.click('button[title="Settings"]') - await page.waitForTimeout(400) - - // Toggle scratch map - const scratchCheckbox = page.locator('label.setting-checkbox:has-text("Show Scratch Map")').locator('input[type="checkbox"]') - await scratchCheckbox.check() - await page.waitForTimeout(300) - - // Just verify it doesn't crash - layer may be empty - const isChecked = await scratchCheckbox.isChecked() - expect(isChecked).toBe(true) - }) - }) - - test.describe('Photos Layer', () => { - test('photos layer settings toggle exists', async ({ page }) => { - // Open settings - await page.click('button[title="Settings"]') - await page.waitForTimeout(400) - - const photosToggle = page.locator('label.setting-checkbox:has-text("Show Photos")') - await expect(photosToggle).toBeVisible() - }) - - test('can toggle photos layer in settings', async ({ page }) => { - // Open settings - await page.click('button[title="Settings"]') - await page.waitForTimeout(400) - - // Toggle photos - const photosCheckbox = page.locator('label.setting-checkbox:has-text("Show Photos")').locator('input[type="checkbox"]') - await photosCheckbox.check() - await page.waitForTimeout(500) - - // Verify it's checked - const isChecked = await photosCheckbox.isChecked() - expect(isChecked).toBe(true) - }) - - test('photo markers appear when photos layer is enabled', async ({ page }) => { - // Open settings - await page.click('button[title="Settings"]') - await page.waitForTimeout(400) - - // Enable photos layer - const photosCheckbox = page.locator('label.setting-checkbox:has-text("Show Photos")').locator('input[type="checkbox"]') - await photosCheckbox.check() - await page.waitForTimeout(500) - - // Check for photo markers (they might not exist if no photos in test data) - const photoMarkers = page.locator('.photo-marker') - const markerCount = await photoMarkers.count() - - // Just verify the test doesn't crash - markers may be 0 if no photos exist - expect(markerCount).toBeGreaterThanOrEqual(0) - }) - }) - - test.describe('Toast Notifications', () => { - test('toast container is initialized', async ({ page }) => { - // Toast container should exist after page load - const toastContainer = page.locator('.toast-container') - await expect(toastContainer).toBeAttached() - }) - - test.skip('success toast appears on data load', async ({ page }) => { - // This test is flaky because toast may disappear quickly - // Just verifying toast system is initialized above - }) - }) - - test.describe('Settings Panel', () => { - test('all layer toggles are present', async ({ page }) => { - // Open settings - await page.click('button[title="Settings"]') - await page.waitForTimeout(400) - - const toggles = [ - 'Show Heatmap', - 'Show Visits', - 'Show Photos', - 'Show Areas', - 'Show Tracks', - 'Show Fog of War', - 'Show Scratch Map' - ] - - for (const toggleText of toggles) { - const toggle = page.locator(`label.setting-checkbox:has-text("${toggleText}")`) - await expect(toggle).toBeVisible() - } - }) - }) - - test.describe('Regression Tests', () => { - test.skip('all previous features still work (z-index overlay issue)', async ({ page }) => { - // Just verify page loads and no JavaScript errors - const errors = [] - page.on('pageerror', error => errors.push(error.message)) - - // Open settings - await page.click('button[title="Settings"]') - await page.waitForTimeout(400) - - // Close settings by clicking the close button (×) - await page.click('.settings-panel .close-btn') - await page.waitForTimeout(400) - - expect(errors).toHaveLength(0) - }) - - test('fog and scratch work alongside other layers', async ({ page }) => { - // Open settings - await page.click('button[title="Settings"]') - await page.waitForTimeout(400) - - // Enable multiple layers - const heatmapCheckbox = page.locator('label.setting-checkbox:has-text("Show Heatmap")').locator('input[type="checkbox"]') - await heatmapCheckbox.check() - - const fogCheckbox = page.locator('label.setting-checkbox:has-text("Show Fog of War")').locator('input[type="checkbox"]') - await fogCheckbox.check() - - await page.waitForTimeout(300) - - // Verify both are enabled - expect(await heatmapCheckbox.isChecked()).toBe(true) - expect(await fogCheckbox.isChecked()).toBe(true) - }) - }) -}) diff --git a/e2e/v2/phase-7-realtime.spec.js b/e2e/v2/phase-7-realtime.spec.js deleted file mode 100644 index 0b46aa39..00000000 --- a/e2e/v2/phase-7-realtime.spec.js +++ /dev/null @@ -1,195 +0,0 @@ -import { test, expect } from '@playwright/test' -import { - navigateToMapsV2, - waitForMapLibre, - hasLayer -} from './helpers/setup.js' - -test.describe('Phase 7: Real-time + Family', () => { - test.beforeEach(async ({ page }) => { - await navigateToMapsV2(page) - await waitForMapLibre(page) - }) - - // Note: Phase 7 realtime controller is currently disabled pending initialization fix - // These tests are kept for when the controller is re-enabled - test.skip('family layer exists', async ({ page }) => { - const hasFamilyLayer = await hasLayer(page, 'family') - expect(hasFamilyLayer).toBe(true) - }) - - test.skip('connection indicator shows', async ({ page }) => { - const indicator = page.locator('.connection-indicator') - await expect(indicator).toBeVisible() - }) - - test.skip('connection indicator shows state', async ({ page }) => { - // Wait for connection to be established - await page.waitForTimeout(2000) - - const indicator = page.locator('.connection-indicator') - await expect(indicator).toBeVisible() - - // Should have either 'connected' or 'disconnected' class - const classes = await indicator.getAttribute('class') - const hasState = classes.includes('connected') || classes.includes('disconnected') - expect(hasState).toBe(true) - }) - - test.skip('family layer has required sub-layers', async ({ page }) => { - const familyExists = await hasLayer(page, 'family') - const labelsExists = await hasLayer(page, 'family-labels') - const pulseExists = await hasLayer(page, 'family-pulse') - - expect(familyExists).toBe(true) - expect(labelsExists).toBe(true) - expect(pulseExists).toBe(true) - }) - - // Regression tests are covered by earlier phase test files (phase-1 through phase-6) - // These are skipped here to avoid duplication - test.describe.skip('Regression Tests', () => { - test('all previous features still work', async ({ page }) => { - const layers = [ - 'points', 'routes', 'heatmap', - 'visits', 'photos', 'areas-fill', - 'tracks', 'fog-scratch' - ] - - for (const layer of layers) { - const exists = await hasLayer(page, layer) - expect(exists).toBe(true) - } - }) - - test('settings panel still works', async ({ page }) => { - // Click settings button - await page.click('button:has-text("Settings")') - - // Wait for panel to appear - await page.waitForSelector('[data-maps-v2-target="settingsPanel"]') - - // Check if panel is visible - const panel = page.locator('[data-maps-v2-target="settingsPanel"]') - await expect(panel).toBeVisible() - }) - - test('layer toggles still work', async ({ page }) => { - // Toggle points layer - await page.click('button:has-text("Points")') - - // Wait a bit for layer to update - await page.waitForTimeout(500) - - // Layer should still exist but visibility might change - const pointsExists = await page.evaluate(() => { - const map = window.mapInstance - return map?.getLayer('points') !== undefined - }) - - expect(pointsExists).toBe(true) - }) - - test('map interactions still work', async ({ page }) => { - // Test zoom - const initialZoom = await page.evaluate(() => window.mapInstance?.getZoom()) - - // Click zoom in button - await page.click('.maplibregl-ctrl-zoom-in') - await page.waitForTimeout(300) - - const newZoom = await page.evaluate(() => window.mapInstance?.getZoom()) - expect(newZoom).toBeGreaterThan(initialZoom) - }) - }) - - test.describe.skip('ActionCable Integration', () => { - test('realtime controller is connected', async ({ page }) => { - // Check if realtime controller is initialized - const hasRealtimeController = await page.evaluate(() => { - const element = document.querySelector('[data-controller*="realtime"]') - return element !== null - }) - - expect(hasRealtimeController).toBe(true) - }) - - test('connection indicator updates class based on connection', async ({ page }) => { - // Get initial state - const indicator = page.locator('.connection-indicator') - const initialClass = await indicator.getAttribute('class') - - // Should have a connection state class - const hasConnectionState = - initialClass.includes('connected') || - initialClass.includes('disconnected') - - expect(hasConnectionState).toBe(true) - }) - }) - - test.describe.skip('Family Layer Functionality', () => { - test('family layer can be updated programmatically', async ({ page }) => { - // Test family layer update method exists - const result = await page.evaluate(() => { - const controller = window.mapInstance?._container?.closest('[data-controller*="maps-v2"]')?._stimulus?.getControllerForElementAndIdentifier - - // Access the familyLayer through the map controller - return typeof window.mapInstance?._container !== 'undefined' - }) - - expect(result).toBe(true) - }) - - test('family layer handles empty state', async ({ page }) => { - // Family layer should exist with no features initially - const familyLayerData = await page.evaluate(() => { - const map = window.mapInstance - const source = map?.getSource('family-source') - return source?._data || null - }) - - expect(familyLayerData).toBeTruthy() - expect(familyLayerData.type).toBe('FeatureCollection') - }) - }) - - test.describe('Performance', () => { - test.skip('page loads within acceptable time', async ({ page }) => { - const startTime = Date.now() - await page.goto('/maps_v2') - await waitForMapLibre(page) - const loadTime = Date.now() - startTime - - // Should load within 10 seconds - expect(loadTime).toBeLessThan(10000) - }) - - test.skip('real-time updates do not cause memory leaks', async ({ page }) => { - // Get initial memory usage - const metrics1 = await page.evaluate(() => { - if (performance.memory) { - return performance.memory.usedJSHeapSize - } - return null - }) - - if (metrics1 === null) { - test.skip() - return - } - - // Wait a bit - await page.waitForTimeout(2000) - - // Get memory usage again - const metrics2 = await page.evaluate(() => { - return performance.memory.usedJSHeapSize - }) - - // Memory should not increase dramatically (allow for 50MB variance) - const memoryIncrease = metrics2 - metrics1 - expect(memoryIncrease).toBeLessThan(50 * 1024 * 1024) - }) - }) -}) diff --git a/e2e/v2/phase-8-performance.spec.js b/e2e/v2/phase-8-performance.spec.js deleted file mode 100644 index 6b03f5a2..00000000 --- a/e2e/v2/phase-8-performance.spec.js +++ /dev/null @@ -1,219 +0,0 @@ -import { test, expect } from '@playwright/test'; -import { closeOnboardingModal } from '../helpers/navigation.js'; -import { - navigateToMapsV2, - waitForMapLibre, - waitForLoadingComplete, - hasMapInstance, - getPointsSourceData, - hasLayer -} from './helpers/setup.js'; - -test.describe('Phase 8: Performance Optimization & Production Polish', () => { - test.beforeEach(async ({ page }) => { - await navigateToMapsV2(page); - await closeOnboardingModal(page); - }); - - test('map loads within reasonable time', async ({ page }) => { - // Note: beforeEach already navigates and waits, so this just verifies - // that the map is ready after the beforeEach hook - await waitForMapLibre(page); - await waitForLoadingComplete(page); - - // Verify map is functional - const hasMap = await hasMapInstance(page); - expect(hasMap).toBe(true); - }); - - test('handles dataset loading', async ({ page }) => { - await waitForMapLibre(page); - await waitForLoadingComplete(page); - - const pointsData = await getPointsSourceData(page); - const pointCount = pointsData?.featureCount || 0; - - console.log(`Loaded ${pointCount} points`); - expect(pointCount).toBeGreaterThanOrEqual(0); - }); - - test('all core layers are present', async ({ page }) => { - await waitForMapLibre(page); - await waitForLoadingComplete(page); - - // Check that core layers exist - const coreLayers = [ - 'points', - 'routes', - 'heatmap', - 'visits', - 'areas-fill', - 'tracks', - 'family' - ]; - - for (const layerName of coreLayers) { - const exists = await hasLayer(page, layerName); - expect(exists).toBe(true); - } - }); - - test('no memory leaks after layer toggling', async ({ page }) => { - await waitForMapLibre(page); - await waitForLoadingComplete(page); - - const initialMemory = await page.evaluate(() => { - return performance.memory?.usedJSHeapSize; - }); - - // Toggle points layer multiple times - for (let i = 0; i < 5; i++) { - const pointsToggle = page.locator('button[data-action*="toggleLayer"][data-layer="points"]'); - if (await pointsToggle.count() > 0) { - await pointsToggle.click(); - await page.waitForTimeout(200); - await pointsToggle.click(); - await page.waitForTimeout(200); - } - } - - const finalMemory = await page.evaluate(() => { - return performance.memory?.usedJSHeapSize; - }); - - if (initialMemory && finalMemory) { - const memoryGrowth = finalMemory - initialMemory; - const growthPercentage = (memoryGrowth / initialMemory) * 100; - - console.log(`Memory growth: ${growthPercentage.toFixed(2)}%`); - - // Memory shouldn't grow more than 50% (conservative threshold) - expect(growthPercentage).toBeLessThan(50); - } - }); - - test('progressive loading shows progress indicator', async ({ page }) => { - await page.goto('/maps_v2'); - await closeOnboardingModal(page); - - // Wait for loading indicator to appear (might be very quick) - const loading = page.locator('[data-maps-v2-target="loading"]'); - - // Try to catch the loading state, but don't fail if it's too fast - const isLoading = await loading.isVisible().catch(() => false); - - if (isLoading) { - // Should show loading text - const loadingText = page.locator('[data-maps-v2-target="loadingText"]'); - if (await loadingText.count() > 0) { - const text = await loadingText.textContent(); - expect(text).toContain('Loading'); - } - } - - // Should finish loading - await waitForLoadingComplete(page); - }); - - test('lazy loading: fog layer not loaded initially', async ({ page }) => { - await waitForMapLibre(page); - await waitForLoadingComplete(page); - - // Check that fog layer is not loaded yet (lazy loaded on demand) - const fogLayerLoaded = await page.evaluate(() => { - const controller = window.mapsV2Controller; - return controller?.fogLayer !== undefined && controller?.fogLayer !== null; - }); - - // Fog should only be loaded if it was enabled in settings - console.log('Fog layer loaded:', fogLayerLoaded); - }); - - test('lazy loading: scratch layer not loaded initially', async ({ page }) => { - await waitForMapLibre(page); - await waitForLoadingComplete(page); - - // Check that scratch layer is not loaded yet (lazy loaded on demand) - const scratchLayerLoaded = await page.evaluate(() => { - const controller = window.mapsV2Controller; - return controller?.scratchLayer !== undefined && controller?.scratchLayer !== null; - }); - - // Scratch should only be loaded if it was enabled in settings - console.log('Scratch layer loaded:', scratchLayerLoaded); - }); - - test('performance monitor logs on disconnect', async ({ page }) => { - // Set up console listener BEFORE navigation - const consoleMessages = []; - page.on('console', msg => { - consoleMessages.push({ - type: msg.type(), - text: msg.text() - }); - }); - - // Now load the page - await waitForMapLibre(page); - await waitForLoadingComplete(page); - - // Navigate away to trigger disconnect - await page.goto('/'); - - // Wait for disconnect to happen - await page.waitForTimeout(1000); - - // Check if performance metrics were logged - const hasPerformanceLog = consoleMessages.some(msg => - msg.text.includes('[Performance]') || - msg.text.includes('Performance Report') || - msg.text.includes('Map data loaded in') - ); - - console.log('Console messages sample:', consoleMessages.slice(-10).map(m => m.text)); - console.log('Has performance log:', hasPerformanceLog); - - // This test is informational - performance logging is a nice-to-have - // Don't fail if it's not found - expect(hasPerformanceLog || true).toBe(true); - }); - - test.describe('Regression Tests', () => { - test('all features work after optimization', async ({ page }) => { - await waitForMapLibre(page); - await waitForLoadingComplete(page); - - // Test that map interaction still works - const hasMap = await hasMapInstance(page); - expect(hasMap).toBe(true); - - // Test that data loaded - const pointsData = await getPointsSourceData(page); - expect(pointsData).toBeTruthy(); - - // Test that layers are present - const hasPointsLayer = await hasLayer(page, 'points'); - expect(hasPointsLayer).toBe(true); - }); - - test('month selector still works', async ({ page }) => { - await waitForMapLibre(page); - await waitForLoadingComplete(page); - - // Find month selector - const monthSelect = page.locator('[data-maps-v2-target="monthSelect"]'); - if (await monthSelect.count() > 0) { - // Change month - await monthSelect.selectOption({ index: 1 }); - - // Wait for reload (with longer timeout) - await page.waitForTimeout(500); - await waitForLoadingComplete(page); - - // Verify map still works - const hasMap = await hasMapInstance(page); - expect(hasMap).toBe(true); - } - }); - }); -}); diff --git a/e2e/v2/realtime/family.spec.js b/e2e/v2/realtime/family.spec.js new file mode 100644 index 00000000..7b022b0e --- /dev/null +++ b/e2e/v2/realtime/family.spec.js @@ -0,0 +1,34 @@ +import { test, expect } from '@playwright/test' +import { closeOnboardingModal } from '../../helpers/navigation.js' +import { navigateToMapsV2, waitForMapLibre, waitForLoadingComplete } from '../helpers/setup.js' + +test.describe('Realtime Family Tracking', () => { + test.beforeEach(async ({ page }) => { + await navigateToMapsV2(page) + await closeOnboardingModal(page) + await waitForMapLibre(page) + await waitForLoadingComplete(page) + }) + + test.describe('Family Layer', () => { + test.skip('family layer exists but is hidden by default', async ({ page }) => { + // Family layer is created but hidden until ActionCable data arrives + const layerExists = await page.evaluate(() => { + const element = document.querySelector('[data-controller="maps-v2"]') + const app = window.Stimulus || window.Application + const controller = app?.getControllerForElementAndIdentifier(element, 'maps-v2') + return controller?.map?.getLayer('family') !== undefined + }) + + // Test requires family setup + expect(layerExists).toBe(true) + }) + }) + + test.describe('ActionCable Connection', () => { + test.skip('establishes ActionCable connection for family tracking', async ({ page }) => { + // This test requires ActionCable setup and family configuration + // Skip for now as it needs backend family data + }) + }) +})