commit a6b849e36f6cd0ea024795887431b65bdb53a74c Author: KJ Date: Sun Sep 29 23:16:24 2024 -0400 First commit. diff --git a/config.php b/config.php new file mode 100644 index 0000000..23ad722 --- /dev/null +++ b/config.php @@ -0,0 +1,16 @@ + diff --git a/public/static/css/pico.red.min.css b/public/static/css/pico.red.min.css new file mode 100644 index 0000000..8ed9789 --- /dev/null +++ b/public/static/css/pico.red.min.css @@ -0,0 +1,4 @@ +@charset "UTF-8";/*! + * Pico CSS ✨ v2.0.6 (https://picocss.com) + * Copyright 2019-2024 - Licensed under MIT + */:root{--pico-font-family-emoji:"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--pico-font-family-sans-serif:system-ui,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,Helvetica,Arial,"Helvetica Neue",sans-serif,var(--pico-font-family-emoji);--pico-font-family-monospace:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace,var(--pico-font-family-emoji);--pico-font-family:var(--pico-font-family-sans-serif);--pico-line-height:1.5;--pico-font-weight:400;--pico-font-size:100%;--pico-text-underline-offset:0.1rem;--pico-border-radius:0.25rem;--pico-border-width:0.0625rem;--pico-outline-width:0.125rem;--pico-transition:0.2s ease-in-out;--pico-spacing:1rem;--pico-typography-spacing-vertical:1rem;--pico-block-spacing-vertical:var(--pico-spacing);--pico-block-spacing-horizontal:var(--pico-spacing);--pico-grid-column-gap:var(--pico-spacing);--pico-grid-row-gap:var(--pico-spacing);--pico-form-element-spacing-vertical:0.75rem;--pico-form-element-spacing-horizontal:1rem;--pico-group-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-group-box-shadow-focus-with-button:0 0 0 var(--pico-outline-width) var(--pico-primary-focus);--pico-group-box-shadow-focus-with-input:0 0 0 0.0625rem var(--pico-form-element-border-color);--pico-modal-overlay-backdrop-filter:blur(0.375rem);--pico-nav-element-spacing-vertical:1rem;--pico-nav-element-spacing-horizontal:0.5rem;--pico-nav-link-spacing-vertical:0.5rem;--pico-nav-link-spacing-horizontal:0.5rem;--pico-nav-breadcrumb-divider:">";--pico-icon-checkbox:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-minus:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");--pico-icon-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-date:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");--pico-icon-time:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-search:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E");--pico-icon-close:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");--pico-icon-loading:url("data:image/svg+xml,%3Csvg fill='none' height='24' width='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' %3E%3Cstyle%3E g %7B animation: rotate 2s linear infinite; transform-origin: center center; %7D circle %7B stroke-dasharray: 75,100; stroke-dashoffset: -5; animation: dash 1.5s ease-in-out infinite; stroke-linecap: round; %7D @keyframes rotate %7B 0%25 %7B transform: rotate(0deg); %7D 100%25 %7B transform: rotate(360deg); %7D %7D @keyframes dash %7B 0%25 %7B stroke-dasharray: 1,100; stroke-dashoffset: 0; %7D 50%25 %7B stroke-dasharray: 44.5,100; stroke-dashoffset: -17.5; %7D 100%25 %7B stroke-dasharray: 44.5,100; stroke-dashoffset: -62; %7D %7D %3C/style%3E%3Cg%3E%3Ccircle cx='12' cy='12' r='10' fill='none' stroke='rgb(136, 145, 164)' stroke-width='4' /%3E%3C/g%3E%3C/svg%3E")}@media (min-width:576px){:root{--pico-font-size:106.25%}}@media (min-width:768px){:root{--pico-font-size:112.5%}}@media (min-width:1024px){:root{--pico-font-size:118.75%}}@media (min-width:1280px){:root{--pico-font-size:125%}}@media (min-width:1536px){:root{--pico-font-size:131.25%}}a{--pico-text-decoration:underline}a.contrast,a.secondary{--pico-text-decoration:underline}small{--pico-font-size:0.875em}h1,h2,h3,h4,h5,h6{--pico-font-weight:700}h1{--pico-font-size:2rem;--pico-line-height:1.125;--pico-typography-spacing-top:3rem}h2{--pico-font-size:1.75rem;--pico-line-height:1.15;--pico-typography-spacing-top:2.625rem}h3{--pico-font-size:1.5rem;--pico-line-height:1.175;--pico-typography-spacing-top:2.25rem}h4{--pico-font-size:1.25rem;--pico-line-height:1.2;--pico-typography-spacing-top:1.874rem}h5{--pico-font-size:1.125rem;--pico-line-height:1.225;--pico-typography-spacing-top:1.6875rem}h6{--pico-font-size:1rem;--pico-line-height:1.25;--pico-typography-spacing-top:1.5rem}tfoot td,tfoot th,thead td,thead th{--pico-font-weight:600;--pico-border-width:0.1875rem}code,kbd,pre,samp{--pico-font-family:var(--pico-font-family-monospace)}kbd{--pico-font-weight:bolder}:where(select,textarea),input:not([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-outline-width:0.0625rem}[type=search]{--pico-border-radius:5rem}[type=checkbox],[type=radio]{--pico-border-width:0.125rem}[type=checkbox][role=switch]{--pico-border-width:0.1875rem}details.dropdown summary:not([role=button]){--pico-outline-width:0.0625rem}nav details.dropdown summary:focus-visible{--pico-outline-width:0.125rem}[role=search]{--pico-border-radius:5rem}[role=group]:has(button.secondary:focus,[type=submit].secondary:focus,[type=button].secondary:focus,[role=button].secondary:focus),[role=search]:has(button.secondary:focus,[type=submit].secondary:focus,[type=button].secondary:focus,[role=button].secondary:focus){--pico-group-box-shadow-focus-with-button:0 0 0 var(--pico-outline-width) var(--pico-secondary-focus)}[role=group]:has(button.contrast:focus,[type=submit].contrast:focus,[type=button].contrast:focus,[role=button].contrast:focus),[role=search]:has(button.contrast:focus,[type=submit].contrast:focus,[type=button].contrast:focus,[role=button].contrast:focus){--pico-group-box-shadow-focus-with-button:0 0 0 var(--pico-outline-width) var(--pico-contrast-focus)}[role=group] [role=button],[role=group] [type=button],[role=group] [type=submit],[role=group] button,[role=search] [role=button],[role=search] [type=button],[role=search] [type=submit],[role=search] button{--pico-form-element-spacing-horizontal:2rem}details summary[role=button]:not(.outline)::after{filter:brightness(0) invert(1)}[aria-busy=true]:not(input,select,textarea):is(button,[type=submit],[type=button],[type=reset],[role=button]):not(.outline)::before{filter:brightness(0) invert(1)}:root:not([data-theme=dark]),[data-theme=light]{--pico-background-color:#fff;--pico-color:#373c44;--pico-text-selection-color:rgba(240, 96, 72, 0.25);--pico-muted-color:#646b79;--pico-muted-border-color:#e7eaf0;--pico-primary:#c52f21;--pico-primary-background:#c52f21;--pico-primary-border:var(--pico-primary-background);--pico-primary-underline:rgba(197, 47, 33, 0.5);--pico-primary-hover:#9b2318;--pico-primary-hover-background:#af291d;--pico-primary-hover-border:var(--pico-primary-hover-background);--pico-primary-hover-underline:var(--pico-primary-hover);--pico-primary-focus:rgba(240, 96, 72, 0.5);--pico-primary-inverse:#fff;--pico-secondary:#5d6b89;--pico-secondary-background:#525f7a;--pico-secondary-border:var(--pico-secondary-background);--pico-secondary-underline:rgba(93, 107, 137, 0.5);--pico-secondary-hover:#48536b;--pico-secondary-hover-background:#48536b;--pico-secondary-hover-border:var(--pico-secondary-hover-background);--pico-secondary-hover-underline:var(--pico-secondary-hover);--pico-secondary-focus:rgba(93, 107, 137, 0.25);--pico-secondary-inverse:#fff;--pico-contrast:#181c25;--pico-contrast-background:#181c25;--pico-contrast-border:var(--pico-contrast-background);--pico-contrast-underline:rgba(24, 28, 37, 0.5);--pico-contrast-hover:#000;--pico-contrast-hover-background:#000;--pico-contrast-hover-border:var(--pico-contrast-hover-background);--pico-contrast-hover-underline:var(--pico-secondary-hover);--pico-contrast-focus:rgba(93, 107, 137, 0.25);--pico-contrast-inverse:#fff;--pico-box-shadow:0.0145rem 0.029rem 0.174rem rgba(129, 145, 181, 0.01698),0.0335rem 0.067rem 0.402rem rgba(129, 145, 181, 0.024),0.0625rem 0.125rem 0.75rem rgba(129, 145, 181, 0.03),0.1125rem 0.225rem 1.35rem rgba(129, 145, 181, 0.036),0.2085rem 0.417rem 2.502rem rgba(129, 145, 181, 0.04302),0.5rem 1rem 6rem rgba(129, 145, 181, 0.06),0 0 0 0.0625rem rgba(129, 145, 181, 0.015);--pico-h1-color:#2d3138;--pico-h2-color:#373c44;--pico-h3-color:#424751;--pico-h4-color:#4d535e;--pico-h5-color:#5c6370;--pico-h6-color:#646b79;--pico-mark-background-color:#fde7c0;--pico-mark-color:#0f1114;--pico-ins-color:#1d6a54;--pico-del-color:#883935;--pico-blockquote-border-color:var(--pico-muted-border-color);--pico-blockquote-footer-color:var(--pico-muted-color);--pico-button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-table-border-color:var(--pico-muted-border-color);--pico-table-row-stripped-background-color:rgba(111, 120, 135, 0.0375);--pico-code-background-color:#f3f5f7;--pico-code-color:#646b79;--pico-code-kbd-background-color:var(--pico-color);--pico-code-kbd-color:var(--pico-background-color);--pico-form-element-background-color:#fbfcfc;--pico-form-element-selected-background-color:#dfe3eb;--pico-form-element-border-color:#cfd5e2;--pico-form-element-color:#23262c;--pico-form-element-placeholder-color:var(--pico-muted-color);--pico-form-element-active-background-color:#fff;--pico-form-element-active-border-color:var(--pico-primary-border);--pico-form-element-focus-color:var(--pico-primary-border);--pico-form-element-disabled-opacity:0.5;--pico-form-element-invalid-border-color:#b86a6b;--pico-form-element-invalid-active-border-color:#c84f48;--pico-form-element-invalid-focus-color:var(--pico-form-element-invalid-active-border-color);--pico-form-element-valid-border-color:#4c9b8a;--pico-form-element-valid-active-border-color:#279977;--pico-form-element-valid-focus-color:var(--pico-form-element-valid-active-border-color);--pico-switch-background-color:#bfc7d9;--pico-switch-checked-background-color:var(--pico-primary-background);--pico-switch-color:#fff;--pico-switch-thumb-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-range-border-color:#dfe3eb;--pico-range-active-border-color:#bfc7d9;--pico-range-thumb-border-color:var(--pico-background-color);--pico-range-thumb-color:var(--pico-secondary-background);--pico-range-thumb-active-color:var(--pico-primary-background);--pico-accordion-border-color:var(--pico-muted-border-color);--pico-accordion-active-summary-color:var(--pico-primary-hover);--pico-accordion-close-summary-color:var(--pico-color);--pico-accordion-open-summary-color:var(--pico-muted-color);--pico-card-background-color:var(--pico-background-color);--pico-card-border-color:var(--pico-muted-border-color);--pico-card-box-shadow:var(--pico-box-shadow);--pico-card-sectioning-background-color:#fbfcfc;--pico-dropdown-background-color:#fff;--pico-dropdown-border-color:#eff1f4;--pico-dropdown-box-shadow:var(--pico-box-shadow);--pico-dropdown-color:var(--pico-color);--pico-dropdown-hover-background-color:#eff1f4;--pico-loading-spinner-opacity:0.5;--pico-modal-overlay-background-color:rgba(232, 234, 237, 0.75);--pico-progress-background-color:#dfe3eb;--pico-progress-color:var(--pico-primary-background);--pico-tooltip-background-color:var(--pico-contrast-background);--pico-tooltip-color:var(--pico-contrast-inverse);--pico-icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(76, 155, 138)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(200, 79, 72)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E");color-scheme:light}:root:not([data-theme=dark]) input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]),[data-theme=light] input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-form-element-focus-color:var(--pico-primary-focus)}@media only screen and (prefers-color-scheme:dark){:root:not([data-theme]){--pico-background-color:#13171f;--pico-color:#c2c7d0;--pico-text-selection-color:rgba(241, 121, 97, 0.1875);--pico-muted-color:#7b8495;--pico-muted-border-color:#202632;--pico-primary:#f17961;--pico-primary-background:#c52f21;--pico-primary-border:var(--pico-primary-background);--pico-primary-underline:rgba(241, 121, 97, 0.5);--pico-primary-hover:#f5a390;--pico-primary-hover-background:#d93526;--pico-primary-hover-border:var(--pico-primary-hover-background);--pico-primary-hover-underline:var(--pico-primary-hover);--pico-primary-focus:rgba(241, 121, 97, 0.375);--pico-primary-inverse:#fff;--pico-secondary:#969eaf;--pico-secondary-background:#525f7a;--pico-secondary-border:var(--pico-secondary-background);--pico-secondary-underline:rgba(150, 158, 175, 0.5);--pico-secondary-hover:#b3b9c5;--pico-secondary-hover-background:#5d6b89;--pico-secondary-hover-border:var(--pico-secondary-hover-background);--pico-secondary-hover-underline:var(--pico-secondary-hover);--pico-secondary-focus:rgba(144, 158, 190, 0.25);--pico-secondary-inverse:#fff;--pico-contrast:#dfe3eb;--pico-contrast-background:#eff1f4;--pico-contrast-border:var(--pico-contrast-background);--pico-contrast-underline:rgba(223, 227, 235, 0.5);--pico-contrast-hover:#fff;--pico-contrast-hover-background:#fff;--pico-contrast-hover-border:var(--pico-contrast-hover-background);--pico-contrast-hover-underline:var(--pico-contrast-hover);--pico-contrast-focus:rgba(207, 213, 226, 0.25);--pico-contrast-inverse:#000;--pico-box-shadow:0.0145rem 0.029rem 0.174rem rgba(7, 9, 12, 0.01698),0.0335rem 0.067rem 0.402rem rgba(7, 9, 12, 0.024),0.0625rem 0.125rem 0.75rem rgba(7, 9, 12, 0.03),0.1125rem 0.225rem 1.35rem rgba(7, 9, 12, 0.036),0.2085rem 0.417rem 2.502rem rgba(7, 9, 12, 0.04302),0.5rem 1rem 6rem rgba(7, 9, 12, 0.06),0 0 0 0.0625rem rgba(7, 9, 12, 0.015);--pico-h1-color:#f0f1f3;--pico-h2-color:#e0e3e7;--pico-h3-color:#c2c7d0;--pico-h4-color:#b3b9c5;--pico-h5-color:#a4acba;--pico-h6-color:#8891a4;--pico-mark-background-color:#014063;--pico-mark-color:#fff;--pico-ins-color:#62af9a;--pico-del-color:#ce7e7b;--pico-blockquote-border-color:var(--pico-muted-border-color);--pico-blockquote-footer-color:var(--pico-muted-color);--pico-button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-table-border-color:var(--pico-muted-border-color);--pico-table-row-stripped-background-color:rgba(111, 120, 135, 0.0375);--pico-code-background-color:#1a1f28;--pico-code-color:#8891a4;--pico-code-kbd-background-color:var(--pico-color);--pico-code-kbd-color:var(--pico-background-color);--pico-form-element-background-color:#1c212c;--pico-form-element-selected-background-color:#2a3140;--pico-form-element-border-color:#2a3140;--pico-form-element-color:#e0e3e7;--pico-form-element-placeholder-color:#8891a4;--pico-form-element-active-background-color:#1a1f28;--pico-form-element-active-border-color:var(--pico-primary-border);--pico-form-element-focus-color:var(--pico-primary-border);--pico-form-element-disabled-opacity:0.5;--pico-form-element-invalid-border-color:#964a50;--pico-form-element-invalid-active-border-color:#b7403b;--pico-form-element-invalid-focus-color:var(--pico-form-element-invalid-active-border-color);--pico-form-element-valid-border-color:#2a7b6f;--pico-form-element-valid-active-border-color:#16896a;--pico-form-element-valid-focus-color:var(--pico-form-element-valid-active-border-color);--pico-switch-background-color:#333c4e;--pico-switch-checked-background-color:var(--pico-primary-background);--pico-switch-color:#fff;--pico-switch-thumb-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-range-border-color:#202632;--pico-range-active-border-color:#2a3140;--pico-range-thumb-border-color:var(--pico-background-color);--pico-range-thumb-color:var(--pico-secondary-background);--pico-range-thumb-active-color:var(--pico-primary-background);--pico-accordion-border-color:var(--pico-muted-border-color);--pico-accordion-active-summary-color:var(--pico-primary-hover);--pico-accordion-close-summary-color:var(--pico-color);--pico-accordion-open-summary-color:var(--pico-muted-color);--pico-card-background-color:#181c25;--pico-card-border-color:var(--pico-card-background-color);--pico-card-box-shadow:var(--pico-box-shadow);--pico-card-sectioning-background-color:#1a1f28;--pico-dropdown-background-color:#181c25;--pico-dropdown-border-color:#202632;--pico-dropdown-box-shadow:var(--pico-box-shadow);--pico-dropdown-color:var(--pico-color);--pico-dropdown-hover-background-color:#202632;--pico-loading-spinner-opacity:0.5;--pico-modal-overlay-background-color:rgba(8, 9, 10, 0.75);--pico-progress-background-color:#202632;--pico-progress-color:var(--pico-primary-background);--pico-tooltip-background-color:var(--pico-contrast-background);--pico-tooltip-color:var(--pico-contrast-inverse);--pico-icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(42, 123, 111)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(150, 74, 80)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E");color-scheme:dark}:root:not([data-theme]) input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-form-element-focus-color:var(--pico-primary-focus)}:root:not([data-theme]) details summary[role=button].contrast:not(.outline)::after{filter:brightness(0)}:root:not([data-theme]) [aria-busy=true]:not(input,select,textarea).contrast:is(button,[type=submit],[type=button],[type=reset],[role=button]):not(.outline)::before{filter:brightness(0)}}[data-theme=dark]{--pico-background-color:#13171f;--pico-color:#c2c7d0;--pico-text-selection-color:rgba(241, 121, 97, 0.1875);--pico-muted-color:#7b8495;--pico-muted-border-color:#202632;--pico-primary:#f17961;--pico-primary-background:#c52f21;--pico-primary-border:var(--pico-primary-background);--pico-primary-underline:rgba(241, 121, 97, 0.5);--pico-primary-hover:#f5a390;--pico-primary-hover-background:#d93526;--pico-primary-hover-border:var(--pico-primary-hover-background);--pico-primary-hover-underline:var(--pico-primary-hover);--pico-primary-focus:rgba(241, 121, 97, 0.375);--pico-primary-inverse:#fff;--pico-secondary:#969eaf;--pico-secondary-background:#525f7a;--pico-secondary-border:var(--pico-secondary-background);--pico-secondary-underline:rgba(150, 158, 175, 0.5);--pico-secondary-hover:#b3b9c5;--pico-secondary-hover-background:#5d6b89;--pico-secondary-hover-border:var(--pico-secondary-hover-background);--pico-secondary-hover-underline:var(--pico-secondary-hover);--pico-secondary-focus:rgba(144, 158, 190, 0.25);--pico-secondary-inverse:#fff;--pico-contrast:#dfe3eb;--pico-contrast-background:#eff1f4;--pico-contrast-border:var(--pico-contrast-background);--pico-contrast-underline:rgba(223, 227, 235, 0.5);--pico-contrast-hover:#fff;--pico-contrast-hover-background:#fff;--pico-contrast-hover-border:var(--pico-contrast-hover-background);--pico-contrast-hover-underline:var(--pico-contrast-hover);--pico-contrast-focus:rgba(207, 213, 226, 0.25);--pico-contrast-inverse:#000;--pico-box-shadow:0.0145rem 0.029rem 0.174rem rgba(7, 9, 12, 0.01698),0.0335rem 0.067rem 0.402rem rgba(7, 9, 12, 0.024),0.0625rem 0.125rem 0.75rem rgba(7, 9, 12, 0.03),0.1125rem 0.225rem 1.35rem rgba(7, 9, 12, 0.036),0.2085rem 0.417rem 2.502rem rgba(7, 9, 12, 0.04302),0.5rem 1rem 6rem rgba(7, 9, 12, 0.06),0 0 0 0.0625rem rgba(7, 9, 12, 0.015);--pico-h1-color:#f0f1f3;--pico-h2-color:#e0e3e7;--pico-h3-color:#c2c7d0;--pico-h4-color:#b3b9c5;--pico-h5-color:#a4acba;--pico-h6-color:#8891a4;--pico-mark-background-color:#014063;--pico-mark-color:#fff;--pico-ins-color:#62af9a;--pico-del-color:#ce7e7b;--pico-blockquote-border-color:var(--pico-muted-border-color);--pico-blockquote-footer-color:var(--pico-muted-color);--pico-button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-table-border-color:var(--pico-muted-border-color);--pico-table-row-stripped-background-color:rgba(111, 120, 135, 0.0375);--pico-code-background-color:#1a1f28;--pico-code-color:#8891a4;--pico-code-kbd-background-color:var(--pico-color);--pico-code-kbd-color:var(--pico-background-color);--pico-form-element-background-color:#1c212c;--pico-form-element-selected-background-color:#2a3140;--pico-form-element-border-color:#2a3140;--pico-form-element-color:#e0e3e7;--pico-form-element-placeholder-color:#8891a4;--pico-form-element-active-background-color:#1a1f28;--pico-form-element-active-border-color:var(--pico-primary-border);--pico-form-element-focus-color:var(--pico-primary-border);--pico-form-element-disabled-opacity:0.5;--pico-form-element-invalid-border-color:#964a50;--pico-form-element-invalid-active-border-color:#b7403b;--pico-form-element-invalid-focus-color:var(--pico-form-element-invalid-active-border-color);--pico-form-element-valid-border-color:#2a7b6f;--pico-form-element-valid-active-border-color:#16896a;--pico-form-element-valid-focus-color:var(--pico-form-element-valid-active-border-color);--pico-switch-background-color:#333c4e;--pico-switch-checked-background-color:var(--pico-primary-background);--pico-switch-color:#fff;--pico-switch-thumb-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-range-border-color:#202632;--pico-range-active-border-color:#2a3140;--pico-range-thumb-border-color:var(--pico-background-color);--pico-range-thumb-color:var(--pico-secondary-background);--pico-range-thumb-active-color:var(--pico-primary-background);--pico-accordion-border-color:var(--pico-muted-border-color);--pico-accordion-active-summary-color:var(--pico-primary-hover);--pico-accordion-close-summary-color:var(--pico-color);--pico-accordion-open-summary-color:var(--pico-muted-color);--pico-card-background-color:#181c25;--pico-card-border-color:var(--pico-card-background-color);--pico-card-box-shadow:var(--pico-box-shadow);--pico-card-sectioning-background-color:#1a1f28;--pico-dropdown-background-color:#181c25;--pico-dropdown-border-color:#202632;--pico-dropdown-box-shadow:var(--pico-box-shadow);--pico-dropdown-color:var(--pico-color);--pico-dropdown-hover-background-color:#202632;--pico-loading-spinner-opacity:0.5;--pico-modal-overlay-background-color:rgba(8, 9, 10, 0.75);--pico-progress-background-color:#202632;--pico-progress-color:var(--pico-primary-background);--pico-tooltip-background-color:var(--pico-contrast-background);--pico-tooltip-color:var(--pico-contrast-inverse);--pico-icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(42, 123, 111)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(150, 74, 80)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E");color-scheme:dark}[data-theme=dark] input:is([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[type=file]){--pico-form-element-focus-color:var(--pico-primary-focus)}[data-theme=dark] details summary[role=button].contrast:not(.outline)::after{filter:brightness(0)}[data-theme=dark] [aria-busy=true]:not(input,select,textarea).contrast:is(button,[type=submit],[type=button],[type=reset],[role=button]):not(.outline)::before{filter:brightness(0)}[type=checkbox],[type=radio],[type=range],progress{accent-color:var(--pico-primary)}*,::after,::before{box-sizing:border-box;background-repeat:no-repeat}::after,::before{text-decoration:inherit;vertical-align:inherit}:where(:root){-webkit-tap-highlight-color:transparent;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;background-color:var(--pico-background-color);color:var(--pico-color);font-weight:var(--pico-font-weight);font-size:var(--pico-font-size);line-height:var(--pico-line-height);font-family:var(--pico-font-family);text-underline-offset:var(--pico-text-underline-offset);text-rendering:optimizeLegibility;overflow-wrap:break-word;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{width:100%;margin:0}main{display:block}body>footer,body>header,body>main{padding-block:var(--pico-block-spacing-vertical)}section{margin-bottom:var(--pico-block-spacing-vertical)}.container,.container-fluid{width:100%;margin-right:auto;margin-left:auto;padding-right:var(--pico-spacing);padding-left:var(--pico-spacing)}@media (min-width:576px){.container{max-width:510px;padding-right:0;padding-left:0}}@media (min-width:768px){.container{max-width:700px}}@media (min-width:1024px){.container{max-width:950px}}@media (min-width:1280px){.container{max-width:1200px}}@media (min-width:1536px){.container{max-width:1450px}}.grid{grid-column-gap:var(--pico-grid-column-gap);grid-row-gap:var(--pico-grid-row-gap);display:grid;grid-template-columns:1fr}@media (min-width:768px){.grid{grid-template-columns:repeat(auto-fit,minmax(0%,1fr))}}.grid>*{min-width:0}.overflow-auto{overflow:auto}b,strong{font-weight:bolder}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}address,blockquote,dl,ol,p,pre,table,ul{margin-top:0;margin-bottom:var(--pico-typography-spacing-vertical);color:var(--pico-color);font-style:normal;font-weight:var(--pico-font-weight)}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:var(--pico-typography-spacing-vertical);color:var(--pico-color);font-weight:var(--pico-font-weight);font-size:var(--pico-font-size);line-height:var(--pico-line-height);font-family:var(--pico-font-family)}h1{--pico-color:var(--pico-h1-color)}h2{--pico-color:var(--pico-h2-color)}h3{--pico-color:var(--pico-h3-color)}h4{--pico-color:var(--pico-h4-color)}h5{--pico-color:var(--pico-h5-color)}h6{--pico-color:var(--pico-h6-color)}:where(article,address,blockquote,dl,figure,form,ol,p,pre,table,ul)~:is(h1,h2,h3,h4,h5,h6){margin-top:var(--pico-typography-spacing-top)}p{margin-bottom:var(--pico-typography-spacing-vertical)}hgroup{margin-bottom:var(--pico-typography-spacing-vertical)}hgroup>*{margin-top:0;margin-bottom:0}hgroup>:not(:first-child):last-child{--pico-color:var(--pico-muted-color);--pico-font-weight:unset;font-size:1rem}:where(ol,ul) li{margin-bottom:calc(var(--pico-typography-spacing-vertical) * .25)}:where(dl,ol,ul) :where(dl,ol,ul){margin:0;margin-top:calc(var(--pico-typography-spacing-vertical) * .25)}ul li{list-style:square}mark{padding:.125rem .25rem;background-color:var(--pico-mark-background-color);color:var(--pico-mark-color);vertical-align:baseline}blockquote{display:block;margin:var(--pico-typography-spacing-vertical) 0;padding:var(--pico-spacing);border-right:none;border-left:.25rem solid var(--pico-blockquote-border-color);border-inline-start:0.25rem solid var(--pico-blockquote-border-color);border-inline-end:none}blockquote footer{margin-top:calc(var(--pico-typography-spacing-vertical) * .5);color:var(--pico-blockquote-footer-color)}abbr[title]{border-bottom:1px dotted;text-decoration:none;cursor:help}ins{color:var(--pico-ins-color);text-decoration:none}del{color:var(--pico-del-color)}::-moz-selection{background-color:var(--pico-text-selection-color)}::selection{background-color:var(--pico-text-selection-color)}:where(a:not([role=button])),[role=link]{--pico-color:var(--pico-primary);--pico-background-color:transparent;--pico-underline:var(--pico-primary-underline);outline:0;background-color:var(--pico-background-color);color:var(--pico-color);-webkit-text-decoration:var(--pico-text-decoration);text-decoration:var(--pico-text-decoration);text-decoration-color:var(--pico-underline);text-underline-offset:0.125em;transition:background-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition),-webkit-text-decoration var(--pico-transition);transition:background-color var(--pico-transition),color var(--pico-transition),text-decoration var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),color var(--pico-transition),text-decoration var(--pico-transition),box-shadow var(--pico-transition),-webkit-text-decoration var(--pico-transition)}:where(a:not([role=button])):is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[role=link]:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-color:var(--pico-primary-hover);--pico-underline:var(--pico-primary-hover-underline);--pico-text-decoration:underline}:where(a:not([role=button])):focus-visible,[role=link]:focus-visible{box-shadow:0 0 0 var(--pico-outline-width) var(--pico-primary-focus)}:where(a:not([role=button])).secondary,[role=link].secondary{--pico-color:var(--pico-secondary);--pico-underline:var(--pico-secondary-underline)}:where(a:not([role=button])).secondary:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[role=link].secondary:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-color:var(--pico-secondary-hover);--pico-underline:var(--pico-secondary-hover-underline)}:where(a:not([role=button])).contrast,[role=link].contrast{--pico-color:var(--pico-contrast);--pico-underline:var(--pico-contrast-underline)}:where(a:not([role=button])).contrast:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[role=link].contrast:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-color:var(--pico-contrast-hover);--pico-underline:var(--pico-contrast-hover-underline)}a[role=button]{display:inline-block}button{margin:0;overflow:visible;font-family:inherit;text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[role=button],[type=button],[type=file]::file-selector-button,[type=reset],[type=submit],button{--pico-background-color:var(--pico-primary-background);--pico-border-color:var(--pico-primary-border);--pico-color:var(--pico-primary-inverse);--pico-box-shadow:var(--pico-button-box-shadow, 0 0 0 rgba(0, 0, 0, 0));padding:var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal);border:var(--pico-border-width) solid var(--pico-border-color);border-radius:var(--pico-border-radius);outline:0;background-color:var(--pico-background-color);box-shadow:var(--pico-box-shadow);color:var(--pico-color);font-weight:var(--pico-font-weight);font-size:1rem;line-height:var(--pico-line-height);text-align:center;text-decoration:none;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;transition:background-color var(--pico-transition),border-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition)}[role=button]:is(:hover,:active,:focus),[role=button]:is([aria-current]:not([aria-current=false])),[type=button]:is(:hover,:active,:focus),[type=button]:is([aria-current]:not([aria-current=false])),[type=file]::file-selector-button:is(:hover,:active,:focus),[type=file]::file-selector-button:is([aria-current]:not([aria-current=false])),[type=reset]:is(:hover,:active,:focus),[type=reset]:is([aria-current]:not([aria-current=false])),[type=submit]:is(:hover,:active,:focus),[type=submit]:is([aria-current]:not([aria-current=false])),button:is(:hover,:active,:focus),button:is([aria-current]:not([aria-current=false])){--pico-background-color:var(--pico-primary-hover-background);--pico-border-color:var(--pico-primary-hover-border);--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0));--pico-color:var(--pico-primary-inverse)}[role=button]:focus,[role=button]:is([aria-current]:not([aria-current=false])):focus,[type=button]:focus,[type=button]:is([aria-current]:not([aria-current=false])):focus,[type=file]::file-selector-button:focus,[type=file]::file-selector-button:is([aria-current]:not([aria-current=false])):focus,[type=reset]:focus,[type=reset]:is([aria-current]:not([aria-current=false])):focus,[type=submit]:focus,[type=submit]:is([aria-current]:not([aria-current=false])):focus,button:focus,button:is([aria-current]:not([aria-current=false])):focus{--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--pico-outline-width) var(--pico-primary-focus)}[type=button],[type=reset],[type=submit]{margin-bottom:var(--pico-spacing)}:is(button,[type=submit],[type=button],[role=button]).secondary,[type=file]::file-selector-button,[type=reset]{--pico-background-color:var(--pico-secondary-background);--pico-border-color:var(--pico-secondary-border);--pico-color:var(--pico-secondary-inverse);cursor:pointer}:is(button,[type=submit],[type=button],[role=button]).secondary:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[type=file]::file-selector-button:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[type=reset]:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-background-color:var(--pico-secondary-hover-background);--pico-border-color:var(--pico-secondary-hover-border);--pico-color:var(--pico-secondary-inverse)}:is(button,[type=submit],[type=button],[role=button]).secondary:focus,:is(button,[type=submit],[type=button],[role=button]).secondary:is([aria-current]:not([aria-current=false])):focus,[type=file]::file-selector-button:focus,[type=file]::file-selector-button:is([aria-current]:not([aria-current=false])):focus,[type=reset]:focus,[type=reset]:is([aria-current]:not([aria-current=false])):focus{--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--pico-outline-width) var(--pico-secondary-focus)}:is(button,[type=submit],[type=button],[role=button]).contrast{--pico-background-color:var(--pico-contrast-background);--pico-border-color:var(--pico-contrast-border);--pico-color:var(--pico-contrast-inverse)}:is(button,[type=submit],[type=button],[role=button]).contrast:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-background-color:var(--pico-contrast-hover-background);--pico-border-color:var(--pico-contrast-hover-border);--pico-color:var(--pico-contrast-inverse)}:is(button,[type=submit],[type=button],[role=button]).contrast:focus,:is(button,[type=submit],[type=button],[role=button]).contrast:is([aria-current]:not([aria-current=false])):focus{--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--pico-outline-width) var(--pico-contrast-focus)}:is(button,[type=submit],[type=button],[role=button]).outline,[type=reset].outline{--pico-background-color:transparent;--pico-color:var(--pico-primary);--pico-border-color:var(--pico-primary)}:is(button,[type=submit],[type=button],[role=button]).outline:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[type=reset].outline:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-background-color:transparent;--pico-color:var(--pico-primary-hover);--pico-border-color:var(--pico-primary-hover)}:is(button,[type=submit],[type=button],[role=button]).outline.secondary,[type=reset].outline{--pico-color:var(--pico-secondary);--pico-border-color:var(--pico-secondary)}:is(button,[type=submit],[type=button],[role=button]).outline.secondary:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),[type=reset].outline:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-color:var(--pico-secondary-hover);--pico-border-color:var(--pico-secondary-hover)}:is(button,[type=submit],[type=button],[role=button]).outline.contrast{--pico-color:var(--pico-contrast);--pico-border-color:var(--pico-contrast)}:is(button,[type=submit],[type=button],[role=button]).outline.contrast:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){--pico-color:var(--pico-contrast-hover);--pico-border-color:var(--pico-contrast-hover)}:where(button,[type=submit],[type=reset],[type=button],[role=button])[disabled],:where(fieldset[disabled]) :is(button,[type=submit],[type=button],[type=reset],[role=button]){opacity:.5;pointer-events:none}:where(table){width:100%;border-collapse:collapse;border-spacing:0;text-indent:0}td,th{padding:calc(var(--pico-spacing)/ 2) var(--pico-spacing);border-bottom:var(--pico-border-width) solid var(--pico-table-border-color);background-color:var(--pico-background-color);color:var(--pico-color);font-weight:var(--pico-font-weight);text-align:left;text-align:start}tfoot td,tfoot th{border-top:var(--pico-border-width) solid var(--pico-table-border-color);border-bottom:0}table.striped tbody tr:nth-child(odd) td,table.striped tbody tr:nth-child(odd) th{background-color:var(--pico-table-row-stripped-background-color)}:where(audio,canvas,iframe,img,svg,video){vertical-align:middle}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}:where(iframe){border-style:none}img{max-width:100%;height:auto;border-style:none}:where(svg:not([fill])){fill:currentColor}svg:not(:root){overflow:hidden}code,kbd,pre,samp{font-size:.875em;font-family:var(--pico-font-family)}pre code{font-size:inherit;font-family:inherit}pre{-ms-overflow-style:scrollbar;overflow:auto}code,kbd,pre{border-radius:var(--pico-border-radius);background:var(--pico-code-background-color);color:var(--pico-code-color);font-weight:var(--pico-font-weight);line-height:initial}code,kbd{display:inline-block;padding:.375rem}pre{display:block;margin-bottom:var(--pico-spacing);overflow-x:auto}pre>code{display:block;padding:var(--pico-spacing);background:0 0;line-height:var(--pico-line-height)}kbd{background-color:var(--pico-code-kbd-background-color);color:var(--pico-code-kbd-color);vertical-align:baseline}figure{display:block;margin:0;padding:0}figure figcaption{padding:calc(var(--pico-spacing) * .5) 0;color:var(--pico-muted-color)}hr{height:0;margin:var(--pico-typography-spacing-vertical) 0;border:0;border-top:1px solid var(--pico-muted-border-color);color:inherit}[hidden],template{display:none!important}canvas{display:inline-block}input,optgroup,select,textarea{margin:0;font-size:1rem;line-height:var(--pico-line-height);font-family:inherit;letter-spacing:inherit}input{overflow:visible}select{text-transform:none}legend{max-width:100%;padding:0;color:inherit;white-space:normal}textarea{overflow:auto}[type=checkbox],[type=radio]{padding:0}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}::-moz-focus-inner{padding:0;border-style:none}:-moz-focusring{outline:0}:-moz-ui-invalid{box-shadow:none}::-ms-expand{display:none}[type=file],[type=range]{padding:0;border-width:0}input:not([type=checkbox],[type=radio],[type=range]){height:calc(1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2)}fieldset{width:100%;margin:0;margin-bottom:var(--pico-spacing);padding:0;border:0}fieldset legend,label{display:block;margin-bottom:calc(var(--pico-spacing) * .375);color:var(--pico-color);font-weight:var(--pico-form-label-font-weight,var(--pico-font-weight))}fieldset legend{margin-bottom:calc(var(--pico-spacing) * .5)}button[type=submit],input:not([type=checkbox],[type=radio]),select,textarea{width:100%}input:not([type=checkbox],[type=radio],[type=range],[type=file]),select,textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal)}input,select,textarea{--pico-background-color:var(--pico-form-element-background-color);--pico-border-color:var(--pico-form-element-border-color);--pico-color:var(--pico-form-element-color);--pico-box-shadow:none;border:var(--pico-border-width) solid var(--pico-border-color);border-radius:var(--pico-border-radius);outline:0;background-color:var(--pico-background-color);box-shadow:var(--pico-box-shadow);color:var(--pico-color);font-weight:var(--pico-font-weight);transition:background-color var(--pico-transition),border-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition)}:where(select,textarea):not([readonly]):is(:active,:focus),input:not([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[readonly]):is(:active,:focus){--pico-background-color:var(--pico-form-element-active-background-color)}:where(select,textarea):not([readonly]):is(:active,:focus),input:not([type=submit],[type=button],[type=reset],[role=switch],[readonly]):is(:active,:focus){--pico-border-color:var(--pico-form-element-active-border-color)}:where(select,textarea):not([readonly]):focus,input:not([type=submit],[type=button],[type=reset],[type=range],[type=file],[readonly]):focus{--pico-box-shadow:0 0 0 var(--pico-outline-width) var(--pico-form-element-focus-color)}:where(fieldset[disabled]) :is(input:not([type=submit],[type=button],[type=reset]),select,textarea),input:not([type=submit],[type=button],[type=reset])[disabled],label[aria-disabled=true],select[disabled],textarea[disabled]{opacity:var(--pico-form-element-disabled-opacity);pointer-events:none}label[aria-disabled=true] input[disabled]{opacity:1}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week],[type=range])[aria-invalid]{padding-right:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem)!important;padding-left:var(--pico-form-element-spacing-horizontal);padding-inline-start:var(--pico-form-element-spacing-horizontal)!important;padding-inline-end:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem)!important;background-position:center right .75rem;background-size:1rem auto;background-repeat:no-repeat}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week],[type=range])[aria-invalid=false]:not(select){background-image:var(--pico-icon-valid)}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week],[type=range])[aria-invalid=true]:not(select){background-image:var(--pico-icon-invalid)}:where(input,select,textarea)[aria-invalid=false]{--pico-border-color:var(--pico-form-element-valid-border-color)}:where(input,select,textarea)[aria-invalid=false]:is(:active,:focus){--pico-border-color:var(--pico-form-element-valid-active-border-color)!important}:where(input,select,textarea)[aria-invalid=false]:is(:active,:focus):not([type=checkbox],[type=radio]){--pico-box-shadow:0 0 0 var(--pico-outline-width) var(--pico-form-element-valid-focus-color)!important}:where(input,select,textarea)[aria-invalid=true]{--pico-border-color:var(--pico-form-element-invalid-border-color)}:where(input,select,textarea)[aria-invalid=true]:is(:active,:focus){--pico-border-color:var(--pico-form-element-invalid-active-border-color)!important}:where(input,select,textarea)[aria-invalid=true]:is(:active,:focus):not([type=checkbox],[type=radio]){--pico-box-shadow:0 0 0 var(--pico-outline-width) var(--pico-form-element-invalid-focus-color)!important}[dir=rtl] :where(input,select,textarea):not([type=checkbox],[type=radio]):is([aria-invalid],[aria-invalid=true],[aria-invalid=false]){background-position:center left .75rem}input::-webkit-input-placeholder,input::placeholder,select:invalid,textarea::-webkit-input-placeholder,textarea::placeholder{color:var(--pico-form-element-placeholder-color);opacity:1}input:not([type=checkbox],[type=radio]),select,textarea{margin-bottom:var(--pico-spacing)}select::-ms-expand{border:0;background-color:transparent}select:not([multiple],[size]){padding-right:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem);padding-left:var(--pico-form-element-spacing-horizontal);padding-inline-start:var(--pico-form-element-spacing-horizontal);padding-inline-end:calc(var(--pico-form-element-spacing-horizontal) + 1.5rem);background-image:var(--pico-icon-chevron);background-position:center right .75rem;background-size:1rem auto;background-repeat:no-repeat}select[multiple] option:checked{background:var(--pico-form-element-selected-background-color);color:var(--pico-form-element-color)}[dir=rtl] select:not([multiple],[size]){background-position:center left .75rem}textarea{display:block;resize:vertical}textarea[aria-invalid]{--pico-icon-height:calc(1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2);background-position:top right .75rem!important;background-size:1rem var(--pico-icon-height)!important}:where(input,select,textarea,fieldset,.grid)+small{display:block;width:100%;margin-top:calc(var(--pico-spacing) * -.75);margin-bottom:var(--pico-spacing);color:var(--pico-muted-color)}:where(input,select,textarea,fieldset,.grid)[aria-invalid=false]+small{color:var(--pico-ins-color)}:where(input,select,textarea,fieldset,.grid)[aria-invalid=true]+small{color:var(--pico-del-color)}label>:where(input,select,textarea){margin-top:calc(var(--pico-spacing) * .25)}label:has([type=checkbox],[type=radio]){width:-moz-fit-content;width:fit-content;cursor:pointer}[type=checkbox],[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:1.25em;height:1.25em;margin-top:-.125em;margin-inline-end:.5em;border-width:var(--pico-border-width);vertical-align:middle;cursor:pointer}[type=checkbox]::-ms-check,[type=radio]::-ms-check{display:none}[type=checkbox]:checked,[type=checkbox]:checked:active,[type=checkbox]:checked:focus,[type=radio]:checked,[type=radio]:checked:active,[type=radio]:checked:focus{--pico-background-color:var(--pico-primary-background);--pico-border-color:var(--pico-primary-border);background-image:var(--pico-icon-checkbox);background-position:center;background-size:.75em auto;background-repeat:no-repeat}[type=checkbox]~label,[type=radio]~label{display:inline-block;margin-bottom:0;cursor:pointer}[type=checkbox]~label:not(:last-of-type),[type=radio]~label:not(:last-of-type){margin-inline-end:1em}[type=checkbox]:indeterminate{--pico-background-color:var(--pico-primary-background);--pico-border-color:var(--pico-primary-border);background-image:var(--pico-icon-minus);background-position:center;background-size:.75em auto;background-repeat:no-repeat}[type=radio]{border-radius:50%}[type=radio]:checked,[type=radio]:checked:active,[type=radio]:checked:focus{--pico-background-color:var(--pico-primary-inverse);border-width:.35em;background-image:none}[type=checkbox][role=switch]{--pico-background-color:var(--pico-switch-background-color);--pico-color:var(--pico-switch-color);width:2.25em;height:1.25em;border:var(--pico-border-width) solid var(--pico-border-color);border-radius:1.25em;background-color:var(--pico-background-color);line-height:1.25em}[type=checkbox][role=switch]:not([aria-invalid]){--pico-border-color:var(--pico-switch-background-color)}[type=checkbox][role=switch]:before{display:block;aspect-ratio:1;height:100%;border-radius:50%;background-color:var(--pico-color);box-shadow:var(--pico-switch-thumb-box-shadow);content:"";transition:margin .1s ease-in-out}[type=checkbox][role=switch]:focus{--pico-background-color:var(--pico-switch-background-color);--pico-border-color:var(--pico-switch-background-color)}[type=checkbox][role=switch]:checked{--pico-background-color:var(--pico-switch-checked-background-color);--pico-border-color:var(--pico-switch-checked-background-color);background-image:none}[type=checkbox][role=switch]:checked::before{margin-inline-start:calc(2.25em - 1.25em)}[type=checkbox][role=switch][disabled]{--pico-background-color:var(--pico-border-color)}[type=checkbox][aria-invalid=false]:checked,[type=checkbox][aria-invalid=false]:checked:active,[type=checkbox][aria-invalid=false]:checked:focus,[type=checkbox][role=switch][aria-invalid=false]:checked,[type=checkbox][role=switch][aria-invalid=false]:checked:active,[type=checkbox][role=switch][aria-invalid=false]:checked:focus{--pico-background-color:var(--pico-form-element-valid-border-color)}[type=checkbox]:checked:active[aria-invalid=true],[type=checkbox]:checked:focus[aria-invalid=true],[type=checkbox]:checked[aria-invalid=true],[type=checkbox][role=switch]:checked:active[aria-invalid=true],[type=checkbox][role=switch]:checked:focus[aria-invalid=true],[type=checkbox][role=switch]:checked[aria-invalid=true]{--pico-background-color:var(--pico-form-element-invalid-border-color)}[type=checkbox][aria-invalid=false]:checked,[type=checkbox][aria-invalid=false]:checked:active,[type=checkbox][aria-invalid=false]:checked:focus,[type=checkbox][role=switch][aria-invalid=false]:checked,[type=checkbox][role=switch][aria-invalid=false]:checked:active,[type=checkbox][role=switch][aria-invalid=false]:checked:focus,[type=radio][aria-invalid=false]:checked,[type=radio][aria-invalid=false]:checked:active,[type=radio][aria-invalid=false]:checked:focus{--pico-border-color:var(--pico-form-element-valid-border-color)}[type=checkbox]:checked:active[aria-invalid=true],[type=checkbox]:checked:focus[aria-invalid=true],[type=checkbox]:checked[aria-invalid=true],[type=checkbox][role=switch]:checked:active[aria-invalid=true],[type=checkbox][role=switch]:checked:focus[aria-invalid=true],[type=checkbox][role=switch]:checked[aria-invalid=true],[type=radio]:checked:active[aria-invalid=true],[type=radio]:checked:focus[aria-invalid=true],[type=radio]:checked[aria-invalid=true]{--pico-border-color:var(--pico-form-element-invalid-border-color)}[type=color]::-webkit-color-swatch-wrapper{padding:0}[type=color]::-moz-focus-inner{padding:0}[type=color]::-webkit-color-swatch{border:0;border-radius:calc(var(--pico-border-radius) * .5)}[type=color]::-moz-color-swatch{border:0;border-radius:calc(var(--pico-border-radius) * .5)}input:not([type=checkbox],[type=radio],[type=range],[type=file]):is([type=date],[type=datetime-local],[type=month],[type=time],[type=week]){--pico-icon-position:0.75rem;--pico-icon-width:1rem;padding-right:calc(var(--pico-icon-width) + var(--pico-icon-position));background-image:var(--pico-icon-date);background-position:center right var(--pico-icon-position);background-size:var(--pico-icon-width) auto;background-repeat:no-repeat}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=time]{background-image:var(--pico-icon-time)}[type=date]::-webkit-calendar-picker-indicator,[type=datetime-local]::-webkit-calendar-picker-indicator,[type=month]::-webkit-calendar-picker-indicator,[type=time]::-webkit-calendar-picker-indicator,[type=week]::-webkit-calendar-picker-indicator{width:var(--pico-icon-width);margin-right:calc(var(--pico-icon-width) * -1);margin-left:var(--pico-icon-position);opacity:0}@-moz-document url-prefix(){[type=date],[type=datetime-local],[type=month],[type=time],[type=week]{padding-right:var(--pico-form-element-spacing-horizontal)!important;background-image:none!important}}[dir=rtl] :is([type=date],[type=datetime-local],[type=month],[type=time],[type=week]){text-align:right}[type=file]{--pico-color:var(--pico-muted-color);margin-left:calc(var(--pico-outline-width) * -1);padding:calc(var(--pico-form-element-spacing-vertical) * .5) 0;padding-left:var(--pico-outline-width);border:0;border-radius:0;background:0 0}[type=file]::file-selector-button{margin-right:calc(var(--pico-spacing)/ 2);padding:calc(var(--pico-form-element-spacing-vertical) * .5) var(--pico-form-element-spacing-horizontal)}[type=file]:is(:hover,:active,:focus)::file-selector-button{--pico-background-color:var(--pico-secondary-hover-background);--pico-border-color:var(--pico-secondary-hover-border)}[type=file]:focus::file-selector-button{--pico-box-shadow:var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--pico-outline-width) var(--pico-secondary-focus)}[type=range]{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:100%;height:1.25rem;background:0 0}[type=range]::-webkit-slider-runnable-track{width:100%;height:.375rem;border-radius:var(--pico-border-radius);background-color:var(--pico-range-border-color);-webkit-transition:background-color var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),box-shadow var(--pico-transition)}[type=range]::-moz-range-track{width:100%;height:.375rem;border-radius:var(--pico-border-radius);background-color:var(--pico-range-border-color);-moz-transition:background-color var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),box-shadow var(--pico-transition)}[type=range]::-ms-track{width:100%;height:.375rem;border-radius:var(--pico-border-radius);background-color:var(--pico-range-border-color);-ms-transition:background-color var(--pico-transition),box-shadow var(--pico-transition);transition:background-color var(--pico-transition),box-shadow var(--pico-transition)}[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.4375rem;border:2px solid var(--pico-range-thumb-border-color);border-radius:50%;background-color:var(--pico-range-thumb-color);cursor:pointer;-webkit-transition:background-color var(--pico-transition),transform var(--pico-transition);transition:background-color var(--pico-transition),transform var(--pico-transition)}[type=range]::-moz-range-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.4375rem;border:2px solid var(--pico-range-thumb-border-color);border-radius:50%;background-color:var(--pico-range-thumb-color);cursor:pointer;-moz-transition:background-color var(--pico-transition),transform var(--pico-transition);transition:background-color var(--pico-transition),transform var(--pico-transition)}[type=range]::-ms-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.4375rem;border:2px solid var(--pico-range-thumb-border-color);border-radius:50%;background-color:var(--pico-range-thumb-color);cursor:pointer;-ms-transition:background-color var(--pico-transition),transform var(--pico-transition);transition:background-color var(--pico-transition),transform var(--pico-transition)}[type=range]:active,[type=range]:focus-within{--pico-range-border-color:var(--pico-range-active-border-color);--pico-range-thumb-color:var(--pico-range-thumb-active-color)}[type=range]:active::-webkit-slider-thumb{transform:scale(1.25)}[type=range]:active::-moz-range-thumb{transform:scale(1.25)}[type=range]:active::-ms-thumb{transform:scale(1.25)}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search]{padding-inline-start:calc(var(--pico-form-element-spacing-horizontal) + 1.75rem);background-image:var(--pico-icon-search);background-position:center left calc(var(--pico-form-element-spacing-horizontal) + .125rem);background-size:1rem auto;background-repeat:no-repeat}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid]{padding-inline-start:calc(var(--pico-form-element-spacing-horizontal) + 1.75rem)!important;background-position:center left 1.125rem,center right .75rem}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid=false]{background-image:var(--pico-icon-search),var(--pico-icon-valid)}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid=true]{background-image:var(--pico-icon-search),var(--pico-icon-invalid)}[dir=rtl] :where(input):not([type=checkbox],[type=radio],[type=range],[type=file])[type=search]{background-position:center right 1.125rem}[dir=rtl] :where(input):not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid]{background-position:center right 1.125rem,center left .75rem}details{display:block;margin-bottom:var(--pico-spacing)}details summary{line-height:1rem;list-style-type:none;cursor:pointer;transition:color var(--pico-transition)}details summary:not([role]){color:var(--pico-accordion-close-summary-color)}details summary::-webkit-details-marker{display:none}details summary::marker{display:none}details summary::-moz-list-bullet{list-style-type:none}details summary::after{display:block;width:1rem;height:1rem;margin-inline-start:calc(var(--pico-spacing,1rem) * .5);float:right;transform:rotate(-90deg);background-image:var(--pico-icon-chevron);background-position:right center;background-size:1rem auto;background-repeat:no-repeat;content:"";transition:transform var(--pico-transition)}details summary:focus{outline:0}details summary:focus:not([role]){color:var(--pico-accordion-active-summary-color)}details summary:focus-visible:not([role]){outline:var(--pico-outline-width) solid var(--pico-primary-focus);outline-offset:calc(var(--pico-spacing,1rem) * 0.5);color:var(--pico-primary)}details summary[role=button]{width:100%;text-align:left}details summary[role=button]::after{height:calc(1rem * var(--pico-line-height,1.5))}details[open]>summary{margin-bottom:var(--pico-spacing)}details[open]>summary:not([role]):not(:focus){color:var(--pico-accordion-open-summary-color)}details[open]>summary::after{transform:rotate(0)}[dir=rtl] details summary{text-align:right}[dir=rtl] details summary::after{float:left;background-position:left center}article{margin-bottom:var(--pico-block-spacing-vertical);padding:var(--pico-block-spacing-vertical) var(--pico-block-spacing-horizontal);border-radius:var(--pico-border-radius);background:var(--pico-card-background-color);box-shadow:var(--pico-card-box-shadow)}article>footer,article>header{margin-right:calc(var(--pico-block-spacing-horizontal) * -1);margin-left:calc(var(--pico-block-spacing-horizontal) * -1);padding:calc(var(--pico-block-spacing-vertical) * .66) var(--pico-block-spacing-horizontal);background-color:var(--pico-card-sectioning-background-color)}article>header{margin-top:calc(var(--pico-block-spacing-vertical) * -1);margin-bottom:var(--pico-block-spacing-vertical);border-bottom:var(--pico-border-width) solid var(--pico-card-border-color);border-top-right-radius:var(--pico-border-radius);border-top-left-radius:var(--pico-border-radius)}article>footer{margin-top:var(--pico-block-spacing-vertical);margin-bottom:calc(var(--pico-block-spacing-vertical) * -1);border-top:var(--pico-border-width) solid var(--pico-card-border-color);border-bottom-right-radius:var(--pico-border-radius);border-bottom-left-radius:var(--pico-border-radius)}details.dropdown{position:relative;border-bottom:none}details.dropdown summary::after,details.dropdown>a::after,details.dropdown>button::after{display:block;width:1rem;height:calc(1rem * var(--pico-line-height,1.5));margin-inline-start:.25rem;float:right;transform:rotate(0) translateX(.2rem);background-image:var(--pico-icon-chevron);background-position:right center;background-size:1rem auto;background-repeat:no-repeat;content:""}nav details.dropdown{margin-bottom:0}details.dropdown summary:not([role]){height:calc(1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2);padding:var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal);border:var(--pico-border-width) solid var(--pico-form-element-border-color);border-radius:var(--pico-border-radius);background-color:var(--pico-form-element-background-color);color:var(--pico-form-element-placeholder-color);line-height:inherit;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;transition:background-color var(--pico-transition),border-color var(--pico-transition),color var(--pico-transition),box-shadow var(--pico-transition)}details.dropdown summary:not([role]):active,details.dropdown summary:not([role]):focus{border-color:var(--pico-form-element-active-border-color);background-color:var(--pico-form-element-active-background-color)}details.dropdown summary:not([role]):focus{box-shadow:0 0 0 var(--pico-outline-width) var(--pico-form-element-focus-color)}details.dropdown summary:not([role]):focus-visible{outline:0}details.dropdown summary:not([role])[aria-invalid=false]{--pico-form-element-border-color:var(--pico-form-element-valid-border-color);--pico-form-element-active-border-color:var(--pico-form-element-valid-focus-color);--pico-form-element-focus-color:var(--pico-form-element-valid-focus-color)}details.dropdown summary:not([role])[aria-invalid=true]{--pico-form-element-border-color:var(--pico-form-element-invalid-border-color);--pico-form-element-active-border-color:var(--pico-form-element-invalid-focus-color);--pico-form-element-focus-color:var(--pico-form-element-invalid-focus-color)}nav details.dropdown{display:inline;margin:calc(var(--pico-nav-element-spacing-vertical) * -1) 0}nav details.dropdown summary::after{transform:rotate(0) translateX(0)}nav details.dropdown summary:not([role]){height:calc(1rem * var(--pico-line-height) + var(--pico-nav-link-spacing-vertical) * 2);padding:calc(var(--pico-nav-link-spacing-vertical) - var(--pico-border-width) * 2) var(--pico-nav-link-spacing-horizontal)}nav details.dropdown summary:not([role]):focus-visible{box-shadow:0 0 0 var(--pico-outline-width) var(--pico-primary-focus)}details.dropdown summary+ul{display:flex;z-index:99;position:absolute;left:0;flex-direction:column;width:100%;min-width:-moz-fit-content;min-width:fit-content;margin:0;margin-top:var(--pico-outline-width);padding:0;border:var(--pico-border-width) solid var(--pico-dropdown-border-color);border-radius:var(--pico-border-radius);background-color:var(--pico-dropdown-background-color);box-shadow:var(--pico-dropdown-box-shadow);color:var(--pico-dropdown-color);white-space:nowrap;opacity:0;transition:opacity var(--pico-transition),transform 0s ease-in-out 1s}details.dropdown summary+ul[dir=rtl]{right:0;left:auto}details.dropdown summary+ul li{width:100%;margin-bottom:0;padding:calc(var(--pico-form-element-spacing-vertical) * .5) var(--pico-form-element-spacing-horizontal);list-style:none}details.dropdown summary+ul li:first-of-type{margin-top:calc(var(--pico-form-element-spacing-vertical) * .5)}details.dropdown summary+ul li:last-of-type{margin-bottom:calc(var(--pico-form-element-spacing-vertical) * .5)}details.dropdown summary+ul li a{display:block;margin:calc(var(--pico-form-element-spacing-vertical) * -.5) calc(var(--pico-form-element-spacing-horizontal) * -1);padding:calc(var(--pico-form-element-spacing-vertical) * .5) var(--pico-form-element-spacing-horizontal);overflow:hidden;border-radius:0;color:var(--pico-dropdown-color);text-decoration:none;text-overflow:ellipsis}details.dropdown summary+ul li a:active,details.dropdown summary+ul li a:focus,details.dropdown summary+ul li a:focus-visible,details.dropdown summary+ul li a:hover,details.dropdown summary+ul li a[aria-current]:not([aria-current=false]){background-color:var(--pico-dropdown-hover-background-color)}details.dropdown summary+ul li label{width:100%}details.dropdown summary+ul li:has(label):hover{background-color:var(--pico-dropdown-hover-background-color)}details.dropdown[open] summary{margin-bottom:0}details.dropdown[open] summary+ul{transform:scaleY(1);opacity:1;transition:opacity var(--pico-transition),transform 0s ease-in-out 0s}details.dropdown[open] summary::before{display:block;z-index:1;position:fixed;width:100vw;height:100vh;inset:0;background:0 0;content:"";cursor:default}label>details.dropdown{margin-top:calc(var(--pico-spacing) * .25)}[role=group],[role=search]{display:inline-flex;position:relative;width:100%;margin-bottom:var(--pico-spacing);border-radius:var(--pico-border-radius);box-shadow:var(--pico-group-box-shadow,0 0 0 transparent);vertical-align:middle;transition:box-shadow var(--pico-transition)}[role=group] input:not([type=checkbox],[type=radio]),[role=group] select,[role=group]>*,[role=search] input:not([type=checkbox],[type=radio]),[role=search] select,[role=search]>*{position:relative;flex:1 1 auto;margin-bottom:0}[role=group] input:not([type=checkbox],[type=radio]):not(:first-child),[role=group] select:not(:first-child),[role=group]>:not(:first-child),[role=search] input:not([type=checkbox],[type=radio]):not(:first-child),[role=search] select:not(:first-child),[role=search]>:not(:first-child){margin-left:0;border-top-left-radius:0;border-bottom-left-radius:0}[role=group] input:not([type=checkbox],[type=radio]):not(:last-child),[role=group] select:not(:last-child),[role=group]>:not(:last-child),[role=search] input:not([type=checkbox],[type=radio]):not(:last-child),[role=search] select:not(:last-child),[role=search]>:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}[role=group] input:not([type=checkbox],[type=radio]):focus,[role=group] select:focus,[role=group]>:focus,[role=search] input:not([type=checkbox],[type=radio]):focus,[role=search] select:focus,[role=search]>:focus{z-index:2}[role=group] [role=button]:not(:first-child),[role=group] [type=button]:not(:first-child),[role=group] [type=reset]:not(:first-child),[role=group] [type=submit]:not(:first-child),[role=group] button:not(:first-child),[role=group] input:not([type=checkbox],[type=radio]):not(:first-child),[role=group] select:not(:first-child),[role=search] [role=button]:not(:first-child),[role=search] [type=button]:not(:first-child),[role=search] [type=reset]:not(:first-child),[role=search] [type=submit]:not(:first-child),[role=search] button:not(:first-child),[role=search] input:not([type=checkbox],[type=radio]):not(:first-child),[role=search] select:not(:first-child){margin-left:calc(var(--pico-border-width) * -1)}[role=group] [role=button],[role=group] [type=button],[role=group] [type=reset],[role=group] [type=submit],[role=group] button,[role=search] [role=button],[role=search] [type=button],[role=search] [type=reset],[role=search] [type=submit],[role=search] button{width:auto}@supports selector(:has(*)){[role=group]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus),[role=search]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus){--pico-group-box-shadow:var(--pico-group-box-shadow-focus-with-button)}[role=group]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) input:not([type=checkbox],[type=radio]),[role=group]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) select,[role=search]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) input:not([type=checkbox],[type=radio]),[role=search]:has(button:focus,[type=submit]:focus,[type=button]:focus,[role=button]:focus) select{border-color:transparent}[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus),[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus){--pico-group-box-shadow:var(--pico-group-box-shadow-focus-with-input)}[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) [role=button],[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=button],[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=submit],[role=group]:has(input:not([type=submit],[type=button]):focus,select:focus) button,[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) [role=button],[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=button],[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) [type=submit],[role=search]:has(input:not([type=submit],[type=button]):focus,select:focus) button{--pico-button-box-shadow:0 0 0 var(--pico-border-width) var(--pico-primary-border);--pico-button-hover-box-shadow:0 0 0 var(--pico-border-width) var(--pico-primary-hover-border)}[role=group] [role=button]:focus,[role=group] [type=button]:focus,[role=group] [type=reset]:focus,[role=group] [type=submit]:focus,[role=group] button:focus,[role=search] [role=button]:focus,[role=search] [type=button]:focus,[role=search] [type=reset]:focus,[role=search] [type=submit]:focus,[role=search] button:focus{box-shadow:none}}[role=search]>:first-child{border-top-left-radius:5rem;border-bottom-left-radius:5rem}[role=search]>:last-child{border-top-right-radius:5rem;border-bottom-right-radius:5rem}[aria-busy=true]:not(input,select,textarea,html){white-space:nowrap}[aria-busy=true]:not(input,select,textarea,html)::before{display:inline-block;width:1em;height:1em;background-image:var(--pico-icon-loading);background-size:1em auto;background-repeat:no-repeat;content:"";vertical-align:-.125em}[aria-busy=true]:not(input,select,textarea,html):not(:empty)::before{margin-inline-end:calc(var(--pico-spacing) * .5)}[aria-busy=true]:not(input,select,textarea,html):empty{text-align:center}[role=button][aria-busy=true],[type=button][aria-busy=true],[type=reset][aria-busy=true],[type=submit][aria-busy=true],a[aria-busy=true],button[aria-busy=true]{pointer-events:none}:root{--pico-scrollbar-width:0px}dialog{display:flex;z-index:999;position:fixed;top:0;right:0;bottom:0;left:0;align-items:center;justify-content:center;width:inherit;min-width:100%;height:inherit;min-height:100%;padding:0;border:0;-webkit-backdrop-filter:var(--pico-modal-overlay-backdrop-filter);backdrop-filter:var(--pico-modal-overlay-backdrop-filter);background-color:var(--pico-modal-overlay-background-color);color:var(--pico-color)}dialog article{width:100%;max-height:calc(100vh - var(--pico-spacing) * 2);margin:var(--pico-spacing);overflow:auto}@media (min-width:576px){dialog article{max-width:510px}}@media (min-width:768px){dialog article{max-width:700px}}dialog article>header>*{margin-bottom:0}dialog article>header .close,dialog article>header :is(a,button)[rel=prev]{margin:0;margin-left:var(--pico-spacing);padding:0;float:right}dialog article>footer{text-align:right}dialog article>footer [role=button],dialog article>footer button{margin-bottom:0}dialog article>footer [role=button]:not(:first-of-type),dialog article>footer button:not(:first-of-type){margin-left:calc(var(--pico-spacing) * .5)}dialog article .close,dialog article :is(a,button)[rel=prev]{display:block;width:1rem;height:1rem;margin-top:calc(var(--pico-spacing) * -1);margin-bottom:var(--pico-spacing);margin-left:auto;border:none;background-image:var(--pico-icon-close);background-position:center;background-size:auto 1rem;background-repeat:no-repeat;background-color:transparent;opacity:.5;transition:opacity var(--pico-transition)}dialog article .close:is([aria-current]:not([aria-current=false]),:hover,:active,:focus),dialog article :is(a,button)[rel=prev]:is([aria-current]:not([aria-current=false]),:hover,:active,:focus){opacity:1}dialog:not([open]),dialog[open=false]{display:none}.modal-is-open{padding-right:var(--pico-scrollbar-width,0);overflow:hidden;pointer-events:none;touch-action:none}.modal-is-open dialog{pointer-events:auto;touch-action:auto}:where(.modal-is-opening,.modal-is-closing) dialog,:where(.modal-is-opening,.modal-is-closing) dialog>article{animation-duration:.2s;animation-timing-function:ease-in-out;animation-fill-mode:both}:where(.modal-is-opening,.modal-is-closing) dialog{animation-duration:.8s;animation-name:modal-overlay}:where(.modal-is-opening,.modal-is-closing) dialog>article{animation-delay:.2s;animation-name:modal}.modal-is-closing dialog,.modal-is-closing dialog>article{animation-delay:0s;animation-direction:reverse}@keyframes modal-overlay{from{-webkit-backdrop-filter:none;backdrop-filter:none;background-color:transparent}}@keyframes modal{from{transform:translateY(-100%);opacity:0}}:where(nav li)::before{float:left;content:"​"}nav,nav ul{display:flex}nav{justify-content:space-between;overflow:visible}nav ol,nav ul{align-items:center;margin-bottom:0;padding:0;list-style:none}nav ol:first-of-type,nav ul:first-of-type{margin-left:calc(var(--pico-nav-element-spacing-horizontal) * -1)}nav ol:last-of-type,nav ul:last-of-type{margin-right:calc(var(--pico-nav-element-spacing-horizontal) * -1)}nav li{display:inline-block;margin:0;padding:var(--pico-nav-element-spacing-vertical) var(--pico-nav-element-spacing-horizontal)}nav li :where(a,[role=link]){display:inline-block;margin:calc(var(--pico-nav-link-spacing-vertical) * -1) calc(var(--pico-nav-link-spacing-horizontal) * -1);padding:var(--pico-nav-link-spacing-vertical) var(--pico-nav-link-spacing-horizontal);border-radius:var(--pico-border-radius)}nav li :where(a,[role=link]):not(:hover){text-decoration:none}nav li [role=button],nav li [type=button],nav li button,nav li input:not([type=checkbox],[type=radio],[type=range],[type=file]),nav li select{height:auto;margin-right:inherit;margin-bottom:0;margin-left:inherit;padding:calc(var(--pico-nav-link-spacing-vertical) - var(--pico-border-width) * 2) var(--pico-nav-link-spacing-horizontal)}nav[aria-label=breadcrumb]{align-items:center;justify-content:start}nav[aria-label=breadcrumb] ul li:not(:first-child){margin-inline-start:var(--pico-nav-link-spacing-horizontal)}nav[aria-label=breadcrumb] ul li a{margin:calc(var(--pico-nav-link-spacing-vertical) * -1) 0;margin-inline-start:calc(var(--pico-nav-link-spacing-horizontal) * -1)}nav[aria-label=breadcrumb] ul li:not(:last-child)::after{display:inline-block;position:absolute;width:calc(var(--pico-nav-link-spacing-horizontal) * 4);margin:0 calc(var(--pico-nav-link-spacing-horizontal) * -1);content:var(--pico-nav-breadcrumb-divider);color:var(--pico-muted-color);text-align:center;text-decoration:none;white-space:nowrap}nav[aria-label=breadcrumb] a[aria-current]:not([aria-current=false]){background-color:transparent;color:inherit;text-decoration:none;pointer-events:none}aside li,aside nav,aside ol,aside ul{display:block}aside li{padding:calc(var(--pico-nav-element-spacing-vertical) * .5) var(--pico-nav-element-spacing-horizontal)}aside li a{display:block}aside li [role=button]{margin:inherit}[dir=rtl] nav[aria-label=breadcrumb] ul li:not(:last-child) ::after{content:"\\"}progress{display:inline-block;vertical-align:baseline}progress{-webkit-appearance:none;-moz-appearance:none;display:inline-block;appearance:none;width:100%;height:.5rem;margin-bottom:calc(var(--pico-spacing) * .5);overflow:hidden;border:0;border-radius:var(--pico-border-radius);background-color:var(--pico-progress-background-color);color:var(--pico-progress-color)}progress::-webkit-progress-bar{border-radius:var(--pico-border-radius);background:0 0}progress[value]::-webkit-progress-value{background-color:var(--pico-progress-color);-webkit-transition:inline-size var(--pico-transition);transition:inline-size var(--pico-transition)}progress::-moz-progress-bar{background-color:var(--pico-progress-color)}@media (prefers-reduced-motion:no-preference){progress:indeterminate{background:var(--pico-progress-background-color) linear-gradient(to right,var(--pico-progress-color) 30%,var(--pico-progress-background-color) 30%) top left/150% 150% no-repeat;animation:progress-indeterminate 1s linear infinite}progress:indeterminate[value]::-webkit-progress-value{background-color:transparent}progress:indeterminate::-moz-progress-bar{background-color:transparent}}@media (prefers-reduced-motion:no-preference){[dir=rtl] progress:indeterminate{animation-direction:reverse}}@keyframes progress-indeterminate{0%{background-position:200% 0}100%{background-position:-200% 0}}[data-tooltip]{position:relative}[data-tooltip]:not(a,button,input){border-bottom:1px dotted;text-decoration:none;cursor:help}[data-tooltip]::after,[data-tooltip]::before,[data-tooltip][data-placement=top]::after,[data-tooltip][data-placement=top]::before{display:block;z-index:99;position:absolute;bottom:100%;left:50%;padding:.25rem .5rem;overflow:hidden;transform:translate(-50%,-.25rem);border-radius:var(--pico-border-radius);background:var(--pico-tooltip-background-color);content:attr(data-tooltip);color:var(--pico-tooltip-color);font-style:normal;font-weight:var(--pico-font-weight);font-size:.875rem;text-decoration:none;text-overflow:ellipsis;white-space:nowrap;opacity:0;pointer-events:none}[data-tooltip]::after,[data-tooltip][data-placement=top]::after{padding:0;transform:translate(-50%,0);border-top:.3rem solid;border-right:.3rem solid transparent;border-left:.3rem solid transparent;border-radius:0;background-color:transparent;content:"";color:var(--pico-tooltip-background-color)}[data-tooltip][data-placement=bottom]::after,[data-tooltip][data-placement=bottom]::before{top:100%;bottom:auto;transform:translate(-50%,.25rem)}[data-tooltip][data-placement=bottom]:after{transform:translate(-50%,-.3rem);border:.3rem solid transparent;border-bottom:.3rem solid}[data-tooltip][data-placement=left]::after,[data-tooltip][data-placement=left]::before{top:50%;right:100%;bottom:auto;left:auto;transform:translate(-.25rem,-50%)}[data-tooltip][data-placement=left]:after{transform:translate(.3rem,-50%);border:.3rem solid transparent;border-left:.3rem solid}[data-tooltip][data-placement=right]::after,[data-tooltip][data-placement=right]::before{top:50%;right:auto;bottom:auto;left:100%;transform:translate(.25rem,-50%)}[data-tooltip][data-placement=right]:after{transform:translate(-.3rem,-50%);border:.3rem solid transparent;border-right:.3rem solid}[data-tooltip]:focus::after,[data-tooltip]:focus::before,[data-tooltip]:hover::after,[data-tooltip]:hover::before{opacity:1}@media (hover:hover) and (pointer:fine){[data-tooltip]:focus::after,[data-tooltip]:focus::before,[data-tooltip]:hover::after,[data-tooltip]:hover::before{--pico-tooltip-slide-to:translate(-50%, -0.25rem);transform:translate(-50%,.75rem);animation-duration:.2s;animation-fill-mode:forwards;animation-name:tooltip-slide;opacity:0}[data-tooltip]:focus::after,[data-tooltip]:hover::after{--pico-tooltip-caret-slide-to:translate(-50%, 0rem);transform:translate(-50%,-.25rem);animation-name:tooltip-caret-slide}[data-tooltip][data-placement=bottom]:focus::after,[data-tooltip][data-placement=bottom]:focus::before,[data-tooltip][data-placement=bottom]:hover::after,[data-tooltip][data-placement=bottom]:hover::before{--pico-tooltip-slide-to:translate(-50%, 0.25rem);transform:translate(-50%,-.75rem);animation-name:tooltip-slide}[data-tooltip][data-placement=bottom]:focus::after,[data-tooltip][data-placement=bottom]:hover::after{--pico-tooltip-caret-slide-to:translate(-50%, -0.3rem);transform:translate(-50%,-.5rem);animation-name:tooltip-caret-slide}[data-tooltip][data-placement=left]:focus::after,[data-tooltip][data-placement=left]:focus::before,[data-tooltip][data-placement=left]:hover::after,[data-tooltip][data-placement=left]:hover::before{--pico-tooltip-slide-to:translate(-0.25rem, -50%);transform:translate(.75rem,-50%);animation-name:tooltip-slide}[data-tooltip][data-placement=left]:focus::after,[data-tooltip][data-placement=left]:hover::after{--pico-tooltip-caret-slide-to:translate(0.3rem, -50%);transform:translate(.05rem,-50%);animation-name:tooltip-caret-slide}[data-tooltip][data-placement=right]:focus::after,[data-tooltip][data-placement=right]:focus::before,[data-tooltip][data-placement=right]:hover::after,[data-tooltip][data-placement=right]:hover::before{--pico-tooltip-slide-to:translate(0.25rem, -50%);transform:translate(-.75rem,-50%);animation-name:tooltip-slide}[data-tooltip][data-placement=right]:focus::after,[data-tooltip][data-placement=right]:hover::after{--pico-tooltip-caret-slide-to:translate(-0.3rem, -50%);transform:translate(-.05rem,-50%);animation-name:tooltip-caret-slide}}@keyframes tooltip-slide{to{transform:var(--pico-tooltip-slide-to);opacity:1}}@keyframes tooltip-caret-slide{50%{opacity:0}to{transform:var(--pico-tooltip-caret-slide-to);opacity:1}}[aria-controls]{cursor:pointer}[aria-disabled=true],[disabled]{cursor:not-allowed}[aria-hidden=false][hidden]{display:initial}[aria-hidden=false][hidden]:not(:focus){clip:rect(0,0,0,0);position:absolute}[tabindex],a,area,button,input,label,select,summary,textarea{-ms-touch-action:manipulation}[dir=rtl]{direction:rtl}@media (prefers-reduced-motion:reduce){:not([aria-busy=true]),:not([aria-busy=true])::after,:not([aria-busy=true])::before{background-attachment:initial!important;animation-duration:1ms!important;animation-delay:-1ms!important;animation-iteration-count:1!important;scroll-behavior:auto!important;transition-delay:0s!important;transition-duration:0s!important}} \ No newline at end of file diff --git a/public/static/css/style.css b/public/static/css/style.css new file mode 100644 index 0000000..0aea06f --- /dev/null +++ b/public/static/css/style.css @@ -0,0 +1,44 @@ +.centered-container { + display: grid; + min-height: 100vh; + place-content: center; +} + +.center { + text-align: center; +} + +.pool-options { + display: grid; + gap: 10px; +} + +.poll-wrapper { + display: grid; + min-height: 100vh; + align-content: center; +} + +.autohide { + animation-name: disapear; + animation-duration: 4000ms; + animation-fill-mode: forwards; +} + +@keyframes disapear{ + 0%{ + opacity: 1; + transform: rotateX(90deg); + } + 50%{ + opacity: 0.5; + transform: rotateX(0deg); + height: auto; + } + 100%{ + display: none; + opacity: 0; + height: 0px; + transform: rotateX(90deg); + } +} diff --git a/public/static/js/htmx.min.js b/public/static/js/htmx.min.js new file mode 100644 index 0000000..c11fbbd --- /dev/null +++ b/public/static/js/htmx.min.js @@ -0,0 +1 @@ +var htmx=function(){"use strict";const Q={onLoad:null,process:null,on:null,off:null,trigger:null,ajax:null,find:null,findAll:null,closest:null,values:function(e,t){const n=cn(e,t||"post");return n.values},remove:null,addClass:null,removeClass:null,toggleClass:null,takeClass:null,swap:null,defineExtension:null,removeExtension:null,logAll:null,logNone:null,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",inlineStyleNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",scrollBehavior:"instant",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get","delete"],selfRequestsOnly:true,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null,disableInheritance:false,responseHandling:[{code:"204",swap:false},{code:"[23]..",swap:true},{code:"[45]..",swap:false,error:true}],allowNestedOobSwaps:true},parseInterval:null,_:null,version:"2.0.2"};Q.onLoad=$;Q.process=Dt;Q.on=be;Q.off=we;Q.trigger=de;Q.ajax=Hn;Q.find=r;Q.findAll=p;Q.closest=g;Q.remove=K;Q.addClass=Y;Q.removeClass=o;Q.toggleClass=W;Q.takeClass=ge;Q.swap=ze;Q.defineExtension=Bn;Q.removeExtension=Un;Q.logAll=z;Q.logNone=J;Q.parseInterval=h;Q._=_;const n={addTriggerHandler:Et,bodyContains:le,canAccessLocalStorage:j,findThisElement:Ee,filterValues:hn,swap:ze,hasAttribute:s,getAttributeValue:te,getClosestAttributeValue:re,getClosestMatch:T,getExpressionVars:Cn,getHeaders:dn,getInputValues:cn,getInternalData:ie,getSwapSpecification:pn,getTriggerSpecs:lt,getTarget:Ce,makeFragment:D,mergeObjects:ue,makeSettleInfo:xn,oobSwap:Te,querySelectorExt:ae,settleImmediately:Gt,shouldCancel:ht,triggerEvent:de,triggerErrorEvent:fe,withExtensions:Bt};const v=["get","post","put","delete","patch"];const O=v.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");const R=e("head");function e(e,t=false){return new RegExp(`<${e}(\\s[^>]*>|>)([\\s\\S]*?)<\\/${e}>`,t?"gim":"im")}function h(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e instanceof Element&&e.getAttribute(t)}function s(e,t){return!!e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function te(e,t){return ee(e,t)||ee(e,"data-"+t)}function u(e){const t=e.parentElement;if(!t&&e.parentNode instanceof ShadowRoot)return e.parentNode;return t}function ne(){return document}function H(e,t){return e.getRootNode?e.getRootNode({composed:t}):ne()}function T(e,t){while(e&&!t(e)){e=u(e)}return e||null}function q(e,t,n){const r=te(t,n);const o=te(t,"hx-disinherit");var i=te(t,"hx-inherit");if(e!==t){if(Q.config.disableInheritance){if(i&&(i==="*"||i.split(" ").indexOf(n)>=0)){return r}else{return null}}if(o&&(o==="*"||o.split(" ").indexOf(n)>=0)){return"unset"}}return r}function re(t,n){let r=null;T(t,function(e){return!!(r=q(t,ce(e),n))});if(r!=="unset"){return r}}function f(e,t){const n=e instanceof Element&&(e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector);return!!n&&n.call(e,t)}function L(e){const t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;const n=t.exec(e);if(n){return n[1].toLowerCase()}else{return""}}function N(e){const t=new DOMParser;return t.parseFromString(e,"text/html")}function A(e,t){while(t.childNodes.length>0){e.append(t.childNodes[0])}}function I(e){const t=ne().createElement("script");se(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}return t}function P(e){return e.matches("script")&&(e.type==="text/javascript"||e.type==="module"||e.type==="")}function k(e){Array.from(e.querySelectorAll("script")).forEach(e=>{if(P(e)){const t=I(e);const n=e.parentNode;try{n.insertBefore(t,e)}catch(e){w(e)}finally{e.remove()}}})}function D(e){const t=e.replace(R,"");const n=L(t);let r;if(n==="html"){r=new DocumentFragment;const i=N(e);A(r,i.body);r.title=i.title}else if(n==="body"){r=new DocumentFragment;const i=N(t);A(r,i.body);r.title=i.title}else{const i=N('");r=i.querySelector("template").content;r.title=i.title;var o=r.querySelector("title");if(o&&o.parentNode===r){o.remove();r.title=o.innerText}}if(r){if(Q.config.allowScriptTags){k(r)}else{r.querySelectorAll("script").forEach(e=>e.remove())}}return r}function oe(e){if(e){e()}}function t(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function M(e){return typeof e==="function"}function X(e){return t(e,"Object")}function ie(e){const t="htmx-internal-data";let n=e[t];if(!n){n=e[t]={}}return n}function F(t){const n=[];if(t){for(let e=0;e=0}function le(e){const t=e.getRootNode&&e.getRootNode();if(t&&t instanceof window.ShadowRoot){return ne().body.contains(t.host)}else{return ne().body.contains(e)}}function U(e){return e.trim().split(/\s+/)}function ue(e,t){for(const n in t){if(t.hasOwnProperty(n)){e[n]=t[n]}}return e}function S(e){try{return JSON.parse(e)}catch(e){w(e);return null}}function j(){const e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function V(t){try{const e=new URL(t);if(e){t=e.pathname+e.search}if(!/^\/$/.test(t)){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function _(e){return vn(ne().body,function(){return eval(e)})}function $(t){const e=Q.on("htmx:load",function(e){t(e.detail.elt)});return e}function z(){Q.logger=function(e,t,n){if(console){console.log(t,e,n)}}}function J(){Q.logger=null}function r(e,t){if(typeof e!=="string"){return e.querySelector(t)}else{return r(ne(),e)}}function p(e,t){if(typeof e!=="string"){return e.querySelectorAll(t)}else{return p(ne(),e)}}function E(){return window}function K(e,t){e=y(e);if(t){E().setTimeout(function(){K(e);e=null},t)}else{u(e).removeChild(e)}}function ce(e){return e instanceof Element?e:null}function G(e){return e instanceof HTMLElement?e:null}function Z(e){return typeof e==="string"?e:null}function d(e){return e instanceof Element||e instanceof Document||e instanceof DocumentFragment?e:null}function Y(e,t,n){e=ce(y(e));if(!e){return}if(n){E().setTimeout(function(){Y(e,t);e=null},n)}else{e.classList&&e.classList.add(t)}}function o(e,t,n){let r=ce(y(e));if(!r){return}if(n){E().setTimeout(function(){o(r,t);r=null},n)}else{if(r.classList){r.classList.remove(t);if(r.classList.length===0){r.removeAttribute("class")}}}}function W(e,t){e=y(e);e.classList.toggle(t)}function ge(e,t){e=y(e);se(e.parentElement.children,function(e){o(e,t)});Y(ce(e),t)}function g(e,t){e=ce(y(e));if(e&&e.closest){return e.closest(t)}else{do{if(e==null||f(e,t)){return e}}while(e=e&&ce(u(e)));return null}}function l(e,t){return e.substring(0,t.length)===t}function pe(e,t){return e.substring(e.length-t.length)===t}function i(e){const t=e.trim();if(l(t,"<")&&pe(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function m(e,t,n){e=y(e);if(t.indexOf("closest ")===0){return[g(ce(e),i(t.substr(8)))]}else if(t.indexOf("find ")===0){return[r(d(e),i(t.substr(5)))]}else if(t==="next"){return[ce(e).nextElementSibling]}else if(t.indexOf("next ")===0){return[me(e,i(t.substr(5)),!!n)]}else if(t==="previous"){return[ce(e).previousElementSibling]}else if(t.indexOf("previous ")===0){return[ye(e,i(t.substr(9)),!!n)]}else if(t==="document"){return[document]}else if(t==="window"){return[window]}else if(t==="body"){return[document.body]}else if(t==="root"){return[H(e,!!n)]}else if(t.indexOf("global ")===0){return m(e,t.slice(7),true)}else{return F(d(H(e,!!n)).querySelectorAll(i(t)))}}var me=function(t,e,n){const r=d(H(t,n)).querySelectorAll(e);for(let e=0;e=0;e--){const o=r[e];if(o.compareDocumentPosition(t)===Node.DOCUMENT_POSITION_FOLLOWING){return o}}};function ae(e,t){if(typeof e!=="string"){return m(e,t)[0]}else{return m(ne().body,e)[0]}}function y(e,t){if(typeof e==="string"){return r(d(t)||document,e)}else{return e}}function xe(e,t,n){if(M(t)){return{target:ne().body,event:Z(e),listener:t}}else{return{target:y(e),event:Z(t),listener:n}}}function be(t,n,r){_n(function(){const e=xe(t,n,r);e.target.addEventListener(e.event,e.listener)});const e=M(n);return e?n:r}function we(t,n,r){_n(function(){const e=xe(t,n,r);e.target.removeEventListener(e.event,e.listener)});return M(n)?n:r}const ve=ne().createElement("output");function Se(e,t){const n=re(e,t);if(n){if(n==="this"){return[Ee(e,t)]}else{const r=m(e,n);if(r.length===0){w('The selector "'+n+'" on '+t+" returned no matches!");return[ve]}else{return r}}}}function Ee(e,t){return ce(T(e,function(e){return te(ce(e),t)!=null}))}function Ce(e){const t=re(e,"hx-target");if(t){if(t==="this"){return Ee(e,"hx-target")}else{return ae(e,t)}}else{const n=ie(e);if(n.boosted){return ne().body}else{return e}}}function Oe(t){const n=Q.config.attributesToSettle;for(let e=0;e0){s=e.substr(0,e.indexOf(":"));t=e.substr(e.indexOf(":")+1,e.length)}else{s=e}const n=ne().querySelectorAll(t);if(n){se(n,function(e){let t;const n=o.cloneNode(true);t=ne().createDocumentFragment();t.appendChild(n);if(!He(s,e)){t=d(n)}const r={shouldSwap:true,target:e,fragment:t};if(!de(e,"htmx:oobBeforeSwap",r))return;e=r.target;if(r.shouldSwap){_e(s,e,e,t,i)}se(i.elts,function(e){de(e,"htmx:oobAfterSwap",r)})});o.parentNode.removeChild(o)}else{o.parentNode.removeChild(o);fe(ne().body,"htmx:oobErrorNoTarget",{content:o})}return e}function qe(e){se(p(e,"[hx-preserve], [data-hx-preserve]"),function(e){const t=te(e,"id");const n=ne().getElementById(t);if(n!=null){e.parentNode.replaceChild(n,e)}})}function Le(l,e,u){se(e.querySelectorAll("[id]"),function(t){const n=ee(t,"id");if(n&&n.length>0){const r=n.replace("'","\\'");const o=t.tagName.replace(":","\\:");const e=d(l);const i=e&&e.querySelector(o+"[id='"+r+"']");if(i&&i!==e){const s=t.cloneNode();Re(t,i);u.tasks.push(function(){Re(t,s)})}}})}function Ne(e){return function(){o(e,Q.config.addedClass);Dt(ce(e));Ae(d(e));de(e,"htmx:load")}}function Ae(e){const t="[autofocus]";const n=G(f(e,t)?e:e.querySelector(t));if(n!=null){n.focus()}}function c(e,t,n,r){Le(e,n,r);while(n.childNodes.length>0){const o=n.firstChild;Y(ce(o),Q.config.addedClass);e.insertBefore(o,t);if(o.nodeType!==Node.TEXT_NODE&&o.nodeType!==Node.COMMENT_NODE){r.tasks.push(Ne(o))}}}function Ie(e,t){let n=0;while(n0}function ze(e,t,r,o){if(!o){o={}}e=y(e);const n=document.activeElement;let i={};try{i={elt:n,start:n?n.selectionStart:null,end:n?n.selectionEnd:null}}catch(e){}const s=xn(e);if(r.swapStyle==="textContent"){e.textContent=t}else{let n=D(t);s.title=n.title;if(o.selectOOB){const u=o.selectOOB.split(",");for(let t=0;t0){E().setTimeout(l,r.settleDelay)}else{l()}}function Je(e,t,n){const r=e.getResponseHeader(t);if(r.indexOf("{")===0){const o=S(r);for(const i in o){if(o.hasOwnProperty(i)){let e=o[i];if(X(e)){n=e.target!==undefined?e.target:n}else{e={value:e}}de(n,i,e)}}}else{const s=r.split(",");for(let e=0;e0){const s=o[0];if(s==="]"){e--;if(e===0){if(n===null){t=t+"true"}o.shift();t+=")})";try{const l=vn(r,function(){return Function(t)()},function(){return true});l.source=t;return l}catch(e){fe(ne().body,"htmx:syntax:error",{error:e,source:t});return null}}}else if(s==="["){e++}if(nt(s,n,i)){t+="(("+i+"."+s+") ? ("+i+"."+s+") : (window."+s+"))"}else{t=t+s}n=o.shift()}}}function b(e,t){let n="";while(e.length>0&&!t.test(e[0])){n+=e.shift()}return n}function ot(e){let t;if(e.length>0&&Qe.test(e[0])){e.shift();t=b(e,et).trim();e.shift()}else{t=b(e,x)}return t}const it="input, textarea, select";function st(e,t,n){const r=[];const o=tt(t);do{b(o,We);const l=o.length;const u=b(o,/[,\[\s]/);if(u!==""){if(u==="every"){const c={trigger:"every"};b(o,We);c.pollInterval=h(b(o,/[,\[\s]/));b(o,We);var i=rt(e,o,"event");if(i){c.eventFilter=i}r.push(c)}else{const a={trigger:u};var i=rt(e,o,"event");if(i){a.eventFilter=i}while(o.length>0&&o[0]!==","){b(o,We);const f=o.shift();if(f==="changed"){a.changed=true}else if(f==="once"){a.once=true}else if(f==="consume"){a.consume=true}else if(f==="delay"&&o[0]===":"){o.shift();a.delay=h(b(o,x))}else if(f==="from"&&o[0]===":"){o.shift();if(Qe.test(o[0])){var s=ot(o)}else{var s=b(o,x);if(s==="closest"||s==="find"||s==="next"||s==="previous"){o.shift();const d=ot(o);if(d.length>0){s+=" "+d}}}a.from=s}else if(f==="target"&&o[0]===":"){o.shift();a.target=ot(o)}else if(f==="throttle"&&o[0]===":"){o.shift();a.throttle=h(b(o,x))}else if(f==="queue"&&o[0]===":"){o.shift();a.queue=b(o,x)}else if(f==="root"&&o[0]===":"){o.shift();a[f]=ot(o)}else if(f==="threshold"&&o[0]===":"){o.shift();a[f]=b(o,x)}else{fe(e,"htmx:syntax:error",{token:o.shift()})}}r.push(a)}}if(o.length===l){fe(e,"htmx:syntax:error",{token:o.shift()})}b(o,We)}while(o[0]===","&&o.shift());if(n){n[t]=r}return r}function lt(e){const t=te(e,"hx-trigger");let n=[];if(t){const r=Q.config.triggerSpecsCache;n=r&&r[t]||st(e,t,r)}if(n.length>0){return n}else if(f(e,"form")){return[{trigger:"submit"}]}else if(f(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(f(e,it)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function ut(e){ie(e).cancelled=true}function ct(e,t,n){const r=ie(e);r.timeout=E().setTimeout(function(){if(le(e)&&r.cancelled!==true){if(!pt(n,e,Xt("hx:poll:trigger",{triggerSpec:n,target:e}))){t(e)}ct(e,t,n)}},n.pollInterval)}function at(e){return location.hostname===e.hostname&&ee(e,"href")&&ee(e,"href").indexOf("#")!==0}function ft(e){return g(e,Q.config.disableSelector)}function dt(t,n,e){if(t instanceof HTMLAnchorElement&&at(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"&&String(ee(t,"method")).toLowerCase()!=="dialog"){n.boosted=true;let r,o;if(t.tagName==="A"){r="get";o=ee(t,"href")}else{const i=ee(t,"method");r=i?i.toLowerCase():"get";if(r==="get"){}o=ee(t,"action")}e.forEach(function(e){mt(t,function(e,t){const n=ce(e);if(ft(n)){a(n);return}he(r,o,n,t)},n,e,true)})}}function ht(e,t){const n=ce(t);if(!n){return false}if(e.type==="submit"||e.type==="click"){if(n.tagName==="FORM"){return true}if(f(n,'input[type="submit"], button')&&g(n,"form")!==null){return true}if(n instanceof HTMLAnchorElement&&n.href&&(n.getAttribute("href")==="#"||n.getAttribute("href").indexOf("#")!==0)){return true}}return false}function gt(e,t){return ie(e).boosted&&e instanceof HTMLAnchorElement&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function pt(e,t,n){const r=e.eventFilter;if(r){try{return r.call(t,n)!==true}catch(e){const o=r.source;fe(ne().body,"htmx:eventFilter:error",{error:e,source:o});return true}}return false}function mt(s,l,e,u,c){const a=ie(s);let t;if(u.from){t=m(s,u.from)}else{t=[s]}if(u.changed){t.forEach(function(e){const t=ie(e);t.lastValue=e.value})}se(t,function(o){const i=function(e){if(!le(s)){o.removeEventListener(u.trigger,i);return}if(gt(s,e)){return}if(c||ht(e,s)){e.preventDefault()}if(pt(u,s,e)){return}const t=ie(e);t.triggerSpec=u;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(s)<0){t.handledFor.push(s);if(u.consume){e.stopPropagation()}if(u.target&&e.target){if(!f(ce(e.target),u.target)){return}}if(u.once){if(a.triggeredOnce){return}else{a.triggeredOnce=true}}if(u.changed){const n=ie(o);const r=o.value;if(n.lastValue===r){return}n.lastValue=r}if(a.delayed){clearTimeout(a.delayed)}if(a.throttle){return}if(u.throttle>0){if(!a.throttle){de(s,"htmx:trigger");l(s,e);a.throttle=E().setTimeout(function(){a.throttle=null},u.throttle)}}else if(u.delay>0){a.delayed=E().setTimeout(function(){de(s,"htmx:trigger");l(s,e)},u.delay)}else{de(s,"htmx:trigger");l(s,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:u.trigger,listener:i,on:o});o.addEventListener(u.trigger,i)})}let yt=false;let xt=null;function bt(){if(!xt){xt=function(){yt=true};window.addEventListener("scroll",xt);setInterval(function(){if(yt){yt=false;se(ne().querySelectorAll("[hx-trigger*='revealed'],[data-hx-trigger*='revealed']"),function(e){wt(e)})}},200)}}function wt(e){if(!s(e,"data-hx-revealed")&&B(e)){e.setAttribute("data-hx-revealed","true");const t=ie(e);if(t.initHash){de(e,"revealed")}else{e.addEventListener("htmx:afterProcessNode",function(){de(e,"revealed")},{once:true})}}}function vt(e,t,n,r){const o=function(){if(!n.loaded){n.loaded=true;t(e)}};if(r>0){E().setTimeout(o,r)}else{o()}}function St(t,n,e){let i=false;se(v,function(r){if(s(t,"hx-"+r)){const o=te(t,"hx-"+r);i=true;n.path=o;n.verb=r;e.forEach(function(e){Et(t,e,n,function(e,t){const n=ce(e);if(g(n,Q.config.disableSelector)){a(n);return}he(r,o,n,t)})})}});return i}function Et(r,e,t,n){if(e.trigger==="revealed"){bt();mt(r,n,t,e);wt(ce(r))}else if(e.trigger==="intersect"){const o={};if(e.root){o.root=ae(r,e.root)}if(e.threshold){o.threshold=parseFloat(e.threshold)}const i=new IntersectionObserver(function(t){for(let e=0;e0){t.polling=true;ct(ce(r),n,e)}else{mt(r,n,t,e)}}function Ct(e){const t=ce(e);if(!t){return false}const n=t.attributes;for(let e=0;e", "+e).join(""));return o}else{return[]}}function qt(e){const t=g(ce(e.target),"button, input[type='submit']");const n=Nt(e);if(n){n.lastButtonClicked=t}}function Lt(e){const t=Nt(e);if(t){t.lastButtonClicked=null}}function Nt(e){const t=g(ce(e.target),"button, input[type='submit']");if(!t){return}const n=y("#"+ee(t,"form"),t.getRootNode())||g(t,"form");if(!n){return}return ie(n)}function At(e){e.addEventListener("click",qt);e.addEventListener("focusin",qt);e.addEventListener("focusout",Lt)}function It(t,e,n){const r=ie(t);if(!Array.isArray(r.onHandlers)){r.onHandlers=[]}let o;const i=function(e){vn(t,function(){if(ft(t)){return}if(!o){o=new Function("event",n)}o.call(t,e)})};t.addEventListener(e,i);r.onHandlers.push({event:e,listener:i})}function Pt(t){ke(t);for(let e=0;eQ.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){fe(ne().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function _t(t){if(!j()){return null}t=V(t);const n=S(localStorage.getItem("htmx-history-cache"))||[];for(let e=0;e=200&&this.status<400){de(ne().body,"htmx:historyCacheMissLoad",i);const e=D(this.response);const t=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;const n=jt();const r=xn(n);Dn(e.title);Ve(n,t,r);Gt(r.tasks);Ut=o;de(ne().body,"htmx:historyRestore",{path:o,cacheMiss:true,serverResponse:this.response})}else{fe(ne().body,"htmx:historyCacheMissLoadError",i)}};e.send()}function Yt(e){zt();e=e||location.pathname+location.search;const t=_t(e);if(t){const n=D(t.content);const r=jt();const o=xn(r);Dn(n.title);Ve(r,n,o);Gt(o.tasks);E().setTimeout(function(){window.scrollTo(0,t.scroll)},0);Ut=e;de(ne().body,"htmx:historyRestore",{path:e,item:t})}else{if(Q.config.refreshOnHistoryMiss){window.location.reload(true)}else{Zt(e)}}}function Wt(e){let t=Se(e,"hx-indicator");if(t==null){t=[e]}se(t,function(e){const t=ie(e);t.requestCount=(t.requestCount||0)+1;e.classList.add.call(e.classList,Q.config.requestClass)});return t}function Qt(e){let t=Se(e,"hx-disabled-elt");if(t==null){t=[]}se(t,function(e){const t=ie(e);t.requestCount=(t.requestCount||0)+1;e.setAttribute("disabled","");e.setAttribute("data-disabled-by-htmx","")});return t}function en(e,t){se(e,function(e){const t=ie(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.classList.remove.call(e.classList,Q.config.requestClass)}});se(t,function(e){const t=ie(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.removeAttribute("disabled");e.removeAttribute("data-disabled-by-htmx")}})}function tn(t,n){for(let e=0;en.indexOf(e)<0)}else{e=e.filter(e=>e!==n)}r.delete(t);se(e,e=>r.append(t,e))}}function sn(t,n,r,o,i){if(o==null||tn(t,o)){return}else{t.push(o)}if(nn(o)){const s=ee(o,"name");let e=o.value;if(o instanceof HTMLSelectElement&&o.multiple){e=F(o.querySelectorAll("option:checked")).map(function(e){return e.value})}if(o instanceof HTMLInputElement&&o.files){e=F(o.files)}rn(s,e,n);if(i){ln(o,r)}}if(o instanceof HTMLFormElement){se(o.elements,function(e){if(t.indexOf(e)>=0){on(e.name,e.value,n)}else{t.push(e)}if(i){ln(e,r)}});new FormData(o).forEach(function(e,t){if(e instanceof File&&e.name===""){return}rn(t,e,n)})}}function ln(e,t){const n=e;if(n.willValidate){de(n,"htmx:validation:validate");if(!n.checkValidity()){t.push({elt:n,message:n.validationMessage,validity:n.validity});de(n,"htmx:validation:failed",{message:n.validationMessage,validity:n.validity})}}}function un(n,e){for(const t of e.keys()){n.delete(t)}e.forEach(function(e,t){n.append(t,e)});return n}function cn(e,t){const n=[];const r=new FormData;const o=new FormData;const i=[];const s=ie(e);if(s.lastButtonClicked&&!le(s.lastButtonClicked)){s.lastButtonClicked=null}let l=e instanceof HTMLFormElement&&e.noValidate!==true||te(e,"hx-validate")==="true";if(s.lastButtonClicked){l=l&&s.lastButtonClicked.formNoValidate!==true}if(t!=="get"){sn(n,o,i,g(e,"form"),l)}sn(n,r,i,e,l);if(s.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&ee(e,"type")==="submit"){const c=s.lastButtonClicked||e;const a=ee(c,"name");rn(a,c.value,o)}const u=Se(e,"hx-include");se(u,function(e){sn(n,r,i,ce(e),l);if(!f(e,"form")){se(d(e).querySelectorAll(it),function(e){sn(n,r,i,e,l)})}});un(r,o);return{errors:i,formData:r,values:An(r)}}function an(e,t,n){if(e!==""){e+="&"}if(String(n)==="[object Object]"){n=JSON.stringify(n)}const r=encodeURIComponent(n);e+=encodeURIComponent(t)+"="+r;return e}function fn(e){e=Ln(e);let n="";e.forEach(function(e,t){n=an(n,t,e)});return n}function dn(e,t,n){const r={"HX-Request":"true","HX-Trigger":ee(e,"id"),"HX-Trigger-Name":ee(e,"name"),"HX-Target":te(t,"id"),"HX-Current-URL":ne().location.href};wn(e,"hx-headers",false,r);if(n!==undefined){r["HX-Prompt"]=n}if(ie(e).boosted){r["HX-Boosted"]="true"}return r}function hn(n,e){const t=re(e,"hx-params");if(t){if(t==="none"){return new FormData}else if(t==="*"){return n}else if(t.indexOf("not ")===0){se(t.substr(4).split(","),function(e){e=e.trim();n.delete(e)});return n}else{const r=new FormData;se(t.split(","),function(t){t=t.trim();if(n.has(t)){n.getAll(t).forEach(function(e){r.append(t,e)})}});return r}}else{return n}}function gn(e){return!!ee(e,"href")&&ee(e,"href").indexOf("#")>=0}function pn(e,t){const n=t||re(e,"hx-swap");const r={swapStyle:ie(e).boosted?"innerHTML":Q.config.defaultSwapStyle,swapDelay:Q.config.defaultSwapDelay,settleDelay:Q.config.defaultSettleDelay};if(Q.config.scrollIntoViewOnBoost&&ie(e).boosted&&!gn(e)){r.show="top"}if(n){const s=U(n);if(s.length>0){for(let e=0;e0?o.join(":"):null;r.scroll=c;r.scrollTarget=i}else if(l.indexOf("show:")===0){const a=l.substr(5);var o=a.split(":");const f=o.pop();var i=o.length>0?o.join(":"):null;r.show=f;r.showTarget=i}else if(l.indexOf("focus-scroll:")===0){const d=l.substr("focus-scroll:".length);r.focusScroll=d=="true"}else if(e==0){r.swapStyle=l}else{w("Unknown modifier in hx-swap: "+l)}}}}return r}function mn(e){return re(e,"hx-encoding")==="multipart/form-data"||f(e,"form")&&ee(e,"enctype")==="multipart/form-data"}function yn(t,n,r){let o=null;Bt(n,function(e){if(o==null){o=e.encodeParameters(t,r,n)}});if(o!=null){return o}else{if(mn(n)){return un(new FormData,Ln(r))}else{return fn(r)}}}function xn(e){return{tasks:[],elts:[e]}}function bn(e,t){const n=e[0];const r=e[e.length-1];if(t.scroll){var o=null;if(t.scrollTarget){o=ce(ae(n,t.scrollTarget))}if(t.scroll==="top"&&(n||o)){o=o||n;o.scrollTop=0}if(t.scroll==="bottom"&&(r||o)){o=o||r;o.scrollTop=o.scrollHeight}}if(t.show){var o=null;if(t.showTarget){let e=t.showTarget;if(t.showTarget==="window"){e="body"}o=ce(ae(n,e))}if(t.show==="top"&&(n||o)){o=o||n;o.scrollIntoView({block:"start",behavior:Q.config.scrollBehavior})}if(t.show==="bottom"&&(r||o)){o=o||r;o.scrollIntoView({block:"end",behavior:Q.config.scrollBehavior})}}}function wn(r,e,o,i){if(i==null){i={}}if(r==null){return i}const s=te(r,e);if(s){let e=s.trim();let t=o;if(e==="unset"){return null}if(e.indexOf("javascript:")===0){e=e.substr(11);t=true}else if(e.indexOf("js:")===0){e=e.substr(3);t=true}if(e.indexOf("{")!==0){e="{"+e+"}"}let n;if(t){n=vn(r,function(){return Function("return ("+e+")")()},{})}else{n=S(e)}for(const l in n){if(n.hasOwnProperty(l)){if(i[l]==null){i[l]=n[l]}}}}return wn(ce(u(r)),e,o,i)}function vn(e,t,n){if(Q.config.allowEval){return t()}else{fe(e,"htmx:evalDisallowedError");return n}}function Sn(e,t){return wn(e,"hx-vars",true,t)}function En(e,t){return wn(e,"hx-vals",false,t)}function Cn(e){return ue(Sn(e),En(e))}function On(t,n,r){if(r!==null){try{t.setRequestHeader(n,r)}catch(e){t.setRequestHeader(n,encodeURIComponent(r));t.setRequestHeader(n+"-URI-AutoEncoded","true")}}}function Rn(t){if(t.responseURL&&typeof URL!=="undefined"){try{const e=new URL(t.responseURL);return e.pathname+e.search}catch(e){fe(ne().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function C(e,t){return t.test(e.getAllResponseHeaders())}function Hn(e,t,n){e=e.toLowerCase();if(n){if(n instanceof Element||typeof n==="string"){return he(e,t,null,null,{targetOverride:y(n),returnPromise:true})}else{return he(e,t,y(n.source),n.event,{handler:n.handler,headers:n.headers,values:n.values,targetOverride:y(n.target),swapOverride:n.swap,select:n.select,returnPromise:true})}}else{return he(e,t,null,null,{returnPromise:true})}}function Tn(e){const t=[];while(e){t.push(e);e=e.parentElement}return t}function qn(e,t,n){let r;let o;if(typeof URL==="function"){o=new URL(t,document.location.href);const i=document.location.origin;r=i===o.origin}else{o=t;r=l(t,document.location.origin)}if(Q.config.selfRequestsOnly){if(!r){return false}}return de(e,"htmx:validateUrl",ue({url:o,sameHost:r},n))}function Ln(e){if(e instanceof FormData)return e;const t=new FormData;for(const n in e){if(e.hasOwnProperty(n)){if(typeof e[n].forEach==="function"){e[n].forEach(function(e){t.append(n,e)})}else if(typeof e[n]==="object"&&!(e[n]instanceof Blob)){t.append(n,JSON.stringify(e[n]))}else{t.append(n,e[n])}}}return t}function Nn(r,o,e){return new Proxy(e,{get:function(t,e){if(typeof e==="number")return t[e];if(e==="length")return t.length;if(e==="push"){return function(e){t.push(e);r.append(o,e)}}if(typeof t[e]==="function"){return function(){t[e].apply(t,arguments);r.delete(o);t.forEach(function(e){r.append(o,e)})}}if(t[e]&&t[e].length===1){return t[e][0]}else{return t[e]}},set:function(e,t,n){e[t]=n;r.delete(o);e.forEach(function(e){r.append(o,e)});return true}})}function An(r){return new Proxy(r,{get:function(e,t){if(typeof t==="symbol"){return Reflect.get(e,t)}if(t==="toJSON"){return()=>Object.fromEntries(r)}if(t in e){if(typeof e[t]==="function"){return function(){return r[t].apply(r,arguments)}}else{return e[t]}}const n=r.getAll(t);if(n.length===0){return undefined}else if(n.length===1){return n[0]}else{return Nn(e,t,n)}},set:function(t,n,e){if(typeof n!=="string"){return false}t.delete(n);if(typeof e.forEach==="function"){e.forEach(function(e){t.append(n,e)})}else if(typeof e==="object"&&!(e instanceof Blob)){t.append(n,JSON.stringify(e))}else{t.append(n,e)}return true},deleteProperty:function(e,t){if(typeof t==="string"){e.delete(t)}return true},ownKeys:function(e){return Reflect.ownKeys(Object.fromEntries(e))},getOwnPropertyDescriptor:function(e,t){return Reflect.getOwnPropertyDescriptor(Object.fromEntries(e),t)}})}function he(t,n,r,o,i,D){let s=null;let l=null;i=i!=null?i:{};if(i.returnPromise&&typeof Promise!=="undefined"){var e=new Promise(function(e,t){s=e;l=t})}if(r==null){r=ne().body}const M=i.handler||Mn;const X=i.select||null;if(!le(r)){oe(s);return e}const u=i.targetOverride||ce(Ce(r));if(u==null||u==ve){fe(r,"htmx:targetError",{target:te(r,"hx-target")});oe(l);return e}let c=ie(r);const a=c.lastButtonClicked;if(a){const L=ee(a,"formaction");if(L!=null){n=L}const N=ee(a,"formmethod");if(N!=null){if(N.toLowerCase()!=="dialog"){t=N}}}const f=re(r,"hx-confirm");if(D===undefined){const K=function(e){return he(t,n,r,o,i,!!e)};const G={target:u,elt:r,path:n,verb:t,triggeringEvent:o,etc:i,issueRequest:K,question:f};if(de(r,"htmx:confirm",G)===false){oe(s);return e}}let d=r;let h=re(r,"hx-sync");let g=null;let F=false;if(h){const A=h.split(":");const I=A[0].trim();if(I==="this"){d=Ee(r,"hx-sync")}else{d=ce(ae(r,I))}h=(A[1]||"drop").trim();c=ie(d);if(h==="drop"&&c.xhr&&c.abortable!==true){oe(s);return e}else if(h==="abort"){if(c.xhr){oe(s);return e}else{F=true}}else if(h==="replace"){de(d,"htmx:abort")}else if(h.indexOf("queue")===0){const Z=h.split(" ");g=(Z[1]||"last").trim()}}if(c.xhr){if(c.abortable){de(d,"htmx:abort")}else{if(g==null){if(o){const P=ie(o);if(P&&P.triggerSpec&&P.triggerSpec.queue){g=P.triggerSpec.queue}}if(g==null){g="last"}}if(c.queuedRequests==null){c.queuedRequests=[]}if(g==="first"&&c.queuedRequests.length===0){c.queuedRequests.push(function(){he(t,n,r,o,i)})}else if(g==="all"){c.queuedRequests.push(function(){he(t,n,r,o,i)})}else if(g==="last"){c.queuedRequests=[];c.queuedRequests.push(function(){he(t,n,r,o,i)})}oe(s);return e}}const p=new XMLHttpRequest;c.xhr=p;c.abortable=F;const m=function(){c.xhr=null;c.abortable=false;if(c.queuedRequests!=null&&c.queuedRequests.length>0){const e=c.queuedRequests.shift();e()}};const B=re(r,"hx-prompt");if(B){var y=prompt(B);if(y===null||!de(r,"htmx:prompt",{prompt:y,target:u})){oe(s);m();return e}}if(f&&!D){if(!confirm(f)){oe(s);m();return e}}let x=dn(r,u,y);if(t!=="get"&&!mn(r)){x["Content-Type"]="application/x-www-form-urlencoded"}if(i.headers){x=ue(x,i.headers)}const U=cn(r,t);let b=U.errors;const j=U.formData;if(i.values){un(j,Ln(i.values))}const V=Ln(Cn(r));const w=un(j,V);let v=hn(w,r);if(Q.config.getCacheBusterParam&&t==="get"){v.set("org.htmx.cache-buster",ee(u,"id")||"true")}if(n==null||n===""){n=ne().location.href}const S=wn(r,"hx-request");const _=ie(r).boosted;let E=Q.config.methodsThatUseUrlParams.indexOf(t)>=0;const C={boosted:_,useUrlParams:E,formData:v,parameters:An(v),unfilteredFormData:w,unfilteredParameters:An(w),headers:x,target:u,verb:t,errors:b,withCredentials:i.credentials||S.credentials||Q.config.withCredentials,timeout:i.timeout||S.timeout||Q.config.timeout,path:n,triggeringEvent:o};if(!de(r,"htmx:configRequest",C)){oe(s);m();return e}n=C.path;t=C.verb;x=C.headers;v=Ln(C.parameters);b=C.errors;E=C.useUrlParams;if(b&&b.length>0){de(r,"htmx:validation:halted",C);oe(s);m();return e}const $=n.split("#");const z=$[0];const O=$[1];let R=n;if(E){R=z;const Y=!v.keys().next().done;if(Y){if(R.indexOf("?")<0){R+="?"}else{R+="&"}R+=fn(v);if(O){R+="#"+O}}}if(!qn(r,R,C)){fe(r,"htmx:invalidPath",C);oe(l);return e}p.open(t.toUpperCase(),R,true);p.overrideMimeType("text/html");p.withCredentials=C.withCredentials;p.timeout=C.timeout;if(S.noHeaders){}else{for(const k in x){if(x.hasOwnProperty(k)){const W=x[k];On(p,k,W)}}}const H={xhr:p,target:u,requestConfig:C,etc:i,boosted:_,select:X,pathInfo:{requestPath:n,finalRequestPath:R,responsePath:null,anchor:O}};p.onload=function(){try{const t=Tn(r);H.pathInfo.responsePath=Rn(p);M(r,H);if(H.keepIndicators!==true){en(T,q)}de(r,"htmx:afterRequest",H);de(r,"htmx:afterOnLoad",H);if(!le(r)){let e=null;while(t.length>0&&e==null){const n=t.shift();if(le(n)){e=n}}if(e){de(e,"htmx:afterRequest",H);de(e,"htmx:afterOnLoad",H)}}oe(s);m()}catch(e){fe(r,"htmx:onLoadError",ue({error:e},H));throw e}};p.onerror=function(){en(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendError",H);oe(l);m()};p.onabort=function(){en(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendAbort",H);oe(l);m()};p.ontimeout=function(){en(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:timeout",H);oe(l);m()};if(!de(r,"htmx:beforeRequest",H)){oe(s);m();return e}var T=Wt(r);var q=Qt(r);se(["loadstart","loadend","progress","abort"],function(t){se([p,p.upload],function(e){e.addEventListener(t,function(e){de(r,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});de(r,"htmx:beforeSend",H);const J=E?null:yn(p,r,v);p.send(J);return e}function In(e,t){const n=t.xhr;let r=null;let o=null;if(C(n,/HX-Push:/i)){r=n.getResponseHeader("HX-Push");o="push"}else if(C(n,/HX-Push-Url:/i)){r=n.getResponseHeader("HX-Push-Url");o="push"}else if(C(n,/HX-Replace-Url:/i)){r=n.getResponseHeader("HX-Replace-Url");o="replace"}if(r){if(r==="false"){return{}}else{return{type:o,path:r}}}const i=t.pathInfo.finalRequestPath;const s=t.pathInfo.responsePath;const l=re(e,"hx-push-url");const u=re(e,"hx-replace-url");const c=ie(e).boosted;let a=null;let f=null;if(l){a="push";f=l}else if(u){a="replace";f=u}else if(c){a="push";f=s||i}if(f){if(f==="false"){return{}}if(f==="true"){f=s||i}if(t.pathInfo.anchor&&f.indexOf("#")===-1){f=f+"#"+t.pathInfo.anchor}return{type:a,path:f}}else{return{}}}function Pn(e,t){var n=new RegExp(e.code);return n.test(t.toString(10))}function kn(e){for(var t=0;t0){E().setTimeout(e,y.swapDelay)}else{e()}}if(f){fe(o,"htmx:responseError",ue({error:"Response Status Error Code "+s.status+" from "+i.pathInfo.requestPath},i))}}const Xn={};function Fn(){return{init:function(e){return null},getSelectors:function(){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,n){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,n,r){return false},encodeParameters:function(e,t,n){return null}}}function Bn(e,t){if(t.init){t.init(n)}Xn[e]=ue(Fn(),t)}function Un(e){delete Xn[e]}function jn(e,n,r){if(n==undefined){n=[]}if(e==undefined){return n}if(r==undefined){r=[]}const t=te(e,"hx-ext");if(t){se(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){r.push(e.slice(7));return}if(r.indexOf(e)<0){const t=Xn[e];if(t&&n.indexOf(t)<0){n.push(t)}}})}return jn(ce(u(e)),n,r)}var Vn=false;ne().addEventListener("DOMContentLoaded",function(){Vn=true});function _n(e){if(Vn||ne().readyState==="complete"){e()}else{ne().addEventListener("DOMContentLoaded",e)}}function $n(){if(Q.config.includeIndicatorStyles!==false){const e=Q.config.inlineStyleNonce?` nonce="${Q.config.inlineStyleNonce}"`:"";ne().head.insertAdjacentHTML("beforeend"," ."+Q.config.indicatorClass+"{opacity:0} ."+Q.config.requestClass+" ."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} ."+Q.config.requestClass+"."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} ")}}function zn(){const e=ne().querySelector('meta[name="htmx-config"]');if(e){return S(e.content)}else{return null}}function Jn(){const e=zn();if(e){Q.config=ue(Q.config,e)}}_n(function(){Jn();$n();let e=ne().body;Dt(e);const t=ne().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){const t=e.target;const n=ie(t);if(n&&n.xhr){n.xhr.abort()}});const n=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(e){if(e.state&&e.state.htmx){Yt();se(t,function(e){de(e,"htmx:restored",{document:ne(),triggerEvent:de})})}else{if(n){n(e)}}};E().setTimeout(function(){de(e,"htmx:load",{});e=null},0)});return Q}(); \ No newline at end of file diff --git a/readme.org b/readme.org new file mode 100644 index 0000000..6c56af0 --- /dev/null +++ b/readme.org @@ -0,0 +1,46 @@ +#+TITLE: Sistema de encuestas simple +#+AUTHOR: KJ + +Un sistema de encuestas con solo 3 opciones a elegir (por si acaso, es posible añadir algunas más, pero se harcodeará esa parte). + +* Especificaciones + +- Encuestas con 3 opciones. +- En otra página ira viendo el resultado. +- Cualquier dispositivo debe ser posible. +- Las opciones se deben poder cambiarse cada 24 horas (iniciar nueva encuesta). + + +* Diseño de la BD + +** Tabla de usuarios + +| users | +|----------| +| id | +| email | +| username | +| password | + +** Encuestas (En el hardcode solo se trabajará con la última creada) + +| pools | +|-------| +| id | +| title | + +** Opciones elegibles como respuesta a la encuesta. + +| pool_options | +|--------------| +| id | +| value | +| pool_id | + +** Votos de los clientes + +| anwers | +|-----------| +| id | +| option_id | +| create_at | diff --git a/src/Controllers/.keep b/src/Controllers/.keep new file mode 100644 index 0000000..e69de29 diff --git a/src/Controllers/Pool/HomeController.php b/src/Controllers/Pool/HomeController.php new file mode 100644 index 0000000..304bd6a --- /dev/null +++ b/src/Controllers/Pool/HomeController.php @@ -0,0 +1,19 @@ + $request->pool + ]); + } +} diff --git a/src/Controllers/Pool/PoolCreateController.php b/src/Controllers/Pool/PoolCreateController.php new file mode 100644 index 0000000..c33cdc2 --- /dev/null +++ b/src/Controllers/Pool/PoolCreateController.php @@ -0,0 +1,27 @@ +title = $request->post->title; + $pool->beginTransaction(); + $pool->save(); + + for($i=1; $i <= 3; $i++) { + $option = new PoolOption; + $option->value = $request->post->{'option'.$i}; + $option->pool_id = $pool->id; + $option->save(); + } + $pool->commit(); + + $request->saved = true; + PoolFormController::handle($request); + } +} diff --git a/src/Controllers/Pool/PoolFormController.php b/src/Controllers/Pool/PoolFormController.php new file mode 100644 index 0000000..26e15f6 --- /dev/null +++ b/src/Controllers/Pool/PoolFormController.php @@ -0,0 +1,13 @@ + $request->saved ?? false + ]); + } +} diff --git a/src/Controllers/Pool/PoolResultController.php b/src/Controllers/Pool/PoolResultController.php new file mode 100644 index 0000000..32f364b --- /dev/null +++ b/src/Controllers/Pool/PoolResultController.php @@ -0,0 +1,14 @@ + $request->pool + ]); + } +} diff --git a/src/Controllers/Pool/PoolVoteController.php b/src/Controllers/Pool/PoolVoteController.php new file mode 100644 index 0000000..b73a163 --- /dev/null +++ b/src/Controllers/Pool/PoolVoteController.php @@ -0,0 +1,19 @@ +pool->isVoted()) { + $vote = new Answer; + $vote->option_id = $request->option->id; + $vote->save(); + $request->pool->createCookie(); + } + + echo '¡Gracias por participar!'; + } +} diff --git a/src/Controllers/User/UserLoginController.php b/src/Controllers/User/UserLoginController.php new file mode 100644 index 0000000..f249ca4 --- /dev/null +++ b/src/Controllers/User/UserLoginController.php @@ -0,0 +1,18 @@ +post->username, $request->post->password)) { + $request->onInvalid('Usuario o contraseña equivocados.'); + return; + } + + HTMX::redirect('/config'); + } +} diff --git a/src/Controllers/User/UserLoginFormController.php b/src/Controllers/User/UserLoginFormController.php new file mode 100644 index 0000000..ceea585 --- /dev/null +++ b/src/Controllers/User/UserLoginFormController.php @@ -0,0 +1,21 @@ +username = 'Paul'; + $user->setPassword('F1rbpul'); + $user->save(); + } + + HTMX::render('Login'); + } +} diff --git a/src/Libs/Crypto.php b/src/Libs/Crypto.php new file mode 100644 index 0000000..7440611 --- /dev/null +++ b/src/Libs/Crypto.php @@ -0,0 +1,111 @@ +getMessage() + ); + } + + static::$databases[$key]->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + static::$databases[$key]->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); + } + return static::$databases[$key]; + } +} +?> diff --git a/src/Libs/HTMLComponent.php b/src/Libs/HTMLComponent.php new file mode 100644 index 0000000..f95c531 --- /dev/null +++ b/src/Libs/HTMLComponent.php @@ -0,0 +1,366 @@ +viewPath.$componentRealName.$this->extension)) { + ob_start(); + include($this->viewPath.$componentRealName.$this->extension); + $this->content = ob_get_clean(); + $this->parse(); + } else { + throw new \Exception( + '"'.$component.'" component not exists.' + ); + } + + return $this; + } + + /** + * Parsea y procesa el contenido en busca de más componentes + * + * @param array $components + * + * @return void + */ + protected function parse(array $components = null): void + { + if (empty($components)) { + preg_match_all( + '/<(([\:]+)?[A-Z][\w1-9:]+) ?([^>]+)?>/s', + $this->content, $matches, PREG_PATTERN_ORDER + ); + $components = $matches[1]; + } + + foreach (array_unique($components) as $component) { + $this->parseComponent($component); + } + } + + /** + * Parsea y procesa un componente + * + * @param string $component + * + * @return void + */ + protected function parseComponent( + string $component, + ): void + { + $this->content = preg_replace_callback( + '/<'.$component.'([^>]+)?>(.+)?<\/'.$component.'>/sU', + function($matches) use($component) { + $properties = isset($matches[1]) ? static::parseProperties($matches[1]) : []; + $instance = new static($this->properties); + $instance->content = $matches[2] ?? ''; + $instance->addProperties($properties); + $instance->load($component); + return $instance->getContent(); + }, + $this->content + ); + } + + /** + * Convierte un a cadena en formato "clave='valor' clave2='valor2'" en un array. + * + * @param string $propertiesString + * + * @return array + */ + protected static function parseProperties(string $propertiesString): array + { + preg_match_all('/([\w]+)=[\'"](.+)?[\'"]/sU', $propertiesString, $matches, PREG_PATTERN_ORDER); + $result = []; + foreach($matches[1] as $index => $property) { + $result[$property] = $matches[2][$index]; + } + preg_match_all('/([\w]+) /si', $propertiesString.' ', $matches, PREG_PATTERN_ORDER); + foreach($matches[1] as $property) { + $result[$property] = $property; + } + return $result; + } + + /** + * Imprime el componente + * + * @return void + */ + public function print(): void + { + print($this->content); + } + + /** + * Renderiza un componente + * + * @param string $component + * @param array $properties + * + * @return void + */ + public static function render(string $component, array $properties = []): void + { + $instance = new static($properties); + $instance->load($component); + $instance->print(); + } + + /** + * Devuelve el componente como string + * + * @return string + */ + public function getContent(): string + { + return $this->content; + } + + /** + * Devuelve el valor de una propiedad. + * + * @param string $key + * @param mixed $default + * + * @return mixed + */ + public function property(string $key, mixed $default = null): mixed + { + return $this->properties[$key] ?? $default; + } + + /** + * Devuelve el valor de una propiedad escapado con htmlspecialchars. + * + * @param string $key + * @param string $default + * + * @return string + */ + public function escapedProperty(string $key, string $default = ''): string + { + if (isset($this->properties[$key])) + return htmlspecialchars($this->properties[$key] ?? ''); + else + return htmlspecialchars($default); + } + + /** + * Función alias/apócope de la función property. + * + * @param string $key + * @param mixed $default + * + * @return mixed + */ + public function p(string $key, mixed $default = null): mixed + { + return $this->property($key, $default); + } + + /** + * Función alias/apócope de la función escapedProperty. + * + * @param string $key + * @param string $default + * + * @return string + */ + public function e(string $key, string $default = ''): string + { + return $this->escapedProperty($key, $default); + } + + /** + * Según el array de nombres de propiedades recibido, los devuelve + * con sus valores como array asociativo. + * Si no recibe ningua propiedad como argumento, devuelve todas las existentes. + * + * @param array $properties + * + * @return array + */ + public function propsToArray(...$properties): array + { + if (empty($properties)) + return $this->properties; + + $result = []; + foreach ($properties as $property) + $result[$property] = $this->properties[$property] ?? null; + + return $result; + } + + /** + * Según el array de nombres de propiedades recibido, devuelve + * como atributos HTML en formato propiedad="valor". + * + * @param array $properties + * + * @return string + */ + public function propsToAtts(...$properties): string + { + $result = ' '; + foreach ($properties as $property) + $result .= $property.'="'.htmlspecialchars($this->property($property, '')).'" '; + + return $result; + } + + /** + * Según el array de nombres de propiedades recibido, devuelve + * como atributos HTML unitarios como checked, required, etc. + * + * @param array $properties + * + * @return string + */ + public function propsToUnary(...$properties): string + { + $result = ' '; + foreach ($properties as $property) + if (isset($this->properties[$property])) + $result .= $property.' '; + + return $result; + } + + /** + * Trata una propiedad como una definición de propiedad HTML unitaria. + * + * @param string $property Propiedad unitaria buscar. + * @param mixed $result Valor a devolver en caso de estar definda la propiedad. Si es nulo, devuelve $property. + * + * @return mixed + */ + public function unary(string $property, mixed $result = null): mixed + { + if (isset($this->properties[$property])) { + if (isset($result)) + return $result; + else + return $property; + } + + return null; + } + + /** + * Añade una nueva propiedad/atributo. + * En caso de que una propiedad exista, la reemplaza. + * + * @param string $key + * @param mixed $value + * + * @return void + */ + public function addProperty(string $key, mixed $value): void + { + $this->properties = array_merge($this->properties, [$key => $value]); + } + + /** + * Añade nuevas propiedades en lote. + * En caso de que una propiedad exista, la reemplaza. + * + * @param array $properties + * + * @return void + */ + public function addProperties(array $properties): void + { + $this->properties = array_merge($this->properties, $properties); + } + + /** + * Intenta devolver la url absoluta a partir de una ruta relativa. + * + * @param string $path + * + * @return string + */ + public static function route(string $path = '/'): string + { + if (defined('SITE_URL') && !empty(SITE_URL)) + return rtrim(SITE_URL, '/').'/'.ltrim($path, '/'); + + return $path; + } + + /** + * Devuelve la ruta de la petición. + * + * @return string + */ + public static function path(): string + { + return Router::currentPath(); + } + + /** + * __get + * + * @param string $index + * @return mixed + */ + public function __get(string $index): mixed + { + $value = $this->property($index); + + if (is_string($value)) + return htmlspecialchars($value); + else + return $value; + } + + /** + * __isset + * + * @param string $key + * + * @return bool + */ + public function __isset(string $key): bool + { + return isset($this->properties[$key]); + } +} diff --git a/src/Libs/HTMX.php b/src/Libs/HTMX.php new file mode 100644 index 0000000..5d6acea --- /dev/null +++ b/src/Libs/HTMX.php @@ -0,0 +1,291 @@ +load($component); + $instance->print(); + } + + /** + * Parsea y procesa el contenido en busca de más componentes + * + * @param array $components + * + * @return void + */ + protected function parse(array $components = null): void + { + $this->parseHTMX(); + parent::parse(); + } + + /** + * Elimina o muestra secciones según etiquetas de comentarios y + * los estados de la petición HTMX. + * + * Los comentarios posibles son: + * + * | Comentario | Descripción | + * |------------------+---------------------------------------------------| + * | htmx | Solo se muestra si la petición es HTMX | + * | nothtmx | Solo se muestra si la petición no es HTMX | + * | boosted | Solo se muestra si la petición es boosted | + * | notboosted | Solo se muestra si la petición no es boosted | + * | nothtmxorboosted | Se muestra si la petición no es HTMX o es boosted | + * | htmxornotboosted | Se muestra si la petición es HTMX o no es boosted | + * + * @return void + */ + protected function parseHTMX(): void { + if (static::isHtmx()) + if (static::isBoosted()) + $replacement = ['$3', '', '$3', '', '$3', '']; + else + $replacement = ['$3', '', '', '$3', '', '$3']; + else + $replacement = ['', '$3', '', '$3', '$3', '']; + + $this->content = trim(preg_replace( + [ + '/([\t \r\n]+)?([\t \r\n]+)?(.+)?([\t \r\n]+)?([\t \r\n]+)?/siU', + '/([\t \r\n]+)?([\t \r\n]+)?(.+)?([\t \r\n]+)?([\t \r\n]+)?/siU', + '/([\t \r\n]+)?([\t \r\n]+)?(.+)?([\t \r\n]+)?([\t \r\n]+)?/siU', + '/([\t \r\n]+)?([\t \r\n]+)?(.+)?([\t \r\n]+)?([\t \r\n]+)?/siU', + '/([\t \r\n]+)?([\t \r\n]+)?(.+)?([\t \r\n]+)?([\t \r\n]+)?/siU', + '/([\t \r\n]+)?([\t \r\n]+)?(.+)?([\t \r\n]+)?([\t \r\n]+)?/siU', + ], + $replacement, + $this->content + )); + } + + /** + * Elimina las líneas en blanco. + * + * @return void + */ + public function deleteEmptyLines(): void + { + $this->content = trim(preg_replace( + '/^[ \t]*[\r\n]+/m', + '', + $this->content + )); + } + + /** + * Verifica si la petición es o no htmx. + * + * @return bool + */ + public static function isHtmx(): bool + { + return isset($_SERVER['HTTP_HX_REQUEST']); + } + + /** + * Verifica que la petición sea HTMX pero no boosteada. + * + * @return bool + */ + public static function isHtmxNotBoosted(): bool + { + return static::isHtmx() && !static::isBoosted(); + } + + /** + * Verifica que la petición no sea HTMX o sea Boosteada. + * + * @return bool + */ + public static function notHtmxOrBoosted(): bool + { + return !static::isHtmx() || static::isBoosted(); + } + + /** + * Redirije a una ruta relativa interna enviando el header adecuado + * si es una petición normal o htmx. + * + * @param string $path + * La ruta relativa a la ruta base. + * + * @return void + */ + public static function redirect(string $path): void + { + if (static::isHtmx()) + header('HX-Redirect: '.Router::basePath().ltrim($path, '/')); + else + Router::redirect($path); + exit; + } + + /** + * Verifica si la petición es o no Boosted. + * + * @return bool + */ + public static function isBoosted(): bool + { + return isset($_SERVER['HTTP_HX_BOOSTED']) && boolval($_SERVER['HTTP_HX_BOOSTED']); + } + + /** + * Devuelve la url actual del navegador cuando se hace la petición HTMX. + * + * @return string + */ + public static function currentURL(): string + { + return $_SERVER['HTTP_HX_CURRENT_URL'] ?? ''; + } + + /** + * Respuesta del hx-prompt si es que ha sido usado. + * + * @return string + */ + public static function prompt(): string + { + return $_SERVER['HTTP_HX_PROMPT'] ?? ''; + } + + /** + * Devuelve el id del target HTMX (si existe). + * + * @return string + */ + public static function target(): string + { + return $_SERVER['HTTP_HX_TARGET'] ?? ''; + } + + /** + * El nombre del elemento que levanta la petición (si existe). + * + * @return string + */ + public static function triggerName(): string + { + return $_SERVER['HTTP_HX_TRIGGER_NAME'] ?? ''; + } + + /** + * El id del elemento que levanta la petición (si existe). + * + * @return string + */ + public static function triggerId(): string + { + return $_SERVER['HTTP_HX_TRIGGER'] ?? ''; + } + + /** + * Solo cuando es una petición HTMX devuelve la propiedad HTMX hx-swap-oob. + * + * @param string $value Valor de hx-swap (por defecto: true). + * @param bool $excludeBoosted Excluir si la petición es boosted (por defecto: false) + * + * @return string + */ + public static function swapOob(string $value = 'true', bool $excludeBoosted = false): string + { + if ($excludeBoosted && static::isBoosted()) + return ''; + + return static::isHtmx() ? 'hx-swap-oob="'.$value.'"' : ''; + } + + /** + * Solo cuando es una petición HTMX devuelve la propiedad HTMX hx-select-oob. + * + * @param string $selector + * + * @return string + */ + public static function selectOob(string $selector): string + { + return static::isHtmx() ? 'hx-select-oob="'.$selector.'"' : ''; + } + + /** + * Fuerza a cambiar la URL del navegador y la coloca en el historial + * mientras que HTMX crea un caché de la página para cuando se de clic + * en regresar. + * + * Equivamente a usar la propiedad htmx hx-push-url. + * + * @param string $url Ruta relativa. + * + * @return void + */ + public static function pushUrl(string $url): void + { + if (static::isHtmx()) + header('HX-Push-Url: '.static::route($url)); + } + + /** + * Fuerza a cambiar la URL del navegador y pero sin colocarla en el historial. + * + * Equivamente a usar la propiedad htmx hx-push-url. + * + * @param string $url Ruta relativa. + * + * @return void + */ + public static function replaceUrl(string $url): void + { + if (static::isHtmx()) + header('HX-Replace-Url: '.static::route($url)); + } + + /** + * Cambia el swap target de HTMX. + * + * @param string $selector + * + * @return void + */ + public static function retarget(string $selector): void + { + header('HX-Retarget: '.$selector); + } + + /** + * Cambia el swap rule de HTMX. + * + * @param string $rule + * + * @return void + */ + public static function reswap(string $rule = 'innerHTML'): void + { + header('HX-Reswap: '.$rule); + } +} diff --git a/src/Libs/Middleware.php b/src/Libs/Middleware.php new file mode 100644 index 0000000..b63df10 --- /dev/null +++ b/src/Libs/Middleware.php @@ -0,0 +1,28 @@ +next); + return call_user_func_array($next, [$req]); + } +} diff --git a/src/Libs/Model.php b/src/Libs/Model.php new file mode 100644 index 0000000..d8a641f --- /dev/null +++ b/src/Libs/Model.php @@ -0,0 +1,938 @@ + ['*'], + 'where' => '', + 'from' => '', + 'leftJoin' => '', + 'rightJoin' => '', + 'innerJoin' => '', + 'orderBy' => '', + 'groupBy' => '', + 'limit' => '' + ]; + + /** + * Sirve para obtener la instancia de la base de datos. + * + * @return PDO + */ + protected static function db(): PDO + { + if (DB_TYPE == 'sqlite') + return Database::getInstance( + type: DB_TYPE, + name: DB_NAME + ); + else + return Database::getInstance( + DB_TYPE, + DB_HOST, + DB_NAME, + DB_USER, + DB_PASS + ); + } + + /** + * Ejecuta PDO::beginTransaction para iniciar una transacción. + * Más info: https://www.php.net/manual/es/pdo.begintransaction.php + * + * @return bool + */ + public function beginTransaction(): bool + { + return static::db()->beginTransaction(); + } + + /** + * Ejecuta PDO::rollBack para deshacher los cambios de una transacción. + * Más info: https://www.php.net/manual/es/pdo.rollback.php + * + * @return bool + */ + public function rollBack(): bool + { + if ( static::db()->inTransaction()) + return static::db()->rollBack(); + else + return true; + } + + /** + * Ejecuta PDO::commit para consignar una transacción. + * Más info: https://www.php.net/manual/es/pdo.commit.php + * + * @return bool + */ + public function commit(): bool + { + if (static::db()->inTransaction()) + return static::db()->commit(); + else + return true; + } + + /** + * Ejecuta una sentencia SQL en la base de datos. + * + * @param string $query + * Contiene la sentencia SQL que se desea ejecutar. + * + * @throws Exception + * En caso de que la sentencia SQL falle, devolverá un error en + * pantalla y hará rolllback en caso de estar dentro de una + * transacción (ver método beginTransacction). + * + * @param bool $resetQuery + * Indica si el query debe reiniciarse o no (por defecto es true). + * + * @return array + * Contiene el resultado de la llamada SQL . + */ + protected static function query(string $query, bool $resetQuery = true): array + { + $db = static::db(); + + try { + $prepared = $db->prepare($query); + $prepared->execute(static::$queryVars); + } catch (PDOException $e) { + if ($db->inTransaction()) + $db->rollBack(); + + $vars = json_encode(static::$queryVars); + + echo "
";
+            throw new Exception(
+                "\nError at query to database.\n" .
+                "Query: $query\n" .
+                "Vars: $vars\n" .
+                "Error:\n" . $e->getMessage()
+            );
+        }
+
+        $result = $prepared->fetchAll();
+
+        if ($resetQuery)
+            static::resetQuery();
+
+        return $result;
+    }
+
+    /**
+     * Reinicia la configuración de la sentencia SQL.
+     * @return void
+     */
+    protected static function resetQuery(): void
+    {
+        static::$querySelect = [
+            'select'              => ['*'],
+            'where'               => '',
+            'from'                => '',
+            'leftJoin'            => '',
+            'rightJoin'           => '',
+            'innerJoin'           => '',
+            'orderBy'             => '',
+            'groupBy'             => '',
+            'limit'               => ''
+        ];
+        static::$queryVars  = [];
+    }
+
+    /**
+     * Construye la sentencia SQL a partir static::$querySelect y una vez
+     * construída, llama a resetQuery.
+     *
+     * @return string
+     *   Contiene la sentencia SQL.
+     */
+    protected static function buildQuery(): string
+    {
+        $sql = 'SELECT '.join(', ', static::$querySelect['select']);
+
+        if (static::$querySelect['from'] != '')
+            $sql .= ' FROM '.static::$querySelect['from'];
+        else
+            $sql .= ' FROM '.static::table();
+
+        if(static::$querySelect['innerJoin'] != '')
+            $sql .= static::$querySelect['innerJoin'];
+
+        if (static::$querySelect['leftJoin'] != '')
+            $sql .= static::$querySelect['leftJoin'];
+
+        if(static::$querySelect['rightJoin'] != '')
+            $sql .= static::$querySelect['rightJoin'];
+
+        if (static::$querySelect['where'] != '')
+            $sql .= ' WHERE '.static::$querySelect['where'];
+
+        if (static::$querySelect['groupBy'] != '')
+            $sql .= ' GROUP BY '.static::$querySelect['groupBy'];
+
+        if (static::$querySelect['orderBy'] != '')
+            $sql .= ' ORDER BY '.static::$querySelect['orderBy'];
+
+        if (static::$querySelect['limit'] != '')
+            $sql .= ' LIMIT '.static::$querySelect['limit'];
+
+        return $sql;
+    }
+
+
+    /**
+     * Configura $queryVars para vincular un valor a un
+     * parámetro de sustitución y devuelve este último.
+     *
+     * @param string $value
+     *   Valor a vincular.
+     *
+     * @return string
+     *   Parámetro de sustitución.
+     */
+    private static function bindValue(string $value): string
+    {
+        $index = ':v_'.count(static::$queryVars);
+        static::$queryVars[$index] = $value;
+        return $index;
+    }
+
+    /**
+     * Crea una instancia del objeto actual a partir de un arreglo.
+     *
+     * @param mixed $elem
+     *   Puede recibir un arreglo o un objeto que contiene los valores
+     *   que tendrán sus atributos.
+     *
+     * @return static
+     *   Retorna un objeto de la clase actual.
+     */
+    protected static function getInstance(array $elem = []): static
+    {
+        $class         = get_called_class();
+        $instance      = new $class;
+        $reflection    = new ReflectionClass($instance);
+        $properties    = $reflection->getProperties();
+        $propertyNames = array_column($properties, 'name');
+
+        foreach ($elem as $key => $value) {
+            $index = array_search($key, $propertyNames);
+            if (is_numeric($index) && isset($value) &&
+                enum_exists($properties[$index]->getType()->getName()))
+                $instance->$key = $properties[$index]->getType()->getName()::tryfrom($value);
+            else
+                $instance->$key = $value;
+        }
+
+        return $instance;
+    }
+
+    /**
+     * Devuelve los atributos a guardar de la case actual.
+     * Los atributos serán aquellos que seran public y
+     * no esten excluidos en static::$ignoresave y aquellos
+     * que sean private o protected pero estén en static::$forceSave.
+     *
+     * @return array
+     *   Contiene los atributos indexados del objeto actual.
+     */
+    protected function getVars(): array
+    {
+        $reflection = new ReflectionClass($this);
+        $properties = $reflection->getProperties(ReflectionProperty::IS_PUBLIC);
+        $result     = [];
+
+        foreach($properties as $property)
+            $result[$property->name] = isset($this->{$property->name})
+                          ? $this->{$property->name} : null;
+
+        foreach (static::$ignoreSave as $del)
+            unset($result[$del]);
+
+        foreach (static::$forceSave as $value)
+            $result[$value] = isset($this->$value)
+                            ? $this->$value: null;
+
+        foreach ($result as $i => $property) {
+            if (gettype($property) == 'boolean')
+                $result[$i] = $property ? '1' : '0';
+
+            if ($property instanceof \UnitEnum)
+                $result[$i] = $property->value;
+        }
+
+        return $result;
+    }
+
+    /**
+     * Devuelve el nombre de la clase actual aunque sea una clase extendida.
+     *
+     * @return string
+     *   Devuelve el nombre de la clase actual.
+     */
+    public static function className(): string
+    {
+        return strtolower(
+            preg_replace(
+                '/(?getVars();
+
+        foreach ($atts as $key => $value) {
+            if (isset($value)) {
+                if (in_array($key, $this->toNull))
+                    $set[]="$key=NULL";
+                else {
+                    $set[]="$key=:$key";
+                    static::$queryVars[':'.$key] = $value;
+                }
+            } else {
+                if (in_array($key, $this->toNull))
+                    $set[]="$key=NULL";
+            }
+        }
+
+        $table = static::table();
+        $pk = static::$primaryKey;
+        $pkv = $this->$pk;
+        $sql = "UPDATE $table SET ".join(', ', $set)." WHERE $pk='$pkv'";
+        static::query($sql);
+    }
+
+    /**
+     * Inserta una nueva fila en la base de datos a partir del
+     * objeto actual.
+     * @return void
+     */
+    protected function add(): void
+    {
+        $db = static::db();
+        $atts = $this->getVars();
+
+        foreach ($atts as $key => $value) {
+            if (isset($value)) {
+                $into[] = "`$key`";
+                $values[] = ":$key";
+                static::$queryVars[":$key"] = $value;
+            }
+        }
+
+        $table = static::table();
+        $sql = "INSERT INTO $table (".join(', ', $into).") VALUES (".join(', ', $values).")";
+        static::query($sql);
+
+        $pk = static::$primaryKey;
+        $this->$pk = $db->lastInsertId();
+    }
+
+    /**
+     * Revisa si el objeto a guardar es nuevo o no y según el resultado
+     * llama a update para actualizar o add para insertar una nueva fila.
+     * @return void
+     */
+    public function save(): void
+    {
+        $pk = static::$primaryKey;
+        if (isset($this->$pk))
+            $this->update();
+        else
+            $this->add();
+    }
+
+    /**
+     * Elimina el objeto actual de la base de datos.
+     * @return void
+     */
+    public function delete(): void {
+        $table = static::table();
+        $pk = static::$primaryKey;
+        $sql = "DELETE FROM $table WHERE $pk=:$pk";
+
+        static::$queryVars[":$pk"] = $this->$pk;
+        static::query($sql);
+    }
+
+    /**
+     * Define SELECT en la sentencia SQL.
+     *
+     * @param array $columns
+     *   Columnas que se selecionarán en la consulta SQL.
+     *
+     * @return static
+     */
+    public static function select(array $columns): static
+    {
+        static::$querySelect['select'] = $columns;
+
+        return new static();
+    }
+
+    /**
+     * Define FROM en la sentencia SQL.
+     *
+     * @param array $tables
+     *   Tablas que se selecionarán en la consulta SQL.
+     *
+     * @return static
+     */
+    public static function from(array $tables): static
+    {
+        static::$querySelect['from'] = join(', ', $tables);
+
+        return new static();
+    }
+
+    /**
+     * Define el WHERE en la sentencia SQL.
+     *
+     * @param string $column
+     *   La columna a comparar.
+     *
+     * @param string $operatorOrValue
+     *   El operador o el valor a comparar como igual en caso de que $value no se defina.
+     *
+     * @param string $value
+     *   (opcional) El valor a comparar en la columna.
+     *
+     * @param bool $no_filter
+     *   (opcional) Se usa cuando $value es una columna o un valor que no requiere filtros
+     *   contra ataques SQLI (por defeco es false).
+     *
+     * @return static
+     */
+    public static function where(
+        string $column,
+        string $operatorOrValue,
+        string $value   = null,
+        bool $no_filter = false
+    ): static
+    {
+        return static::and(
+            $column,
+            $operatorOrValue,
+            $value,
+            $no_filter
+        );
+    }
+
+    /**
+     * Define AND en la sentencia SQL (se puede anidar).
+     *
+     * @param string $column
+     *   La columna a comparar.
+     *
+     * @param string $operatorOrValue
+     *   El operador o el valor a comparar como igual en caso de que $value no se defina.
+     *
+     * @param string $value
+     *   (opcional) El valor el valor a comparar en la columna.
+     *
+     * @param bool $no_filter
+     *   (opcional) Se usa cuando $value es una columna o un valor que no requiere filtros
+     *   contra ataques SQLI (por defecto es false).
+     *
+     * @return static
+     */
+    public static function and(
+        string $column,
+        string $operatorOrValue,
+        string $value   = null,
+        bool $no_filter = false
+    ): static
+    {
+        if (is_null($value)) {
+            $value = $operatorOrValue;
+            $operatorOrValue = '=';
+        }
+
+        if (!$no_filter)
+            $value = static::bindValue($value);
+
+        if (static::$querySelect['where'] == '')
+            static::$querySelect['where'] = "$column $operatorOrValue $value";
+        else
+            static::$querySelect['where'] .= " AND $column $operatorOrValue $value";
+
+        return new static();
+    }
+
+    /**
+     * Define OR en la sentencia SQL (se puede anidar).
+     *
+     * @param string $column
+     *   La columna a comparar.
+     *
+     * @param string $operatorOrValue
+     *   El operador o el valor a comparar como igual en caso de que $value no se defina.
+     *
+     * @param string $value
+     *   (opcional) El valor el valor a comparar en la columna.
+     *
+     * @param bool $no_filter
+     *   (opcional) Se usa cuando $value es una columna o un valor que no requiere filtros
+     *   contra ataques SQLI (por defecto es false).
+     *
+     * @return static
+     */
+    public static function or(
+        string $column,
+        string $operatorOrValue,
+        string $value   = null,
+        bool $no_filter = false
+    ): static
+    {
+        if (is_null($value)) {
+            $value = $operatorOrValue;
+            $operatorOrValue = '=';
+        }
+
+        if (!$no_filter)
+            $value = static::bindValue($value);
+
+        if (static::$querySelect['where'] == '')
+            static::$querySelect['where'] = "$column $operatorOrValue $value";
+        else
+            static::$querySelect['where'] .= " OR $column $operatorOrValue $value";
+
+        return new static();
+    }
+
+    /**
+     * Define WHERE usando IN en la sentencia SQL.
+     *
+     * @param string $column
+     *   La columna a comparar.
+     *
+     * @param array $arr
+     *   Arreglo con todos los valores a comparar con la columna.
+     *
+     * @param bool $in
+     *   Define si se tienen que comprobar negativa o positivamente.
+     *
+     * @return static
+     */
+    public static function where_in(
+        string $column,
+        array $arr,
+        bool $in = true
+    ): static
+    {
+        $arrIn = [];
+        foreach($arr as $value) {
+            $arrIn[] = static::bindValue($value);
+        }
+
+        if ($in)
+            $where_in = "$column IN (".join(', ', $arrIn).")";
+        else
+            $where_in = "$column NOT IN (".join(', ', $arrIn).")";
+
+        if (static::$querySelect['where'] == '')
+            static::$querySelect['where'] = $where_in;
+        else
+            static::$querySelect['where'] .= " AND $where_in";
+
+        return new static();
+    }
+
+    /**
+     * Define LEFT JOIN en la sentencia SQL.
+     *
+     * @param string $table
+     *   Tabla que se va a juntar a la del objeto actual.
+     *
+     * @param string $columnA
+     *   Columna a comparar para hacer el join.
+     *
+     * @param string $operatorOrColumnB
+     *   Operador o columna a comparar como igual para hacer el join en caso de que $columnB no se defina.
+     *
+     * @param string $columnB
+     *   (opcional) Columna a comparar para hacer el join.
+     *
+     * @return static
+     */
+    public static function leftJoin(
+        string $table,
+        string $columnA,
+        string $operatorOrColumnB,
+        string $columnB = null
+    ): static
+    {
+        if (is_null($columnB)) {
+            $columnB = $operatorOrColumnB;
+            $operatorOrColumnB = '=';
+        }
+
+        static::$querySelect['leftJoin'] .= ' LEFT JOIN ' . $table . ' ON ' . "$columnA$operatorOrColumnB$columnB";
+
+        return new static();
+    }
+
+    /**
+     * Define RIGHT JOIN en la sentencia SQL.
+     *
+     * @param string $table
+     *   Tabla que se va a juntar a la del objeto actual.
+     *
+     * @param string $columnA
+     *   Columna a comparar para hacer el join.
+     *
+     * @param string $operatorOrColumnB
+     *   Operador o columna a comparar como igual para hacer el join en caso de que $columnB no se defina.
+     *
+     * @param string $columnB
+     *   (opcional) Columna a comparar para hacer el join.
+     *
+     * @return static
+     */
+    public static function rightJoin(
+        string $table,
+        string $columnA,
+        string $operatorOrColumnB,
+        string $columnB = null
+    ): static
+    {
+        if (is_null($columnB)) {
+            $columnB = $operatorOrColumnB;
+            $operatorOrColumnB = '=';
+        }
+
+        static::$querySelect['rightJoin'] .= ' RIGHT JOIN ' . $table . ' ON ' . "$columnA$operatorOrColumnB$columnB";
+
+        return new static();
+    }
+
+    /**
+     * Define INNER JOIN en la sentencia SQL.
+     *
+     * @param string $table
+     *   Tabla que se va a juntar a la del objeto actual.
+     *
+     * @param string $columnA
+     *   Columna a comparar para hacer el join.
+     *
+     * @param string $operatorOrColumnB
+     *   Operador o columna a comparar como igual para hacer el join en caso de que $columnB no se defina.
+     *
+     * @param string $columnB
+     *   (opcional) Columna a comparar para hacer el join.
+     *
+     * @return static
+     */
+    public static function innerJoin(
+        string $table,
+        string $columnA,
+        string $operatorOrColumnB,
+        string $columnB = null
+    ): static
+    {
+        if (is_null($columnB)) {
+            $columnB = $operatorOrColumnB;
+            $operatorOrColumnB = '=';
+        }
+
+        static::$querySelect['innerJoin'] .= ' INNER JOIN ' . $table . ' ON ' . "$columnA$operatorOrColumnB$columnB";
+
+        return new static();
+    }
+
+    /**
+     * Define GROUP BY en la sentencia SQL.
+     *
+     * @param array $arr
+     *   Columnas por las que se agrupará.
+     *
+     * @return static
+     */
+    public static function groupBy(array $arr): static
+    {
+        static::$querySelect['groupBy'] = join(', ', $arr);
+        return new static();
+    }
+
+    /**
+     * Define LIMIT en la sentencia SQL.
+     *
+     * @param int $offsetOrQuantity
+     *   Define el las filas a ignorar o la cantidad a tomar en
+     *   caso de que $quantity no esté definido.
+     * @param int $quantity
+     *   Define la cantidad máxima de filas a tomar.
+     *
+     * @return static
+     */
+    public static function limit(int $offsetOrQuantity, ?int $quantity = null): static
+    {
+        if (is_null($quantity))
+            static::$querySelect['limit'] = $offsetOrQuantity;
+        else
+            static::$querySelect['limit'] = $offsetOrQuantity.', '.$quantity;
+
+        return new static();
+    }
+
+    /**
+     * Define ORDER BY en la sentencia SQL.
+     *
+     * @param string $value
+     *   Columna por la que se ordenará.
+     *
+     * @param string $order
+     *   (opcional) Define si el orden será de manera ascendente (ASC),
+     *   descendente (DESC) o aleatorio (RAND).
+     *
+     * @return static
+     */
+    public static function orderBy(string $value, string $order = 'ASC'): static
+    {
+        if ($value == "RAND") {
+            static::$querySelect['orderBy'] = 'RAND()';
+            return new static();
+        }
+
+        if (!(strtoupper($order) == 'ASC' || strtoupper($order) == 'DESC'))
+            $order = 'ASC';
+
+        static::$querySelect['orderBy'] = $value.' '.$order;
+
+        return new static();
+    }
+
+    /**
+     * Retorna la cantidad de filas que hay en un query.
+     *
+     * @param bool $resetQuery
+     *   (opcional) Indica si el query debe reiniciarse o no (por defecto es true).
+     *
+     * @param bool $useLimit
+     *   (opcional) Permite usar limit para estabecer un máximo inical y final para contar.
+     *   Requiere que se haya definido antes el límite (por defecto en false).
+     *
+     * @return int
+     */
+    public static function count(bool $resetQuery = true, bool $useLimit = false): int
+    {
+        if (!$resetQuery)
+            $backup = [
+                'select'              => static::$querySelect['select'],
+                'limit'               => static::$querySelect['limit'],
+                'orderBy'             => static::$querySelect['orderBy']
+            ];
+
+        if ($useLimit && static::$querySelect['limit'] != '') {
+            static::$querySelect['select']  = ['1'];
+            static::$querySelect['orderBy'] = '';
+
+            $sql         = 'SELECT COUNT(1) AS quantity FROM ('.static::buildQuery().') AS counted';
+            $queryResult = static::query($sql, $resetQuery);
+            $result      = $queryResult[0]['quantity'];
+        } else {
+            static::$querySelect['select']  = ["COUNT(".static::table().".".static::$primaryKey.") as quantity"];
+            static::$querySelect['limit']   = '1';
+            static::$querySelect['orderBy'] = '';
+
+            $sql = static::buildQuery();
+            $queryResult = static::query($sql, $resetQuery);
+            $result      = $queryResult[0]['quantity'];
+        }
+
+        if (!$resetQuery) {
+            static::$querySelect['select']  = $backup['select'];
+            static::$querySelect['limit']   = $backup['limit'];
+            static::$querySelect['orderBy'] = $backup['orderBy'];
+        }
+
+        return $result;
+    }
+
+    /**
+     * Obtiene una instancia según su primary key (generalmente id).
+     * Si no encuentra una instancia, devuelve nulo.
+     *
+     * @param mixed $id
+     *
+     * @return static|null
+     */
+    public static function getById(mixed $id): ?static
+    {
+        return static::where(static::$primaryKey, $id)->getFirst();
+    }
+
+    /**
+     * Realiza una búsqueda en la tabla de la instancia actual.
+     *
+     * @param string $search
+     *   Contenido a buscar.
+     *
+     * @param array $in
+     *   (opcional) Columnas en las que se va a buscar (null para buscar en todas).
+     *
+     * @return static
+     */
+    public static function search(string $search, array $in = null): static
+    {
+        if ($in == null) {
+            $className = get_called_class();
+            $in = array_keys((new $className())->getVars());
+        }
+
+        $search = static::bindValue($search);
+        $where  = [];
+
+        if (DB_TYPE == 'sqlite')
+            foreach($in as $row)
+                $where[] = "$row LIKE '%' || $search || '%'";
+        else
+            foreach($in as $row)
+                $where[] = "$row LIKE CONCAT('%', $search, '%')";
+
+
+        if (static::$querySelect['where']=='')
+            static::$querySelect['where'] = join(' OR ', $where);
+        else
+            static::$querySelect['where'] = static::$querySelect['where'] .' AND ('.join(' OR ', $where).')';
+
+        return new static();
+    }
+
+    /**
+     * Obtener los resultados de la consulta SQL.
+     *
+     * @param bool $resetQuery
+     *   (opcional) Indica si el query debe reiniciarse o no (por defecto es true).
+     *
+     * @return array
+     *   Arreglo con instancias del la clase actual resultantes del query.
+     */
+    public static function get(bool $resetQuery = true): array
+    {
+        $sql = static::buildQuery();
+        $result = static::query($sql, $resetQuery);
+
+        $instances = [];
+
+        foreach ($result as $row) {
+            $instances[] = static::getInstance($row);
+        }
+
+        return $instances;
+    }
+
+    /**
+     * El primer elemento de la consulta SQL.
+     *
+     * @param bool $resetQuery
+     *   (opcional) Indica si el query debe reiniciarse o no (por defecto es true).
+     *
+     * @return static|null
+     *   Puede retornar una instancia de la clase actual o null.
+     */
+    public static function getFirst(bool $resetQuery = true): ?static
+    {
+        static::limit(1);
+        $instances = static::get($resetQuery);
+        return empty($instances) ? null : $instances[0];
+    }
+
+    /**
+     * Obtener todos los elementos del la tabla de la instancia actual.
+     *
+     * @return array
+     *   Contiene un arreglo de instancias de la clase actual.
+     */
+    public static function all(): array
+    {
+        $sql = 'SELECT * FROM '.static::table();
+        $result = static::query($sql);
+
+        $instances = [];
+
+        foreach ($result as $row)
+            $instances[] = static::getInstance($row);
+
+        return $instances;
+    }
+
+    /**
+     * Permite definir como nulo el valor de un atributo.
+     * Sólo funciona para actualizar un elemento de la BD, no para insertar.
+     *
+     * @param string|array $atts
+     *   Atributo o arreglo de atributos que se definirán como nulos.
+     *
+     * @return void
+     */
+    public function setNull(string|array $atts): void
+    {
+        if (is_array($atts)) {
+            foreach ($atts as $att)
+                if (!in_array($att, $this->toNull))
+                    $this->toNull[] = $att;
+            return;
+        }
+
+        if (!in_array($atts, $this->toNull))
+            $this->toNull[] = $atts;
+    }
+}
+?>
diff --git a/src/Libs/Neuron.php b/src/Libs/Neuron.php
new file mode 100644
index 0000000..3c78242
--- /dev/null
+++ b/src/Libs/Neuron.php
@@ -0,0 +1,54 @@
+ $value)
+            $this->{$key} = $value;
+    }
+
+    /**
+     * __get
+     *
+     * @param string $index
+     * @return null
+     */
+    public function __get(string $index): null
+    {
+        return null;
+    }
+}
+
+?>
diff --git a/src/Libs/Request.php b/src/Libs/Request.php
new file mode 100644
index 0000000..4a33e7b
--- /dev/null
+++ b/src/Libs/Request.php
@@ -0,0 +1,144 @@
+path   = Router::currentPath();
+        $this->get    = new Neuron($_GET);
+        $this->post   = new Neuron($_POST);
+        $this->put    = new Neuron();
+        $this->patch  = new Neuron();
+
+        $contentType = isset($_SERVER["CONTENT_TYPE"]) ? trim($_SERVER["CONTENT_TYPE"]) : '';
+        if ($contentType === "application/json")
+            $this->json = new Neuron(
+                (object) json_decode(trim(file_get_contents("php://input")), false)
+            );
+        else {
+            $this->json   = new Neuron();
+            if (in_array($_SERVER['REQUEST_METHOD'], ['PUT', 'PATCH'])) {
+                parse_str(file_get_contents("php://input"), $input_vars);
+                $this->{strtolower($_SERVER['REQUEST_METHOD'])} = new Neuron($input_vars);
+            }
+        }
+
+        $this->params = new Neuron();
+    }
+
+    /**
+     * Corre las validaciones e intenta continuar con la pila de callbacks.
+     *
+     * @return mixed
+     */
+    public function handle(): mixed
+    {
+        if ($this->validate())
+            return Middleware::next($this);
+
+        return null;
+    }
+
+    /**
+     * Inicia la validación que se haya configurado.
+     *
+     * @return bool
+     */
+    public function validate(): bool
+    {
+        $actual = match($_SERVER['REQUEST_METHOD']) {
+            'GET', 'DELETE' => $this->get,
+            default => $this->{strtolower($_SERVER['REQUEST_METHOD'])}
+        };
+
+        if (Validator::validateList(static::paramRules(), $this->params) &&
+            Validator::validateList(static::getRules(),   $this->get   ) &&
+            Validator::validateList(static::rules(),      $actual))
+            return true;
+
+        if (isset(static::messages()[Validator::$lastFailed]))
+            $error =  static::messages()[Validator::$lastFailed];
+        else {
+
+            $error = 'Error: validation failed of '.preg_replace('/\./', ' as ', Validator::$lastFailed, 1);
+        }
+
+        return static::onInvalid($error);
+    }
+
+    /**
+     * Reglas para el método actual.
+     *
+     * @return array
+     */
+    public static function rules(): array {
+        return [];
+    }
+
+    /**
+     * Reglas para los parámetros por URL.
+     *
+     * @return array
+     */
+    public static function paramRules(): array {
+        return [];
+    }
+
+    /**
+     * Reglas para los parámetros GET.
+     *
+     * @return array
+     */
+    public static function getRules(): array {
+        return [];
+    }
+
+    /**
+     * Mensajes de error en caso de fallar una validación.
+     *
+     * @return array
+     */
+    public static function messages(): array {
+        return [];
+    }
+
+    /**
+     * Función a ejecutar cuando se ha detectado un valor no válido.
+     *
+     * @param string $error
+     *
+     * @return false
+     */
+    protected function onInvalid(string $error): false
+    {
+        http_response_code(422);
+        print($error);
+        return false;
+    }
+}
diff --git a/src/Libs/Router.php b/src/Libs/Router.php
new file mode 100644
index 0000000..7e62152
--- /dev/null
+++ b/src/Libs/Router.php
@@ -0,0 +1,374 @@
+Error 404 - Página no encontrada';
+    }
+
+    /**
+     * __construct
+     */
+    private function __construct() {}
+
+    /**
+     * Parsea para deectar las pseudovariables (ej: {variable})
+     *
+     * @param string $path
+     *   Ruta con pseudovariables.
+     *
+     * @param callable $callback
+     *   Callback que será llamado cuando la ruta configurada en $path coincida.
+     *
+     * @return array
+     *   Arreglo con 2 índices:
+     *     path        - Contiene la ruta con las pseudovariables reeplazadas por expresiones regulares.
+     *     callback    - Contiene el callback en formato Namespace\Clase::Método.
+     */
+    private static function parse(string $path, callable $callback): array
+    {
+        preg_match_all('/{(\w+)}/s', $path, $matches, PREG_PATTERN_ORDER);
+        $paramNames = $matches[1];
+
+        $path = preg_quote($path, '/');
+        $path = preg_replace(
+            ['/\\\{\w+\\\}/s'],
+            ['([^\/]+)'],
+            $path);
+
+        return [
+            'path'       => $path,
+            'callback'   => [$callback],
+            'paramNames' => $paramNames
+        ];
+    }
+
+
+    /**
+     * Devuelve el ruta base o raiz del proyecto sobre la que trabajará el router.
+     *
+     * Ej: Si la url del sistema está en "https://ejemplo.com/duckbrain"
+     *     entonces la ruta base sería "/duckbrain"
+     *
+     * @return string
+     */
+    public static function basePath(): string
+    {
+        if (defined('SITE_URL') && !empty(SITE_URL))
+            return rtrim(parse_url(SITE_URL, PHP_URL_PATH), '/').'/';
+        return str_replace($_SERVER['DOCUMENT_ROOT'], '/', ROOT_DIR);
+    }
+
+    /**
+     * Redirije a una ruta relativa interna.
+     *
+     * @param string $path
+     *   La ruta relativa a la ruta base.
+     *
+     * Ej: Si nuesto sistema está en "https://ejemplo.com/duckbrain"
+     *     llamamos a Router::redirect('/docs'), entonces seremos
+     *     redirigidos a "https://ejemplo.com/duckbrain/docs".
+     * @return void
+     */
+    public static function redirect(string $path): void
+    {
+        header('Location: '.static::basePath().ltrim($path, '/'));
+        exit;
+    }
+
+    /**
+     * Añade un middleware a la última ruta usada.
+     * Solo se puede usar un middleware a la vez.
+     *
+     * @param callable $callback
+     * @param int $prioriry
+     *
+     * @return static
+     *   Devuelve la instancia actual.
+     */
+    public static function middleware(callable $callback, int $priority = null): static
+    {
+        if (!isset(static::$last))
+            return new static();
+
+        $method = static::$last[0];
+        $index = static::$last[1];
+
+        if (isset($priority) && $priority <= 0)
+            $priority = 1;
+
+        if (is_null($priority) || $priority >= count(static::$$method[$index]['callback']))
+            static::$$method[$index]['callback'][] = $callback;
+        else {
+            static::$$method[$index]['callback'] = array_merge(
+                array_slice(static::$$method[$index]['callback'], 0, $priority),
+                [$callback],
+                array_slice(static::$$method[$index]['callback'], $priority)
+            );
+        }
+
+        return new static();
+    }
+
+    /**
+     * Reconfigura el callback final de la última ruta.
+     *
+     * @param callable $callback
+     *
+     * @return static
+     */
+    public static function reconfigure(callable $callback): static
+    {
+        if (empty(static::$last))
+            return new static();
+
+        $method = static::$last[0];
+        $index  = static::$last[1];
+
+        static::$$method[$index]['callback'][0] = $callback;
+
+        return new static();
+    }
+
+    /**
+     * Configura calquier método para todas las rutas.
+     *
+     * En caso de no recibir un callback, busca la ruta actual
+     * solo configura la ruta como la última configurada
+     * siempre y cuando la misma haya sido configurada previamente.
+     *
+     * @param string $method
+     *    Método http.
+     * @param string $path
+     *    Ruta con pseudovariables.
+     * @param callable|null $callback
+     *
+     * @return
+     *    Devuelve la instancia actual.
+     */
+    public static function configure(string $method, string $path, ?callable $callback = null): static
+    {
+        if (is_null($callback)) {
+            $path = preg_quote($path, '/');
+            $path = preg_replace(
+                ['/\\\{\w+\\\}/s'],
+                ['([^\/]+)'],
+                $path);
+
+            foreach(static::$$method as $index => $router)
+                if ($router['path'] == $path) {
+                    static::$last = [$method, $index];
+                    break;
+                }
+
+            return new static();
+        }
+
+        static::$$method[] = static::parse($path, $callback);
+        static::$last = [$method, count(static::$$method)-1];
+        return new static();
+    }
+
+    /**
+     * Define los routers para el método GET.
+     *
+     * @param string $path
+     *   Ruta con pseudovariables.
+     * @param callable $callback
+     *   Callback que será llamado cuando la ruta configurada en $path coincida.
+     *
+     * @return static
+     *   Devuelve la instancia actual.
+     */
+    public static function get(string $path, callable $callback = null): static
+    {
+        return static::configure('get', $path, $callback);
+    }
+
+    /**
+     * Define los routers para el método POST.
+     *
+     * @param string $path
+     *   Ruta con pseudovariables.
+     * @param callable $callback
+     *   Callback que será llamado cuando la ruta configurada en $path coincida.
+     *
+     * @return static
+     *   Devuelve la instancia actual.
+     */
+    public static function post(string $path, callable $callback = null): static
+    {
+        return static::configure('post', $path, $callback);
+    }
+
+    /**
+     * Define los routers para el método PUT.
+     *
+     * @param string $path
+     *   Ruta con pseudovariables.
+     * @param callable $callback
+     *   Callback que será llamado cuando la ruta configurada en $path coincida.
+     *
+     * @return static
+     *   Devuelve la instancia actual
+     */
+
+    public static function put(string $path, callable $callback = null): static
+    {
+        return static::configure('put', $path, $callback);
+    }
+
+    /**
+     * Define los routers para el método PATCH.
+     *
+     * @param string $path
+     *   Ruta con pseudovariables.
+     * @param callable $callback
+     *   Callback que será llamado cuando la ruta configurada en $path coincida.
+     *
+     * @return static
+     *   Devuelve la instancia actual
+     */
+    public static function patch(string $path, callable $callback = null): static
+    {
+        return static::configure('patch', $path, $callback);
+    }
+
+    /**
+     * Define los routers para el método DELETE.
+     *
+     * @param string $path
+     *   Ruta con pseudovariables
+     * @param callable $callback
+     *   Callback que será llamado cuando la ruta configurada en $path coincida.
+     *
+     * @return static
+     *   Devuelve la instancia actual
+     */
+    public static function delete(string $path, callable $callback = null): static
+    {
+        return static::configure('delete', $path, $callback);
+    }
+
+    /**
+     * Devuelve la ruta actual tomando como raíz la ruta de instalación de DuckBrain.
+     *
+     * @return string
+     */
+    public static function currentPath() : string
+    {
+        return preg_replace('/'.preg_quote(static::basePath(), '/').'/',
+                            '/', strtok($_SERVER['REQUEST_URI'], '?'), 1);
+    }
+
+    /**
+     * Aplica la configuración de rutas.
+     *
+     * @param string $path (opcional) Ruta a usar. Si no se define, detecta la ruta actual.
+     *
+     * @return void
+     */
+    public static function apply(string $path = null): void
+    {
+        $path    = $path ?? static::currentPath();
+        $routers = match($_SERVER['REQUEST_METHOD']) { // Según el método selecciona un arreglo de routers
+            'POST'   => static::$post,
+            'PUT'    => static::$put,
+            'PATCH'  => static::$patch,
+            'DELETE' => static::$delete,
+            default  => static::$get
+        };
+
+        foreach ($routers as $router) { // revisa todos los routers para ver si coinciden con la ruta actual
+            if (preg_match_all('/^'.$router['path'].'\/?$/si',$path, $matches, PREG_PATTERN_ORDER)) {
+                unset($matches[0]);
+
+                // Objtener un reflection del callback
+                $lastCallback = $router['callback'][0];
+                if ($lastCallback instanceof \Closure) { // si es función anónima
+                    $reflectionCallback = new \ReflectionFunction($lastCallback);
+                } else {
+                    if (is_string($lastCallback))
+                        $lastCallback = preg_split('/::/', $lastCallback);
+
+                    // Revisamos su es un método o solo una función
+                    if (count($lastCallback) == 2)
+                        $reflectionCallback = new \ReflectionMethod($lastCallback[0], $lastCallback[1]);
+                    else
+                        $reflectionCallback = new \ReflectionFunction($lastCallback[0]);
+                }
+
+                // Obtener los parámetros
+                $arguments  = $reflectionCallback->getParameters();
+                if (isset($arguments[0])) {
+                    // Obtenemos la clase del primer parámetro
+                    $argumentClass = strval($arguments[0]->getType());
+
+                    // Verificamos si la clase está o no tipada
+                    if (empty($argumentClass)) {
+                        $request = new Request;
+                    } else {
+                        $request = new $argumentClass;
+
+                        // Verificamos que sea instancia de Request (requerimiento)
+                        if (!($request instanceof Request))
+                            throw new \Exception('Bad argument type on router callback.');
+                    }
+                } else {
+                    $request = new Request;
+                }
+
+                // Comprobando y guardando los parámetros variables de la ruta
+                if (isset($matches[1])) {
+                    foreach ($matches as $index => $match) {
+                        $paramName                   = $router['paramNames'][$index-1];
+                        $request->params->$paramName = urldecode($match[0]);
+                    }
+                }
+
+                // Llama a la validación y luego procesa la cola de callbacks
+                $request->next = $router['callback'];
+                $data          = $request->handle();
+
+                // Por defecto imprime como JSON si se retorna algo
+                if (isset($data)) {
+                    header('Content-Type: application/json');
+                    print(json_encode($data));
+                }
+
+                return;
+            }
+        }
+
+        // Si no hay router que coincida llamamos a $notFoundCallBack
+        call_user_func_array(static::$notFoundCallback, [new Request]);
+    }
+}
diff --git a/src/Libs/Validator.php b/src/Libs/Validator.php
new file mode 100644
index 0000000..e9fb438
--- /dev/null
+++ b/src/Libs/Validator.php
@@ -0,0 +1,201 @@
+ $rules) {
+            $rules = preg_split('/\|/', $rules);
+            foreach ($rules as $rule) {
+                if (static::checkRule($haystack->{$target}, $rule))
+                    continue;
+                static::$lastFailed = $target.'.'.$rule;
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Revisa si una regla se cumple.
+     *
+     * @param mixed  $subject Lo que se va a verfificar.
+     * @param string $rule    La regla a probar.
+     *
+     * @return bool
+     */
+    public static function checkRule(mixed $subject, string $rule): bool
+    {
+        $arguments    = preg_split('/[:,]/', $rule);
+        $rule         = [static::class, $arguments[0]];
+        $arguments[0] = $subject;
+
+        if (is_callable($rule))
+            return call_user_func_array($rule, $arguments);
+
+        throw new \Exception('Bad rule: "'.preg_split('/::/', $rule)[1].'"' );
+    }
+
+    /**
+     * Verifica la regla de manera negativa.
+     *
+     * @param mixed $subject Lo que se va a verfificar.
+     * @param mixed $rule    La regla a probar.
+     *
+     * @return bool
+     */
+    public static function not(mixed $subject, ...$rule): bool
+    {
+        return !static::checkRule($subject, join(':', $rule));
+    }
+
+    /**
+     * Comprueba que que esté definido/exista.
+     *
+     * @param mixed $subject
+     *
+     * @return bool
+     */
+    public static function exists(mixed $subject): bool
+    {
+        return isset($subject);
+    }
+
+    /**
+     * Comprueba que que esté definido y no esté vacío.
+     *
+     * @param mixed $subject
+     *
+     * @return bool
+     */
+    public static function required(mixed $subject): bool
+    {
+        return isset($subject) && !empty($subject);
+    }
+
+    /**
+     * number
+     *
+     * @param mixed $subject
+     *
+     * @return bool
+     */
+    public static function number(mixed $subject): bool
+    {
+        return is_numeric($subject);
+    }
+
+    /**
+     * int
+     *
+     * @param mixed $subject
+     *
+     * @return bool
+     */
+    public static function int(mixed $subject): bool
+    {
+        return filter_var($subject, FILTER_VALIDATE_INT);
+    }
+
+    /**
+     * float
+     *
+     * @param mixed $subject
+     *
+     * @return bool
+     */
+    public static function float(mixed $subject): bool
+    {
+        return filter_var($subject, FILTER_VALIDATE_FLOAT);
+    }
+
+    /**
+     * bool
+     *
+     * @param mixed $subject
+     *
+     * @return bool
+     */
+    public static function bool(mixed $subject): bool
+    {
+        return filter_var($subject, FILTER_VALIDATE_BOOLEAN);
+    }
+
+    /**
+     * email
+     *
+     * @param mixed $subject
+     *
+     * @return bool
+     */
+    public static function email(mixed $subject): bool
+    {
+        return filter_var($subject, FILTER_VALIDATE_EMAIL);
+    }
+
+    /**
+     * url
+     *
+     * @param mixed $subject
+     *
+     * @return bool
+     */
+    public static function url(mixed $subject): bool
+    {
+        return filter_var($subject, FILTER_VALIDATE_URL);
+    }
+
+    /**
+     * enum
+     *
+     * @param mixed $subject
+     * @param mixed $values
+     *
+     * @return bool
+     */
+    public static function enum(mixed $subject, ...$values): bool
+    {
+        return in_array($subject, $values);
+    }
+}
diff --git a/src/Middlewares/.keep b/src/Middlewares/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/src/Models/.keep b/src/Models/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/src/Models/Answer.php b/src/Models/Answer.php
new file mode 100644
index 0000000..8909fe2
--- /dev/null
+++ b/src/Models/Answer.php
@@ -0,0 +1,15 @@
+create_at = date('Y-m-d H:i:s');
+        parent::add();
+    }
+}
diff --git a/src/Models/Pool.php b/src/Models/Pool.php
new file mode 100644
index 0000000..f7b27db
--- /dev/null
+++ b/src/Models/Pool.php
@@ -0,0 +1,65 @@
+votes))
+            $this->votes = Answer::where_in('option_id', array_column($this->getOptions(), 'id'))->count();
+
+        return $this->votes;
+    }
+
+    /**
+     * Devuelve las opciones de la encuesta.
+     *
+     * @return array
+     */
+    public function getOptions(): array
+    {
+        if (!isset($this->options))
+            $this->options = PoolOption::where('pool_id', $this->id)->get();
+
+        return $this->options;
+    }
+
+
+
+    /**
+     * Crea la cookie del voto para evitar doble votación.
+     *
+     * @return void
+     */
+    public function createCookie(): void
+    {
+        $cookieValue = Crypto::encrypt64(json_encode([
+            'pid' => $this->id,
+            'exp' => time()+43200
+        ]));
+        setcookie('POOL_VOTE', $cookieValue, time()+43200, parse_url(SITE_URL, PHP_URL_PATH));
+    }
+
+    /**
+     * Verifica si ya se ha emitido su voto en la encuesta actual.
+     *
+     * @return bool
+     */
+    public function isVoted(): bool
+    {
+         if (!isset($_COOKIE['POOL_VOTE']))
+            return false;
+
+        $cookie = json_decode(Crypto::decrypt64($_COOKIE['POOL_VOTE']));
+
+        return (json_last_error() == JSON_ERROR_NONE &&
+                $cookie->exp > time() &&
+                $cookie->pid == $this->id);
+    }
+}
diff --git a/src/Models/PoolOption.php b/src/Models/PoolOption.php
new file mode 100644
index 0000000..3ef7ad8
--- /dev/null
+++ b/src/Models/PoolOption.php
@@ -0,0 +1,18 @@
+votes))
+            $this->votes = Answer::where('option_id', $this->id)->count();
+
+        return $this->votes;
+    }
+}
diff --git a/src/Models/User.php b/src/Models/User.php
new file mode 100644
index 0000000..610d190
--- /dev/null
+++ b/src/Models/User.php
@@ -0,0 +1,95 @@
+password = password_hash($password, PASSWORD_DEFAULT);
+    }
+
+    /**
+     * Iniciar sesión.
+     *
+     * @param string $username
+     * @param string $password
+     *
+     * @return bool
+     */
+    public static function login(string $username, string $password): bool
+    {
+        $user = User::where('username', $username)->getFirst();
+
+        if(isset($user) && password_verify($password, $user->password)) {
+            $user->createLoginCookie();
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Crea la cookie de sesión.
+     * @return void
+     */
+    public function createLoginCookie(): void
+    {
+        setcookie('POOL_LC', $this->getSessionToken(), time()+1209600, parse_url(SITE_URL, PHP_URL_PATH));
+    }
+
+    /**
+     * Cerrar sessión.
+     * @return void
+     */
+    public static function logout(): void {
+        setcookie('POOL_LC', '', 0);
+    }
+
+    /**
+     * Verifica si el hay un usuario logueado.
+     *
+     * @return static|null
+     */
+    public static function isLogged(): static|null
+    {
+        if (!isset($_COOKIE['POOL_LC']))
+            return null;
+
+        $cookie = json_decode(Crypto::decrypt64($_COOKIE['POOL_LC']));
+
+        if (json_last_error() == JSON_ERROR_NONE &&
+            isset($cookie->expire) &&
+            $cookie->expire > time())
+            return User::where('username', $cookie->username)->getFirst();
+
+        return null;
+    }
+
+    /**
+     * Devuelve una llave encriptada asociada al correo del usuario.
+     *
+     * @return string
+     */
+    public function getSessionToken(int $expiration = 1209600): string
+    {
+        return Crypto::encrypt64(
+            json_encode([
+                'username' => $this->username,
+                'expire'   => time() + $expiration
+            ])
+        );
+    }
+}
diff --git a/src/Requests/LoginRequest.php b/src/Requests/LoginRequest.php
new file mode 100644
index 0000000..1381d01
--- /dev/null
+++ b/src/Requests/LoginRequest.php
@@ -0,0 +1,30 @@
+ 'required',
+            'password' => 'required',
+        ];
+    }
+    public static function messages(): array
+    {
+        return [
+            'username.required' => 'No se ha definido un usuario.',
+            'password.required' => 'No se ha definido una contraseña.',
+        ];
+    }
+
+    public function onInvalid(string $error): false
+    {
+        HTMX::retarget('.alert');
+        echo $error;
+        return false;
+    }
+}
diff --git a/src/Requests/PoolCreateRequest.php b/src/Requests/PoolCreateRequest.php
new file mode 100644
index 0000000..7dc334d
--- /dev/null
+++ b/src/Requests/PoolCreateRequest.php
@@ -0,0 +1,33 @@
+ 'required',
+            'option1' => 'required',
+            'option2' => 'required',
+            'option3' => 'required',
+        ];
+    }
+
+    public static function messages(): array
+    {
+        return [
+            'title.required' => 'No se ha definido un título.',
+            'option1.required' => 'No se ha definido la opción 01.',
+            'option2.required' => 'No se ha definido la opción 02.',
+            'option3.required' => 'No se ha definido la opción 03.',
+        ];
+    }
+
+    public function onInvalid(string $error): false
+    {
+        HTMX::retarget('.alert');
+        echo $error;
+        return false;
+    }
+}
diff --git a/src/Requests/PoolRequest.php b/src/Requests/PoolRequest.php
new file mode 100644
index 0000000..564995b
--- /dev/null
+++ b/src/Requests/PoolRequest.php
@@ -0,0 +1,18 @@
+pool = Pool::orderBy('id', 'DESC')->getFirst();
+
+        if (is_null($this->pool))
+            return $this->onInvalid('Aún no hay una encuesta configurada.');
+
+        return parent::validate();
+    }
+}
diff --git a/src/Requests/UserRequest.php b/src/Requests/UserRequest.php
new file mode 100644
index 0000000..ac39a34
--- /dev/null
+++ b/src/Requests/UserRequest.php
@@ -0,0 +1,21 @@
+user = User::isLogged();
+        if (is_null($this->user)) {
+            HTMX::redirect('/login');
+            return false;
+        }
+
+        return parent::validate();
+    }
+}
diff --git a/src/Requests/VoteRequest.php b/src/Requests/VoteRequest.php
new file mode 100644
index 0000000..6c48663
--- /dev/null
+++ b/src/Requests/VoteRequest.php
@@ -0,0 +1,32 @@
+pool->id != $this->params->pid)
+            return $this->onInvalid('Voto no válido.');
+
+        $this->option = PoolOption::getById($this->params->vid);
+        if (is_null($this->option) || $this->option->pool_id != $this->params->pid)
+            return $this->onInvalid('Voto no válido.');
+
+        return true;
+    }
+
+    public static function paramRules(): array
+    {
+        return [
+            'pid' => 'int',
+            'vid' => 'int'
+        ];
+    }
+
+}
diff --git a/src/Routers/.keep b/src/Routers/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/src/Routers/pool.php b/src/Routers/pool.php
new file mode 100644
index 0000000..5f00d8a
--- /dev/null
+++ b/src/Routers/pool.php
@@ -0,0 +1,16 @@
+
+
+  
+    
+    
+    
+    
+    <?=$this->title?>
+  
+  
+    
+ content?> +
+ + + diff --git a/src/Views/Login.php b/src/Views/Login.php new file mode 100644 index 0000000..d711c81 --- /dev/null +++ b/src/Views/Login.php @@ -0,0 +1,38 @@ + + +
+
+
+

Iniciar sesión

+
+
+
+
+ + +
+ + + +
+
+
+
+ +
diff --git a/src/Views/Pool.php b/src/Views/Pool.php new file mode 100644 index 0000000..6fae8bc --- /dev/null +++ b/src/Views/Pool.php @@ -0,0 +1,23 @@ + +
+
+
+

pool->title)?>

+
+
+ pool->isVoted()): ?> + ¡Gracias por participar! + + pool->getOptions() as $option): ?> +
+ value)?> +
+ + +
+
+
+
diff --git a/src/Views/PoolConfig.php b/src/Views/PoolConfig.php new file mode 100644 index 0000000..918f6ab --- /dev/null +++ b/src/Views/PoolConfig.php @@ -0,0 +1,55 @@ + +
+
+
+

Configurar nueva encuesta

+
+
+
+
+ + + + +
+ + + +
+ saved): ?> +
+ Se ha creado la encuesta!! +
+ +
+
+
+
diff --git a/src/Views/Result.php b/src/Views/Result.php new file mode 100644 index 0000000..d8641bf --- /dev/null +++ b/src/Views/Result.php @@ -0,0 +1,37 @@ + + +
+ +
+
+

pool->title)?>

+
+
+ pool->getOptions() as $option): ?> + votes() == 0) + $percent = 0; + else + $percent = round(($option->votes() * 100 / $this->pool->votes()), 2); + ?> + + +
+
+ + Total de votos: pool->votes()?> + +
+
+ +
+
+