From dad5fa9c4f04683abd3e05467a59bc93f6f5075b Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Sat, 29 Nov 2025 20:53:44 +0100 Subject: [PATCH] Extract js to modules from maps_v2_controller.js --- app/assets/builds/tailwind.css | 4 +- .../svg/icons/lucide/outline/circle-plus.svg | 1 + .../area_creation_v2_controller.js | 204 ++ .../maps_v2/area_selection_manager.js | 540 +++++ .../controllers/maps_v2/places_manager.js | 271 +++ .../controllers/maps_v2/routes_manager.js | 360 +++ .../controllers/maps_v2/settings_manager.js | 271 +++ .../controllers/maps_v2/visits_manager.js | 143 ++ .../controllers/maps_v2_controller.js | 1771 +-------------- .../controllers/maps_v2_controller_old.js | 1988 +++++++++++++++++ .../maps_v2/_area_creation_modal.html.erb | 86 + app/views/maps_v2/_settings_panel.html.erb | 22 +- app/views/maps_v2/index.html.erb | 6 +- 13 files changed, 3960 insertions(+), 1707 deletions(-) create mode 100644 app/assets/svg/icons/lucide/outline/circle-plus.svg create mode 100644 app/javascript/controllers/area_creation_v2_controller.js create mode 100644 app/javascript/controllers/maps_v2/area_selection_manager.js create mode 100644 app/javascript/controllers/maps_v2/places_manager.js create mode 100644 app/javascript/controllers/maps_v2/routes_manager.js create mode 100644 app/javascript/controllers/maps_v2/settings_manager.js create mode 100644 app/javascript/controllers/maps_v2/visits_manager.js create mode 100644 app/javascript/controllers/maps_v2_controller_old.js create mode 100644 app/views/maps_v2/_area_creation_modal.html.erb diff --git a/app/assets/builds/tailwind.css b/app/assets/builds/tailwind.css index 29ddad83..0b1a3145 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-primary{--chkbg:var(--fallback-p,oklch(var(--p)/1));--chkfg:var(--fallback-pc,oklch(var(--pc)/1));--tw-border-opacity:1;border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)))}.checkbox-primary:focus-visible{outline-color:var(--fallback-p,oklch(var(--p)/1))}.checkbox-primary:checked,.checkbox-primary[aria-checked=true],.checkbox-primary[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)))}.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,.\!modal:not(dialog:not(.modal-open)){animation:modal-pop .2s ease-out!important;background-color:#0006!important}.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:target .modal-box,.\!modal[open] .modal-box,.modal-toggle:checked+.\!modal .modal-box{--tw-translate-y:0px!important;--tw-scale-x:1!important;--tw-scale-y:1!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}.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-xs{height:1rem;width:1rem}[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}.sticky{position:sticky}.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-3{right:.75rem}.right-5{right:1.25rem}.top-0{top:0}.top-16{top:4rem}.top-2{top:.5rem}.top-3{top:.75rem}.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}.z-\[9999\]{z-index:9999}.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-3\.5{height:.875rem}.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}.max-h-full{max-height:100%}.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-3\.5{width:.875rem}.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-1\.5{gap:.375rem}.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-base-content\/20{border-color:var(--fallback-bc,oklch(var(--bc)/.2))}.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-primary{--tw-border-opacity:1;border-color:var(--fallback-p,oklch(var(--p)/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-primary\/10{background-color:var(--fallback-p,oklch(var(--p)/.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\/40{color:var(--fallback-bc,oklch(var(--bc)/.4))}.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-md:hover{--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);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.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\:col-span-2{grid-column:span 2/span 2}.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 + );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-info{background-color:var(--fallback-in,oklch(var(--in)/var(--tw-bg-opacity)));color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)))}.badge-info,.badge-success{border-color:transparent;--tw-bg-opacity:1;--tw-text-opacity:1}.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-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,.badge-warning{border-color:transparent;--tw-bg-opacity:1;--tw-text-opacity:1}.badge-error{background-color:var(--fallback-er,oklch(var(--er)/var(--tw-bg-opacity)));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-primary{--chkbg:var(--fallback-p,oklch(var(--p)/1));--chkfg:var(--fallback-pc,oklch(var(--pc)/1));--tw-border-opacity:1;border-color:var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)))}.checkbox-primary:focus-visible{outline-color:var(--fallback-p,oklch(var(--p)/1))}.checkbox-primary:checked,.checkbox-primary[aria-checked=true],.checkbox-primary[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)))}.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,.\!modal:not(dialog:not(.modal-open)){animation:modal-pop .2s ease-out!important;background-color:#0006!important}.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:target .modal-box,.\!modal[open] .modal-box,.modal-toggle:checked+.\!modal .modal-box{--tw-translate-y:0px!important;--tw-scale-x:1!important;--tw-scale-y:1!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}.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-xs{height:1rem;width:1rem}[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}.sticky{position:sticky}.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-3{right:.75rem}.right-5{right:1.25rem}.top-0{top:0}.top-16{top:4rem}.top-2{top:.5rem}.top-3{top:.75rem}.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}.z-\[9999\]{z-index:9999}.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-3\.5{height:.875rem}.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}.max-h-full{max-height:100%}.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-3\.5{width:.875rem}.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-xl{max-width:36rem}.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-decimal{list-style-type:decimal}.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-1\.5{gap:.375rem}.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-base-content\/20{border-color:var(--fallback-bc,oklch(var(--bc)/.2))}.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-primary{--tw-border-opacity:1;border-color:var(--fallback-p,oklch(var(--p)/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-primary\/10{background-color:var(--fallback-p,oklch(var(--p)/.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\/40{color:var(--fallback-bc,oklch(var(--bc)/.4))}.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-md:hover{--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);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.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/assets/svg/icons/lucide/outline/circle-plus.svg b/app/assets/svg/icons/lucide/outline/circle-plus.svg new file mode 100644 index 00000000..92ef2e69 --- /dev/null +++ b/app/assets/svg/icons/lucide/outline/circle-plus.svg @@ -0,0 +1 @@ + diff --git a/app/javascript/controllers/area_creation_v2_controller.js b/app/javascript/controllers/area_creation_v2_controller.js new file mode 100644 index 00000000..998bd40c --- /dev/null +++ b/app/javascript/controllers/area_creation_v2_controller.js @@ -0,0 +1,204 @@ +import { Controller } from '@hotwired/stimulus' +import { Toast } from 'maps_v2/components/toast' + +/** + * Area creation controller for Maps V2 + * Handles area creation workflow with area drawer + */ +export default class extends Controller { + static targets = [ + 'modal', + 'form', + 'nameInput', + 'latitudeInput', + 'longitudeInput', + 'radiusInput', + 'radiusDisplay', + 'locationDisplay', + 'submitButton', + 'submitSpinner', + 'submitText' + ] + + static values = { + apiKey: String + } + + static outlets = ['area-drawer'] + + connect() { + console.log('[Area Creation V2] Connected') + this.latitude = null + this.longitude = null + this.radius = null + this.mapsController = null + } + + /** + * Open modal and start drawing mode + * @param {number} lat - Initial latitude (optional) + * @param {number} lng - Initial longitude (optional) + * @param {object} mapsController - Maps V2 controller reference + */ + open(lat = null, lng = null, mapsController = null) { + console.log('[Area Creation V2] Opening modal', { lat, lng }) + + this.mapsController = mapsController + this.latitude = lat + this.longitude = lng + this.radius = 100 // Default radius in meters + + // Update hidden inputs if coordinates provided + if (lat && lng) { + this.latitudeInputTarget.value = lat + this.longitudeInputTarget.value = lng + this.radiusInputTarget.value = this.radius + this.updateLocationDisplay(lat, lng) + this.updateRadiusDisplay(this.radius) + } + + // Clear form + this.nameInputTarget.value = '' + + // Show modal + this.modalTarget.classList.add('modal-open') + + // Start drawing mode if area-drawer outlet is available + if (this.hasAreaDrawerOutlet) { + console.log('[Area Creation V2] Starting drawing mode') + this.areaDrawerOutlet.startDrawing() + } else { + console.warn('[Area Creation V2] Area drawer outlet not found') + } + } + + /** + * Close modal and cancel drawing + */ + close() { + console.log('[Area Creation V2] Closing modal') + + this.modalTarget.classList.remove('modal-open') + + // Cancel drawing mode + if (this.hasAreaDrawerOutlet) { + this.areaDrawerOutlet.cancelDrawing() + } + + // Reset form + this.formTarget.reset() + this.latitude = null + this.longitude = null + this.radius = null + } + + /** + * Handle area drawn event from area-drawer + */ + handleAreaDrawn(event) { + console.log('[Area Creation V2] Area drawn', event.detail) + + const { area } = event.detail + const [lng, lat] = area.center + const radius = Math.round(area.radius) + + this.latitude = lat + this.longitude = lng + this.radius = radius + + // Update form fields + this.latitudeInputTarget.value = lat + this.longitudeInputTarget.value = lng + this.radiusInputTarget.value = radius + + // Update displays + this.updateLocationDisplay(lat, lng) + this.updateRadiusDisplay(radius) + + console.log('[Area Creation V2] Form updated with drawn area') + } + + /** + * Update location display + */ + updateLocationDisplay(lat, lng) { + this.locationDisplayTarget.value = `${lat.toFixed(6)}, ${lng.toFixed(6)}` + } + + /** + * Update radius display + */ + updateRadiusDisplay(radius) { + this.radiusDisplayTarget.value = `${radius.toLocaleString()}` + } + + /** + * Handle form submission + */ + async submit(event) { + event.preventDefault() + + console.log('[Area Creation V2] Submitting form') + + // Validate + if (!this.latitude || !this.longitude || !this.radius) { + Toast.error('Please draw an area on the map first') + return + } + + const formData = new FormData(this.formTarget) + const name = formData.get('name') + + if (!name || name.trim() === '') { + Toast.error('Please enter an area name') + this.nameInputTarget.focus() + return + } + + // Show loading state + this.submitButtonTarget.disabled = true + this.submitSpinnerTarget.classList.remove('hidden') + this.submitTextTarget.textContent = 'Creating...' + + try { + const response = await fetch('/api/v1/areas', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.apiKeyValue}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: name.trim(), + latitude: this.latitude, + longitude: this.longitude, + radius: this.radius + }) + }) + + if (!response.ok) { + const error = await response.json() + throw new Error(error.message || 'Failed to create area') + } + + const data = await response.json() + console.log('[Area Creation V2] Area created:', data) + + Toast.success(`Area "${name}" created successfully`) + + // Dispatch event to notify maps controller + document.dispatchEvent(new CustomEvent('area:created', { + detail: { area: data } + })) + + this.close() + } catch (error) { + console.error('[Area Creation V2] Failed to create area:', error) + Toast.error(error.message || 'Failed to create area') + } finally { + // Reset button state + this.submitButtonTarget.disabled = false + this.submitSpinnerTarget.classList.add('hidden') + this.submitTextTarget.textContent = 'Create Area' + } + } +} diff --git a/app/javascript/controllers/maps_v2/area_selection_manager.js b/app/javascript/controllers/maps_v2/area_selection_manager.js new file mode 100644 index 00000000..699be9ea --- /dev/null +++ b/app/javascript/controllers/maps_v2/area_selection_manager.js @@ -0,0 +1,540 @@ +import { SelectionLayer } from 'maps_v2/layers/selection_layer' +import { SelectedPointsLayer } from 'maps_v2/layers/selected_points_layer' +import { pointsToGeoJSON } from 'maps_v2/utils/geojson_transformers' +import { VisitCard } from 'maps_v2/components/visit_card' +import { Toast } from 'maps_v2/components/toast' + +/** + * Manages area selection and bulk operations for Maps V2 + * Handles selection mode, visit cards, and bulk actions (merge, confirm, decline) + */ +export class AreaSelectionManager { + constructor(controller) { + this.controller = controller + this.map = controller.map + this.api = controller.api + this.selectionLayer = null + this.selectedPointsLayer = null + this.selectedVisits = [] + this.selectedVisitIds = new Set() + } + + /** + * Start area selection mode + */ + async startSelectArea() { + console.log('[Maps V2] Starting area selection mode') + + // Initialize selection layer if not exists + if (!this.selectionLayer) { + this.selectionLayer = new SelectionLayer(this.map, { + visible: true, + onSelectionComplete: this.handleAreaSelected.bind(this) + }) + + this.selectionLayer.add({ + type: 'FeatureCollection', + features: [] + }) + + console.log('[Maps V2] Selection layer initialized') + } + + // Initialize selected points layer if not exists + if (!this.selectedPointsLayer) { + this.selectedPointsLayer = new SelectedPointsLayer(this.map, { + visible: true + }) + + this.selectedPointsLayer.add({ + type: 'FeatureCollection', + features: [] + }) + + console.log('[Maps V2] Selected points layer initialized') + } + + // Enable selection mode + this.selectionLayer.enableSelectionMode() + + // Update UI - replace Select Area button with Cancel Selection button + if (this.controller.hasSelectAreaButtonTarget) { + this.controller.selectAreaButtonTarget.innerHTML = ` + + + + + Cancel Selection + ` + this.controller.selectAreaButtonTarget.dataset.action = 'click->maps-v2#cancelAreaSelection' + } + + Toast.info('Draw a rectangle on the map to select points') + } + + /** + * Handle area selection completion + */ + async handleAreaSelected(bounds) { + console.log('[Maps V2] Area selected:', bounds) + + try { + Toast.info('Fetching data in selected area...') + + const [points, visits] = await Promise.all([ + this.api.fetchPointsInArea({ + start_at: this.controller.startDateValue, + end_at: this.controller.endDateValue, + min_longitude: bounds.minLng, + max_longitude: bounds.maxLng, + min_latitude: bounds.minLat, + max_latitude: bounds.maxLat + }), + this.api.fetchVisitsInArea({ + start_at: this.controller.startDateValue, + end_at: this.controller.endDateValue, + sw_lat: bounds.minLat, + sw_lng: bounds.minLng, + ne_lat: bounds.maxLat, + ne_lng: bounds.maxLng + }) + ]) + + console.log('[Maps V2] Found', points.length, 'points and', visits.length, 'visits in area') + + if (points.length === 0 && visits.length === 0) { + Toast.info('No data found in selected area') + this.cancelAreaSelection() + return + } + + // Convert points to GeoJSON and display + if (points.length > 0) { + const geojson = pointsToGeoJSON(points) + this.selectedPointsLayer.updateSelectedPoints(geojson) + this.selectedPointsLayer.show() + } + + // Display visits in side panel and on map + if (visits.length > 0) { + this.displaySelectedVisits(visits) + } + + // Update UI - show action buttons + if (this.controller.hasSelectionActionsTarget) { + this.controller.selectionActionsTarget.classList.remove('hidden') + } + + // Update delete button text with count + if (this.controller.hasDeleteButtonTextTarget) { + this.controller.deleteButtonTextTarget.textContent = `Delete ${points.length} Point${points.length === 1 ? '' : 's'}` + } + + // Disable selection mode + this.selectionLayer.disableSelectionMode() + + const messages = [] + if (points.length > 0) messages.push(`${points.length} point${points.length === 1 ? '' : 's'}`) + if (visits.length > 0) messages.push(`${visits.length} visit${visits.length === 1 ? '' : 's'}`) + + Toast.success(`Selected ${messages.join(' and ')}`) + } catch (error) { + console.error('[Maps V2] Failed to fetch data in area:', error) + Toast.error('Failed to fetch data in selected area') + this.cancelAreaSelection() + } + } + + /** + * Display selected visits in side panel + */ + displaySelectedVisits(visits) { + if (!this.controller.hasSelectedVisitsContainerTarget) return + + this.selectedVisits = visits + this.selectedVisitIds = new Set() + + const cardsHTML = visits.map(visit => + VisitCard.create(visit, { isSelected: false }) + ).join('') + + this.controller.selectedVisitsContainerTarget.innerHTML = ` +
+
+ + + + +

Visits in Area (${visits.length})

+
+ ${cardsHTML} +
+ ` + + this.controller.selectedVisitsContainerTarget.classList.remove('hidden') + this.attachVisitCardListeners() + + requestAnimationFrame(() => { + this.updateBulkActions() + }) + } + + /** + * Attach event listeners to visit cards + */ + attachVisitCardListeners() { + this.controller.element.querySelectorAll('[data-visit-select]').forEach(checkbox => { + checkbox.addEventListener('change', (e) => { + const visitId = parseInt(e.target.dataset.visitSelect) + if (e.target.checked) { + this.selectedVisitIds.add(visitId) + } else { + this.selectedVisitIds.delete(visitId) + } + this.updateBulkActions() + }) + }) + + this.controller.element.querySelectorAll('[data-visit-confirm]').forEach(btn => { + btn.addEventListener('click', async (e) => { + const visitId = parseInt(e.currentTarget.dataset.visitConfirm) + await this.confirmVisit(visitId) + }) + }) + + this.controller.element.querySelectorAll('[data-visit-decline]').forEach(btn => { + btn.addEventListener('click', async (e) => { + const visitId = parseInt(e.currentTarget.dataset.visitDecline) + await this.declineVisit(visitId) + }) + }) + } + + /** + * Update bulk action buttons visibility and attach listeners + */ + updateBulkActions() { + const selectedCount = this.selectedVisitIds.size + + const existingBulkActions = this.controller.element.querySelectorAll('.bulk-actions-inline') + existingBulkActions.forEach(el => el.remove()) + + if (selectedCount >= 2) { + const selectedVisitCards = Array.from(this.controller.element.querySelectorAll('.visit-card')) + .filter(card => { + const visitId = parseInt(card.dataset.visitId) + return this.selectedVisitIds.has(visitId) + }) + + if (selectedVisitCards.length > 0) { + const lastSelectedCard = selectedVisitCards[selectedVisitCards.length - 1] + + const bulkActionsDiv = document.createElement('div') + bulkActionsDiv.className = 'bulk-actions-inline mb-2' + bulkActionsDiv.innerHTML = ` +
+
+ + + + ${selectedCount} visit${selectedCount === 1 ? '' : 's'} selected +
+
+ + + +
+
+ ` + + lastSelectedCard.insertAdjacentElement('afterend', bulkActionsDiv) + + const mergeBtn = bulkActionsDiv.querySelector('[data-bulk-merge]') + const confirmBtn = bulkActionsDiv.querySelector('[data-bulk-confirm]') + const declineBtn = bulkActionsDiv.querySelector('[data-bulk-decline]') + + if (mergeBtn) mergeBtn.addEventListener('click', () => this.bulkMergeVisits()) + if (confirmBtn) confirmBtn.addEventListener('click', () => this.bulkConfirmVisits()) + if (declineBtn) declineBtn.addEventListener('click', () => this.bulkDeclineVisits()) + } + } + } + + /** + * Confirm a single visit + */ + async confirmVisit(visitId) { + try { + await this.api.updateVisitStatus(visitId, 'confirmed') + Toast.success('Visit confirmed') + await this.refreshSelectedVisits() + } catch (error) { + console.error('[Maps V2] Failed to confirm visit:', error) + Toast.error('Failed to confirm visit') + } + } + + /** + * Decline a single visit + */ + async declineVisit(visitId) { + try { + await this.api.updateVisitStatus(visitId, 'declined') + Toast.success('Visit declined') + await this.refreshSelectedVisits() + } catch (error) { + console.error('[Maps V2] Failed to decline visit:', error) + Toast.error('Failed to decline visit') + } + } + + /** + * Bulk merge selected visits + */ + async bulkMergeVisits() { + const visitIds = Array.from(this.selectedVisitIds) + + if (visitIds.length < 2) { + Toast.error('Select at least 2 visits to merge') + return + } + + if (!confirm(`Merge ${visitIds.length} visits into one?`)) { + return + } + + try { + Toast.info('Merging visits...') + const mergedVisit = await this.api.mergeVisits(visitIds) + Toast.success('Visits merged successfully') + + this.selectedVisitIds.clear() + this.replaceVisitsWithMerged(visitIds, mergedVisit) + this.updateBulkActions() + } catch (error) { + console.error('[Maps V2] Failed to merge visits:', error) + Toast.error('Failed to merge visits') + } + } + + /** + * Bulk confirm selected visits + */ + async bulkConfirmVisits() { + const visitIds = Array.from(this.selectedVisitIds) + + try { + Toast.info('Confirming visits...') + await this.api.bulkUpdateVisits(visitIds, 'confirmed') + Toast.success(`Confirmed ${visitIds.length} visits`) + + this.selectedVisitIds.clear() + await this.refreshSelectedVisits() + } catch (error) { + console.error('[Maps V2] Failed to confirm visits:', error) + Toast.error('Failed to confirm visits') + } + } + + /** + * Bulk decline selected visits + */ + async bulkDeclineVisits() { + const visitIds = Array.from(this.selectedVisitIds) + + if (!confirm(`Decline ${visitIds.length} visits?`)) { + return + } + + try { + Toast.info('Declining visits...') + await this.api.bulkUpdateVisits(visitIds, 'declined') + Toast.success(`Declined ${visitIds.length} visits`) + + this.selectedVisitIds.clear() + await this.refreshSelectedVisits() + } catch (error) { + console.error('[Maps V2] Failed to decline visits:', error) + Toast.error('Failed to decline visits') + } + } + + /** + * Replace merged visit cards with the new merged visit + */ + replaceVisitsWithMerged(oldVisitIds, mergedVisit) { + const container = this.controller.element.querySelector('.selected-visits-list') + if (!container) return + + const mergedStartTime = new Date(mergedVisit.started_at).getTime() + const allCards = Array.from(container.querySelectorAll('.visit-card')) + + let insertBeforeCard = null + for (const card of allCards) { + const cardId = parseInt(card.dataset.visitId) + if (oldVisitIds.includes(cardId)) continue + + const cardVisit = this.selectedVisits.find(v => v.id === cardId) + if (cardVisit) { + const cardStartTime = new Date(cardVisit.started_at).getTime() + if (cardStartTime > mergedStartTime) { + insertBeforeCard = card + break + } + } + } + + oldVisitIds.forEach(id => { + const card = this.controller.element.querySelector(`.visit-card[data-visit-id="${id}"]`) + if (card) card.remove() + }) + + this.selectedVisits = this.selectedVisits.filter(v => !oldVisitIds.includes(v.id)) + this.selectedVisits.push(mergedVisit) + this.selectedVisits.sort((a, b) => new Date(a.started_at) - new Date(b.started_at)) + + const newCardHTML = VisitCard.create(mergedVisit, { isSelected: false }) + + if (insertBeforeCard) { + insertBeforeCard.insertAdjacentHTML('beforebegin', newCardHTML) + } else { + container.insertAdjacentHTML('beforeend', newCardHTML) + } + + const header = container.querySelector('h3') + if (header) { + header.textContent = `Visits in Area (${this.selectedVisits.length})` + } + + this.attachVisitCardListeners() + } + + /** + * Refresh selected visits after changes + */ + async refreshSelectedVisits() { + const bounds = this.selectionLayer.currentRect + if (!bounds) return + + try { + const visits = await this.api.fetchVisitsInArea({ + start_at: this.controller.startDateValue, + end_at: this.controller.endDateValue, + sw_lat: bounds.start.lat < bounds.end.lat ? bounds.start.lat : bounds.end.lat, + sw_lng: bounds.start.lng < bounds.end.lng ? bounds.start.lng : bounds.end.lng, + ne_lat: bounds.start.lat > bounds.end.lat ? bounds.start.lat : bounds.end.lat, + ne_lng: bounds.start.lng > bounds.end.lng ? bounds.start.lng : bounds.end.lng + }) + + this.displaySelectedVisits(visits) + } catch (error) { + console.error('[Maps V2] Failed to refresh visits:', error) + } + } + + /** + * Cancel area selection + */ + cancelAreaSelection() { + console.log('[Maps V2] Cancelling area selection') + + if (this.selectionLayer) { + this.selectionLayer.disableSelectionMode() + this.selectionLayer.clearSelection() + } + + if (this.selectedPointsLayer) { + this.selectedPointsLayer.clearSelection() + } + + if (this.controller.hasSelectedVisitsContainerTarget) { + this.controller.selectedVisitsContainerTarget.classList.add('hidden') + this.controller.selectedVisitsContainerTarget.innerHTML = '' + } + + if (this.controller.hasSelectedVisitsBulkActionsTarget) { + this.controller.selectedVisitsBulkActionsTarget.classList.add('hidden') + } + + this.selectedVisits = [] + this.selectedVisitIds = new Set() + + if (this.controller.hasSelectAreaButtonTarget) { + this.controller.selectAreaButtonTarget.innerHTML = ` + + + + + + + + Select Area + ` + this.controller.selectAreaButtonTarget.classList.remove('btn-error') + this.controller.selectAreaButtonTarget.classList.add('btn', 'btn-outline') + this.controller.selectAreaButtonTarget.dataset.action = 'click->maps-v2#startSelectArea' + } + + if (this.controller.hasSelectionActionsTarget) { + this.controller.selectionActionsTarget.classList.add('hidden') + } + + Toast.info('Selection cancelled') + } + + /** + * Delete selected points + */ + async deleteSelectedPoints() { + const pointCount = this.selectedPointsLayer.getCount() + const pointIds = this.selectedPointsLayer.getSelectedPointIds() + + if (pointIds.length === 0) { + Toast.error('No points selected') + return + } + + const confirmed = confirm( + `Are you sure you want to delete ${pointCount} point${pointCount === 1 ? '' : 's'}? This action cannot be undone.` + ) + + if (!confirmed) return + + console.log('[Maps V2] Deleting', pointIds.length, 'points') + + try { + Toast.info('Deleting points...') + const result = await this.api.bulkDeletePoints(pointIds) + + console.log('[Maps V2] Deleted', result.count, 'points') + + this.cancelAreaSelection() + + await this.controller.loadMapData({ + showLoading: false, + fitBounds: false, + showToast: false + }) + + Toast.success(`Deleted ${result.count} point${result.count === 1 ? '' : 's'}`) + } catch (error) { + console.error('[Maps V2] Failed to delete points:', error) + Toast.error('Failed to delete points. Please try again.') + } + } +} diff --git a/app/javascript/controllers/maps_v2/places_manager.js b/app/javascript/controllers/maps_v2/places_manager.js new file mode 100644 index 00000000..8b642d0c --- /dev/null +++ b/app/javascript/controllers/maps_v2/places_manager.js @@ -0,0 +1,271 @@ +import { SettingsManager } from 'maps_v2/utils/settings_manager' +import { Toast } from 'maps_v2/components/toast' + +/** + * Manages places-related operations for Maps V2 + * Including place creation, tag filtering, and layer management + */ +export class PlacesManager { + constructor(controller) { + this.controller = controller + this.layerManager = controller.layerManager + this.api = controller.api + this.dataLoader = controller.dataLoader + this.settings = controller.settings + } + + /** + * Toggle places layer + */ + togglePlaces(event) { + const enabled = event.target.checked + SettingsManager.updateSetting('placesEnabled', enabled) + + const placesLayer = this.layerManager.getLayer('places') + if (placesLayer) { + if (enabled) { + placesLayer.show() + if (this.controller.hasPlacesFiltersTarget) { + this.controller.placesFiltersTarget.style.display = 'block' + } + this.initializePlaceTagFilters() + } else { + placesLayer.hide() + if (this.controller.hasPlacesFiltersTarget) { + this.controller.placesFiltersTarget.style.display = 'none' + } + } + } + } + + /** + * Initialize place tag filters (enable all by default or restore saved state) + */ + initializePlaceTagFilters() { + const savedFilters = this.settings.placesTagFilters + + if (savedFilters && savedFilters.length > 0) { + this.restoreSavedTagFilters(savedFilters) + } else { + this.enableAllTagsInitial() + } + } + + /** + * Restore saved tag filters + */ + restoreSavedTagFilters(savedFilters) { + const tagCheckboxes = document.querySelectorAll('input[name="place_tag_ids[]"]') + + tagCheckboxes.forEach(checkbox => { + const value = checkbox.value === 'untagged' ? checkbox.value : parseInt(checkbox.value) + const shouldBeChecked = savedFilters.includes(value) + + if (checkbox.checked !== shouldBeChecked) { + checkbox.checked = shouldBeChecked + + const badge = checkbox.nextElementSibling + const color = badge.style.borderColor + + if (shouldBeChecked) { + badge.classList.remove('badge-outline') + badge.style.backgroundColor = color + badge.style.color = 'white' + } else { + badge.classList.add('badge-outline') + badge.style.backgroundColor = 'transparent' + badge.style.color = color + } + } + }) + + this.syncEnableAllTagsToggle() + this.loadPlacesWithTags(savedFilters) + } + + /** + * Enable all tags initially + */ + enableAllTagsInitial() { + if (this.controller.hasEnableAllPlaceTagsToggleTarget) { + this.controller.enableAllPlaceTagsToggleTarget.checked = true + } + + const tagCheckboxes = document.querySelectorAll('input[name="place_tag_ids[]"]') + const allTagIds = [] + + tagCheckboxes.forEach(checkbox => { + checkbox.checked = true + + const badge = checkbox.nextElementSibling + const color = badge.style.borderColor + badge.classList.remove('badge-outline') + badge.style.backgroundColor = color + badge.style.color = 'white' + + const value = checkbox.value === 'untagged' ? checkbox.value : parseInt(checkbox.value) + allTagIds.push(value) + }) + + SettingsManager.updateSetting('placesTagFilters', allTagIds) + this.loadPlacesWithTags(allTagIds) + } + + /** + * Get selected place tag IDs + */ + getSelectedPlaceTags() { + return Array.from( + document.querySelectorAll('input[name="place_tag_ids[]"]:checked') + ).map(cb => { + const value = cb.value + return value === 'untagged' ? value : parseInt(value) + }) + } + + /** + * Filter places by selected tags + */ + filterPlacesByTags(event) { + const badge = event.target.nextElementSibling + const color = badge.style.borderColor + + if (event.target.checked) { + badge.classList.remove('badge-outline') + badge.style.backgroundColor = color + badge.style.color = 'white' + } else { + badge.classList.add('badge-outline') + badge.style.backgroundColor = 'transparent' + badge.style.color = color + } + + this.syncEnableAllTagsToggle() + + const checkedTags = this.getSelectedPlaceTags() + SettingsManager.updateSetting('placesTagFilters', checkedTags) + this.loadPlacesWithTags(checkedTags) + } + + /** + * Sync "Enable All Tags" toggle with individual tag states + */ + syncEnableAllTagsToggle() { + if (!this.controller.hasEnableAllPlaceTagsToggleTarget) return + + const tagCheckboxes = document.querySelectorAll('input[name="place_tag_ids[]"]') + const allChecked = Array.from(tagCheckboxes).every(cb => cb.checked) + + this.controller.enableAllPlaceTagsToggleTarget.checked = allChecked + } + + /** + * Load places filtered by tags + */ + async loadPlacesWithTags(tagIds = []) { + try { + let places = [] + + if (tagIds.length > 0) { + places = await this.api.fetchPlaces({ tag_ids: tagIds }) + } + + const placesGeoJSON = this.dataLoader.placesToGeoJSON(places) + + const placesLayer = this.layerManager.getLayer('places') + if (placesLayer) { + placesLayer.update(placesGeoJSON) + } + } catch (error) { + console.error('[Maps V2] Failed to load places:', error) + } + } + + /** + * Toggle all place tags on/off + */ + toggleAllPlaceTags(event) { + const enableAll = event.target.checked + const tagCheckboxes = document.querySelectorAll('input[name="place_tag_ids[]"]') + + tagCheckboxes.forEach(checkbox => { + if (checkbox.checked !== enableAll) { + checkbox.checked = enableAll + + const badge = checkbox.nextElementSibling + const color = badge.style.borderColor + + if (enableAll) { + badge.classList.remove('badge-outline') + badge.style.backgroundColor = color + badge.style.color = 'white' + } else { + badge.classList.add('badge-outline') + badge.style.backgroundColor = 'transparent' + badge.style.color = color + } + } + }) + + const selectedTags = this.getSelectedPlaceTags() + SettingsManager.updateSetting('placesTagFilters', selectedTags) + this.loadPlacesWithTags(selectedTags) + } + + /** + * Start create place mode + */ + startCreatePlace() { + console.log('[Maps V2] Starting create place mode') + + if (this.controller.hasSettingsPanelTarget && this.controller.settingsPanelTarget.classList.contains('open')) { + this.controller.toggleSettings() + } + + this.controller.map.getCanvas().style.cursor = 'crosshair' + Toast.info('Click on the map to place a place') + + this.handleCreatePlaceClick = (e) => { + const { lng, lat } = e.lngLat + + document.dispatchEvent(new CustomEvent('place:create', { + detail: { latitude: lat, longitude: lng } + })) + + this.controller.map.getCanvas().style.cursor = '' + } + + this.controller.map.once('click', this.handleCreatePlaceClick) + } + + /** + * Handle place creation event - reload places and update layer + */ + async handlePlaceCreated(event) { + console.log('[Maps V2] Place created, reloading places...', event.detail) + + try { + const selectedTags = this.getSelectedPlaceTags() + + const places = await this.api.fetchPlaces({ + tag_ids: selectedTags + }) + + console.log('[Maps V2] Fetched places:', places.length) + + const placesGeoJSON = this.dataLoader.placesToGeoJSON(places) + + console.log('[Maps V2] Converted to GeoJSON:', placesGeoJSON.features.length, 'features') + + const placesLayer = this.layerManager.getLayer('places') + if (placesLayer) { + placesLayer.update(placesGeoJSON) + console.log('[Maps V2] Places layer updated successfully') + } else { + console.warn('[Maps V2] Places layer not found, cannot update') + } + } catch (error) { + console.error('[Maps V2] Failed to reload places:', error) + } + } +} diff --git a/app/javascript/controllers/maps_v2/routes_manager.js b/app/javascript/controllers/maps_v2/routes_manager.js new file mode 100644 index 00000000..3e091300 --- /dev/null +++ b/app/javascript/controllers/maps_v2/routes_manager.js @@ -0,0 +1,360 @@ +import { SettingsManager } from 'maps_v2/utils/settings_manager' +import { Toast } from 'maps_v2/components/toast' +import { lazyLoader } from 'maps_v2/utils/lazy_loader' + +/** + * Manages routes-related operations for Maps V2 + * Including speed-colored routes, route generation, and layer management + */ +export class RoutesManager { + constructor(controller) { + this.controller = controller + this.map = controller.map + this.layerManager = controller.layerManager + this.settings = controller.settings + } + + /** + * 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) + } + + if (this.controller.hasRoutesOptionsTarget) { + this.controller.routesOptionsTarget.style.display = visible ? 'block' : 'none' + } + + SettingsManager.updateSetting('routesVisible', visible) + } + + /** + * Toggle speed-colored routes + */ + async toggleSpeedColoredRoutes(event) { + const enabled = event.target.checked + SettingsManager.updateSetting('speedColoredRoutesEnabled', enabled) + + if (this.controller.hasSpeedColorScaleContainerTarget) { + this.controller.speedColorScaleContainerTarget.classList.toggle('hidden', !enabled) + } + + await this.reloadRoutes() + } + + /** + * Open speed color editor modal + */ + openSpeedColorEditor() { + const currentScale = this.controller.speedColorScaleInputTarget.value || + '0:#00ff00|15:#00ffff|30:#ff00ff|50:#ffff00|100:#ff3300' + + let modal = document.getElementById('speed-color-editor-modal') + if (!modal) { + modal = this.createSpeedColorEditorModal(currentScale) + document.body.appendChild(modal) + } else { + const controller = this.controller.application.getControllerForElementAndIdentifier(modal, 'speed-color-editor') + if (controller) { + controller.colorStopsValue = currentScale + controller.loadColorStops() + } + } + + const checkbox = modal.querySelector('.modal-toggle') + if (checkbox) { + checkbox.checked = true + } + } + + /** + * Create speed color editor modal element + */ + createSpeedColorEditorModal(currentScale) { + const modal = document.createElement('div') + modal.id = 'speed-color-editor-modal' + modal.setAttribute('data-controller', 'speed-color-editor') + modal.setAttribute('data-speed-color-editor-color-stops-value', currentScale) + modal.setAttribute('data-action', 'speed-color-editor:save->maps-v2#handleSpeedColorSave') + + modal.innerHTML = ` + + + ` + + return modal + } + + /** + * Handle speed color save event from editor + */ + handleSpeedColorSave(event) { + const newScale = event.detail.colorStops + + this.controller.speedColorScaleInputTarget.value = newScale + SettingsManager.updateSetting('speedColorScale', newScale) + + if (this.controller.speedColoredToggleTarget.checked) { + this.reloadRoutes() + } + } + + /** + * Reload routes layer + */ + async reloadRoutes() { + this.controller.showLoading('Reloading routes...') + + try { + const pointsLayer = this.layerManager.getLayer('points') + const points = pointsLayer?.data?.features?.map(f => ({ + latitude: f.geometry.coordinates[1], + longitude: f.geometry.coordinates[0], + timestamp: f.properties.timestamp + })) || [] + + const distanceThresholdMeters = this.settings.metersBetweenRoutes || 500 + const timeThresholdMinutes = this.settings.minutesBetweenRoutes || 60 + + const { calculateSpeed, getSpeedColor } = await import('maps_v2/utils/speed_colors') + + const routesGeoJSON = await this.generateRoutesWithSpeedColors( + points, + { distanceThresholdMeters, timeThresholdMinutes }, + calculateSpeed, + getSpeedColor + ) + + this.layerManager.updateLayer('routes', routesGeoJSON) + + } catch (error) { + console.error('Failed to reload routes:', error) + Toast.error('Failed to reload routes') + } finally { + this.controller.hideLoading() + } + } + + /** + * Generate routes with speed coloring + */ + async generateRoutesWithSpeedColors(points, options, calculateSpeed, getSpeedColor) { + const { RoutesLayer } = await import('maps_v2/layers/routes_layer') + const useSpeedColors = this.settings.speedColoredRoutesEnabled || false + const speedColorScale = this.settings.speedColorScale || '0:#00ff00|15:#00ffff|30:#ff00ff|50:#ffff00|100:#ff3300' + + const routesGeoJSON = RoutesLayer.pointsToRoutes(points, options) + + if (!useSpeedColors) { + return routesGeoJSON + } + + routesGeoJSON.features = routesGeoJSON.features.map((feature, index) => { + const segment = points.slice( + points.findIndex(p => p.timestamp === feature.properties.startTime), + points.findIndex(p => p.timestamp === feature.properties.endTime) + 1 + ) + + if (segment.length >= 2) { + const speed = calculateSpeed(segment[0], segment[segment.length - 1]) + const color = getSpeedColor(speed, useSpeedColors, speedColorScale) + feature.properties.speed = speed + feature.properties.color = color + } + + return feature + }) + + return routesGeoJSON + } + + /** + * Toggle heatmap visibility + */ + toggleHeatmap(event) { + const enabled = event.target.checked + SettingsManager.updateSetting('heatmapEnabled', enabled) + + const heatmapLayer = this.layerManager.getLayer('heatmap') + if (heatmapLayer) { + if (enabled) { + heatmapLayer.show() + } else { + heatmapLayer.hide() + } + } + } + + /** + * Toggle fog of war layer + */ + toggleFog(event) { + const enabled = event.target.checked + SettingsManager.updateSetting('fogEnabled', enabled) + + const fogLayer = this.layerManager.getLayer('fog') + if (fogLayer) { + fogLayer.toggle(enabled) + } else { + console.warn('Fog layer not yet initialized') + } + } + + /** + * Toggle scratch map layer + */ + async toggleScratch(event) { + const enabled = event.target.checked + SettingsManager.updateSetting('scratchEnabled', enabled) + + try { + const scratchLayer = this.layerManager.getLayer('scratch') + if (!scratchLayer && enabled) { + const ScratchLayer = await lazyLoader.loadLayer('scratch') + const newScratchLayer = new ScratchLayer(this.map, { + visible: true, + apiClient: this.controller.api + }) + const pointsLayer = this.layerManager.getLayer('points') + const pointsData = pointsLayer?.data || { type: 'FeatureCollection', features: [] } + await newScratchLayer.add(pointsData) + this.layerManager.layers.scratchLayer = newScratchLayer + } else if (scratchLayer) { + if (enabled) { + scratchLayer.show() + } else { + scratchLayer.hide() + } + } + } catch (error) { + console.error('Failed to toggle scratch layer:', error) + Toast.error('Failed to load scratch layer') + } + } + + /** + * Toggle photos layer + */ + togglePhotos(event) { + const enabled = event.target.checked + SettingsManager.updateSetting('photosEnabled', enabled) + + const photosLayer = this.layerManager.getLayer('photos') + if (photosLayer) { + if (enabled) { + photosLayer.show() + } else { + photosLayer.hide() + } + } + } + + /** + * Toggle areas layer + */ + toggleAreas(event) { + const enabled = event.target.checked + SettingsManager.updateSetting('areasEnabled', enabled) + + const areasLayer = this.layerManager.getLayer('areas') + if (areasLayer) { + if (enabled) { + areasLayer.show() + } else { + areasLayer.hide() + } + } + } + + /** + * Toggle tracks layer + */ + toggleTracks(event) { + const enabled = event.target.checked + SettingsManager.updateSetting('tracksEnabled', enabled) + + const tracksLayer = this.layerManager.getLayer('tracks') + if (tracksLayer) { + if (enabled) { + tracksLayer.show() + } else { + tracksLayer.hide() + } + } + } + + /** + * Toggle points layer visibility + */ + togglePoints(event) { + const element = event.currentTarget + const visible = element.checked + + const pointsLayer = this.layerManager.getLayer('points') + if (pointsLayer) { + pointsLayer.toggle(visible) + } + + SettingsManager.updateSetting('pointsVisible', visible) + } +} diff --git a/app/javascript/controllers/maps_v2/settings_manager.js b/app/javascript/controllers/maps_v2/settings_manager.js new file mode 100644 index 00000000..c825e181 --- /dev/null +++ b/app/javascript/controllers/maps_v2/settings_manager.js @@ -0,0 +1,271 @@ +import { SettingsManager } from 'maps_v2/utils/settings_manager' +import { getMapStyle } from 'maps_v2/utils/style_manager' +import { Toast } from 'maps_v2/components/toast' + +/** + * Handles all settings-related operations for Maps V2 + * Including toggles, advanced settings, and UI synchronization + */ +export class SettingsController { + constructor(controller) { + this.controller = controller + this.settings = controller.settings + } + + // Lazy getters for properties that may not be initialized yet + get map() { + return this.controller.map + } + + get layerManager() { + return this.controller.layerManager + } + + /** + * Load settings (sync from backend and localStorage) + */ + async loadSettings() { + this.settings = await SettingsManager.sync() + this.controller.settings = this.settings + console.log('[Maps V2] Settings loaded:', this.settings) + return this.settings + } + + /** + * Sync UI controls with loaded settings + */ + syncToggleStates() { + const controller = this.controller + + // Sync layer toggles + const toggleMap = { + pointsToggle: 'pointsVisible', + routesToggle: 'routesVisible', + heatmapToggle: 'heatmapEnabled', + visitsToggle: 'visitsEnabled', + photosToggle: 'photosEnabled', + areasToggle: 'areasEnabled', + placesToggle: 'placesEnabled', + fogToggle: 'fogEnabled', + scratchToggle: 'scratchEnabled', + speedColoredToggle: 'speedColoredRoutesEnabled' + } + + Object.entries(toggleMap).forEach(([targetName, settingKey]) => { + const target = `${targetName}Target` + if (controller[target]) { + controller[target].checked = this.settings[settingKey] + } + }) + + // Show/hide visits search based on initial toggle state + if (controller.hasVisitsToggleTarget && controller.hasVisitsSearchTarget) { + controller.visitsSearchTarget.style.display = controller.visitsToggleTarget.checked ? 'block' : 'none' + } + + // Show/hide places filters based on initial toggle state + if (controller.hasPlacesToggleTarget && controller.hasPlacesFiltersTarget) { + controller.placesFiltersTarget.style.display = controller.placesToggleTarget.checked ? 'block' : 'none' + } + + // Sync route opacity slider + if (controller.hasRouteOpacityRangeTarget) { + controller.routeOpacityRangeTarget.value = (this.settings.routeOpacity || 1.0) * 100 + } + + // Sync map style dropdown + const mapStyleSelect = controller.element.querySelector('select[name="mapStyle"]') + if (mapStyleSelect) { + mapStyleSelect.value = this.settings.mapStyle || 'light' + } + + // Sync fog of war settings + const fogRadiusInput = controller.element.querySelector('input[name="fogOfWarRadius"]') + if (fogRadiusInput) { + fogRadiusInput.value = this.settings.fogOfWarRadius || 1000 + if (controller.hasFogRadiusValueTarget) { + controller.fogRadiusValueTarget.textContent = `${fogRadiusInput.value}m` + } + } + + const fogThresholdInput = controller.element.querySelector('input[name="fogOfWarThreshold"]') + if (fogThresholdInput) { + fogThresholdInput.value = this.settings.fogOfWarThreshold || 1 + if (controller.hasFogThresholdValueTarget) { + controller.fogThresholdValueTarget.textContent = fogThresholdInput.value + } + } + + // Sync route generation settings + const metersBetweenInput = controller.element.querySelector('input[name="metersBetweenRoutes"]') + if (metersBetweenInput) { + metersBetweenInput.value = this.settings.metersBetweenRoutes || 500 + if (controller.hasMetersBetweenValueTarget) { + controller.metersBetweenValueTarget.textContent = `${metersBetweenInput.value}m` + } + } + + const minutesBetweenInput = controller.element.querySelector('input[name="minutesBetweenRoutes"]') + if (minutesBetweenInput) { + minutesBetweenInput.value = this.settings.minutesBetweenRoutes || 60 + if (controller.hasMinutesBetweenValueTarget) { + controller.minutesBetweenValueTarget.textContent = `${minutesBetweenInput.value}min` + } + } + + // Sync speed-colored routes settings + if (controller.hasSpeedColorScaleInputTarget) { + const colorScale = this.settings.speedColorScale || '0:#00ff00|15:#00ffff|30:#ff00ff|50:#ffff00|100:#ff3300' + controller.speedColorScaleInputTarget.value = colorScale + } + if (controller.hasSpeedColorScaleContainerTarget && controller.hasSpeedColoredToggleTarget) { + const isEnabled = controller.speedColoredToggleTarget.checked + controller.speedColorScaleContainerTarget.classList.toggle('hidden', !isEnabled) + } + + // Sync points rendering mode radio buttons + const pointsRenderingRadios = controller.element.querySelectorAll('input[name="pointsRenderingMode"]') + pointsRenderingRadios.forEach(radio => { + radio.checked = radio.value === (this.settings.pointsRenderingMode || 'raw') + }) + + // Sync speed-colored routes toggle + const speedColoredRoutesToggle = controller.element.querySelector('input[name="speedColoredRoutes"]') + if (speedColoredRoutesToggle) { + speedColoredRoutesToggle.checked = this.settings.speedColoredRoutes || false + } + + console.log('[Maps V2] UI controls synced with settings') + } + + /** + * Update map style from settings + */ + async updateMapStyle(event) { + const styleName = event.target.value + SettingsManager.updateSetting('mapStyle', styleName) + + const style = await getMapStyle(styleName) + + // Clear layer references + this.layerManager.clearLayerReferences() + + this.map.setStyle(style) + + // Reload layers after style change + this.map.once('style.load', () => { + console.log('Style loaded, reloading map data') + this.controller.loadMapData() + }) + } + + /** + * Reset settings to defaults + */ + resetSettings() { + if (confirm('Reset all settings to defaults? This will reload the page.')) { + SettingsManager.resetToDefaults() + window.location.reload() + } + } + + /** + * 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) + } + + SettingsManager.updateSetting('routeOpacity', opacity) + } + + /** + * 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 + if (settings.pointsRenderingMode || settings.speedColoredRoutes !== undefined) { + Toast.info('Reloading map data with new settings...') + await this.controller.loadMapData() + } + } + + // Display value update methods + updateFogRadiusDisplay(event) { + if (this.controller.hasFogRadiusValueTarget) { + this.controller.fogRadiusValueTarget.textContent = `${event.target.value}m` + } + } + + updateFogThresholdDisplay(event) { + if (this.controller.hasFogThresholdValueTarget) { + this.controller.fogThresholdValueTarget.textContent = event.target.value + } + } + + updateMetersBetweenDisplay(event) { + if (this.controller.hasMetersBetweenValueTarget) { + this.controller.metersBetweenValueTarget.textContent = `${event.target.value}m` + } + } + + updateMinutesBetweenDisplay(event) { + if (this.controller.hasMinutesBetweenValueTarget) { + this.controller.minutesBetweenValueTarget.textContent = `${event.target.value}min` + } + } +} diff --git a/app/javascript/controllers/maps_v2/visits_manager.js b/app/javascript/controllers/maps_v2/visits_manager.js new file mode 100644 index 00000000..6d61974f --- /dev/null +++ b/app/javascript/controllers/maps_v2/visits_manager.js @@ -0,0 +1,143 @@ +import { SettingsManager } from 'maps_v2/utils/settings_manager' +import { Toast } from 'maps_v2/components/toast' + +/** + * Manages visits-related operations for Maps V2 + * Including visit creation, filtering, and layer management + */ +export class VisitsManager { + constructor(controller) { + this.controller = controller + this.layerManager = controller.layerManager + this.filterManager = controller.filterManager + this.api = controller.api + this.dataLoader = controller.dataLoader + } + + /** + * Toggle visits layer + */ + toggleVisits(event) { + const enabled = event.target.checked + SettingsManager.updateSetting('visitsEnabled', enabled) + + const visitsLayer = this.layerManager.getLayer('visits') + if (visitsLayer) { + if (enabled) { + visitsLayer.show() + if (this.controller.hasVisitsSearchTarget) { + this.controller.visitsSearchTarget.style.display = 'block' + } + } else { + visitsLayer.hide() + if (this.controller.hasVisitsSearchTarget) { + this.controller.visitsSearchTarget.style.display = 'none' + } + } + } + } + + /** + * Search visits + */ + searchVisits(event) { + const searchTerm = event.target.value.toLowerCase() + const visitsLayer = this.layerManager.getLayer('visits') + this.filterManager.filterAndUpdateVisits( + searchTerm, + this.filterManager.getCurrentVisitFilter(), + visitsLayer + ) + } + + /** + * Filter visits by status + */ + filterVisits(event) { + const filter = event.target.value + this.filterManager.setCurrentVisitFilter(filter) + const searchTerm = document.getElementById('visits-search')?.value.toLowerCase() || '' + const visitsLayer = this.layerManager.getLayer('visits') + this.filterManager.filterAndUpdateVisits(searchTerm, filter, visitsLayer) + } + + /** + * Start create visit mode + */ + startCreateVisit() { + console.log('[Maps V2] Starting create visit mode') + + if (this.controller.hasSettingsPanelTarget && this.controller.settingsPanelTarget.classList.contains('open')) { + this.controller.toggleSettings() + } + + this.controller.map.getCanvas().style.cursor = 'crosshair' + Toast.info('Click on the map to place a visit') + + this.handleCreateVisitClick = (e) => { + const { lng, lat } = e.lngLat + this.openVisitCreationModal(lat, lng) + this.controller.map.getCanvas().style.cursor = '' + } + + this.controller.map.once('click', this.handleCreateVisitClick) + } + + /** + * Open visit creation modal + */ + openVisitCreationModal(lat, lng) { + console.log('[Maps V2] Opening visit creation modal', { lat, lng }) + + const modalElement = document.querySelector('[data-controller="visit-creation-v2"]') + + if (!modalElement) { + console.error('[Maps V2] Visit creation modal not found') + Toast.error('Visit creation modal not available') + return + } + + const controller = this.controller.application.getControllerForElementAndIdentifier( + modalElement, + 'visit-creation-v2' + ) + + if (controller) { + controller.open(lat, lng, this.controller) + } else { + console.error('[Maps V2] Visit creation controller not found') + Toast.error('Visit creation controller not available') + } + } + + /** + * Handle visit creation event - reload visits and update layer + */ + async handleVisitCreated(event) { + console.log('[Maps V2] Visit created, reloading visits...', event.detail) + + try { + const visits = await this.api.fetchVisits({ + start_at: this.controller.startDateValue, + end_at: this.controller.endDateValue + }) + + console.log('[Maps V2] Fetched visits:', visits.length) + + this.filterManager.setAllVisits(visits) + const visitsGeoJSON = this.dataLoader.visitsToGeoJSON(visits) + + console.log('[Maps V2] Converted to GeoJSON:', visitsGeoJSON.features.length, 'features') + + const visitsLayer = this.layerManager.getLayer('visits') + if (visitsLayer) { + visitsLayer.update(visitsGeoJSON) + console.log('[Maps V2] Visits layer updated successfully') + } else { + console.warn('[Maps V2] Visits layer not found, cannot update') + } + } catch (error) { + console.error('[Maps V2] Failed to reload visits:', error) + } + } +} diff --git a/app/javascript/controllers/maps_v2_controller.js b/app/javascript/controllers/maps_v2_controller.js index c57fafc7..95db760e 100644 --- a/app/javascript/controllers/maps_v2_controller.js +++ b/app/javascript/controllers/maps_v2_controller.js @@ -12,11 +12,11 @@ import { DataLoader } from './maps_v2/data_loader' import { EventHandlers } from './maps_v2/event_handlers' import { FilterManager } from './maps_v2/filter_manager' import { DateManager } from './maps_v2/date_manager' -import { lazyLoader } from 'maps_v2/utils/lazy_loader' -import { SelectionLayer } from 'maps_v2/layers/selection_layer' -import { SelectedPointsLayer } from 'maps_v2/layers/selected_points_layer' -import { pointsToGeoJSON } from 'maps_v2/utils/geojson_transformers' -import { VisitCard } from 'maps_v2/components/visit_card' +import { SettingsController } from './maps_v2/settings_manager' +import { AreaSelectionManager } from './maps_v2/area_selection_manager' +import { VisitsManager } from './maps_v2/visits_manager' +import { PlacesManager } from './maps_v2/places_manager' +import { RoutesManager } from './maps_v2/routes_manager' /** * Main map controller for Maps V2 @@ -54,7 +54,6 @@ export default class extends Controller { 'visitsToggle', 'photosToggle', 'areasToggle', - // 'tracksToggle', 'placesToggle', 'fogToggle', 'scratchToggle', @@ -74,14 +73,14 @@ export default class extends Controller { async connect() { this.cleanup = new CleanupHelper() - // Initialize settings manager with API key for backend sync + // Initialize API and settings SettingsManager.initialize(this.apiKeyValue) - - // Sync settings from backend (will fall back to localStorage if needed) - await this.loadSettings() + this.settingsController = new SettingsController(this) + await this.settingsController.loadSettings() + this.settings = this.settingsController.settings // Sync toggle states with loaded settings - this.syncToggleStates() + this.settingsController.syncToggleStates() await this.initializeMap() this.initializeAPI() @@ -92,18 +91,23 @@ export default class extends Controller { this.eventHandlers = new EventHandlers(this.map) this.filterManager = new FilterManager(this.dataLoader) + // Initialize feature managers + this.areaSelectionManager = new AreaSelectionManager(this) + this.visitsManager = new VisitsManager(this) + this.placesManager = new PlacesManager(this) + this.routesManager = new RoutesManager(this) + // Initialize search manager this.initializeSearch() - // Listen for visit creation events - this.boundHandleVisitCreated = this.handleVisitCreated.bind(this) + // Listen for visit and place creation events + this.boundHandleVisitCreated = this.visitsManager.handleVisitCreated.bind(this.visitsManager) this.cleanup.addEventListener(document, 'visit:created', this.boundHandleVisitCreated) - // Listen for place creation events - this.boundHandlePlaceCreated = this.handlePlaceCreated.bind(this) + this.boundHandlePlaceCreated = this.placesManager.handlePlaceCreated.bind(this.placesManager) this.cleanup.addEventListener(document, 'place:created', this.boundHandlePlaceCreated) - // Format initial dates from backend to match V1 API format + // Format initial dates this.startDateValue = DateManager.formatDateForAPI(new Date(this.startDateValue)) this.endDateValue = DateManager.formatDateForAPI(new Date(this.endDateValue)) console.log('[Maps V2] Initial dates:', this.startDateValue, 'to', this.endDateValue) @@ -118,133 +122,10 @@ export default class extends Controller { performanceMonitor.logReport() } - /** - * Load settings (sync from backend and localStorage) - */ - async loadSettings() { - this.settings = await SettingsManager.sync() - 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', - placesToggle: 'placesEnabled', - // tracksToggle: 'tracksEnabled', - fogToggle: 'fogEnabled', - scratchToggle: 'scratchEnabled', - speedColoredToggle: 'speedColoredRoutesEnabled' - } - - Object.entries(toggleMap).forEach(([targetName, settingKey]) => { - const target = `${targetName}Target` - if (this[target]) { - this[target].checked = this.settings[settingKey] - } - }) - - // Show/hide visits search based on initial toggle state - if (this.hasVisitsToggleTarget && this.hasVisitsSearchTarget) { - if (this.visitsToggleTarget.checked) { - this.visitsSearchTarget.style.display = 'block' - } else { - this.visitsSearchTarget.style.display = 'none' - } - } - - // Show/hide places filters based on initial toggle state - if (this.hasPlacesToggleTarget && this.hasPlacesFiltersTarget) { - if (this.placesToggleTarget.checked) { - this.placesFiltersTarget.style.display = 'block' - } else { - this.placesFiltersTarget.style.display = 'none' - } - } - - // 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 speed-colored routes settings - if (this.hasSpeedColorScaleInputTarget) { - const colorScale = this.settings.speedColorScale || '0:#00ff00|15:#00ffff|30:#ff00ff|50:#ffff00|100:#ff3300' - this.speedColorScaleInputTarget.value = colorScale - } - if (this.hasSpeedColorScaleContainerTarget && this.hasSpeedColoredToggleTarget) { - const isEnabled = this.speedColoredToggleTarget.checked - this.speedColorScaleContainerTarget.classList.toggle('hidden', !isEnabled) - } - - // 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 */ async initializeMap() { - // Get map style from local files (async) const style = await getMapStyle(this.settings.mapStyle) this.map = new maplibregl.Map({ @@ -254,7 +135,6 @@ export default class extends Controller { zoom: 2 }) - // Add navigation controls this.map.addControl(new maplibregl.NavigationControl(), 'top-right') } @@ -280,172 +160,8 @@ export default class extends Controller { console.log('[Maps V2] Search manager initialized') } - /** - * Handle visit creation event - reload visits and update layer - */ - async handleVisitCreated(event) { - console.log('[Maps V2] Visit created, reloading visits...', event.detail) - - try { - // Fetch updated visits - const visits = await this.api.fetchVisits({ - start_at: this.startDateValue, - end_at: this.endDateValue - }) - - console.log('[Maps V2] Fetched visits:', visits.length) - - // Update FilterManager with all visits (for search functionality) - this.filterManager.setAllVisits(visits) - - // Convert to GeoJSON - const visitsGeoJSON = this.dataLoader.visitsToGeoJSON(visits) - - console.log('[Maps V2] Converted to GeoJSON:', visitsGeoJSON.features.length, 'features') - - // Get the visits layer and update it - const visitsLayer = this.layerManager.getLayer('visits') - if (visitsLayer) { - visitsLayer.update(visitsGeoJSON) - console.log('[Maps V2] Visits layer updated successfully') - } else { - console.warn('[Maps V2] Visits layer not found, cannot update') - } - } catch (error) { - console.error('[Maps V2] Failed to reload visits:', error) - } - } - - /** - * Handle place creation event - reload places and update layer - */ - async handlePlaceCreated(event) { - console.log('[Maps V2] Place created, reloading places...', event.detail) - - try { - // Get currently selected tag filters - const selectedTags = this.getSelectedPlaceTags() - - // Fetch updated places with filters - const places = await this.api.fetchPlaces({ - tag_ids: selectedTags - }) - - console.log('[Maps V2] Fetched places:', places.length) - - // Convert to GeoJSON - const placesGeoJSON = this.dataLoader.placesToGeoJSON(places) - - console.log('[Maps V2] Converted to GeoJSON:', placesGeoJSON.features.length, 'features') - - // Get the places layer and update it - const placesLayer = this.layerManager.getLayer('places') - if (placesLayer) { - placesLayer.update(placesGeoJSON) - console.log('[Maps V2] Places layer updated successfully') - } else { - console.warn('[Maps V2] Places layer not found, cannot update') - } - } catch (error) { - console.error('[Maps V2] Failed to reload places:', error) - } - } - - /** - * Start create visit mode - * Allows user to click on map to create a new visit - */ - startCreateVisit() { - console.log('[Maps V2] Starting create visit mode') - - // Close settings panel - if (this.hasSettingsPanelTarget && this.settingsPanelTarget.classList.contains('open')) { - this.toggleSettings() - } - - // Change cursor to crosshair - this.map.getCanvas().style.cursor = 'crosshair' - - // Show info message - Toast.info('Click on the map to place a visit') - - // Add map click listener - this.handleCreateVisitClick = (e) => { - const { lng, lat } = e.lngLat - this.openVisitCreationModal(lat, lng) - // Reset cursor - this.map.getCanvas().style.cursor = '' - } - - this.map.once('click', this.handleCreateVisitClick) - } - - /** - * Open visit creation modal - */ - openVisitCreationModal(lat, lng) { - console.log('[Maps V2] Opening visit creation modal', { lat, lng }) - - // Find the visit creation controller - const modalElement = document.querySelector('[data-controller="visit-creation-v2"]') - - if (!modalElement) { - console.error('[Maps V2] Visit creation modal not found') - Toast.error('Visit creation modal not available') - return - } - - // Get the controller instance - const controller = this.application.getControllerForElementAndIdentifier( - modalElement, - 'visit-creation-v2' - ) - - if (controller) { - controller.open(lat, lng, this) - } else { - console.error('[Maps V2] Visit creation controller not found') - Toast.error('Visit creation controller not available') - } - } - - /** - * Start create place mode - * Allows user to click on map to create a new place - */ - startCreatePlace() { - console.log('[Maps V2] Starting create place mode') - - // Close settings panel - if (this.hasSettingsPanelTarget && this.settingsPanelTarget.classList.contains('open')) { - this.toggleSettings() - } - - // Change cursor to crosshair - this.map.getCanvas().style.cursor = 'crosshair' - - // Show info message - Toast.info('Click on the map to place a place') - - // Add map click listener - this.handleCreatePlaceClick = (e) => { - const { lng, lat } = e.lngLat - - // Dispatch event for place creation modal (reuse existing controller) - document.dispatchEvent(new CustomEvent('place:create', { - detail: { latitude: lat, longitude: lng } - })) - - // Reset cursor - this.map.getCanvas().style.cursor = '' - } - - this.map.once('click', this.handleCreatePlaceClick) - } - /** * Load map data from API - * @param {Object} options - { showLoading, fitBounds, showToast } */ async loadMapData(options = {}) { const { @@ -461,17 +177,14 @@ export default class extends Controller { } try { - // Fetch all map data const data = await this.dataLoader.fetchMapData( this.startDateValue, this.endDateValue, showLoading ? this.updateLoadingProgress.bind(this) : null ) - // Store visits for filtering this.filterManager.setAllVisits(data.visits) - // Add all layers when style is ready const addAllLayers = async () => { await this.layerManager.addAllLayers( data.pointsGeoJSON, @@ -483,7 +196,6 @@ export default class extends Controller { data.placesGeoJSON ) - // Setup event handlers this.layerManager.setupLayerEventHandlers({ handlePointClick: this.eventHandlers.handlePointClick.bind(this.eventHandlers), handleVisitClick: this.eventHandlers.handleVisitClick.bind(this.eventHandlers), @@ -492,7 +204,6 @@ export default class extends Controller { }) } - // Use 'load' event which fires when map is fully initialized if (this.map.loaded()) { await addAllLayers() } else { @@ -501,12 +212,10 @@ export default class extends Controller { }) } - // Fit map to data bounds (optional) if (fitBounds && data.points.length > 0) { this.fitMapToBounds(data.pointsGeoJSON) } - // Show success toast (optional) if (showToast) { Toast.success(`Loaded ${data.points.length} location ${data.points.length === 1 ? 'point' : 'points'}`) } @@ -548,8 +257,6 @@ export default class extends Controller { this.endDateValue = endDate console.log('[Maps V2] Date range changed:', this.startDateValue, 'to', this.endDateValue) - - // Reload data this.loadMapData() } @@ -577,70 +284,6 @@ export default class extends Controller { } } - /** - * Toggle layer visibility - */ - toggleLayer(event) { - 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 (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 points layer visibility - */ - togglePoints(event) { - const element = event.currentTarget - const visible = element.checked - - const pointsLayer = this.layerManager.getLayer('points') - if (pointsLayer) { - pointsLayer.toggle(visible) - } - - // Save setting - 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) - } - - // Show/hide routes options panel - if (this.hasRoutesOptionsTarget) { - this.routesOptionsTarget.style.display = visible ? 'block' : 'none' - } - - // Save setting - SettingsManager.updateSetting('routesVisible', visible) - } - /** * Toggle settings panel */ @@ -650,1339 +293,73 @@ export default class extends Controller { } } - /** - * Update map style from settings - */ - async updateMapStyle(event) { - const styleName = event.target.value - SettingsManager.updateSetting('mapStyle', styleName) + // ===== Delegated Methods to Managers ===== - const style = await getMapStyle(styleName) + // Settings Controller methods + updateMapStyle(event) { return this.settingsController.updateMapStyle(event) } + resetSettings() { return this.settingsController.resetSettings() } + updateRouteOpacity(event) { return this.settingsController.updateRouteOpacity(event) } + updateAdvancedSettings(event) { return this.settingsController.updateAdvancedSettings(event) } + updateFogRadiusDisplay(event) { return this.settingsController.updateFogRadiusDisplay(event) } + updateFogThresholdDisplay(event) { return this.settingsController.updateFogThresholdDisplay(event) } + updateMetersBetweenDisplay(event) { return this.settingsController.updateMetersBetweenDisplay(event) } + updateMinutesBetweenDisplay(event) { return this.settingsController.updateMinutesBetweenDisplay(event) } - // Clear layer references - this.layerManager.clearLayerReferences() + // Area Selection Manager methods + startSelectArea() { return this.areaSelectionManager.startSelectArea() } + cancelAreaSelection() { return this.areaSelectionManager.cancelAreaSelection() } + deleteSelectedPoints() { return this.areaSelectionManager.deleteSelectedPoints() } - this.map.setStyle(style) + // Visits Manager methods + toggleVisits(event) { return this.visitsManager.toggleVisits(event) } + searchVisits(event) { return this.visitsManager.searchVisits(event) } + filterVisits(event) { return this.visitsManager.filterVisits(event) } + startCreateVisit() { return this.visitsManager.startCreateVisit() } - // Reload layers after style change - this.map.once('style.load', () => { - console.log('Style loaded, reloading map data') - this.loadMapData() - }) - } + // Places Manager methods + togglePlaces(event) { return this.placesManager.togglePlaces(event) } + filterPlacesByTags(event) { return this.placesManager.filterPlacesByTags(event) } + toggleAllPlaceTags(event) { return this.placesManager.toggleAllPlaceTags(event) } + startCreatePlace() { return this.placesManager.startCreatePlace() } - /** - * Toggle heatmap visibility - */ - toggleHeatmap(event) { - const enabled = event.target.checked - SettingsManager.updateSetting('heatmapEnabled', enabled) + // Area creation + startCreateArea() { + console.log('[Maps V2] Starting create area mode') - const heatmapLayer = this.layerManager.getLayer('heatmap') - if (heatmapLayer) { - if (enabled) { - heatmapLayer.show() - } else { - heatmapLayer.hide() - } - } - } - - /** - * Reset settings to defaults - */ - resetSettings() { - if (confirm('Reset all settings to defaults? This will reload the page.')) { - SettingsManager.resetToDefaults() - window.location.reload() - } - } - - /** - * 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) + if (this.hasSettingsPanelTarget && this.settingsPanelTarget.classList.contains('open')) { + this.toggleSettings() } - // 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 - */ - toggleVisits(event) { - const enabled = event.target.checked - SettingsManager.updateSetting('visitsEnabled', enabled) - - const visitsLayer = this.layerManager.getLayer('visits') - if (visitsLayer) { - if (enabled) { - visitsLayer.show() - // Show visits search - if (this.hasVisitsSearchTarget) { - this.visitsSearchTarget.style.display = 'block' - } - } else { - visitsLayer.hide() - // Hide visits search - if (this.hasVisitsSearchTarget) { - this.visitsSearchTarget.style.display = 'none' - } - } - } - } - - /** - * Toggle places layer - */ - togglePlaces(event) { - const enabled = event.target.checked - SettingsManager.updateSetting('placesEnabled', enabled) - - const placesLayer = this.layerManager.getLayer('places') - if (placesLayer) { - if (enabled) { - placesLayer.show() - // Show places filters - if (this.hasPlacesFiltersTarget) { - this.placesFiltersTarget.style.display = 'block' - } - - // Initialize tag filters: enable all tags if no saved selection exists - this.initializePlaceTagFilters() - } else { - placesLayer.hide() - // Hide places filters - if (this.hasPlacesFiltersTarget) { - this.placesFiltersTarget.style.display = 'none' - } - } - } - } - - /** - * Initialize place tag filters (enable all by default or restore saved state) - */ - initializePlaceTagFilters() { - const savedFilters = this.settings.placesTagFilters - - if (savedFilters && savedFilters.length > 0) { - // Restore saved tag selection - this.restoreSavedTagFilters(savedFilters) - } else { - // Default: enable all tags - this.enableAllTagsInitial() - } - } - - /** - * Restore saved tag filters - */ - restoreSavedTagFilters(savedFilters) { - const tagCheckboxes = document.querySelectorAll('input[name="place_tag_ids[]"]') - - tagCheckboxes.forEach(checkbox => { - const value = checkbox.value === 'untagged' ? checkbox.value : parseInt(checkbox.value) - const shouldBeChecked = savedFilters.includes(value) - - if (checkbox.checked !== shouldBeChecked) { - checkbox.checked = shouldBeChecked - - // Update badge styling - const badge = checkbox.nextElementSibling - const color = badge.style.borderColor - - if (shouldBeChecked) { - badge.classList.remove('badge-outline') - badge.style.backgroundColor = color - badge.style.color = 'white' - } else { - badge.classList.add('badge-outline') - badge.style.backgroundColor = 'transparent' - badge.style.color = color - } - } - }) - - // Sync "Enable All Tags" toggle - this.syncEnableAllTagsToggle() - - // Load places with restored filters - this.loadPlacesWithTags(savedFilters) - } - - /** - * Enable all tags initially - */ - enableAllTagsInitial() { - if (this.hasEnableAllPlaceTagsToggleTarget) { - this.enableAllPlaceTagsToggleTarget.checked = true - } - - const tagCheckboxes = document.querySelectorAll('input[name="place_tag_ids[]"]') - const allTagIds = [] - - tagCheckboxes.forEach(checkbox => { - checkbox.checked = true - - // Update badge styling - const badge = checkbox.nextElementSibling - const color = badge.style.borderColor - badge.classList.remove('badge-outline') - badge.style.backgroundColor = color - badge.style.color = 'white' - - // Collect tag IDs - const value = checkbox.value === 'untagged' ? checkbox.value : parseInt(checkbox.value) - allTagIds.push(value) - }) - - // Save to settings - SettingsManager.updateSetting('placesTagFilters', allTagIds) - - // Load places with all tags - this.loadPlacesWithTags(allTagIds) - } - - /** - * Get selected place tag IDs - */ - getSelectedPlaceTags() { - return Array.from( - document.querySelectorAll('input[name="place_tag_ids[]"]:checked') - ).map(cb => { - const value = cb.value - // Keep "untagged" as string, convert others to integers - return value === 'untagged' ? value : parseInt(value) - }) - } - - /** - * Filter places by selected tags - */ - filterPlacesByTags(event) { - // Update badge styles - const badge = event.target.nextElementSibling - const color = badge.style.borderColor - - if (event.target.checked) { - badge.classList.remove('badge-outline') - badge.style.backgroundColor = color - badge.style.color = 'white' - } else { - badge.classList.add('badge-outline') - badge.style.backgroundColor = 'transparent' - badge.style.color = color - } - - // Sync "Enable All Tags" toggle state - this.syncEnableAllTagsToggle() - - // Get all checked tag checkboxes - const checkedTags = this.getSelectedPlaceTags() - - // Save selection to settings - SettingsManager.updateSetting('placesTagFilters', checkedTags) - - // Reload places with selected tags (empty array = show NO places) - this.loadPlacesWithTags(checkedTags) - } - - /** - * Sync "Enable All Tags" toggle with individual tag states - */ - syncEnableAllTagsToggle() { - if (!this.hasEnableAllPlaceTagsToggleTarget) return - - const tagCheckboxes = document.querySelectorAll('input[name="place_tag_ids[]"]') - const allChecked = Array.from(tagCheckboxes).every(cb => cb.checked) - const noneChecked = Array.from(tagCheckboxes).every(cb => !cb.checked) - - // Update toggle state without triggering change event - this.enableAllPlaceTagsToggleTarget.checked = allChecked - } - - /** - * Load places filtered by tags - */ - async loadPlacesWithTags(tagIds = []) { - try { - let places = [] - - if (tagIds.length > 0) { - // Fetch places with selected tags - places = await this.api.fetchPlaces({ tag_ids: tagIds }) - } - // If tagIds is empty, places remains empty array = show NO places - - const placesGeoJSON = this.dataLoader.placesToGeoJSON(places) - - const placesLayer = this.layerManager.getLayer('places') - if (placesLayer) { - placesLayer.update(placesGeoJSON) - } - } catch (error) { - console.error('[Maps V2] Failed to load places:', error) - } - } - - /** - * Toggle all place tags on/off - */ - toggleAllPlaceTags(event) { - const enableAll = event.target.checked - const tagCheckboxes = document.querySelectorAll('input[name="place_tag_ids[]"]') - - tagCheckboxes.forEach(checkbox => { - if (checkbox.checked !== enableAll) { - checkbox.checked = enableAll - - // Update badge styling - const badge = checkbox.nextElementSibling - const color = badge.style.borderColor - - if (enableAll) { - badge.classList.remove('badge-outline') - badge.style.backgroundColor = color - badge.style.color = 'white' - } else { - badge.classList.add('badge-outline') - badge.style.backgroundColor = 'transparent' - badge.style.color = color - } - } - }) - - // Get selected tags - const selectedTags = this.getSelectedPlaceTags() - - // Save selection to settings - SettingsManager.updateSetting('placesTagFilters', selectedTags) - - // Reload places with selected tags - this.loadPlacesWithTags(selectedTags) - } - - /** - * Toggle photos layer - */ - togglePhotos(event) { - const enabled = event.target.checked - SettingsManager.updateSetting('photosEnabled', enabled) - - const photosLayer = this.layerManager.getLayer('photos') - if (photosLayer) { - if (enabled) { - photosLayer.show() - } else { - photosLayer.hide() - } - } - } - - /** - * Search visits - */ - searchVisits(event) { - const searchTerm = event.target.value.toLowerCase() - const visitsLayer = this.layerManager.getLayer('visits') - this.filterManager.filterAndUpdateVisits( - searchTerm, - this.filterManager.getCurrentVisitFilter(), - visitsLayer - ) - } - - /** - * Filter visits by status - */ - filterVisits(event) { - const filter = event.target.value - this.filterManager.setCurrentVisitFilter(filter) - const searchTerm = document.getElementById('visits-search')?.value.toLowerCase() || '' - const visitsLayer = this.layerManager.getLayer('visits') - this.filterManager.filterAndUpdateVisits(searchTerm, filter, visitsLayer) - } - - /** - * Toggle areas layer - */ - toggleAreas(event) { - const enabled = event.target.checked - SettingsManager.updateSetting('areasEnabled', enabled) - - const areasLayer = this.layerManager.getLayer('areas') - if (areasLayer) { - if (enabled) { - areasLayer.show() - } else { - areasLayer.hide() - } - } - } - - /** - * Toggle tracks layer - */ - toggleTracks(event) { - const enabled = event.target.checked - SettingsManager.updateSetting('tracksEnabled', enabled) - - const tracksLayer = this.layerManager.getLayer('tracks') - if (tracksLayer) { - if (enabled) { - tracksLayer.show() - } else { - tracksLayer.hide() - } - } - } - - /** - * Toggle fog of war layer - */ - toggleFog(event) { - const enabled = event.target.checked - SettingsManager.updateSetting('fogEnabled', enabled) - - const fogLayer = this.layerManager.getLayer('fog') - if (fogLayer) { - fogLayer.toggle(enabled) - } else { - console.warn('Fog layer not yet initialized') - } - } - - /** - * Toggle scratch map layer - */ - async toggleScratch(event) { - const enabled = event.target.checked - SettingsManager.updateSetting('scratchEnabled', enabled) - - try { - const scratchLayer = this.layerManager.getLayer('scratch') - if (!scratchLayer && enabled) { - // Lazy load scratch layer - const ScratchLayer = await lazyLoader.loadLayer('scratch') - const newScratchLayer = new ScratchLayer(this.map, { - visible: true, - apiClient: this.api - }) - const pointsLayer = this.layerManager.getLayer('points') - const pointsData = pointsLayer?.data || { type: 'FeatureCollection', features: [] } - await newScratchLayer.add(pointsData) - this.layerManager.layers.scratchLayer = newScratchLayer - } else if (scratchLayer) { - if (enabled) { - scratchLayer.show() - } else { - scratchLayer.hide() - } - } - } catch (error) { - console.error('Failed to toggle scratch layer:', error) - Toast.error('Failed to load scratch layer') - } - } - - /** - * Toggle speed-colored routes - */ - async toggleSpeedColoredRoutes(event) { - const enabled = event.target.checked - SettingsManager.updateSetting('speedColoredRoutesEnabled', enabled) - - // Show/hide color scale container - if (this.hasSpeedColorScaleContainerTarget) { - this.speedColorScaleContainerTarget.classList.toggle('hidden', !enabled) - } - - // Reload routes with speed colors - await this.reloadRoutes() - } - - /** - * Open speed color editor modal - */ - openSpeedColorEditor() { - const currentScale = this.speedColorScaleInputTarget.value || '0:#00ff00|15:#00ffff|30:#ff00ff|50:#ffff00|100:#ff3300' - - // Create modal if it doesn't exist - let modal = document.getElementById('speed-color-editor-modal') - if (!modal) { - modal = this.createSpeedColorEditorModal(currentScale) - document.body.appendChild(modal) - } else { - // Update existing modal with current scale - const controller = this.application.getControllerForElementAndIdentifier(modal, 'speed-color-editor') - if (controller) { - controller.colorStopsValue = currentScale - controller.loadColorStops() - } - } - - // Show modal - const checkbox = modal.querySelector('.modal-toggle') - if (checkbox) { - checkbox.checked = true - } - } - - /** - * Create speed color editor modal element - */ - createSpeedColorEditorModal(currentScale) { - const modal = document.createElement('div') - modal.id = 'speed-color-editor-modal' - modal.setAttribute('data-controller', 'speed-color-editor') - modal.setAttribute('data-speed-color-editor-color-stops-value', currentScale) - modal.setAttribute('data-action', 'speed-color-editor:save->maps-v2#handleSpeedColorSave') - - modal.innerHTML = ` - - - ` - - return modal - } - - /** - * Handle speed color save event from editor - */ - handleSpeedColorSave(event) { - const newScale = event.detail.colorStops - - // Save to settings - this.speedColorScaleInputTarget.value = newScale - SettingsManager.updateSetting('speedColorScale', newScale) - - // Reload routes if speed colors are enabled - if (this.speedColoredToggleTarget.checked) { - this.reloadRoutes() - } - } - - /** - * Reload routes layer - */ - async reloadRoutes() { - this.showLoading('Reloading routes...') - - try { - const pointsLayer = this.layerManager.getLayer('points') - const points = pointsLayer?.data?.features?.map(f => ({ - latitude: f.geometry.coordinates[1], - longitude: f.geometry.coordinates[0], - timestamp: f.properties.timestamp - })) || [] - - // Get route generation settings - const distanceThresholdMeters = this.settings.metersBetweenRoutes || 500 - const timeThresholdMinutes = this.settings.minutesBetweenRoutes || 60 - - // Import speed colors utility - const { calculateSpeed, getSpeedColor } = await import('maps_v2/utils/speed_colors') - - // Generate routes with speed coloring if enabled - const routesGeoJSON = await this.generateRoutesWithSpeedColors( - points, - { distanceThresholdMeters, timeThresholdMinutes }, - calculateSpeed, - getSpeedColor - ) - - // Update routes layer - this.layerManager.updateLayer('routes', routesGeoJSON) - - } catch (error) { - console.error('Failed to reload routes:', error) - Toast.error('Failed to reload routes') - } finally { - this.hideLoading() - } - } - - /** - * Generate routes with speed coloring - */ - async generateRoutesWithSpeedColors(points, options, calculateSpeed, getSpeedColor) { - const { RoutesLayer } = await import('maps_v2/layers/routes_layer') - const useSpeedColors = this.settings.speedColoredRoutesEnabled || false - const speedColorScale = this.settings.speedColorScale || '0:#00ff00|15:#00ffff|30:#ff00ff|50:#ffff00|100:#ff3300' - - // Use RoutesLayer static method to generate basic routes - const routesGeoJSON = RoutesLayer.pointsToRoutes(points, options) - - if (!useSpeedColors) { - return routesGeoJSON - } - - // Add speed colors to route segments - routesGeoJSON.features = routesGeoJSON.features.map((feature, index) => { - const segment = points.slice( - points.findIndex(p => p.timestamp === feature.properties.startTime), - points.findIndex(p => p.timestamp === feature.properties.endTime) + 1 - ) - - if (segment.length >= 2) { - const speed = calculateSpeed(segment[0], segment[segment.length - 1]) - const color = getSpeedColor(speed, useSpeedColors, speedColorScale) - feature.properties.speed = speed - feature.properties.color = color - } - - return feature - }) - - return routesGeoJSON - } - - /** - * Start area selection mode - */ - async startSelectArea() { - console.log('[Maps V2] Starting area selection mode') - - // Keep settings panel open during selection mode - // (Don't close it) - - // Initialize selection layer if not exists - if (!this.selectionLayer) { - this.selectionLayer = new SelectionLayer(this.map, { - visible: true, - onSelectionComplete: this.handleAreaSelected.bind(this) - }) - - // Add layer to map immediately (map is already loaded at this point) - this.selectionLayer.add({ - type: 'FeatureCollection', - features: [] - }) - - console.log('[Maps V2] Selection layer initialized') - } - - // Initialize selected points layer if not exists - if (!this.selectedPointsLayer) { - this.selectedPointsLayer = new SelectedPointsLayer(this.map, { - visible: true - }) - - // Add layer to map immediately (map is already loaded at this point) - this.selectedPointsLayer.add({ - type: 'FeatureCollection', - features: [] - }) - - console.log('[Maps V2] Selected points layer initialized') - } - - // Enable selection mode - this.selectionLayer.enableSelectionMode() - - // Update UI - replace Select Area button with Cancel Selection button - if (this.hasSelectAreaButtonTarget) { - this.selectAreaButtonTarget.innerHTML = ` - - - - - Cancel Selection - ` - // Change action to cancel - this.selectAreaButtonTarget.dataset.action = 'click->maps-v2#cancelAreaSelection' - } - - Toast.info('Draw a rectangle on the map to select points') - } - - /** - * Handle area selection completion - */ - async handleAreaSelected(bounds) { - console.log('[Maps V2] Area selected:', bounds) - - try { - // Fetch both points and visits within the selected area - Toast.info('Fetching data in selected area...') - - const [points, visits] = await Promise.all([ - this.api.fetchPointsInArea({ - start_at: this.startDateValue, - end_at: this.endDateValue, - min_longitude: bounds.minLng, - max_longitude: bounds.maxLng, - min_latitude: bounds.minLat, - max_latitude: bounds.maxLat - }), - this.api.fetchVisitsInArea({ - start_at: this.startDateValue, - end_at: this.endDateValue, - sw_lat: bounds.minLat, - sw_lng: bounds.minLng, - ne_lat: bounds.maxLat, - ne_lng: bounds.maxLng - }) - ]) - - console.log('[Maps V2] Found', points.length, 'points and', visits.length, 'visits in area') - - if (points.length === 0 && visits.length === 0) { - Toast.info('No data found in selected area') - this.cancelAreaSelection() - return - } - - // Convert points to GeoJSON and display - if (points.length > 0) { - const geojson = pointsToGeoJSON(points) - this.selectedPointsLayer.updateSelectedPoints(geojson) - this.selectedPointsLayer.show() - } - - // Display visits in side panel and on map - if (visits.length > 0) { - this.displaySelectedVisits(visits) - } - - // Update UI - show action buttons - if (this.hasSelectionActionsTarget) { - this.selectionActionsTarget.classList.remove('hidden') - } - - // Update delete button text with count - if (this.hasDeleteButtonTextTarget) { - this.deleteButtonTextTarget.textContent = `Delete ${points.length} Point${points.length === 1 ? '' : 's'}` - } - - // Disable selection mode - this.selectionLayer.disableSelectionMode() - - const messages = [] - if (points.length > 0) messages.push(`${points.length} point${points.length === 1 ? '' : 's'}`) - if (visits.length > 0) messages.push(`${visits.length} visit${visits.length === 1 ? '' : 's'}`) - - Toast.success(`Selected ${messages.join(' and ')}`) - } catch (error) { - console.error('[Maps V2] Failed to fetch data in area:', error) - Toast.error('Failed to fetch data in selected area') - this.cancelAreaSelection() - } - } - - /** - * Display selected visits in side panel - */ - displaySelectedVisits(visits) { - if (!this.hasSelectedVisitsContainerTarget) return - - // Store visits for later use - this.selectedVisits = visits - this.selectedVisitIds = new Set() - - // Generate HTML for all visit cards - const cardsHTML = visits.map(visit => - VisitCard.create(visit, { - isSelected: false - }) - ).join('') - - // Update container - this.selectedVisitsContainerTarget.innerHTML = ` -
-
- - - - -

Visits in Area (${visits.length})

-
- ${cardsHTML} -
- ` - - // Show container - this.selectedVisitsContainerTarget.classList.remove('hidden') - - // Attach event listeners - this.attachVisitCardListeners() - - // Update bulk actions after DOM updates (removes them if no visits selected) - requestAnimationFrame(() => { - this.updateBulkActions() - }) - } - - /** - * Attach event listeners to visit cards - */ - attachVisitCardListeners() { - // Checkbox selection - this.element.querySelectorAll('[data-visit-select]').forEach(checkbox => { - checkbox.addEventListener('change', (e) => { - const visitId = parseInt(e.target.dataset.visitSelect) - if (e.target.checked) { - this.selectedVisitIds.add(visitId) - } else { - this.selectedVisitIds.delete(visitId) - } - this.updateBulkActions() - }) - }) - - // Confirm button - this.element.querySelectorAll('[data-visit-confirm]').forEach(btn => { - btn.addEventListener('click', async (e) => { - const button = e.currentTarget - const visitId = parseInt(button.dataset.visitConfirm) - await this.confirmVisit(visitId) - }) - }) - - // Decline button - this.element.querySelectorAll('[data-visit-decline]').forEach(btn => { - btn.addEventListener('click', async (e) => { - const button = e.currentTarget - const visitId = parseInt(button.dataset.visitDecline) - await this.declineVisit(visitId) - }) - }) - } - - /** - * Update bulk action buttons visibility and attach listeners - */ - updateBulkActions() { - const selectedCount = this.selectedVisitIds.size - - // Remove any existing bulk action buttons from visit cards - const existingBulkActions = this.element.querySelectorAll('.bulk-actions-inline') - existingBulkActions.forEach(el => el.remove()) - - if (selectedCount >= 2) { - // Find the last (lowest) selected visit card - const selectedVisitCards = Array.from(this.element.querySelectorAll('.visit-card')) - .filter(card => { - const visitId = parseInt(card.dataset.visitId) - return this.selectedVisitIds.has(visitId) - }) - - if (selectedVisitCards.length > 0) { - const lastSelectedCard = selectedVisitCards[selectedVisitCards.length - 1] - - // Create bulk actions element - const bulkActionsDiv = document.createElement('div') - bulkActionsDiv.className = 'bulk-actions-inline mb-2' - bulkActionsDiv.innerHTML = ` -
-
- - - - ${selectedCount} visit${selectedCount === 1 ? '' : 's'} selected -
-
- - - -
-
- ` - - // Insert after the last selected card - lastSelectedCard.insertAdjacentElement('afterend', bulkActionsDiv) - - // Attach listeners - const mergeBtn = bulkActionsDiv.querySelector('[data-bulk-merge]') - const confirmBtn = bulkActionsDiv.querySelector('[data-bulk-confirm]') - const declineBtn = bulkActionsDiv.querySelector('[data-bulk-decline]') - - if (mergeBtn) { - mergeBtn.addEventListener('click', () => this.bulkMergeVisits()) - } - if (confirmBtn) { - confirmBtn.addEventListener('click', () => this.bulkConfirmVisits()) - } - if (declineBtn) { - declineBtn.addEventListener('click', () => this.bulkDeclineVisits()) - } - } - } - } - - /** - * Confirm a single visit - */ - async confirmVisit(visitId) { - try { - await this.api.updateVisitStatus(visitId, 'confirmed') - Toast.success('Visit confirmed') - // Refresh the visit card - await this.refreshSelectedVisits() - } catch (error) { - console.error('[Maps V2] Failed to confirm visit:', error) - Toast.error('Failed to confirm visit') - } - } - - /** - * Decline a single visit - */ - async declineVisit(visitId) { - try { - await this.api.updateVisitStatus(visitId, 'declined') - Toast.success('Visit declined') - // Refresh the visit card - await this.refreshSelectedVisits() - } catch (error) { - console.error('[Maps V2] Failed to decline visit:', error) - Toast.error('Failed to decline visit') - } - } - - /** - * Bulk merge selected visits - */ - async bulkMergeVisits() { - const visitIds = Array.from(this.selectedVisitIds) - - if (visitIds.length < 2) { - Toast.error('Select at least 2 visits to merge') + const modalElement = document.querySelector('[data-controller="area-creation-v2"]') + if (!modalElement) { + console.error('[Maps V2] Area creation modal not found') + Toast.error('Area creation modal not available') return } - if (!confirm(`Merge ${visitIds.length} visits into one?`)) { - return - } - - try { - Toast.info('Merging visits...') - const mergedVisit = await this.api.mergeVisits(visitIds) - Toast.success('Visits merged successfully') - - // Clear selection state - this.selectedVisitIds.clear() - - // Remove the old visit cards and add the merged one - this.replaceVisitsWithMerged(visitIds, mergedVisit) - - // Update bulk actions (will remove the panel since selection is cleared) - this.updateBulkActions() - } catch (error) { - console.error('[Maps V2] Failed to merge visits:', error) - Toast.error('Failed to merge visits') - } - } - - /** - * Bulk confirm selected visits - */ - async bulkConfirmVisits() { - const visitIds = Array.from(this.selectedVisitIds) - - try { - Toast.info('Confirming visits...') - await this.api.bulkUpdateVisits(visitIds, 'confirmed') - Toast.success(`Confirmed ${visitIds.length} visits`) - - // Clear selection state before refreshing - this.selectedVisitIds.clear() - - await this.refreshSelectedVisits() - } catch (error) { - console.error('[Maps V2] Failed to confirm visits:', error) - Toast.error('Failed to confirm visits') - } - } - - /** - * Bulk decline selected visits - */ - async bulkDeclineVisits() { - const visitIds = Array.from(this.selectedVisitIds) - - if (!confirm(`Decline ${visitIds.length} visits?`)) { - return - } - - try { - Toast.info('Declining visits...') - await this.api.bulkUpdateVisits(visitIds, 'declined') - Toast.success(`Declined ${visitIds.length} visits`) - - // Clear selection state before refreshing - this.selectedVisitIds.clear() - - await this.refreshSelectedVisits() - } catch (error) { - console.error('[Maps V2] Failed to decline visits:', error) - Toast.error('Failed to decline visits') - } - } - - /** - * Replace merged visit cards with the new merged visit - */ - replaceVisitsWithMerged(oldVisitIds, mergedVisit) { - const container = this.element.querySelector('.selected-visits-list') - if (!container) return - - // Find the correct position to insert BEFORE removing old cards - const mergedStartTime = new Date(mergedVisit.started_at).getTime() - const allCards = Array.from(container.querySelectorAll('.visit-card')) - - let insertBeforeCard = null - for (const card of allCards) { - const cardId = parseInt(card.dataset.visitId) - - // Skip cards that we're about to remove - if (oldVisitIds.includes(cardId)) continue - - // Find the visit data for this card - const cardVisit = this.selectedVisits.find(v => v.id === cardId) - if (cardVisit) { - const cardStartTime = new Date(cardVisit.started_at).getTime() - if (cardStartTime > mergedStartTime) { - insertBeforeCard = card - break - } - } - } - - // Remove old visit cards from DOM - oldVisitIds.forEach(id => { - const card = this.element.querySelector(`.visit-card[data-visit-id="${id}"]`) - if (card) { - card.remove() - } - }) - - // Update the selectedVisits array and sort by started_at - this.selectedVisits = this.selectedVisits.filter(v => !oldVisitIds.includes(v.id)) - this.selectedVisits.push(mergedVisit) - this.selectedVisits.sort((a, b) => new Date(a.started_at) - new Date(b.started_at)) - - // Create new visit card HTML - const newCardHTML = VisitCard.create(mergedVisit, { isSelected: false }) - - // Insert the new card in the correct position - if (insertBeforeCard) { - insertBeforeCard.insertAdjacentHTML('beforebegin', newCardHTML) - } else { - // If no card starts after this one, append to the end - container.insertAdjacentHTML('beforeend', newCardHTML) - } - - // Update header count - const header = container.querySelector('h3') - if (header) { - header.textContent = `Visits in Area (${this.selectedVisits.length})` - } - - // Attach event listeners to the new card - this.attachVisitCardListeners() - } - - /** - * Refresh selected visits after changes - */ - async refreshSelectedVisits() { - // Re-fetch visits in the same area - const bounds = this.selectionLayer.currentRect - if (!bounds) return - - try { - const visits = await this.api.fetchVisitsInArea({ - start_at: this.startDateValue, - end_at: this.endDateValue, - sw_lat: bounds.start.lat < bounds.end.lat ? bounds.start.lat : bounds.end.lat, - sw_lng: bounds.start.lng < bounds.end.lng ? bounds.start.lng : bounds.end.lng, - ne_lat: bounds.start.lat > bounds.end.lat ? bounds.start.lat : bounds.end.lat, - ne_lng: bounds.start.lng > bounds.end.lng ? bounds.start.lng : bounds.end.lng - }) - - this.displaySelectedVisits(visits) - } catch (error) { - console.error('[Maps V2] Failed to refresh visits:', error) - } - } - - /** - * Cancel area selection - */ - cancelAreaSelection() { - console.log('[Maps V2] Cancelling area selection') - - // Clear selection layers - if (this.selectionLayer) { - this.selectionLayer.disableSelectionMode() - this.selectionLayer.clearSelection() - } - - if (this.selectedPointsLayer) { - this.selectedPointsLayer.clearSelection() - } - - // Clear visits - if (this.hasSelectedVisitsContainerTarget) { - this.selectedVisitsContainerTarget.classList.add('hidden') - this.selectedVisitsContainerTarget.innerHTML = '' - } - - if (this.hasSelectedVisitsBulkActionsTarget) { - this.selectedVisitsBulkActionsTarget.classList.add('hidden') - } - - // Clear stored data - this.selectedVisits = [] - this.selectedVisitIds = new Set() - - // Update UI - restore Select Area button - if (this.hasSelectAreaButtonTarget) { - this.selectAreaButtonTarget.innerHTML = ` - - - - - - - - Select Area - ` - this.selectAreaButtonTarget.classList.remove('btn-error') - this.selectAreaButtonTarget.classList.add('btn', 'btn-outline') - // Restore original action - this.selectAreaButtonTarget.dataset.action = 'click->maps-v2#startSelectArea' - } - - if (this.hasSelectionActionsTarget) { - this.selectionActionsTarget.classList.add('hidden') - } - - Toast.info('Selection cancelled') - } - - /** - * Delete selected points - */ - async deleteSelectedPoints() { - const pointCount = this.selectedPointsLayer.getCount() - const pointIds = this.selectedPointsLayer.getSelectedPointIds() - - if (pointIds.length === 0) { - Toast.error('No points selected') - return - } - - // Confirm deletion - const confirmed = confirm( - `Are you sure you want to delete ${pointCount} point${pointCount === 1 ? '' : 's'}? This action cannot be undone.` + const controller = this.application.getControllerForElementAndIdentifier( + modalElement, + 'area-creation-v2' ) - if (!confirmed) { - return - } - - console.log('[Maps V2] Deleting', pointIds.length, 'points') - - try { - Toast.info('Deleting points...') - - // Call bulk delete API - const result = await this.api.bulkDeletePoints(pointIds) - - console.log('[Maps V2] Deleted', result.count, 'points') - - // Clear selection first - this.cancelAreaSelection() - - // Reload map data silently (no loading overlay, no camera movement, no success toast) - await this.loadMapData({ - showLoading: false, - fitBounds: false, - showToast: false - }) - - // Show success toast after reload - Toast.success(`Deleted ${result.count} point${result.count === 1 ? '' : 's'}`) - } catch (error) { - console.error('[Maps V2] Failed to delete points:', error) - Toast.error('Failed to delete points. Please try again.') + if (controller) { + controller.open(null, null, this) + } else { + console.error('[Maps V2] Area creation controller not found') + Toast.error('Area creation controller not available') } } + + // Routes Manager methods + togglePoints(event) { return this.routesManager.togglePoints(event) } + toggleRoutes(event) { return this.routesManager.toggleRoutes(event) } + toggleHeatmap(event) { return this.routesManager.toggleHeatmap(event) } + toggleFog(event) { return this.routesManager.toggleFog(event) } + toggleScratch(event) { return this.routesManager.toggleScratch(event) } + togglePhotos(event) { return this.routesManager.togglePhotos(event) } + toggleAreas(event) { return this.routesManager.toggleAreas(event) } + toggleTracks(event) { return this.routesManager.toggleTracks(event) } + toggleSpeedColoredRoutes(event) { return this.routesManager.toggleSpeedColoredRoutes(event) } + openSpeedColorEditor() { return this.routesManager.openSpeedColorEditor() } + handleSpeedColorSave(event) { return this.routesManager.handleSpeedColorSave(event) } } diff --git a/app/javascript/controllers/maps_v2_controller_old.js b/app/javascript/controllers/maps_v2_controller_old.js new file mode 100644 index 00000000..c57fafc7 --- /dev/null +++ b/app/javascript/controllers/maps_v2_controller_old.js @@ -0,0 +1,1988 @@ +import { Controller } from '@hotwired/stimulus' +import maplibregl from 'maplibre-gl' +import { ApiClient } from 'maps_v2/services/api_client' +import { SettingsManager } from 'maps_v2/utils/settings_manager' +import { SearchManager } from 'maps_v2/utils/search_manager' +import { Toast } from 'maps_v2/components/toast' +import { performanceMonitor } from 'maps_v2/utils/performance_monitor' +import { CleanupHelper } from 'maps_v2/utils/cleanup_helper' +import { getMapStyle } from 'maps_v2/utils/style_manager' +import { LayerManager } from './maps_v2/layer_manager' +import { DataLoader } from './maps_v2/data_loader' +import { EventHandlers } from './maps_v2/event_handlers' +import { FilterManager } from './maps_v2/filter_manager' +import { DateManager } from './maps_v2/date_manager' +import { lazyLoader } from 'maps_v2/utils/lazy_loader' +import { SelectionLayer } from 'maps_v2/layers/selection_layer' +import { SelectedPointsLayer } from 'maps_v2/layers/selected_points_layer' +import { pointsToGeoJSON } from 'maps_v2/utils/geojson_transformers' +import { VisitCard } from 'maps_v2/components/visit_card' + +/** + * Main map controller for Maps V2 + * Coordinates between different managers and handles UI interactions + */ +export default class extends Controller { + static values = { + apiKey: String, + startDate: String, + endDate: String + } + + static targets = [ + 'container', + 'loading', + 'loadingText', + 'monthSelect', + 'clusterToggle', + 'settingsPanel', + 'visitsSearch', + 'routeOpacityRange', + 'placesFilters', + 'enableAllPlaceTagsToggle', + 'fogRadiusValue', + 'fogThresholdValue', + 'metersBetweenValue', + 'minutesBetweenValue', + // Search + 'searchInput', + 'searchResults', + // Layer toggles + 'pointsToggle', + 'routesToggle', + 'heatmapToggle', + 'visitsToggle', + 'photosToggle', + 'areasToggle', + // 'tracksToggle', + 'placesToggle', + 'fogToggle', + 'scratchToggle', + // Speed-colored routes + 'routesOptions', + 'speedColoredToggle', + 'speedColorScaleContainer', + 'speedColorScaleInput', + // Area selection + 'selectAreaButton', + 'selectionActions', + 'deleteButtonText', + 'selectedVisitsContainer', + 'selectedVisitsBulkActions' + ] + + async connect() { + this.cleanup = new CleanupHelper() + + // Initialize settings manager with API key for backend sync + SettingsManager.initialize(this.apiKeyValue) + + // 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() + + // Initialize managers + this.layerManager = new LayerManager(this.map, this.settings, this.api) + this.dataLoader = new DataLoader(this.api, this.apiKeyValue) + this.eventHandlers = new EventHandlers(this.map) + this.filterManager = new FilterManager(this.dataLoader) + + // Initialize search manager + this.initializeSearch() + + // Listen for visit creation events + this.boundHandleVisitCreated = this.handleVisitCreated.bind(this) + this.cleanup.addEventListener(document, 'visit:created', this.boundHandleVisitCreated) + + // Listen for place creation events + this.boundHandlePlaceCreated = this.handlePlaceCreated.bind(this) + this.cleanup.addEventListener(document, 'place:created', this.boundHandlePlaceCreated) + + // Format initial dates from backend to match V1 API format + this.startDateValue = DateManager.formatDateForAPI(new Date(this.startDateValue)) + this.endDateValue = DateManager.formatDateForAPI(new Date(this.endDateValue)) + console.log('[Maps V2] Initial dates:', this.startDateValue, 'to', this.endDateValue) + + this.loadMapData() + } + + disconnect() { + this.searchManager?.destroy() + this.cleanup.cleanup() + this.map?.remove() + performanceMonitor.logReport() + } + + /** + * Load settings (sync from backend and localStorage) + */ + async loadSettings() { + this.settings = await SettingsManager.sync() + 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', + placesToggle: 'placesEnabled', + // tracksToggle: 'tracksEnabled', + fogToggle: 'fogEnabled', + scratchToggle: 'scratchEnabled', + speedColoredToggle: 'speedColoredRoutesEnabled' + } + + Object.entries(toggleMap).forEach(([targetName, settingKey]) => { + const target = `${targetName}Target` + if (this[target]) { + this[target].checked = this.settings[settingKey] + } + }) + + // Show/hide visits search based on initial toggle state + if (this.hasVisitsToggleTarget && this.hasVisitsSearchTarget) { + if (this.visitsToggleTarget.checked) { + this.visitsSearchTarget.style.display = 'block' + } else { + this.visitsSearchTarget.style.display = 'none' + } + } + + // Show/hide places filters based on initial toggle state + if (this.hasPlacesToggleTarget && this.hasPlacesFiltersTarget) { + if (this.placesToggleTarget.checked) { + this.placesFiltersTarget.style.display = 'block' + } else { + this.placesFiltersTarget.style.display = 'none' + } + } + + // 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 speed-colored routes settings + if (this.hasSpeedColorScaleInputTarget) { + const colorScale = this.settings.speedColorScale || '0:#00ff00|15:#00ffff|30:#ff00ff|50:#ffff00|100:#ff3300' + this.speedColorScaleInputTarget.value = colorScale + } + if (this.hasSpeedColorScaleContainerTarget && this.hasSpeedColoredToggleTarget) { + const isEnabled = this.speedColoredToggleTarget.checked + this.speedColorScaleContainerTarget.classList.toggle('hidden', !isEnabled) + } + + // 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 + */ + async initializeMap() { + // Get map style from local files (async) + const style = await getMapStyle(this.settings.mapStyle) + + this.map = new maplibregl.Map({ + container: this.containerTarget, + style: style, + center: [0, 0], + zoom: 2 + }) + + // Add navigation controls + this.map.addControl(new maplibregl.NavigationControl(), 'top-right') + } + + /** + * Initialize API client + */ + initializeAPI() { + this.api = new ApiClient(this.apiKeyValue) + } + + /** + * Initialize location search + */ + initializeSearch() { + if (!this.hasSearchInputTarget || !this.hasSearchResultsTarget) { + console.warn('[Maps V2] Search targets not found, search functionality disabled') + return + } + + this.searchManager = new SearchManager(this.map, this.apiKeyValue) + this.searchManager.initialize(this.searchInputTarget, this.searchResultsTarget) + + console.log('[Maps V2] Search manager initialized') + } + + /** + * Handle visit creation event - reload visits and update layer + */ + async handleVisitCreated(event) { + console.log('[Maps V2] Visit created, reloading visits...', event.detail) + + try { + // Fetch updated visits + const visits = await this.api.fetchVisits({ + start_at: this.startDateValue, + end_at: this.endDateValue + }) + + console.log('[Maps V2] Fetched visits:', visits.length) + + // Update FilterManager with all visits (for search functionality) + this.filterManager.setAllVisits(visits) + + // Convert to GeoJSON + const visitsGeoJSON = this.dataLoader.visitsToGeoJSON(visits) + + console.log('[Maps V2] Converted to GeoJSON:', visitsGeoJSON.features.length, 'features') + + // Get the visits layer and update it + const visitsLayer = this.layerManager.getLayer('visits') + if (visitsLayer) { + visitsLayer.update(visitsGeoJSON) + console.log('[Maps V2] Visits layer updated successfully') + } else { + console.warn('[Maps V2] Visits layer not found, cannot update') + } + } catch (error) { + console.error('[Maps V2] Failed to reload visits:', error) + } + } + + /** + * Handle place creation event - reload places and update layer + */ + async handlePlaceCreated(event) { + console.log('[Maps V2] Place created, reloading places...', event.detail) + + try { + // Get currently selected tag filters + const selectedTags = this.getSelectedPlaceTags() + + // Fetch updated places with filters + const places = await this.api.fetchPlaces({ + tag_ids: selectedTags + }) + + console.log('[Maps V2] Fetched places:', places.length) + + // Convert to GeoJSON + const placesGeoJSON = this.dataLoader.placesToGeoJSON(places) + + console.log('[Maps V2] Converted to GeoJSON:', placesGeoJSON.features.length, 'features') + + // Get the places layer and update it + const placesLayer = this.layerManager.getLayer('places') + if (placesLayer) { + placesLayer.update(placesGeoJSON) + console.log('[Maps V2] Places layer updated successfully') + } else { + console.warn('[Maps V2] Places layer not found, cannot update') + } + } catch (error) { + console.error('[Maps V2] Failed to reload places:', error) + } + } + + /** + * Start create visit mode + * Allows user to click on map to create a new visit + */ + startCreateVisit() { + console.log('[Maps V2] Starting create visit mode') + + // Close settings panel + if (this.hasSettingsPanelTarget && this.settingsPanelTarget.classList.contains('open')) { + this.toggleSettings() + } + + // Change cursor to crosshair + this.map.getCanvas().style.cursor = 'crosshair' + + // Show info message + Toast.info('Click on the map to place a visit') + + // Add map click listener + this.handleCreateVisitClick = (e) => { + const { lng, lat } = e.lngLat + this.openVisitCreationModal(lat, lng) + // Reset cursor + this.map.getCanvas().style.cursor = '' + } + + this.map.once('click', this.handleCreateVisitClick) + } + + /** + * Open visit creation modal + */ + openVisitCreationModal(lat, lng) { + console.log('[Maps V2] Opening visit creation modal', { lat, lng }) + + // Find the visit creation controller + const modalElement = document.querySelector('[data-controller="visit-creation-v2"]') + + if (!modalElement) { + console.error('[Maps V2] Visit creation modal not found') + Toast.error('Visit creation modal not available') + return + } + + // Get the controller instance + const controller = this.application.getControllerForElementAndIdentifier( + modalElement, + 'visit-creation-v2' + ) + + if (controller) { + controller.open(lat, lng, this) + } else { + console.error('[Maps V2] Visit creation controller not found') + Toast.error('Visit creation controller not available') + } + } + + /** + * Start create place mode + * Allows user to click on map to create a new place + */ + startCreatePlace() { + console.log('[Maps V2] Starting create place mode') + + // Close settings panel + if (this.hasSettingsPanelTarget && this.settingsPanelTarget.classList.contains('open')) { + this.toggleSettings() + } + + // Change cursor to crosshair + this.map.getCanvas().style.cursor = 'crosshair' + + // Show info message + Toast.info('Click on the map to place a place') + + // Add map click listener + this.handleCreatePlaceClick = (e) => { + const { lng, lat } = e.lngLat + + // Dispatch event for place creation modal (reuse existing controller) + document.dispatchEvent(new CustomEvent('place:create', { + detail: { latitude: lat, longitude: lng } + })) + + // Reset cursor + this.map.getCanvas().style.cursor = '' + } + + this.map.once('click', this.handleCreatePlaceClick) + } + + /** + * Load map data from API + * @param {Object} options - { showLoading, fitBounds, showToast } + */ + async loadMapData(options = {}) { + const { + showLoading = true, + fitBounds = true, + showToast = true + } = options + + performanceMonitor.mark('load-map-data') + + if (showLoading) { + this.showLoading() + } + + try { + // Fetch all map data + const data = await this.dataLoader.fetchMapData( + this.startDateValue, + this.endDateValue, + showLoading ? this.updateLoadingProgress.bind(this) : null + ) + + // Store visits for filtering + this.filterManager.setAllVisits(data.visits) + + // Add all layers when style is ready + const addAllLayers = async () => { + await this.layerManager.addAllLayers( + data.pointsGeoJSON, + data.routesGeoJSON, + data.visitsGeoJSON, + data.photosGeoJSON, + data.areasGeoJSON, + data.tracksGeoJSON, + data.placesGeoJSON + ) + + // Setup event handlers + this.layerManager.setupLayerEventHandlers({ + handlePointClick: this.eventHandlers.handlePointClick.bind(this.eventHandlers), + handleVisitClick: this.eventHandlers.handleVisitClick.bind(this.eventHandlers), + handlePhotoClick: this.eventHandlers.handlePhotoClick.bind(this.eventHandlers), + handlePlaceClick: this.eventHandlers.handlePlaceClick.bind(this.eventHandlers) + }) + } + + // Use 'load' event which fires when map is fully initialized + if (this.map.loaded()) { + await addAllLayers() + } else { + this.map.once('load', async () => { + await addAllLayers() + }) + } + + // Fit map to data bounds (optional) + if (fitBounds && data.points.length > 0) { + this.fitMapToBounds(data.pointsGeoJSON) + } + + // Show success toast (optional) + if (showToast) { + Toast.success(`Loaded ${data.points.length} location ${data.points.length === 1 ? 'point' : 'points'}`) + } + + } catch (error) { + console.error('Failed to load map data:', error) + Toast.error('Failed to load location data. Please try again.') + } finally { + if (showLoading) { + this.hideLoading() + } + const duration = performanceMonitor.measure('load-map-data') + console.log(`[Performance] Map data loaded in ${duration}ms`) + } + } + + /** + * Fit map to data bounds + */ + fitMapToBounds(geojson) { + const coordinates = geojson.features.map(f => f.geometry.coordinates) + + const bounds = coordinates.reduce((bounds, coord) => { + return bounds.extend(coord) + }, new maplibregl.LngLatBounds(coordinates[0], coordinates[0])) + + this.map.fitBounds(bounds, { + padding: 50, + maxZoom: 15 + }) + } + + /** + * Month selector changed + */ + monthChanged(event) { + const { startDate, endDate } = DateManager.parseMonthSelector(event.target.value) + this.startDateValue = startDate + this.endDateValue = endDate + + console.log('[Maps V2] Date range changed:', this.startDateValue, 'to', this.endDateValue) + + // Reload data + this.loadMapData() + } + + /** + * Show loading indicator + */ + showLoading() { + this.loadingTarget.classList.remove('hidden') + } + + /** + * Hide loading indicator + */ + hideLoading() { + this.loadingTarget.classList.add('hidden') + } + + /** + * Update loading progress + */ + updateLoadingProgress({ loaded, totalPages, progress }) { + if (this.hasLoadingTextTarget) { + const percentage = Math.round(progress * 100) + this.loadingTextTarget.textContent = `Loading... ${percentage}%` + } + } + + /** + * Toggle layer visibility + */ + toggleLayer(event) { + 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 (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 points layer visibility + */ + togglePoints(event) { + const element = event.currentTarget + const visible = element.checked + + const pointsLayer = this.layerManager.getLayer('points') + if (pointsLayer) { + pointsLayer.toggle(visible) + } + + // Save setting + 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) + } + + // Show/hide routes options panel + if (this.hasRoutesOptionsTarget) { + this.routesOptionsTarget.style.display = visible ? 'block' : 'none' + } + + // Save setting + SettingsManager.updateSetting('routesVisible', visible) + } + + /** + * Toggle settings panel + */ + toggleSettings() { + if (this.hasSettingsPanelTarget) { + this.settingsPanelTarget.classList.toggle('open') + } + } + + /** + * Update map style from settings + */ + async updateMapStyle(event) { + const styleName = event.target.value + SettingsManager.updateSetting('mapStyle', styleName) + + const style = await getMapStyle(styleName) + + // Clear layer references + this.layerManager.clearLayerReferences() + + this.map.setStyle(style) + + // Reload layers after style change + this.map.once('style.load', () => { + console.log('Style loaded, reloading map data') + this.loadMapData() + }) + } + + /** + * Toggle heatmap visibility + */ + toggleHeatmap(event) { + const enabled = event.target.checked + SettingsManager.updateSetting('heatmapEnabled', enabled) + + const heatmapLayer = this.layerManager.getLayer('heatmap') + if (heatmapLayer) { + if (enabled) { + heatmapLayer.show() + } else { + heatmapLayer.hide() + } + } + } + + /** + * Reset settings to defaults + */ + resetSettings() { + if (confirm('Reset all settings to defaults? This will reload the page.')) { + SettingsManager.resetToDefaults() + window.location.reload() + } + } + + /** + * 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 + */ + toggleVisits(event) { + const enabled = event.target.checked + SettingsManager.updateSetting('visitsEnabled', enabled) + + const visitsLayer = this.layerManager.getLayer('visits') + if (visitsLayer) { + if (enabled) { + visitsLayer.show() + // Show visits search + if (this.hasVisitsSearchTarget) { + this.visitsSearchTarget.style.display = 'block' + } + } else { + visitsLayer.hide() + // Hide visits search + if (this.hasVisitsSearchTarget) { + this.visitsSearchTarget.style.display = 'none' + } + } + } + } + + /** + * Toggle places layer + */ + togglePlaces(event) { + const enabled = event.target.checked + SettingsManager.updateSetting('placesEnabled', enabled) + + const placesLayer = this.layerManager.getLayer('places') + if (placesLayer) { + if (enabled) { + placesLayer.show() + // Show places filters + if (this.hasPlacesFiltersTarget) { + this.placesFiltersTarget.style.display = 'block' + } + + // Initialize tag filters: enable all tags if no saved selection exists + this.initializePlaceTagFilters() + } else { + placesLayer.hide() + // Hide places filters + if (this.hasPlacesFiltersTarget) { + this.placesFiltersTarget.style.display = 'none' + } + } + } + } + + /** + * Initialize place tag filters (enable all by default or restore saved state) + */ + initializePlaceTagFilters() { + const savedFilters = this.settings.placesTagFilters + + if (savedFilters && savedFilters.length > 0) { + // Restore saved tag selection + this.restoreSavedTagFilters(savedFilters) + } else { + // Default: enable all tags + this.enableAllTagsInitial() + } + } + + /** + * Restore saved tag filters + */ + restoreSavedTagFilters(savedFilters) { + const tagCheckboxes = document.querySelectorAll('input[name="place_tag_ids[]"]') + + tagCheckboxes.forEach(checkbox => { + const value = checkbox.value === 'untagged' ? checkbox.value : parseInt(checkbox.value) + const shouldBeChecked = savedFilters.includes(value) + + if (checkbox.checked !== shouldBeChecked) { + checkbox.checked = shouldBeChecked + + // Update badge styling + const badge = checkbox.nextElementSibling + const color = badge.style.borderColor + + if (shouldBeChecked) { + badge.classList.remove('badge-outline') + badge.style.backgroundColor = color + badge.style.color = 'white' + } else { + badge.classList.add('badge-outline') + badge.style.backgroundColor = 'transparent' + badge.style.color = color + } + } + }) + + // Sync "Enable All Tags" toggle + this.syncEnableAllTagsToggle() + + // Load places with restored filters + this.loadPlacesWithTags(savedFilters) + } + + /** + * Enable all tags initially + */ + enableAllTagsInitial() { + if (this.hasEnableAllPlaceTagsToggleTarget) { + this.enableAllPlaceTagsToggleTarget.checked = true + } + + const tagCheckboxes = document.querySelectorAll('input[name="place_tag_ids[]"]') + const allTagIds = [] + + tagCheckboxes.forEach(checkbox => { + checkbox.checked = true + + // Update badge styling + const badge = checkbox.nextElementSibling + const color = badge.style.borderColor + badge.classList.remove('badge-outline') + badge.style.backgroundColor = color + badge.style.color = 'white' + + // Collect tag IDs + const value = checkbox.value === 'untagged' ? checkbox.value : parseInt(checkbox.value) + allTagIds.push(value) + }) + + // Save to settings + SettingsManager.updateSetting('placesTagFilters', allTagIds) + + // Load places with all tags + this.loadPlacesWithTags(allTagIds) + } + + /** + * Get selected place tag IDs + */ + getSelectedPlaceTags() { + return Array.from( + document.querySelectorAll('input[name="place_tag_ids[]"]:checked') + ).map(cb => { + const value = cb.value + // Keep "untagged" as string, convert others to integers + return value === 'untagged' ? value : parseInt(value) + }) + } + + /** + * Filter places by selected tags + */ + filterPlacesByTags(event) { + // Update badge styles + const badge = event.target.nextElementSibling + const color = badge.style.borderColor + + if (event.target.checked) { + badge.classList.remove('badge-outline') + badge.style.backgroundColor = color + badge.style.color = 'white' + } else { + badge.classList.add('badge-outline') + badge.style.backgroundColor = 'transparent' + badge.style.color = color + } + + // Sync "Enable All Tags" toggle state + this.syncEnableAllTagsToggle() + + // Get all checked tag checkboxes + const checkedTags = this.getSelectedPlaceTags() + + // Save selection to settings + SettingsManager.updateSetting('placesTagFilters', checkedTags) + + // Reload places with selected tags (empty array = show NO places) + this.loadPlacesWithTags(checkedTags) + } + + /** + * Sync "Enable All Tags" toggle with individual tag states + */ + syncEnableAllTagsToggle() { + if (!this.hasEnableAllPlaceTagsToggleTarget) return + + const tagCheckboxes = document.querySelectorAll('input[name="place_tag_ids[]"]') + const allChecked = Array.from(tagCheckboxes).every(cb => cb.checked) + const noneChecked = Array.from(tagCheckboxes).every(cb => !cb.checked) + + // Update toggle state without triggering change event + this.enableAllPlaceTagsToggleTarget.checked = allChecked + } + + /** + * Load places filtered by tags + */ + async loadPlacesWithTags(tagIds = []) { + try { + let places = [] + + if (tagIds.length > 0) { + // Fetch places with selected tags + places = await this.api.fetchPlaces({ tag_ids: tagIds }) + } + // If tagIds is empty, places remains empty array = show NO places + + const placesGeoJSON = this.dataLoader.placesToGeoJSON(places) + + const placesLayer = this.layerManager.getLayer('places') + if (placesLayer) { + placesLayer.update(placesGeoJSON) + } + } catch (error) { + console.error('[Maps V2] Failed to load places:', error) + } + } + + /** + * Toggle all place tags on/off + */ + toggleAllPlaceTags(event) { + const enableAll = event.target.checked + const tagCheckboxes = document.querySelectorAll('input[name="place_tag_ids[]"]') + + tagCheckboxes.forEach(checkbox => { + if (checkbox.checked !== enableAll) { + checkbox.checked = enableAll + + // Update badge styling + const badge = checkbox.nextElementSibling + const color = badge.style.borderColor + + if (enableAll) { + badge.classList.remove('badge-outline') + badge.style.backgroundColor = color + badge.style.color = 'white' + } else { + badge.classList.add('badge-outline') + badge.style.backgroundColor = 'transparent' + badge.style.color = color + } + } + }) + + // Get selected tags + const selectedTags = this.getSelectedPlaceTags() + + // Save selection to settings + SettingsManager.updateSetting('placesTagFilters', selectedTags) + + // Reload places with selected tags + this.loadPlacesWithTags(selectedTags) + } + + /** + * Toggle photos layer + */ + togglePhotos(event) { + const enabled = event.target.checked + SettingsManager.updateSetting('photosEnabled', enabled) + + const photosLayer = this.layerManager.getLayer('photos') + if (photosLayer) { + if (enabled) { + photosLayer.show() + } else { + photosLayer.hide() + } + } + } + + /** + * Search visits + */ + searchVisits(event) { + const searchTerm = event.target.value.toLowerCase() + const visitsLayer = this.layerManager.getLayer('visits') + this.filterManager.filterAndUpdateVisits( + searchTerm, + this.filterManager.getCurrentVisitFilter(), + visitsLayer + ) + } + + /** + * Filter visits by status + */ + filterVisits(event) { + const filter = event.target.value + this.filterManager.setCurrentVisitFilter(filter) + const searchTerm = document.getElementById('visits-search')?.value.toLowerCase() || '' + const visitsLayer = this.layerManager.getLayer('visits') + this.filterManager.filterAndUpdateVisits(searchTerm, filter, visitsLayer) + } + + /** + * Toggle areas layer + */ + toggleAreas(event) { + const enabled = event.target.checked + SettingsManager.updateSetting('areasEnabled', enabled) + + const areasLayer = this.layerManager.getLayer('areas') + if (areasLayer) { + if (enabled) { + areasLayer.show() + } else { + areasLayer.hide() + } + } + } + + /** + * Toggle tracks layer + */ + toggleTracks(event) { + const enabled = event.target.checked + SettingsManager.updateSetting('tracksEnabled', enabled) + + const tracksLayer = this.layerManager.getLayer('tracks') + if (tracksLayer) { + if (enabled) { + tracksLayer.show() + } else { + tracksLayer.hide() + } + } + } + + /** + * Toggle fog of war layer + */ + toggleFog(event) { + const enabled = event.target.checked + SettingsManager.updateSetting('fogEnabled', enabled) + + const fogLayer = this.layerManager.getLayer('fog') + if (fogLayer) { + fogLayer.toggle(enabled) + } else { + console.warn('Fog layer not yet initialized') + } + } + + /** + * Toggle scratch map layer + */ + async toggleScratch(event) { + const enabled = event.target.checked + SettingsManager.updateSetting('scratchEnabled', enabled) + + try { + const scratchLayer = this.layerManager.getLayer('scratch') + if (!scratchLayer && enabled) { + // Lazy load scratch layer + const ScratchLayer = await lazyLoader.loadLayer('scratch') + const newScratchLayer = new ScratchLayer(this.map, { + visible: true, + apiClient: this.api + }) + const pointsLayer = this.layerManager.getLayer('points') + const pointsData = pointsLayer?.data || { type: 'FeatureCollection', features: [] } + await newScratchLayer.add(pointsData) + this.layerManager.layers.scratchLayer = newScratchLayer + } else if (scratchLayer) { + if (enabled) { + scratchLayer.show() + } else { + scratchLayer.hide() + } + } + } catch (error) { + console.error('Failed to toggle scratch layer:', error) + Toast.error('Failed to load scratch layer') + } + } + + /** + * Toggle speed-colored routes + */ + async toggleSpeedColoredRoutes(event) { + const enabled = event.target.checked + SettingsManager.updateSetting('speedColoredRoutesEnabled', enabled) + + // Show/hide color scale container + if (this.hasSpeedColorScaleContainerTarget) { + this.speedColorScaleContainerTarget.classList.toggle('hidden', !enabled) + } + + // Reload routes with speed colors + await this.reloadRoutes() + } + + /** + * Open speed color editor modal + */ + openSpeedColorEditor() { + const currentScale = this.speedColorScaleInputTarget.value || '0:#00ff00|15:#00ffff|30:#ff00ff|50:#ffff00|100:#ff3300' + + // Create modal if it doesn't exist + let modal = document.getElementById('speed-color-editor-modal') + if (!modal) { + modal = this.createSpeedColorEditorModal(currentScale) + document.body.appendChild(modal) + } else { + // Update existing modal with current scale + const controller = this.application.getControllerForElementAndIdentifier(modal, 'speed-color-editor') + if (controller) { + controller.colorStopsValue = currentScale + controller.loadColorStops() + } + } + + // Show modal + const checkbox = modal.querySelector('.modal-toggle') + if (checkbox) { + checkbox.checked = true + } + } + + /** + * Create speed color editor modal element + */ + createSpeedColorEditorModal(currentScale) { + const modal = document.createElement('div') + modal.id = 'speed-color-editor-modal' + modal.setAttribute('data-controller', 'speed-color-editor') + modal.setAttribute('data-speed-color-editor-color-stops-value', currentScale) + modal.setAttribute('data-action', 'speed-color-editor:save->maps-v2#handleSpeedColorSave') + + modal.innerHTML = ` + + + ` + + return modal + } + + /** + * Handle speed color save event from editor + */ + handleSpeedColorSave(event) { + const newScale = event.detail.colorStops + + // Save to settings + this.speedColorScaleInputTarget.value = newScale + SettingsManager.updateSetting('speedColorScale', newScale) + + // Reload routes if speed colors are enabled + if (this.speedColoredToggleTarget.checked) { + this.reloadRoutes() + } + } + + /** + * Reload routes layer + */ + async reloadRoutes() { + this.showLoading('Reloading routes...') + + try { + const pointsLayer = this.layerManager.getLayer('points') + const points = pointsLayer?.data?.features?.map(f => ({ + latitude: f.geometry.coordinates[1], + longitude: f.geometry.coordinates[0], + timestamp: f.properties.timestamp + })) || [] + + // Get route generation settings + const distanceThresholdMeters = this.settings.metersBetweenRoutes || 500 + const timeThresholdMinutes = this.settings.minutesBetweenRoutes || 60 + + // Import speed colors utility + const { calculateSpeed, getSpeedColor } = await import('maps_v2/utils/speed_colors') + + // Generate routes with speed coloring if enabled + const routesGeoJSON = await this.generateRoutesWithSpeedColors( + points, + { distanceThresholdMeters, timeThresholdMinutes }, + calculateSpeed, + getSpeedColor + ) + + // Update routes layer + this.layerManager.updateLayer('routes', routesGeoJSON) + + } catch (error) { + console.error('Failed to reload routes:', error) + Toast.error('Failed to reload routes') + } finally { + this.hideLoading() + } + } + + /** + * Generate routes with speed coloring + */ + async generateRoutesWithSpeedColors(points, options, calculateSpeed, getSpeedColor) { + const { RoutesLayer } = await import('maps_v2/layers/routes_layer') + const useSpeedColors = this.settings.speedColoredRoutesEnabled || false + const speedColorScale = this.settings.speedColorScale || '0:#00ff00|15:#00ffff|30:#ff00ff|50:#ffff00|100:#ff3300' + + // Use RoutesLayer static method to generate basic routes + const routesGeoJSON = RoutesLayer.pointsToRoutes(points, options) + + if (!useSpeedColors) { + return routesGeoJSON + } + + // Add speed colors to route segments + routesGeoJSON.features = routesGeoJSON.features.map((feature, index) => { + const segment = points.slice( + points.findIndex(p => p.timestamp === feature.properties.startTime), + points.findIndex(p => p.timestamp === feature.properties.endTime) + 1 + ) + + if (segment.length >= 2) { + const speed = calculateSpeed(segment[0], segment[segment.length - 1]) + const color = getSpeedColor(speed, useSpeedColors, speedColorScale) + feature.properties.speed = speed + feature.properties.color = color + } + + return feature + }) + + return routesGeoJSON + } + + /** + * Start area selection mode + */ + async startSelectArea() { + console.log('[Maps V2] Starting area selection mode') + + // Keep settings panel open during selection mode + // (Don't close it) + + // Initialize selection layer if not exists + if (!this.selectionLayer) { + this.selectionLayer = new SelectionLayer(this.map, { + visible: true, + onSelectionComplete: this.handleAreaSelected.bind(this) + }) + + // Add layer to map immediately (map is already loaded at this point) + this.selectionLayer.add({ + type: 'FeatureCollection', + features: [] + }) + + console.log('[Maps V2] Selection layer initialized') + } + + // Initialize selected points layer if not exists + if (!this.selectedPointsLayer) { + this.selectedPointsLayer = new SelectedPointsLayer(this.map, { + visible: true + }) + + // Add layer to map immediately (map is already loaded at this point) + this.selectedPointsLayer.add({ + type: 'FeatureCollection', + features: [] + }) + + console.log('[Maps V2] Selected points layer initialized') + } + + // Enable selection mode + this.selectionLayer.enableSelectionMode() + + // Update UI - replace Select Area button with Cancel Selection button + if (this.hasSelectAreaButtonTarget) { + this.selectAreaButtonTarget.innerHTML = ` + + + + + Cancel Selection + ` + // Change action to cancel + this.selectAreaButtonTarget.dataset.action = 'click->maps-v2#cancelAreaSelection' + } + + Toast.info('Draw a rectangle on the map to select points') + } + + /** + * Handle area selection completion + */ + async handleAreaSelected(bounds) { + console.log('[Maps V2] Area selected:', bounds) + + try { + // Fetch both points and visits within the selected area + Toast.info('Fetching data in selected area...') + + const [points, visits] = await Promise.all([ + this.api.fetchPointsInArea({ + start_at: this.startDateValue, + end_at: this.endDateValue, + min_longitude: bounds.minLng, + max_longitude: bounds.maxLng, + min_latitude: bounds.minLat, + max_latitude: bounds.maxLat + }), + this.api.fetchVisitsInArea({ + start_at: this.startDateValue, + end_at: this.endDateValue, + sw_lat: bounds.minLat, + sw_lng: bounds.minLng, + ne_lat: bounds.maxLat, + ne_lng: bounds.maxLng + }) + ]) + + console.log('[Maps V2] Found', points.length, 'points and', visits.length, 'visits in area') + + if (points.length === 0 && visits.length === 0) { + Toast.info('No data found in selected area') + this.cancelAreaSelection() + return + } + + // Convert points to GeoJSON and display + if (points.length > 0) { + const geojson = pointsToGeoJSON(points) + this.selectedPointsLayer.updateSelectedPoints(geojson) + this.selectedPointsLayer.show() + } + + // Display visits in side panel and on map + if (visits.length > 0) { + this.displaySelectedVisits(visits) + } + + // Update UI - show action buttons + if (this.hasSelectionActionsTarget) { + this.selectionActionsTarget.classList.remove('hidden') + } + + // Update delete button text with count + if (this.hasDeleteButtonTextTarget) { + this.deleteButtonTextTarget.textContent = `Delete ${points.length} Point${points.length === 1 ? '' : 's'}` + } + + // Disable selection mode + this.selectionLayer.disableSelectionMode() + + const messages = [] + if (points.length > 0) messages.push(`${points.length} point${points.length === 1 ? '' : 's'}`) + if (visits.length > 0) messages.push(`${visits.length} visit${visits.length === 1 ? '' : 's'}`) + + Toast.success(`Selected ${messages.join(' and ')}`) + } catch (error) { + console.error('[Maps V2] Failed to fetch data in area:', error) + Toast.error('Failed to fetch data in selected area') + this.cancelAreaSelection() + } + } + + /** + * Display selected visits in side panel + */ + displaySelectedVisits(visits) { + if (!this.hasSelectedVisitsContainerTarget) return + + // Store visits for later use + this.selectedVisits = visits + this.selectedVisitIds = new Set() + + // Generate HTML for all visit cards + const cardsHTML = visits.map(visit => + VisitCard.create(visit, { + isSelected: false + }) + ).join('') + + // Update container + this.selectedVisitsContainerTarget.innerHTML = ` +
+
+ + + + +

Visits in Area (${visits.length})

+
+ ${cardsHTML} +
+ ` + + // Show container + this.selectedVisitsContainerTarget.classList.remove('hidden') + + // Attach event listeners + this.attachVisitCardListeners() + + // Update bulk actions after DOM updates (removes them if no visits selected) + requestAnimationFrame(() => { + this.updateBulkActions() + }) + } + + /** + * Attach event listeners to visit cards + */ + attachVisitCardListeners() { + // Checkbox selection + this.element.querySelectorAll('[data-visit-select]').forEach(checkbox => { + checkbox.addEventListener('change', (e) => { + const visitId = parseInt(e.target.dataset.visitSelect) + if (e.target.checked) { + this.selectedVisitIds.add(visitId) + } else { + this.selectedVisitIds.delete(visitId) + } + this.updateBulkActions() + }) + }) + + // Confirm button + this.element.querySelectorAll('[data-visit-confirm]').forEach(btn => { + btn.addEventListener('click', async (e) => { + const button = e.currentTarget + const visitId = parseInt(button.dataset.visitConfirm) + await this.confirmVisit(visitId) + }) + }) + + // Decline button + this.element.querySelectorAll('[data-visit-decline]').forEach(btn => { + btn.addEventListener('click', async (e) => { + const button = e.currentTarget + const visitId = parseInt(button.dataset.visitDecline) + await this.declineVisit(visitId) + }) + }) + } + + /** + * Update bulk action buttons visibility and attach listeners + */ + updateBulkActions() { + const selectedCount = this.selectedVisitIds.size + + // Remove any existing bulk action buttons from visit cards + const existingBulkActions = this.element.querySelectorAll('.bulk-actions-inline') + existingBulkActions.forEach(el => el.remove()) + + if (selectedCount >= 2) { + // Find the last (lowest) selected visit card + const selectedVisitCards = Array.from(this.element.querySelectorAll('.visit-card')) + .filter(card => { + const visitId = parseInt(card.dataset.visitId) + return this.selectedVisitIds.has(visitId) + }) + + if (selectedVisitCards.length > 0) { + const lastSelectedCard = selectedVisitCards[selectedVisitCards.length - 1] + + // Create bulk actions element + const bulkActionsDiv = document.createElement('div') + bulkActionsDiv.className = 'bulk-actions-inline mb-2' + bulkActionsDiv.innerHTML = ` +
+
+ + + + ${selectedCount} visit${selectedCount === 1 ? '' : 's'} selected +
+
+ + + +
+
+ ` + + // Insert after the last selected card + lastSelectedCard.insertAdjacentElement('afterend', bulkActionsDiv) + + // Attach listeners + const mergeBtn = bulkActionsDiv.querySelector('[data-bulk-merge]') + const confirmBtn = bulkActionsDiv.querySelector('[data-bulk-confirm]') + const declineBtn = bulkActionsDiv.querySelector('[data-bulk-decline]') + + if (mergeBtn) { + mergeBtn.addEventListener('click', () => this.bulkMergeVisits()) + } + if (confirmBtn) { + confirmBtn.addEventListener('click', () => this.bulkConfirmVisits()) + } + if (declineBtn) { + declineBtn.addEventListener('click', () => this.bulkDeclineVisits()) + } + } + } + } + + /** + * Confirm a single visit + */ + async confirmVisit(visitId) { + try { + await this.api.updateVisitStatus(visitId, 'confirmed') + Toast.success('Visit confirmed') + // Refresh the visit card + await this.refreshSelectedVisits() + } catch (error) { + console.error('[Maps V2] Failed to confirm visit:', error) + Toast.error('Failed to confirm visit') + } + } + + /** + * Decline a single visit + */ + async declineVisit(visitId) { + try { + await this.api.updateVisitStatus(visitId, 'declined') + Toast.success('Visit declined') + // Refresh the visit card + await this.refreshSelectedVisits() + } catch (error) { + console.error('[Maps V2] Failed to decline visit:', error) + Toast.error('Failed to decline visit') + } + } + + /** + * Bulk merge selected visits + */ + async bulkMergeVisits() { + const visitIds = Array.from(this.selectedVisitIds) + + if (visitIds.length < 2) { + Toast.error('Select at least 2 visits to merge') + return + } + + if (!confirm(`Merge ${visitIds.length} visits into one?`)) { + return + } + + try { + Toast.info('Merging visits...') + const mergedVisit = await this.api.mergeVisits(visitIds) + Toast.success('Visits merged successfully') + + // Clear selection state + this.selectedVisitIds.clear() + + // Remove the old visit cards and add the merged one + this.replaceVisitsWithMerged(visitIds, mergedVisit) + + // Update bulk actions (will remove the panel since selection is cleared) + this.updateBulkActions() + } catch (error) { + console.error('[Maps V2] Failed to merge visits:', error) + Toast.error('Failed to merge visits') + } + } + + /** + * Bulk confirm selected visits + */ + async bulkConfirmVisits() { + const visitIds = Array.from(this.selectedVisitIds) + + try { + Toast.info('Confirming visits...') + await this.api.bulkUpdateVisits(visitIds, 'confirmed') + Toast.success(`Confirmed ${visitIds.length} visits`) + + // Clear selection state before refreshing + this.selectedVisitIds.clear() + + await this.refreshSelectedVisits() + } catch (error) { + console.error('[Maps V2] Failed to confirm visits:', error) + Toast.error('Failed to confirm visits') + } + } + + /** + * Bulk decline selected visits + */ + async bulkDeclineVisits() { + const visitIds = Array.from(this.selectedVisitIds) + + if (!confirm(`Decline ${visitIds.length} visits?`)) { + return + } + + try { + Toast.info('Declining visits...') + await this.api.bulkUpdateVisits(visitIds, 'declined') + Toast.success(`Declined ${visitIds.length} visits`) + + // Clear selection state before refreshing + this.selectedVisitIds.clear() + + await this.refreshSelectedVisits() + } catch (error) { + console.error('[Maps V2] Failed to decline visits:', error) + Toast.error('Failed to decline visits') + } + } + + /** + * Replace merged visit cards with the new merged visit + */ + replaceVisitsWithMerged(oldVisitIds, mergedVisit) { + const container = this.element.querySelector('.selected-visits-list') + if (!container) return + + // Find the correct position to insert BEFORE removing old cards + const mergedStartTime = new Date(mergedVisit.started_at).getTime() + const allCards = Array.from(container.querySelectorAll('.visit-card')) + + let insertBeforeCard = null + for (const card of allCards) { + const cardId = parseInt(card.dataset.visitId) + + // Skip cards that we're about to remove + if (oldVisitIds.includes(cardId)) continue + + // Find the visit data for this card + const cardVisit = this.selectedVisits.find(v => v.id === cardId) + if (cardVisit) { + const cardStartTime = new Date(cardVisit.started_at).getTime() + if (cardStartTime > mergedStartTime) { + insertBeforeCard = card + break + } + } + } + + // Remove old visit cards from DOM + oldVisitIds.forEach(id => { + const card = this.element.querySelector(`.visit-card[data-visit-id="${id}"]`) + if (card) { + card.remove() + } + }) + + // Update the selectedVisits array and sort by started_at + this.selectedVisits = this.selectedVisits.filter(v => !oldVisitIds.includes(v.id)) + this.selectedVisits.push(mergedVisit) + this.selectedVisits.sort((a, b) => new Date(a.started_at) - new Date(b.started_at)) + + // Create new visit card HTML + const newCardHTML = VisitCard.create(mergedVisit, { isSelected: false }) + + // Insert the new card in the correct position + if (insertBeforeCard) { + insertBeforeCard.insertAdjacentHTML('beforebegin', newCardHTML) + } else { + // If no card starts after this one, append to the end + container.insertAdjacentHTML('beforeend', newCardHTML) + } + + // Update header count + const header = container.querySelector('h3') + if (header) { + header.textContent = `Visits in Area (${this.selectedVisits.length})` + } + + // Attach event listeners to the new card + this.attachVisitCardListeners() + } + + /** + * Refresh selected visits after changes + */ + async refreshSelectedVisits() { + // Re-fetch visits in the same area + const bounds = this.selectionLayer.currentRect + if (!bounds) return + + try { + const visits = await this.api.fetchVisitsInArea({ + start_at: this.startDateValue, + end_at: this.endDateValue, + sw_lat: bounds.start.lat < bounds.end.lat ? bounds.start.lat : bounds.end.lat, + sw_lng: bounds.start.lng < bounds.end.lng ? bounds.start.lng : bounds.end.lng, + ne_lat: bounds.start.lat > bounds.end.lat ? bounds.start.lat : bounds.end.lat, + ne_lng: bounds.start.lng > bounds.end.lng ? bounds.start.lng : bounds.end.lng + }) + + this.displaySelectedVisits(visits) + } catch (error) { + console.error('[Maps V2] Failed to refresh visits:', error) + } + } + + /** + * Cancel area selection + */ + cancelAreaSelection() { + console.log('[Maps V2] Cancelling area selection') + + // Clear selection layers + if (this.selectionLayer) { + this.selectionLayer.disableSelectionMode() + this.selectionLayer.clearSelection() + } + + if (this.selectedPointsLayer) { + this.selectedPointsLayer.clearSelection() + } + + // Clear visits + if (this.hasSelectedVisitsContainerTarget) { + this.selectedVisitsContainerTarget.classList.add('hidden') + this.selectedVisitsContainerTarget.innerHTML = '' + } + + if (this.hasSelectedVisitsBulkActionsTarget) { + this.selectedVisitsBulkActionsTarget.classList.add('hidden') + } + + // Clear stored data + this.selectedVisits = [] + this.selectedVisitIds = new Set() + + // Update UI - restore Select Area button + if (this.hasSelectAreaButtonTarget) { + this.selectAreaButtonTarget.innerHTML = ` + + + + + + + + Select Area + ` + this.selectAreaButtonTarget.classList.remove('btn-error') + this.selectAreaButtonTarget.classList.add('btn', 'btn-outline') + // Restore original action + this.selectAreaButtonTarget.dataset.action = 'click->maps-v2#startSelectArea' + } + + if (this.hasSelectionActionsTarget) { + this.selectionActionsTarget.classList.add('hidden') + } + + Toast.info('Selection cancelled') + } + + /** + * Delete selected points + */ + async deleteSelectedPoints() { + const pointCount = this.selectedPointsLayer.getCount() + const pointIds = this.selectedPointsLayer.getSelectedPointIds() + + if (pointIds.length === 0) { + Toast.error('No points selected') + return + } + + // Confirm deletion + const confirmed = confirm( + `Are you sure you want to delete ${pointCount} point${pointCount === 1 ? '' : 's'}? This action cannot be undone.` + ) + + if (!confirmed) { + return + } + + console.log('[Maps V2] Deleting', pointIds.length, 'points') + + try { + Toast.info('Deleting points...') + + // Call bulk delete API + const result = await this.api.bulkDeletePoints(pointIds) + + console.log('[Maps V2] Deleted', result.count, 'points') + + // Clear selection first + this.cancelAreaSelection() + + // Reload map data silently (no loading overlay, no camera movement, no success toast) + await this.loadMapData({ + showLoading: false, + fitBounds: false, + showToast: false + }) + + // Show success toast after reload + Toast.success(`Deleted ${result.count} point${result.count === 1 ? '' : 's'}`) + } catch (error) { + console.error('[Maps V2] Failed to delete points:', error) + Toast.error('Failed to delete points. Please try again.') + } + } +} diff --git a/app/views/maps_v2/_area_creation_modal.html.erb b/app/views/maps_v2/_area_creation_modal.html.erb new file mode 100644 index 00000000..2ad73c00 --- /dev/null +++ b/app/views/maps_v2/_area_creation_modal.html.erb @@ -0,0 +1,86 @@ +
+ +
diff --git a/app/views/maps_v2/_settings_panel.html.erb b/app/views/maps_v2/_settings_panel.html.erb index 768d15c9..2b009e07 100644 --- a/app/views/maps_v2/_settings_panel.html.erb +++ b/app/views/maps_v2/_settings_panel.html.erb @@ -92,7 +92,7 @@

- Search for a location to navigate on the map + Search for a location to find places you visited

@@ -367,7 +367,7 @@ max="100" step="10" value="100" - class="range range-primary range-sm" + class="range range-sm" data-maps-v2-target="routeOpacityRange" data-action="input->maps-v2#updateRouteOpacity" />
@@ -391,7 +391,7 @@ max="2000" step="5" value="1000" - class="range range-primary range-sm" + class="range range-sm" data-action="input->maps-v2#updateFogRadiusDisplay" />
5m @@ -412,7 +412,7 @@ max="10" step="1" value="1" - class="range range-primary range-sm" + class="range range-sm" data-action="input->maps-v2#updateFogThresholdDisplay" />
1 @@ -436,7 +436,7 @@ max="5000" step="100" value="500" - class="range range-primary range-sm" + class="range range-sm" data-action="input->maps-v2#updateMetersBetweenDisplay" />
100m @@ -457,7 +457,7 @@ max="180" step="1" value="60" - class="range range-primary range-sm" + class="range range-sm" data-action="input->maps-v2#updateMinutesBetweenDisplay" />
1min @@ -569,12 +569,20 @@ + + +
diff --git a/app/views/maps_v2/index.html.erb b/app/views/maps_v2/index.html.erb index d7ffd633..8072e8a4 100644 --- a/app/views/maps_v2/index.html.erb +++ b/app/views/maps_v2/index.html.erb @@ -2,10 +2,11 @@ <%= render 'shared/map/date_navigation_v2', start_at: @start_at, end_at: @end_at %> -
<%= render 'maps_v2/visit_creation_modal' %> + + <%= render 'maps_v2/area_creation_modal' %> + <%= render 'shared/place_creation_modal' %>