From 52ee90ac9cccd986ed4d97dcd30ab133067d8a2e Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Tue, 13 Aug 2024 18:25:48 +0200 Subject: [PATCH] Replace google places api with photon api by komoot --- .env.development | 2 +- CHANGELOG.md | 6 +- app/assets/builds/tailwind.css | 2 +- app/controllers/visits_controller.rb | 2 +- app/models/place.rb | 2 +- app/models/point.rb | 2 + app/models/visit.rb | 2 +- .../reverse_geocoding/places/fetch_data.rb | 94 ++++++++++++------- app/services/visits/suggest.rb | 20 ++-- app/views/visits/_buttons.html.erb | 4 +- app/views/visits/_modal.html.erb | 9 +- app/views/visits/_visit.html.erb | 2 +- config/initializers/00_constants.rb | 2 +- config/initializers/geocoder.rb | 11 +-- docker-compose.yml | 4 +- spec/models/place_spec.rb | 2 +- spec/requests/visits_spec.rb | 4 +- spec/services/visits/group_points_spec.rb | 24 ++++- spec/services/visits/group_spec.rb | 37 ++++++++ spec/services/visits/prepare_spec.rb | 38 ++++++++ spec/services/visits/suggest_spec.rb | 56 +++++++++++ 21 files changed, 255 insertions(+), 70 deletions(-) diff --git a/.env.development b/.env.development index 6ba8d6cb..fee9e3ce 100644 --- a/.env.development +++ b/.env.development @@ -4,4 +4,4 @@ DATABASE_PASSWORD=password DATABASE_NAME=dawarich_development DATABASE_PORT=5432 REDIS_URL=redis://localhost:6379/1 -GOOGLE_PLACES_API_KEY='' +PHOTON_API_HOST='photon.chibi.rodeo' diff --git a/CHANGELOG.md b/CHANGELOG.md index 4def5a86..407e7a5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ The visit suggestion release. 1. With this release deployment, data migration will work, starting visits suggestion process for all users. 2. After initial visit suggestion process, new suggestions will be calculated every 24 hours, based on points for last 7 days. -3. If you have enabled reverse geocoding and provided Google Places API key, Dawarich will try to reverse geocode your visit and suggest specific places you might have visited, such as cafes, restaurants, parks, etc. If reverse geocoding is not enabled, or Google Places API key is not provided, Dawarich will not try to suggest places but you'll be able to rename the visit yourself. +3. If you have enabled reverse geocoding and provided Photon Api Host, Dawarich will try to reverse geocode your visit and suggest specific places you might have visited, such as cafes, restaurants, parks, etc. If reverse geocoding is not enabled, or Photon Api Host is not provided, Dawarich will not try to suggest places but you'll be able to rename the visit yourself. 4. You can confirm or decline the visit suggestion. If you confirm the visit, it will be added to your timeline. If you decline the visit, it will be removed from your timeline. You'll be able to see all your confirmed, declined and suggested visits on the Visits page. - [x] Get places from Google Places API based on visit coordinates @@ -20,12 +20,12 @@ The visit suggestion release. - [x] Draw visit radius based on radius of points in the visit - [x] Add a possibility to rename the visit - [x] Make it look acceptable -- [ ] Create only uniq google places suggestions +- [ ] Make visits suggestion an idempotent process ### Added -- `GOOGLE_PLACES_API_KEY` environment variable to the `docker-compose.yml` file to allow user to set the Google Places API key for reverse geocoding +- `PHOTON_API_HOST` environment variable to the `docker-compose.yml` file to allow user to set the Photon API hpst for reverse geocoding - A "Map" button to each visit on the Visits page to allow user to see the visit on the map - Visits suggestion functionality. Read more on that in the release description - Tabs to the Visits page to allow user to switch between confirmed, declined and suggested visits diff --git a/app/assets/builds/tailwind.css b/app/assets/builds/tailwind.css index 7a6a13b1..45a69e85 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}.toggle{--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;flex-shrink:0;height:1.5rem;transition:background,box-shadow var(--animation-input,.2s) ease-out;width:3rem}.alert-info{--tw-text-opacity:1;--alert-bg:var(--fallback-in,oklch(var(--in)/1));--alert-bg-mix:var(--fallback-b1,oklch(var(--b1)/1));border-color:var(--fallback-in,oklch(var(--in)/.2));color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)))}.badge-primary{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;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-outline{--tw-border-opacity:0.5;background-color:initial;border-color:currentColor;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>* .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-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-info{--btn-color:var(--in)}.btn-success{--btn-color:var(--su)}.btn-warning{--btn-color:var(--wa)}.btn-error{--btn-color:var(--er)}}.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{--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;background-color:initial;border-color:#0000;border-width:1px;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);color:currentColor;outline-color:currentColor}.btn-ghost.btn-active{background-color:var(--fallback-bc,oklch(var(--bc)/.2));border-color:#0000}.btn-link.btn-active{background-color:initial;border-color:#0000;text-decoration-line:underline}.btn-outline.btn-active{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));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.btn-active{--tw-text-opacity:1;color:var(--fallback-sc,oklch(var(--sc)/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;--tw-bg-opacity:0.2;--tw-text-opacity:0.2;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}.btn:is(input[type=checkbox]:checked),.btn:is(input[type=radio]:checked){--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;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)))}.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{--tw-border-opacity:1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));border-width:1px}.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,#0000 65%,var(--chkbg) 65.99%),linear-gradient(45deg,#0000 75%,var(--chkbg) 75.99%),linear-gradient(-45deg,var(--chkbg) 40%,#0000 40.99%),linear-gradient(45deg,var(--chkbg) 30%,var(--chkfg) 30.99%,var(--chkfg) 40%,#0000 40.99%),linear-gradient(-45deg,var(--chkfg) 50%,var(--chkbg) 50.99%);background-repeat:no-repeat}.checkbox:indeterminate{animation:checkmark var(--animation-input,.2s) ease-out;background-image:linear-gradient(90deg,#0000 80%,var(--chkbg) 80%),linear-gradient(-90deg,#0000 80%,var(--chkbg) 80%),linear-gradient(0deg,var(--chkbg) 43%,var(--chkfg) 43%,var(--chkfg) 57%,var(--chkbg) 57%);background-repeat:no-repeat}.checkbox:disabled,.checkbox:indeterminate{--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)))}.checkbox:disabled{border-color:#0000;cursor:not-allowed;opacity:.2}@keyframes checkmark{0%{background-position-y:5px}50%{background-position-y:-2px}to{background-position-y:0}}.divider:not(:empty){gap:1rem}.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]{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:0.2;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));cursor:not-allowed}.file-input-disabled::-moz-placeholder,.file-input[disabled]::-moz-placeholder{--tw-placeholder-opacity:0.2;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)))}.file-input-disabled::placeholder,.file-input[disabled]::placeholder{--tw-placeholder-opacity:0.2;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)))}.file-input-disabled::file-selector-button,.file-input[disabled]::file-selector-button{--tw-border-opacity:0;--tw-bg-opacity:0.2;--tw-text-opacity:0.2;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}.label-text{--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));font-size:.875rem;line-height:1.25rem}.input input{--tw-bg-opacity:1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));background-color:initial}.input input:focus{outline:2px solid #0000;outline-offset:2px}.input[list]::-webkit-calendar-picker-indicator{line-height:1em}.input-bordered,.input:focus,.input:focus-within{border-color:var(--fallback-bc,oklch(var(--bc)/.2))}.input:focus,.input:focus-within{box-shadow:none;outline-color:var(--fallback-bc,oklch(var(--bc)/.2));outline-offset:2px;outline-style:solid;outline-width:2px}.input-ghost{--tw-bg-opacity:0.05}.input-ghost:focus,.input-ghost:focus-within{--tw-bg-opacity:1;--tw-text-opacity:1;box-shadow:none;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}.input-disabled,.input:disabled,.input[disabled]{--tw-border-opacity:1;--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));color:var(--fallback-bc,oklch(var(--bc)/.4));cursor:not-allowed}.input-disabled::-moz-placeholder,.input:disabled::-moz-placeholder,.input[disabled]::-moz-placeholder{--tw-placeholder-opacity:0.2;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)))}.input-disabled::placeholder,.input:disabled::placeholder,.input[disabled]::placeholder{--tw-placeholder-opacity:0.2;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)))}.input::-webkit-date-and-time-value{text-align:inherit}.link:focus{outline:2px solid #0000;outline-offset:2px}.link:focus-visible{outline:2px solid currentColor;outline-offset:2px}: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{--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));bottom:.75rem;content:"";inset-inline-start:0;opacity:.1;position:absolute;top:.75rem;width:1px}.menu :where(li:not(.menu-title)>:not(ul,details,.menu-title,.btn)),.menu :where(li:not(.menu-title)>details>summary:not(.menu-title)){text-wrap:balance;border-radius:var(--rounded-btn,.5rem);padding:.5rem 1rem;text-align:start;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{--tw-text-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/.1));color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));cursor:pointer;outline:2px solid #0000;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;--tw-text-opacity:1;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));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-code:before{border-radius:9999px;box-shadow:1.4em 0,2.8em 0,4.2em 0;content:"";display:block;height:.75rem;margin-bottom:1rem;opacity:.3;width:.75rem}.mockup-code pre{padding-right:1.25rem}.mockup-code pre:before{content:"";margin-right:2ch}.mockup-phone .display{border-radius:40px;margin-top:-25px;overflow:hidden}.mockup-browser .mockup-browser-toolbar .input{--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));direction:ltr;display:block;height:1.75rem;margin-left:auto;margin-right:auto;overflow:hidden;padding-left:2rem;position:relative;text-overflow:ellipsis;white-space:nowrap;width:24rem}.mockup-browser .mockup-browser-toolbar .input:before{--tw-translate-y:-50%;aspect-ratio:1/1;border-color:currentColor;border-radius:9999px;border-width:2px;height:.75rem;left:.5rem}.mockup-browser .mockup-browser-toolbar .input:after,.mockup-browser .mockup-browser-toolbar .input:before{content:"";opacity:.6;position:absolute;top: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))}.mockup-browser .mockup-browser-toolbar .input:after{--tw-translate-y:25%;--tw-rotate:-45deg;border-color:currentColor;border-radius:9999px;border-width:1px;height:.5rem;left:1.25rem}.modal::backdrop,.modal:not(dialog:not(.modal-open)){animation:modal-pop .2s ease-out;background-color:#0006}.modal-backdrop{align-self:stretch;color:#0000;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))}@keyframes modal-pop{0%{opacity:0}}.progress::-moz-progress-bar{--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));border-radius:var(--rounded-box,1rem)}.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%,#0000 10%,#0000 90%);background-position-x:15%;background-size:200%}.progress::-webkit-progress-bar{background-color:initial;border-radius:var(--rounded-box,1rem)}.progress::-webkit-progress-value{--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));border-radius:var(--rounded-box,1rem)}.progress:indeterminate::-moz-progress-bar{animation:progress-loading 5s ease-in-out infinite;background-color:initial;background-image:repeating-linear-gradient(90deg,var(--progress-color) -1%,var(--progress-color) 10%,#0000 10%,#0000 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;--tw-bg-opacity:1;--tw-text-opacity:1;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)))}.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}}@keyframes rating-pop{0%{transform:translateY(-.125em)}40%{transform:translateY(-.125em)}to{transform:translateY(0)}}.select:focus{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}.select-disabled,.select:disabled,.select[disabled]{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:0.2;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));cursor:not-allowed}.select-disabled::-moz-placeholder,.select:disabled::-moz-placeholder,.select[disabled]::-moz-placeholder{--tw-placeholder-opacity:0.2;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)))}.select-disabled::placeholder,.select:disabled::placeholder,.select[disabled]::placeholder{--tw-placeholder-opacity:0.2;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)))}.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}.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){--tw-border-opacity:1;--tw-text-opacity:1;border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)))}.tab:focus{outline:2px solid #0000;outline-offset:2px}.tab:focus-visible{outline:2px solid currentColor;outline-offset:-5px}.tab-disabled,.tab[disabled]{--tw-text-opacity:0.2;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));cursor:not-allowed}.tabs-bordered>.tab{--tw-border-opacity:0.2;border-bottom-width:calc(var(--tab-border, 1px) + 1px);border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));border-style:solid}.tabs-lifted>.tab{border:var(--tab-border,1px) solid #0000;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{--tab-grad:calc(69% - var(--tab-border, 1px));--radius-start:radial-gradient(circle at top left,#0000 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,#0000 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);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}.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}.tabs-lifted>.tab.tab-active:not(.tab-disabled):not([disabled]):last-child:before,.tabs-lifted>.tab:is(input:checked):last-child:before,[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-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,[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-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;--tw-text-opacity:1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));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 :where(thead,tbody) :where(tr:first-child:last-child),.table :where(thead,tbody) :where(tr:not(:last-child)){--tw-border-opacity:1;border-bottom-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));border-bottom-width:1px}.table :where(thead,tfoot){color:var(--fallback-bc,oklch(var(--bc)/.6));font-size:.75rem;font-weight:700;line-height:1rem;white-space:nowrap}.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{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));border-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-border-opacity)));border-radius:var(--rounded-box,1rem);border-width:1px;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);padding:.5rem 1rem}@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]{--handleoffsetcalculator:var(--handleoffset);--tw-text-opacity:1;background-image:none;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;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}.toggle:indeterminate,[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:disabled{--tw-border-opacity:1;--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;background-color:initial;border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));cursor:not-allowed;opacity:.3}.glass,.glass.btn-active{-webkit-backdrop-filter:blur(var(--glass-blur,40px));backdrop-filter:blur(var(--glass-blur,40px));background-color:initial;background-image:linear-gradient(135deg,rgb(255 255 255/var(--glass-opacity,30%)) 0,#0000 100%),linear-gradient(var(--glass-reflex-degree,100deg),rgb(255 255 255/var(--glass-reflex-opacity,10%)) 25%,#0000 25%);border:none;box-shadow:0 0 0 1px rgb(255 255 255/var(--glass-border-opacity,10%)) inset,0 0 0 2px #0000000d;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:initial;background-image:linear-gradient(135deg,rgb(255 255 255/var(--glass-opacity,30%)) 0,#0000 100%),linear-gradient(var(--glass-reflex-degree,100deg),rgb(255 255 255/var(--glass-reflex-opacity,10%)) 25%,#0000 25%);border:none;box-shadow:0 0 0 1px rgb(255 255 255/var(--glass-border-opacity,10%)) inset,0 0 0 2px #0000000d;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}.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-square:where(.btn-xs){height:1.5rem;padding:0;width:1.5rem}.btn-square:where(.btn-sm){height:2rem;padding:0;width:2rem}.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}[type=checkbox].checkbox-sm{height:1.25rem;width:1.25rem}.input-sm{font-size:.875rem;height:2rem;line-height:2rem;padding-left:.75rem;padding-right:.75rem}.menu-horizontal{display:inline-flex;flex-direction:row}.menu-horizontal>li:not(.menu-title)>details>ul{position:absolute}.stats-vertical{grid-auto-flow:row}.tabs-md :where(.tab){--tab-padding:1rem;font-size:.875rem;height:2rem;line-height:1.25rem;line-height:2}.tabs-lg :where(.tab){--tab-padding:1.25rem;font-size:1.125rem;height:3rem;line-height:1.75rem;line-height:2}.tabs-sm :where(.tab){--tab-padding:0.75rem;font-size:.875rem;height:1.5rem;line-height:.75rem}.tabs-xs :where(.tab){--tab-padding:0.5rem;font-size:.75rem;height:1.25rem;line-height:.75rem}.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)}.tooltip{--tooltip-offset:calc(100% + 1px + var(--tooltip-tail, 0px))}.tooltip:before{--tw-content:attr(data-tip);content:var(--tw-content);pointer-events:none;position:absolute;z-index:1}.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%)}.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}.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){--tw-bg-opacity:1;--tw-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a;--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));border-radius:var(--rounded-box,1rem);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){--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;max-width:none;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%}.modal-middle :where(.modal-box){--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);max-width:32rem;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:91.666667%}.modal-bottom :where(.modal-box){--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);max-width:none;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%}.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}.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{--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));display:inline-block;position:relative;text-align:center}.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:has(:focus-visible):after,.tooltip:has(:focus-visible):before,.tooltip:hover:after,.tooltip:hover: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) #0000 #0000 #0000;bottom:var(--tooltip-tail-offset);left:50%;right:auto;top:auto;transform:translateX(-50%)}.tooltip-bottom:after{border-color:#0000 #0000 var(--tooltip-color) #0000;bottom:auto;left:50%;right:auto;top:var(--tooltip-tail-offset);transform:translateX(-50%)}.visible{visibility:visible}.invisible{visibility:hidden}.static{position:static}.fixed{position:fixed}.relative{position:relative}.right-5{right:1.25rem}.top-5{top:1.25rem}.z-10{z-index:10}.z-\[1\]{z-index:1}.m-0{margin:0}.m-1{margin:.25rem}.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-5{margin-bottom:1.25rem;margin-top:1.25rem}.mb-10{margin-bottom:2.5rem}.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}.ml-2{margin-left:.5rem}.ml-4{margin-left:1rem}.mr-2{margin-right:.5rem}.mr-4{margin-right:1rem}.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}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-96{height:24rem}.h-\[25rem\]{height:25rem}.h-fit{height:-moz-fit-content;height:fit-content}.min-h-80{min-height:20rem}.min-h-screen{min-height:100vh}.w-1\/5{width:20%}.w-10\/12{width:83.333333%}.w-4\/5{width:80%}.w-5{width:1.25rem}.w-52{width:13rem}.w-6{width:1.5rem}.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-5xl{max-width:64rem}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.flex-shrink-0,.shrink-0{flex-shrink:0}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.gap-6{gap:1.5rem}.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-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-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)))}.justify-self-end{justify-self:end}.justify-self-center{justify-self:center}.overflow-x-auto{overflow-x:auto}.rounded-box{border-radius:var(--rounded-box,1rem)}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-t-none{border-top-left-radius:0;border-top-right-radius:0}.border-base-300{--tw-border-opacity:1;border-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-border-opacity)))}.border-blue-300{--tw-border-opacity:1;border-color:rgb(147 197 253/var(--tw-border-opacity))}.border-red-300{--tw-border-opacity:1;border-color:rgb(252 165 165/var(--tw-border-opacity))}.bg-base-100{--tw-bg-opacity:1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)))}.bg-base-200{--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)))}.bg-base-300{--tw-bg-opacity:1;background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)))}.bg-blue-100{--tw-bg-opacity:1;background-color:rgb(219 234 254/var(--tw-bg-opacity))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity))}.bg-blue-600{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.bg-gray-500{--tw-bg-opacity:1;background-color:rgb(107 114 128/var(--tw-bg-opacity))}.bg-green-50{--tw-bg-opacity:1;background-color:rgb(240 253 244/var(--tw-bg-opacity))}.bg-green-500{--tw-bg-opacity:1;background-color:rgb(34 197 94/var(--tw-bg-opacity))}.bg-neutral{--tw-bg-opacity:1;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity))}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity))}.bg-secondary-content{--tw-bg-opacity:1;background-color:var(--fallback-sc,oklch(var(--sc)/var(--tw-bg-opacity)))}.stroke-current{stroke:currentColor}.stroke-info{stroke:var(--fallback-in,oklch(var(--in)/1))}.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}.px-1{padding-left:.25rem;padding-right:.25rem}.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-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}.text-center{text-align:center}.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-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-semibold{font-weight:600}.normal-case{text-transform:none}.italic{font-style:italic}.text-base-content{--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}.text-blue-600{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity))}.text-blue-700{--tw-text-opacity:1;color:rgb(29 78 216/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-green-500{--tw-text-opacity:1;color:rgb(34 197 94/var(--tw-text-opacity))}.text-neutral-content{--tw-text-opacity:1;color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)))}.text-primary{--tw-text-opacity:1;color:var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity)))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity))}.text-secondary{--tw-text-opacity:1;color:var(--fallback-s,oklch(var(--s)/var(--tw-text-opacity)))}.text-success{--tw-text-opacity:1;color:var(--fallback-su,oklch(var(--su)/var(--tw-text-opacity)))}.text-warning{--tw-text-opacity:1;color:var(--fallback-wa,oklch(var(--wa)/var(--tw-text-opacity)))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.underline{text-decoration-line:underline}.decoration-dotted{text-decoration-style:dotted}.opacity-0{opacity:0}.shadow{--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--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 #00000040;--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-lg,.shadow-xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a;--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color)}.blur{--tw-blur:blur(8px)}.blur,.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-opacity{transition-duration:.15s;transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-300{transition-duration:.3s}@tailwind daisyui;@media (hover:hover){.hover\:btn-ghost:hover:hover{border-color:#0000}@supports (color:oklch(0 0 0)){.hover\:btn-ghost:hover:hover{background-color:var(--fallback-bc,oklch(var(--bc)/.2))}}}.hover\:btn-ghost:hover{--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;background-color:initial;border-color:#0000;border-width:1px;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);color:currentColor;outline-color:currentColor}.hover\:btn-ghost:hover.btn-active{background-color:var(--fallback-bc,oklch(var(--bc)/.2));border-color:#0000}.focus\:input-ghost:focus{--tw-bg-opacity:0.05}.focus\:input-ghost:focus:focus,.focus\:input-ghost:focus:focus-within{--tw-bg-opacity:1;--tw-text-opacity:1;box-shadow:none;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}@media not all and (min-width:768px){.max-md\:timeline-compact,.max-md\:timeline-compact + );position:relative}.timeline>li>hr{border-width:0;width:100%}:where(.timeline>li>hr):first-child{grid-column-start:1;grid-row-start:2}:where(.timeline>li>hr):last-child{grid-column-end:none;grid-column-start:3;grid-row-end:auto;grid-row-start:2}.timeline-start{align-self:flex-end;grid-column-end:4;grid-column-start:1;grid-row-end:2;grid-row-start:1;justify-self:center;margin:.25rem}.timeline-middle{grid-column-start:2;grid-row-start:2}.timeline-end{align-self:flex-start;grid-column-end:4;grid-column-start:1;grid-row-end:4;grid-row-start:3;justify-self:center;margin:.25rem}.toggle{--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;flex-shrink:0;height:1.5rem;transition:background,box-shadow var(--animation-input,.2s) ease-out;width:3rem}.alert-info{--tw-text-opacity:1;--alert-bg:var(--fallback-in,oklch(var(--in)/1));--alert-bg-mix:var(--fallback-b1,oklch(var(--b1)/1));border-color:var(--fallback-in,oklch(var(--in)/.2));color:var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity)))}.badge-primary{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;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-outline{--tw-border-opacity:0.5;background-color:initial;border-color:currentColor;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>* .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-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-info{--btn-color:var(--in)}.btn-success{--btn-color:var(--su)}.btn-warning{--btn-color:var(--wa)}.btn-error{--btn-color:var(--er)}}.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{--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;background-color:initial;border-color:#0000;border-width:1px;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);color:currentColor;outline-color:currentColor}.btn-ghost.btn-active{background-color:var(--fallback-bc,oklch(var(--bc)/.2));border-color:#0000}.btn-link.btn-active{background-color:initial;border-color:#0000;text-decoration-line:underline}.btn-outline.btn-active{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));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.btn-active{--tw-text-opacity:1;color:var(--fallback-sc,oklch(var(--sc)/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;--tw-bg-opacity:0.2;--tw-text-opacity:0.2;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}.btn:is(input[type=checkbox]:checked),.btn:is(input[type=radio]:checked){--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;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)))}.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{--tw-border-opacity:1;border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));border-width:1px}.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,#0000 65%,var(--chkbg) 65.99%),linear-gradient(45deg,#0000 75%,var(--chkbg) 75.99%),linear-gradient(-45deg,var(--chkbg) 40%,#0000 40.99%),linear-gradient(45deg,var(--chkbg) 30%,var(--chkfg) 30.99%,var(--chkfg) 40%,#0000 40.99%),linear-gradient(-45deg,var(--chkfg) 50%,var(--chkbg) 50.99%);background-repeat:no-repeat}.checkbox:indeterminate{animation:checkmark var(--animation-input,.2s) ease-out;background-image:linear-gradient(90deg,#0000 80%,var(--chkbg) 80%),linear-gradient(-90deg,#0000 80%,var(--chkbg) 80%),linear-gradient(0deg,var(--chkbg) 43%,var(--chkfg) 43%,var(--chkfg) 57%,var(--chkbg) 57%);background-repeat:no-repeat}.checkbox:disabled,.checkbox:indeterminate{--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)))}.checkbox:disabled{border-color:#0000;cursor:not-allowed;opacity:.2}@keyframes checkmark{0%{background-position-y:5px}50%{background-position-y:-2px}to{background-position-y:0}}.divider:not(:empty){gap:1rem}.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]{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:0.2;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));cursor:not-allowed}.file-input-disabled::-moz-placeholder,.file-input[disabled]::-moz-placeholder{--tw-placeholder-opacity:0.2;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)))}.file-input-disabled::placeholder,.file-input[disabled]::placeholder{--tw-placeholder-opacity:0.2;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)))}.file-input-disabled::file-selector-button,.file-input[disabled]::file-selector-button{--tw-border-opacity:0;--tw-bg-opacity:0.2;--tw-text-opacity:0.2;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}.label-text{--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));font-size:.875rem;line-height:1.25rem}.input input{--tw-bg-opacity:1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));background-color:initial}.input input:focus{outline:2px solid #0000;outline-offset:2px}.input[list]::-webkit-calendar-picker-indicator{line-height:1em}.input-bordered,.input:focus,.input:focus-within{border-color:var(--fallback-bc,oklch(var(--bc)/.2))}.input:focus,.input:focus-within{box-shadow:none;outline-color:var(--fallback-bc,oklch(var(--bc)/.2));outline-offset:2px;outline-style:solid;outline-width:2px}.input-ghost{--tw-bg-opacity:0.05}.input-ghost:focus,.input-ghost:focus-within{--tw-bg-opacity:1;--tw-text-opacity:1;box-shadow:none;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}.input-disabled,.input:disabled,.input[disabled]{--tw-border-opacity:1;--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));color:var(--fallback-bc,oklch(var(--bc)/.4));cursor:not-allowed}.input-disabled::-moz-placeholder,.input:disabled::-moz-placeholder,.input[disabled]::-moz-placeholder{--tw-placeholder-opacity:0.2;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)))}.input-disabled::placeholder,.input:disabled::placeholder,.input[disabled]::placeholder{--tw-placeholder-opacity:0.2;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)))}.input::-webkit-date-and-time-value{text-align:inherit}.link:focus{outline:2px solid #0000;outline-offset:2px}.link:focus-visible{outline:2px solid currentColor;outline-offset:2px}: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{--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));bottom:.75rem;content:"";inset-inline-start:0;opacity:.1;position:absolute;top:.75rem;width:1px}.menu :where(li:not(.menu-title)>:not(ul,details,.menu-title,.btn)),.menu :where(li:not(.menu-title)>details>summary:not(.menu-title)){text-wrap:balance;border-radius:var(--rounded-btn,.5rem);padding:.5rem 1rem;text-align:start;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{--tw-text-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/.1));color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));cursor:pointer;outline:2px solid #0000;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;--tw-text-opacity:1;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)));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-code:before{border-radius:9999px;box-shadow:1.4em 0,2.8em 0,4.2em 0;content:"";display:block;height:.75rem;margin-bottom:1rem;opacity:.3;width:.75rem}.mockup-code pre{padding-right:1.25rem}.mockup-code pre:before{content:"";margin-right:2ch}.mockup-phone .display{border-radius:40px;margin-top:-25px;overflow:hidden}.mockup-browser .mockup-browser-toolbar .input{--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));direction:ltr;display:block;height:1.75rem;margin-left:auto;margin-right:auto;overflow:hidden;padding-left:2rem;position:relative;text-overflow:ellipsis;white-space:nowrap;width:24rem}.mockup-browser .mockup-browser-toolbar .input:before{--tw-translate-y:-50%;aspect-ratio:1/1;border-color:currentColor;border-radius:9999px;border-width:2px;height:.75rem;left:.5rem}.mockup-browser .mockup-browser-toolbar .input:after,.mockup-browser .mockup-browser-toolbar .input:before{content:"";opacity:.6;position:absolute;top: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))}.mockup-browser .mockup-browser-toolbar .input:after{--tw-translate-y:25%;--tw-rotate:-45deg;border-color:currentColor;border-radius:9999px;border-width:1px;height:.5rem;left:1.25rem}.modal::backdrop,.modal:not(dialog:not(.modal-open)){animation:modal-pop .2s ease-out;background-color:#0006}.modal-backdrop{align-self:stretch;color:#0000;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))}@keyframes modal-pop{0%{opacity:0}}.progress::-moz-progress-bar{--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));border-radius:var(--rounded-box,1rem)}.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%,#0000 10%,#0000 90%);background-position-x:15%;background-size:200%}.progress::-webkit-progress-bar{background-color:initial;border-radius:var(--rounded-box,1rem)}.progress::-webkit-progress-value{--tw-bg-opacity:1;background-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity)));border-radius:var(--rounded-box,1rem)}.progress:indeterminate::-moz-progress-bar{animation:progress-loading 5s ease-in-out infinite;background-color:initial;background-image:repeating-linear-gradient(90deg,var(--progress-color) -1%,var(--progress-color) 10%,#0000 10%,#0000 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;--tw-bg-opacity:1;--tw-text-opacity:1;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)))}.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}}@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]{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:0.2;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));border-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));cursor:not-allowed}.select-disabled::-moz-placeholder,.select:disabled::-moz-placeholder,.select[disabled]::-moz-placeholder{--tw-placeholder-opacity:0.2;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)))}.select-disabled::placeholder,.select:disabled::placeholder,.select[disabled]::placeholder{--tw-placeholder-opacity:0.2;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity)))}.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}.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){--tw-border-opacity:1;--tw-text-opacity:1;border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)))}.tab:focus{outline:2px solid #0000;outline-offset:2px}.tab:focus-visible{outline:2px solid currentColor;outline-offset:-5px}.tab-disabled,.tab[disabled]{--tw-text-opacity:0.2;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));cursor:not-allowed}.tabs-bordered>.tab{--tw-border-opacity:0.2;border-bottom-width:calc(var(--tab-border, 1px) + 1px);border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));border-style:solid}.tabs-lifted>.tab{border:var(--tab-border,1px) solid #0000;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{--tab-grad:calc(69% - var(--tab-border, 1px));--radius-start:radial-gradient(circle at top left,#0000 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,#0000 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);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}.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}.tabs-lifted>.tab.tab-active:not(.tab-disabled):not([disabled]):last-child:before,.tabs-lifted>.tab:is(input:checked):last-child:before,[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-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,[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-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;--tw-text-opacity:1;background-color:var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));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 :where(thead,tbody) :where(tr:first-child:last-child),.table :where(thead,tbody) :where(tr:not(:last-child)){--tw-border-opacity:1;border-bottom-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));border-bottom-width:1px}.table :where(thead,tfoot){color:var(--fallback-bc,oklch(var(--bc)/.6));font-size:.75rem;font-weight:700;line-height:1rem;white-space:nowrap}.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{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));border-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-border-opacity)));border-radius:var(--rounded-box,1rem);border-width:1px;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);padding:.5rem 1rem}@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]{--handleoffsetcalculator:var(--handleoffset);--tw-text-opacity:1;background-image:none;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;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}.toggle:indeterminate,[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:disabled{--tw-border-opacity:1;--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;background-color:initial;border-color:var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity)));cursor:not-allowed;opacity:.3}.glass,.glass.btn-active{-webkit-backdrop-filter:blur(var(--glass-blur,40px));backdrop-filter:blur(var(--glass-blur,40px));background-color:initial;background-image:linear-gradient(135deg,rgb(255 255 255/var(--glass-opacity,30%)) 0,#0000 100%),linear-gradient(var(--glass-reflex-degree,100deg),rgb(255 255 255/var(--glass-reflex-opacity,10%)) 25%,#0000 25%);border:none;box-shadow:0 0 0 1px rgb(255 255 255/var(--glass-border-opacity,10%)) inset,0 0 0 2px #0000000d;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:initial;background-image:linear-gradient(135deg,rgb(255 255 255/var(--glass-opacity,30%)) 0,#0000 100%),linear-gradient(var(--glass-reflex-degree,100deg),rgb(255 255 255/var(--glass-reflex-opacity,10%)) 25%,#0000 25%);border:none;box-shadow:0 0 0 1px rgb(255 255 255/var(--glass-border-opacity,10%)) inset,0 0 0 2px #0000000d;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}.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-square:where(.btn-xs){height:1.5rem;padding:0;width:1.5rem}.btn-square:where(.btn-sm){height:2rem;padding:0;width:2rem}.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}[type=checkbox].checkbox-sm{height:1.25rem;width:1.25rem}.input-sm{font-size:.875rem;height:2rem;line-height:2rem;padding-left:.75rem;padding-right:.75rem}.menu-horizontal{display:inline-flex;flex-direction:row}.menu-horizontal>li:not(.menu-title)>details>ul{position:absolute}.stats-vertical{grid-auto-flow:row}.tabs-md :where(.tab){--tab-padding:1rem;font-size:.875rem;height:2rem;line-height:1.25rem;line-height:2}.tabs-lg :where(.tab){--tab-padding:1.25rem;font-size:1.125rem;height:3rem;line-height:1.75rem;line-height:2}.tabs-sm :where(.tab){--tab-padding:0.75rem;font-size:.875rem;height:1.5rem;line-height:.75rem}.tabs-xs :where(.tab){--tab-padding:0.5rem;font-size:.75rem;height:1.25rem;line-height:.75rem}.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)}.tooltip{--tooltip-offset:calc(100% + 1px + var(--tooltip-tail, 0px))}.tooltip:before{--tw-content:attr(data-tip);content:var(--tw-content);pointer-events:none;position:absolute;z-index:1}.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%)}.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}.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){--tw-bg-opacity:1;--tw-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a;--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));border-radius:var(--rounded-box,1rem);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){--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;max-width:none;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%}.modal-middle :where(.modal-box){--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);max-width:32rem;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:91.666667%}.modal-bottom :where(.modal-box){--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);max-width:none;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%}.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}.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{--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));display:inline-block;position:relative;text-align:center}.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:has(:focus-visible):after,.tooltip:has(:focus-visible):before,.tooltip:hover:after,.tooltip:hover: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) #0000 #0000 #0000;bottom:var(--tooltip-tail-offset);left:50%;right:auto;top:auto;transform:translateX(-50%)}.tooltip-bottom:after{border-color:#0000 #0000 var(--tooltip-color) #0000;bottom:auto;left:50%;right:auto;top:var(--tooltip-tail-offset);transform:translateX(-50%)}.visible{visibility:visible}.invisible{visibility:hidden}.static{position:static}.fixed{position:fixed}.relative{position:relative}.right-5{right:1.25rem}.top-5{top:1.25rem}.z-10{z-index:10}.z-\[1\]{z-index:1}.m-0{margin:0}.m-1{margin:.25rem}.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-5{margin-bottom:1.25rem;margin-top:1.25rem}.mb-10{margin-bottom:2.5rem}.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}.ml-2{margin-left:.5rem}.ml-4{margin-left:1rem}.mr-2{margin-right:.5rem}.mr-4{margin-right:1rem}.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}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-96{height:24rem}.h-\[25rem\]{height:25rem}.h-fit{height:-moz-fit-content;height:fit-content}.min-h-80{min-height:20rem}.min-h-screen{min-height:100vh}.w-1\/5{width:20%}.w-10\/12{width:83.333333%}.w-4\/5{width:80%}.w-5{width:1.25rem}.w-52{width:13rem}.w-6{width:1.5rem}.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-5xl{max-width:64rem}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.flex-shrink-0,.shrink-0{flex-shrink:0}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.gap-6{gap:1.5rem}.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-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-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)))}.justify-self-end{justify-self:end}.justify-self-center{justify-self:center}.overflow-x-auto{overflow-x:auto}.rounded-box{border-radius:var(--rounded-box,1rem)}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-t-none{border-top-left-radius:0;border-top-right-radius:0}.border-base-300{--tw-border-opacity:1;border-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-border-opacity)))}.border-blue-300{--tw-border-opacity:1;border-color:rgb(147 197 253/var(--tw-border-opacity))}.border-red-300{--tw-border-opacity:1;border-color:rgb(252 165 165/var(--tw-border-opacity))}.bg-base-100{--tw-bg-opacity:1;background-color:var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)))}.bg-base-200{--tw-bg-opacity:1;background-color:var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)))}.bg-base-300{--tw-bg-opacity:1;background-color:var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)))}.bg-blue-100{--tw-bg-opacity:1;background-color:rgb(219 234 254/var(--tw-bg-opacity))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity))}.bg-blue-600{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.bg-gray-500{--tw-bg-opacity:1;background-color:rgb(107 114 128/var(--tw-bg-opacity))}.bg-green-50{--tw-bg-opacity:1;background-color:rgb(240 253 244/var(--tw-bg-opacity))}.bg-green-500{--tw-bg-opacity:1;background-color:rgb(34 197 94/var(--tw-bg-opacity))}.bg-neutral{--tw-bg-opacity:1;background-color:var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity)))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity))}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity))}.bg-secondary-content{--tw-bg-opacity:1;background-color:var(--fallback-sc,oklch(var(--sc)/var(--tw-bg-opacity)))}.stroke-current{stroke:currentColor}.stroke-info{stroke:var(--fallback-in,oklch(var(--in)/1))}.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}.px-1{padding-left:.25rem;padding-right:.25rem}.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-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}.text-center{text-align:center}.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-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-semibold{font-weight:600}.normal-case{text-transform:none}.italic{font-style:italic}.text-base-content{--tw-text-opacity:1;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}.text-blue-600{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity))}.text-blue-700{--tw-text-opacity:1;color:rgb(29 78 216/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-green-500{--tw-text-opacity:1;color:rgb(34 197 94/var(--tw-text-opacity))}.text-neutral-content{--tw-text-opacity:1;color:var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity)))}.text-primary{--tw-text-opacity:1;color:var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity)))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity))}.text-secondary{--tw-text-opacity:1;color:var(--fallback-s,oklch(var(--s)/var(--tw-text-opacity)))}.text-success{--tw-text-opacity:1;color:var(--fallback-su,oklch(var(--su)/var(--tw-text-opacity)))}.text-warning{--tw-text-opacity:1;color:var(--fallback-wa,oklch(var(--wa)/var(--tw-text-opacity)))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.underline{text-decoration-line:underline}.decoration-dotted{text-decoration-style:dotted}.opacity-0{opacity:0}.shadow{--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--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 #00000040;--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-lg,.shadow-xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a;--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color)}.blur{--tw-blur:blur(8px)}.blur,.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-opacity{transition-duration:.15s;transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-300{transition-duration:.3s}.duration-200{transition-duration:.2s}@tailwind daisyui;@media (hover:hover){.hover\:btn-ghost:hover:hover{border-color:#0000}@supports (color:oklch(0 0 0)){.hover\:btn-ghost:hover:hover{background-color:var(--fallback-bc,oklch(var(--bc)/.2))}}}.hover\:btn-ghost:hover{--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;background-color:initial;border-color:#0000;border-width:1px;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);color:currentColor;outline-color:currentColor}.hover\:btn-ghost:hover.btn-active{background-color:var(--fallback-bc,oklch(var(--bc)/.2));border-color:#0000}.focus\:input-ghost:focus{--tw-bg-opacity:0.05}.focus\:input-ghost:focus:focus,.focus\:input-ghost:focus:focus-within{--tw-bg-opacity:1;--tw-text-opacity:1;box-shadow:none;color:var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)))}@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}}.hover\:cursor-pointer:hover{cursor:pointer}.hover\:no-underline:hover{text-decoration-line:none}.hover\:shadow-blue-500\/50:hover{--tw-shadow-color:#3b82f680;--tw-shadow:var(--tw-shadow-colored)}.group:hover .group-hover\:opacity-100{opacity:1}@media (min-width:640px){.sm\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:768px){.md\:w-2\/12{width:16.666667%}.md\:w-2\/3{width:66.666667%}.md\:w-2\/6{width:33.333333%}.md\:grid-cols-2{grid-template-columns:repeat(2,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\:flex{display:flex}.lg\:hidden{display:none}.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\:text-left{text-align:left}} \ No newline at end of file diff --git a/app/controllers/visits_controller.rb b/app/controllers/visits_controller.rb index 1baafdbf..19433fd1 100644 --- a/app/controllers/visits_controller.rb +++ b/app/controllers/visits_controller.rb @@ -22,7 +22,7 @@ class VisitsController < ApplicationController def update if @visit.update(visit_params) - redirect_to visits_url, notice: 'Visit was successfully updated.', status: :see_other + redirect_back(fallback_location: visits_path(status: :suggested)) else render :edit, status: :unprocessable_entity end diff --git a/app/models/place.rb b/app/models/place.rb index a0d96e01..e2b24658 100644 --- a/app/models/place.rb +++ b/app/models/place.rb @@ -10,7 +10,7 @@ class Place < ApplicationRecord has_many :place_visits, dependent: :destroy has_many :suggested_visits, through: :place_visits, source: :visit - enum source: { manual: 0, google_places: 1 } + enum source: { manual: 0, photon: 1 } def async_reverse_geocode return unless REVERSE_GEOCODING_ENABLED diff --git a/app/models/point.rb b/app/models/point.rb index 645961b6..64d9579c 100644 --- a/app/models/point.rb +++ b/app/models/point.rb @@ -19,6 +19,8 @@ class Point < ApplicationRecord scope :reverse_geocoded, -> { where.not(city: nil, country: nil) } scope :not_reverse_geocoded, -> { where(city: nil, country: nil) } + scope :visited, -> { where.not(visit_id: nil) } + scope :not_visited, -> { where(visit_id: nil) } after_create :async_reverse_geocode diff --git a/app/models/visit.rb b/app/models/visit.rb index f192d36c..4f254135 100644 --- a/app/models/visit.rb +++ b/app/models/visit.rb @@ -26,7 +26,7 @@ class Visit < ApplicationRecord radius = points.map { Geocoder::Calculations.distance_between(center, [_1.latitude, _1.longitude]) }.max - radius >= 15 ? radius : 15 + radius && radius >= 15 ? radius : 15 end def center diff --git a/app/services/reverse_geocoding/places/fetch_data.rb b/app/services/reverse_geocoding/places/fetch_data.rb index 8f9c5359..0ed4e236 100644 --- a/app/services/reverse_geocoding/places/fetch_data.rb +++ b/app/services/reverse_geocoding/places/fetch_data.rb @@ -1,55 +1,56 @@ # frozen_string_literal: true +# This class uses Komoot's Photon API class ReverseGeocoding::Places::FetchData attr_reader :place + IGNORED_OSM_VALUES = %w[house residential yes detached].freeze + IGNORED_OSM_KEYS = %w[highway railway].freeze + def initialize(place_id) @place = Place.find(place_id) end def call - if GOOGLE_PLACES_API_KEY.blank? - Rails.logger.warn('GOOGLE_PLACES_API_KEY is not set') - + if ::PHOTON_API_HOST.blank? + Rails.logger.warn('PHOTON_API_HOST is not set') return end - # return if place.reverse_geocoded? - - google_places = google_places_client.spots(place.latitude, place.longitude, radius: 10) - first_place = google_places.shift + first_place = reverse_geocoded_places.shift update_place(first_place) add_suggested_place_to_a_visit - google_places.each { |google_place| fetch_and_create_place(google_place) } + reverse_geocoded_places.each { |reverse_geocoded_place| fetch_and_create_place(reverse_geocoded_place) } end private - def google_places_client - @google_places_client ||= GooglePlaces::Client.new(GOOGLE_PLACES_API_KEY) - end + def update_place(reverse_geocoded_place) + return if reverse_geocoded_place.nil? + + data = reverse_geocoded_place.data - def update_place(google_place) place.update!( - name: google_place.name, - latitude: google_place.lat, - longitude: google_place.lng, - city: google_place.city, - country: google_place.country, - geodata: google_place.json_result_object, - source: :google_places, + name: place_name(data), + latitude: data['geometry']['coordinates'][1], + longitude: data['geometry']['coordinates'][0], + city: data['properties']['city'], + country: data['properties']['country'], + geodata: data, + source: Place.sources[:photon], reverse_geocoded_at: Time.current ) end - def fetch_and_create_place(google_place) - new_place = find_google_place(google_place) + def fetch_and_create_place(reverse_geocoded_place) + data = reverse_geocoded_place.data + new_place = find_place(data) - new_place.name = google_place.name - new_place.city = google_place.city - new_place.country = google_place.country - new_place.geodata = google_place.json_result_object - new_place.source = :google_places + new_place.name = place_name(data) + new_place.city = data['properties']['city'] + new_place.country = data['properties']['country'] + new_place.geodata = data + new_place.source = :photon new_place.save! @@ -61,22 +62,45 @@ class ReverseGeocoding::Places::FetchData end def add_suggested_place_to_a_visit(suggested_place: place) - # 1. Find all visits that are close to the place - # 2. Add the place to the visit as a suggestion visits = Place.near([suggested_place.latitude, suggested_place.longitude], 0.1).flat_map(&:visits) - # This is a very naive implementation, we should probably check if the place is already suggested - visits.each { |visit| visit.suggested_places << suggested_place } + visits.each do |visit| + next if visit.suggested_places.include?(suggested_place) + + visit.suggested_places << suggested_place + end end - def find_google_place(google_place) - place = Place.where("geodata ->> 'place_id' = ?", google_place['place_id']).first + def find_place(place_data) + found_place = Place.where( + "geodata->'properties'->>'osm_id' = ?", place_data['properties']['osm_id'].to_s + ).first - return place if place.present? + return found_place if found_place.present? Place.find_or_initialize_by( - latitude: google_place['geometry']['location']['lat'], - longitude: google_place['geometry']['location']['lng'] + latitude: place_data['geometry']['coordinates'][1].to_f.round(5), + longitude: place_data['geometry']['coordinates'][0].to_f.round(5) ) end + + def place_name(data) + name = data.dig('properties', 'name') + type = data.dig('properties', 'osm_value')&.capitalize&.gsub('_', ' ') + address = "#{data.dig('properties', 'postcode')} #{data.dig('properties', 'street')}" + address += " #{data.dig('properties', 'housenumber')}" if data.dig('properties', 'housenumber').present? + + name ||= address + + "#{name} (#{type})" + end + + def reverse_geocoded_places + data = Geocoder.search([place.latitude, place.longitude], limit: 10, distance_sort: true) + + data.reject do |place| + place.data['properties']['osm_value'].in?(IGNORED_OSM_VALUES) || + place.data['properties']['osm_key'].in?(IGNORED_OSM_KEYS) + end + end end diff --git a/app/services/visits/suggest.rb b/app/services/visits/suggest.rb index ddef6ab0..8ae5c911 100644 --- a/app/services/visits/suggest.rb +++ b/app/services/visits/suggest.rb @@ -6,7 +6,7 @@ class Visits::Suggest def initialize(user, start_at:, end_at:) @start_at = start_at.to_i @end_at = end_at.to_i - @points = user.tracked_points.order(timestamp: :asc).where(timestamp: start_at..end_at) + @points = user.tracked_points.not_visited.order(timestamp: :asc).where(timestamp: start_at..end_at) @user = user end @@ -18,9 +18,9 @@ class Visits::Suggest create_visits_notification(user) - return unless reverse_geocoding_enabled? + nil unless reverse_geocoding_enabled? - reverse_geocode(visits) + # reverse_geocode(visits) end private @@ -40,8 +40,7 @@ class Visits::Suggest search_params = { user_id: user.id, duration: visit_data[:duration], - started_at: Time.zone.at(visit_data[:points].first.timestamp), - ended_at: Time.zone.at(visit_data[:points].last.timestamp) + started_at: Time.zone.at(visit_data[:points].first.timestamp) } if visit_data[:area].present? @@ -52,6 +51,7 @@ class Visits::Suggest visit = Visit.find_or_initialize_by(search_params) visit.name = visit_data[:place]&.name || visit_data[:area]&.name if visit.name.blank? + visit.ended_at = Time.zone.at(visit_data[:points].last.timestamp) visit.save! visit_data[:points].each { |point| point.update!(visit_id: visit.id) } @@ -62,12 +62,12 @@ class Visits::Suggest end end - def reverse_geocode(places) - places.each(&:async_reverse_geocode) + def reverse_geocode(visits) + visits.each(&:async_reverse_geocode) end def reverse_geocoding_enabled? - ::REVERSE_GEOCODING_ENABLED && ::GOOGLE_PLACES_API_KEY.present? + ::REVERSE_GEOCODING_ENABLED && ::PHOTON_API_HOST.present? end def create_visits_notification(user) @@ -84,8 +84,8 @@ class Visits::Suggest def create_place(visit) place = Place.find_or_initialize_by( - latitude: visit[:latitude], - longitude: visit[:longitude] + latitude: visit[:latitude].to_f.round(5), + longitude: visit[:longitude].to_f.round(5) ) place.name = Place::DEFAULT_NAME diff --git a/app/views/visits/_buttons.html.erb b/app/views/visits/_buttons.html.erb index 777c819e..e258e450 100644 --- a/app/views/visits/_buttons.html.erb +++ b/app/views/visits/_buttons.html.erb @@ -1,2 +1,2 @@ -<%= button_to 'Confirm', visit_path(visit, 'visit[status]': :confirmed), method: :patch, data: { turbo: false }, class: 'btn btn-xs btn-success' %> -<%= button_to 'Decline', visit_path(visit, 'visit[status]': :declined), method: :patch, data: { turbo: false }, class: 'btn btn-xs btn-error mx-1' %> +<%= link_to 'Confirm', visit_path(visit, 'visit[status]': :confirmed), method: :patch, data: { turbo_method: :patch }, class: 'btn btn-xs btn-success' %> +<%= link_to 'Decline', visit_path(visit, 'visit[status]': :declined), method: :patch, data: { turbo_method: :patch }, class: 'btn btn-xs btn-error mx-1' %> diff --git a/app/views/visits/_modal.html.erb b/app/views/visits/_modal.html.erb index 5bc3b73d..a9102927 100644 --- a/app/views/visits/_modal.html.erb +++ b/app/views/visits/_modal.html.erb @@ -18,7 +18,14 @@ data-controller="visit-modal-places" data-id="<%= visit.id %>"> <% if visit.suggested_places.any? %> - <%= select_tag :place_id, options_for_select(visit.suggested_places.map { |place| [place.name, place.id] }, visit.suggested_places.first.id), class: 'w-full', data: { action: 'change->visit-modal-places#selectPlace' } %> + <%= select_tag :place_id, + options_for_select( + visit.suggested_places.map { |place| [place.name, place.id] }, + (visit.place_id || visit.suggested_places.first.id) + ), + class: 'w-full select select-bordered', + data: { action: 'change->visit-modal-places#selectPlace' } + %> <% end %> diff --git a/app/views/visits/_visit.html.erb b/app/views/visits/_visit.html.erb index 59d1a639..d1f08f37 100644 --- a/app/views/visits/_visit.html.erb +++ b/app/views/visits/_visit.html.erb @@ -4,7 +4,7 @@ <%= render 'visits/name', visit: visit %>
<%= "#{visit.started_at.strftime('%H:%M')} - #{visit.ended_at.strftime('%H:%M')}" %>
-
+
<%= render 'visits/buttons', visit: visit if visit.suggested? %> diff --git a/config/initializers/00_constants.rb b/config/initializers/00_constants.rb index 50994c44..7251d114 100644 --- a/config/initializers/00_constants.rb +++ b/config/initializers/00_constants.rb @@ -2,4 +2,4 @@ MIN_MINUTES_SPENT_IN_CITY = ENV.fetch('MIN_MINUTES_SPENT_IN_CITY', 60).to_i REVERSE_GEOCODING_ENABLED = ENV.fetch('REVERSE_GEOCODING_ENABLED', 'true') == 'true' -GOOGLE_PLACES_API_KEY = '' +PHOTON_API_HOST = ENV.fetch('PHOTON_API_HOST', nil) diff --git a/config/initializers/geocoder.rb b/config/initializers/geocoder.rb index 3494608e..75bae757 100644 --- a/config/initializers/geocoder.rb +++ b/config/initializers/geocoder.rb @@ -13,12 +13,11 @@ config = { expiration: 1.day # Defaults to `nil` # prefix: "another_key:" # Defaults to `geocoder:` }, - always_raise: :all + always_raise: :all, + + use_https: false, + lookup: :photon, + photon: { host: 'photon.chibi.rodeo' } } -if GOOGLE_PLACES_API_KEY.present? - config[:lookup] = :google - config[:api_key] = GOOGLE_PLACES_API_KEY -end - Geocoder.configure(config) diff --git a/docker-compose.yml b/docker-compose.yml index 2daf8212..03b0839f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -47,7 +47,7 @@ services: APPLICATION_HOSTS: localhost TIME_ZONE: Europe/London APPLICATION_PROTOCOL: http - GOOGLE_PLACES_API_KEY: '' + PHOTON_API_HOST: '' logging: driver: "json-file" options: @@ -80,7 +80,7 @@ services: APPLICATION_HOSTS: localhost BACKGROUND_PROCESSING_CONCURRENCY: 10 APPLICATION_PROTOCOL: http - GOOGLE_PLACES_API_KEY: '' + PHOTON_API_HOST: '' logging: driver: "json-file" options: diff --git a/spec/models/place_spec.rb b/spec/models/place_spec.rb index 89f02581..3a7bcd21 100644 --- a/spec/models/place_spec.rb +++ b/spec/models/place_spec.rb @@ -16,7 +16,7 @@ RSpec.describe Place, type: :model do end describe 'enums' do - it { is_expected.to define_enum_for(:source).with_values(%i[manual google_places]) } + it { is_expected.to define_enum_for(:source).with_values(%i[manual photon]) } end describe 'methods' do diff --git a/spec/requests/visits_spec.rb b/spec/requests/visits_spec.rb index dbb0a411..4e3b3dc2 100644 --- a/spec/requests/visits_spec.rb +++ b/spec/requests/visits_spec.rb @@ -107,10 +107,10 @@ RSpec.describe '/visits', type: :request do expect(visit.reload.status).to eq('declined') end - it 'redirects to the visit index page' do + it 'redirects to the visits index page' do patch visit_url(visit), params: { visit: { status: :confirmed } } - expect(response).to redirect_to(visits_url) + expect(response).to redirect_to(visits_url(status: :suggested)) end end end diff --git a/spec/services/visits/group_points_spec.rb b/spec/services/visits/group_points_spec.rb index dffd74ac..3dff1296 100644 --- a/spec/services/visits/group_points_spec.rb +++ b/spec/services/visits/group_points_spec.rb @@ -3,6 +3,28 @@ require 'rails_helper' RSpec.describe Visits::GroupPoints do - describe '#call' do + describe '#group_points_by_radius' do + it 'groups points by radius' do + day_points = [ + build(:point, latitude: 0, longitude: 0, timestamp: 1.day.ago), + build(:point, latitude: 0.00001, longitude: 0.00001, timestamp: 1.day.ago + 1.minute), + build(:point, latitude: 0.00002, longitude: 0.00002, timestamp: 1.day.ago + 2.minutes), + build(:point, latitude: 0.00003, longitude: 0.00003, timestamp: 1.day.ago + 3.minutes), + build(:point, latitude: 0.00004, longitude: 0.00004, timestamp: 1.day.ago + 4.minutes), + build(:point, latitude: 0.00005, longitude: 0.00005, timestamp: 1.day.ago + 5.minutes), + build(:point, latitude: 0.00006, longitude: 0.00006, timestamp: 1.day.ago + 6.minutes), + build(:point, latitude: 0.00007, longitude: 0.00007, timestamp: 1.day.ago + 7.minutes), + build(:point, latitude: 0.00008, longitude: 0.00008, timestamp: 1.day.ago + 8.minutes), + build(:point, latitude: 0.00009, longitude: 0.00009, timestamp: 1.day.ago + 9.minutes), + build(:point, latitude: 0.0001, longitude: 0.0009, timestamp: 1.day.ago + 9.minutes) + ] + + grouped_points = described_class.new(day_points).group_points_by_radius + + expect(grouped_points.size).to eq(1) + expect(grouped_points.first.size).to eq(10) + # The last point is too far from the first point + expect(grouped_points.first).not_to include(day_points.last) + end end end diff --git a/spec/services/visits/group_spec.rb b/spec/services/visits/group_spec.rb index 387fce67..b6e1c373 100644 --- a/spec/services/visits/group_spec.rb +++ b/spec/services/visits/group_spec.rb @@ -4,5 +4,42 @@ require 'rails_helper' RSpec.describe Visits::Group do describe '#call' do + let(:time_threshold_minutes) { 30 } + let(:merge_threshold_minutes) { 15 } + + subject(:group) do + described_class.new(time_threshold_minutes:, merge_threshold_minutes:) + end + + context 'when points are too far apart' do + it 'groups points into separate visits' do + points = [ + build(:point, latitude: 0, longitude: 0, timestamp: 1.day.ago), + build(:point, latitude: 0.00001, longitude: 0.00001, timestamp: 1.day.ago + 5.minutes), + build(:point, latitude: 0.00002, longitude: 0.00002, timestamp: 1.day.ago + 10.minutes), + build(:point, latitude: 0.00003, longitude: 0.00003, timestamp: 1.day.ago + 15.minutes), + build(:point, latitude: 0.00004, longitude: 0.00004, timestamp: 1.day.ago + 20.minutes), + build(:point, latitude: 0.00005, longitude: 0.00005, timestamp: 1.day.ago + 25.minutes), + build(:point, latitude: 0.00006, longitude: 0.00006, timestamp: 1.day.ago + 30.minutes), + build(:point, latitude: 0.00007, longitude: 0.00007, timestamp: 1.day.ago + 35.minutes), + build(:point, latitude: 0.00008, longitude: 0.00008, timestamp: 1.day.ago + 40.minutes), + build(:point, latitude: 0.00009, longitude: 0.00009, timestamp: 1.day.ago + 45.minutes), + build(:point, latitude: 0.0001, longitude: 0.0001, timestamp: 1.day.ago + 50.minutes), + build(:point, latitude: 0.00011, longitude: 0.00011, timestamp: 1.day.ago + 55.minutes), + build(:point, latitude: 0.00011, longitude: 0.00011, timestamp: 1.day.ago + 95.minutes), + build(:point, latitude: 0.00011, longitude: 0.00011, timestamp: 1.day.ago + 100.minutes), + build(:point, latitude: 0.00011, longitude: 0.00011, timestamp: 1.day.ago + 105.minutes) + ] + expect(group.call(points)).to \ + eq({ + "#{time_formatter(1.day.ago)} - #{time_formatter(1.day.ago + 55.minutes)}" => points[0..11], + "#{time_formatter(1.day.ago + 95.minutes)} - #{time_formatter(1.day.ago + 105.minutes)}" => points[12..-1] + }) + end + end + end + + def time_formatter(time) + Time.zone.at(time).strftime('%Y-%m-%d %H:%M') end end diff --git a/spec/services/visits/prepare_spec.rb b/spec/services/visits/prepare_spec.rb index 1dcbf905..f4dde1bb 100644 --- a/spec/services/visits/prepare_spec.rb +++ b/spec/services/visits/prepare_spec.rb @@ -4,5 +4,43 @@ require 'rails_helper' RSpec.describe Visits::Prepare do describe '#call' do + let(:points) do + [ + build(:point, latitude: 0, longitude: 0, timestamp: 1.day.ago), + build(:point, latitude: 0.00001, longitude: 0.00001, timestamp: 1.day.ago + 5.minutes), + build(:point, latitude: 0.00002, longitude: 0.00002, timestamp: 1.day.ago + 10.minutes), + build(:point, latitude: 0.00003, longitude: 0.00003, timestamp: 1.day.ago + 15.minutes), + build(:point, latitude: 0.00004, longitude: 0.00004, timestamp: 1.day.ago + 20.minutes), + build(:point, latitude: 0.00005, longitude: 0.00005, timestamp: 1.day.ago + 25.minutes), + build(:point, latitude: 0.00006, longitude: 0.00006, timestamp: 1.day.ago + 30.minutes), + build(:point, latitude: 0.00007, longitude: 0.00007, timestamp: 1.day.ago + 35.minutes), + build(:point, latitude: 0.00008, longitude: 0.00008, timestamp: 1.day.ago + 40.minutes), + build(:point, latitude: 0.00009, longitude: 0.00009, timestamp: 1.day.ago + 45.minutes), + build(:point, latitude: 0.0001, longitude: 0.0001, timestamp: 1.day.ago + 50.minutes), + build(:point, latitude: 0.00011, longitude: 0.00011, timestamp: 1.day.ago + 55.minutes), + build(:point, latitude: 0.00011, longitude: 0.00011, timestamp: 1.day.ago + 95.minutes), + build(:point, latitude: 0.00011, longitude: 0.00011, timestamp: 1.day.ago + 100.minutes), + build(:point, latitude: 0.00011, longitude: 0.00011, timestamp: 1.day.ago + 105.minutes) + ] + end + + subject { described_class.new(points).call } + + it 'returns correct visits' do + expect(subject).to eq [ + { + date: 1.day.ago.to_date.to_s, + visits: [ + { + latitude: 0.0, + longitude: 0.0, + radius: 10, + points:, + duration: 105 + } + ] + } + ] + end end end diff --git a/spec/services/visits/suggest_spec.rb b/spec/services/visits/suggest_spec.rb index d7998538..ec3387e4 100644 --- a/spec/services/visits/suggest_spec.rb +++ b/spec/services/visits/suggest_spec.rb @@ -4,5 +4,61 @@ require 'rails_helper' RSpec.describe Visits::Suggest do describe '#call' do + let!(:user) { create(:user) } + let(:start_at) { 1.week.ago } + let(:end_at) { Time.current } + + let!(:points) do + [ + create(:point, user:, timestamp: start_at), + create(:point, user:, timestamp: start_at + 5.minutes), + create(:point, user:, timestamp: start_at + 10.minutes), + create(:point, user:, timestamp: start_at + 15.minutes), + create(:point, user:, timestamp: start_at + 20.minutes), + create(:point, user:, timestamp: start_at + 25.minutes), + create(:point, user:, timestamp: start_at + 30.minutes), + create(:point, user:, timestamp: start_at + 35.minutes), + create(:point, user:, timestamp: start_at + 40.minutes), + create(:point, user:, timestamp: start_at + 45.minutes), + create(:point, user:, timestamp: start_at + 50.minutes), + create(:point, user:, timestamp: start_at + 55.minutes), + create(:point, user:, timestamp: start_at + 95.minutes), + create(:point, user:, timestamp: start_at + 100.minutes), + create(:point, user:, timestamp: start_at + 105.minutes) + ] + end + + subject { described_class.new(user, start_at:, end_at:).call } + + it 'creates places' do + expect { subject }.to change(Place, :count).by(1) + end + + it 'creates visits' do + expect { subject }.to change(Visit, :count).by(1) + end + + it 'creates visits notification' do + expect { subject }.to change(Notification, :count).by(1) + end + + it 'does not reverse geocodes visits' do + expect_any_instance_of(Visit).to_not receive(:async_reverse_geocode).and_call_original + + subject + end + + context 'when reverse geocoding is enabled' do + before do + stub_const('REVERSE_GEOCODING_ENABLED', true) + stub_const('PHOTON_API_HOST', 'http://localhost:2323') + end + + it 'reverse geocodes visits' do + expect_any_instance_of(Visit).to receive(:async_reverse_geocode).and_call_original + + subject + end + end end end