Jake Vanderwerf
2025-11-10 e9967fa22781d922ba4eb8fb44fe72d200ac4b14
=IconsManager.php update
2 files deleted
100 files modified
6 files added
7218 ■■■■■ changed files
JVBase.php 9 ●●●●● patch | view | raw | blame | history
assets/css/dash.min.css 2 ●●● patch | view | raw | blame | history
assets/css/forms.min.css 2 ●●● patch | view | raw | blame | history
assets/css/icons.css 1 ●●●● patch | view | raw | blame | history
assets/css/icons.min.css 1 ●●●● patch | view | raw | blame | history
assets/css/nav.min.css 2 ●●● patch | view | raw | blame | history
assets/js/Gallery.js 350 ●●●●● patch | view | raw | blame | history
assets/js/concise/FormController.js 266 ●●●● patch | view | raw | blame | history
assets/js/concise/JVBase.js 1 ●●●● patch | view | raw | blame | history
assets/js/concise/Media.js 235 ●●●● patch | view | raw | blame | history
assets/js/concise/PopulateForm.js 75 ●●●● patch | view | raw | blame | history
assets/js/concise/Queue.js 6 ●●●● patch | view | raw | blame | history
assets/js/concise/Referral.js 4 ●●●● patch | view | raw | blame | history
assets/js/concise/TaxonomySelector.js 26 ●●●●● patch | view | raw | blame | history
assets/js/concise/UploadManager.js 107 ●●●●● patch | view | raw | blame | history
assets/js/concise/View.js 22 ●●●● patch | view | raw | blame | history
assets/js/concise/quill.js 28 ●●●● patch | view | raw | blame | history
assets/js/dash/CRUD.js 10 ●●●●● patch | view | raw | blame | history
assets/js/dash/FavouritesManager.js 6 ●●●● patch | view | raw | blame | history
assets/js/dash/NewsManager.js 2 ●●● patch | view | raw | blame | history
assets/js/dash/TaxonomyCreator.js 56 ●●●●● patch | view | raw | blame | history
assets/js/dash/UtilityFunctions.js 7 ●●●●● patch | view | raw | blame | history
assets/js/min/creator.min.js 2 ●●● patch | view | raw | blame | history
assets/js/min/crud.min.js 2 ●●● patch | view | raw | blame | history
assets/js/min/favouritesManager.min.js 2 ●●● patch | view | raw | blame | history
assets/js/min/form.min.js 2 ●●● patch | view | raw | blame | history
assets/js/min/gallery.min.js 2 ●●● patch | view | raw | blame | history
assets/js/min/media.min.js 1 ●●●● patch | view | raw | blame | history
assets/js/min/news.min.js 2 ●●● patch | view | raw | blame | history
assets/js/min/populate.min.js 2 ●●● patch | view | raw | blame | history
assets/js/min/queue.min.js 2 ●●● patch | view | raw | blame | history
assets/js/min/quill.min.js 2 ●●● patch | view | raw | blame | history
assets/js/min/referral.min.js 2 ●●● patch | view | raw | blame | history
assets/js/min/selector.min.js 2 ●●● patch | view | raw | blame | history
assets/js/min/uploader.min.js 2 ●●● patch | view | raw | blame | history
assets/js/min/utility.min.js 2 ●●● patch | view | raw | blame | history
assets/js/min/view.min.js 2 ●●● patch | view | raw | blame | history
base/_setup.php 1 ●●●● patch | view | raw | blame | history
base/email.php 97 ●●●●● patch | view | raw | blame | history
build/feed/render.php 8 ●●●● patch | view | raw | blame | history
build/glossary/view.asset.php 2 ●●● patch | view | raw | blame | history
build/glossary/view.js 2 ●●● patch | view | raw | blame | history
build/summary/render.php 19 ●●●●● patch | view | raw | blame | history
cleanup.php 4 ●●● patch | view | raw | blame | history
icons.php 761 ●●●●● patch | view | raw | blame | history
iconsOld.php 761 ●●●●● patch | view | raw | blame | history
inc/blocks/CustomBlocks.php 109 ●●●●● patch | view | raw | blame | history
inc/blocks/FeedBlock.php 12 ●●●● patch | view | raw | blame | history
inc/forms/PostSelectorOld.php 2 ●●●●● patch | view | raw | blame | history
inc/forms/TaxonomySelector.php 8 ●●●● patch | view | raw | blame | history
inc/forms/TaxonomySelectorOld.php 6 ●●●● patch | view | raw | blame | history
inc/helpers/all.php 2 ●●● patch | view | raw | blame | history
inc/helpers/breadcrumbs.php 2 ●●● patch | view | raw | blame | history
inc/helpers/crud.php 2 ●●● patch | view | raw | blame | history
inc/helpers/formatting.php 14 ●●●● patch | view | raw | blame | history
inc/helpers/forms.php 2 ●●● patch | view | raw | blame | history
inc/helpers/icons.php 54 ●●●●● patch | view | raw | blame | history
inc/helpers/legacy.php 4 ●●● patch | view | raw | blame | history
inc/helpers/media.php 6 ●●●● patch | view | raw | blame | history
inc/helpers/members.php 8 ●●●● patch | view | raw | blame | history
inc/helpers/renderFields.php 38 ●●●● patch | view | raw | blame | history
inc/helpers/terms.php 4 ●●●● patch | view | raw | blame | history
inc/helpers/ui.php 28 ●●●● patch | view | raw | blame | history
inc/integrations/Facebook.php 2 ●●● patch | view | raw | blame | history
inc/integrations/GoogleMyBusiness.php 17 ●●●●● patch | view | raw | blame | history
inc/integrations/Helcim.php 4 ●●●● patch | view | raw | blame | history
inc/integrations/Integrations.php 2 ●●● patch | view | raw | blame | history
inc/integrations/PostMark.php 52 ●●●● patch | view | raw | blame | history
inc/integrations/Square.php 6 ●●●● patch | view | raw | blame | history
inc/managers/AdminPages.php 2 ●●● patch | view | raw | blame | history
inc/managers/CRUDManager.php 128 ●●●●● patch | view | raw | blame | history
inc/managers/DashboardManager.php 63 ●●●●● patch | view | raw | blame | history
inc/managers/DirectoryManager.php 2 ●●● patch | view | raw | blame | history
inc/managers/EmailManager.php 324 ●●●● patch | view | raw | blame | history
inc/managers/IconsManager.php 434 ●●●●● patch | view | raw | blame | history
inc/managers/LoginManager.php 802 ●●●●● patch | view | raw | blame | history
inc/managers/LoginManagerOld.php 6 ●●●● patch | view | raw | blame | history
inc/managers/MagicLinkManager.php 14 ●●●●● patch | view | raw | blame | history
inc/managers/NotificationManager.php 4 ●●●● patch | view | raw | blame | history
inc/managers/OperationQueue.php 5 ●●●● patch | view | raw | blame | history
inc/managers/ReferralManager.php 221 ●●●●● patch | view | raw | blame | history
inc/managers/RegisterManager.php 4 ●●●● patch | view | raw | blame | history
inc/managers/TaxonomyRelationships.php 13 ●●●● patch | view | raw | blame | history
inc/managers/UserTermsManager.php 33 ●●●●● patch | view | raw | blame | history
inc/managers/_setup.php 7 ●●●●● patch | view | raw | blame | history
inc/meta/MetaForm.php 37 ●●●● patch | view | raw | blame | history
inc/meta/MetaFormOld.php 44 ●●●● patch | view | raw | blame | history
inc/meta/MetaManager.php 4 ●●●● patch | view | raw | blame | history
inc/meta/MetaRenderer.php 8 ●●●● patch | view | raw | blame | history
inc/meta/_setup.php 3 ●●●●● patch | view | raw | blame | history
inc/registry/CheckCustomTables.php 6 ●●●● patch | view | raw | blame | history
inc/rest/RestRouteManager.php 420 ●●●●● patch | view | raw | blame | history
inc/rest/_setup.php 9 ●●●●● patch | view | raw | blame | history
inc/rest/routes/ApprovalRoutes.php 7 ●●●● patch | view | raw | blame | history
inc/rest/routes/ContentRoutes.php 255 ●●●●● patch | view | raw | blame | history
inc/rest/routes/FavouritesRoutes.php 8 ●●●●● patch | view | raw | blame | history
inc/rest/routes/Invitations.php 57 ●●●● patch | view | raw | blame | history
inc/rest/routes/LoginRoutes.php 667 ●●●●● patch | view | raw | blame | history
inc/rest/routes/MagicLinkRoutes.php 88 ●●●● patch | view | raw | blame | history
inc/rest/routes/ReferralRoutes.php 65 ●●●●● patch | view | raw | blame | history
inc/templates.php 6 ●●●● patch | view | raw | blame | history
inc/users/UserSettings.php 12 ●●●● patch | view | raw | blame | history
jvb.php 105 ●●●●● patch | view | raw | blame | history
src/feed/render.php 8 ●●●● patch | view | raw | blame | history
src/glossary/view.js 17 ●●●● patch | view | raw | blame | history
src/summary/render.php 8 ●●●● patch | view | raw | blame | history
templates/dashboard/sections/favourites.php 18 ●●●● patch | view | raw | blame | history
templates/dashboard/sections/news.php 18 ●●●● patch | view | raw | blame | history
JVBase.php
@@ -25,6 +25,7 @@
use JVBase\rest\routes\QueueRoutes;
use JVBase\rest\routes\ErrorRoutes;
use JVBase\rest\routes\FormRoutes;
use JVBase\rest\routes\LoginRoutes;
use JVBase\rest\routes\NewsRoutes;
use JVBase\rest\routes\ReferralRoutes;
use JVBase\rest\routes\MagicLinkRoutes;
@@ -61,6 +62,7 @@
        'gmb' => 'JVBase\integrations\GoogleMyBusiness',
        'cloudflare' => 'JVBase\integrations\Cloudflare',
        'umami' => 'JVBase\integrations\Umami',
        'postmark' => 'JVBase\integrations\PostMark',
    ];
    public static function getInstance():JVB
@@ -104,6 +106,7 @@
        }
        $this->routes = [
            'login'         => new LoginRoutes(),
            'integrations'  => new IntegrationsRoutes(),
        ];
        if (jvbSiteUsesFeedBlock()) {
@@ -117,10 +120,10 @@
            $this->routes['term'] = new TermRoutes();
        }
        $this->routes['queue']  = new QueueRoutes();
        $this->routes['settings']= new SettingsRoutes();
        $this->routes['upload'] = new UploadRoutes();
        if (jvbSiteHasDashboard()) {
            $this->routes['upload'] = new UploadRoutes();
            $this->routes['settings']= new SettingsRoutes();
            $this->routes['queue']  = new QueueRoutes();
            $this->routes['error']  = new ErrorRoutes();
            $this->routes['admin']  = new AdminRoutes();
            $this->routes['content']= new ContentRoutes();
assets/css/dash.min.css
@@ -1 +1 @@
:target{outline:0!important;padding:0!important}.dashboard h1:first-of-type{margin-top:4rem!important}main>footer{max-width:100%!important;position:fixed;z-index:var(--z-top);bottom:0;left:0;right:0;width:100%;margin:4rem 0 0 0!important;height:var(--height);padding:0!important;background-color:var(--base);box-shadow:var(--shadow)}main>*{max-width:min(768px,90vw)!important;margin:0 auto!important}main h1{margin:0!important;font-size:var(--large)}.item-grid .item{position:relative}img{width:100%;height:auto;aspect-ratio:1;object-fit:cover}.replace{margin-bottom:var(--offHeight)!important}.item-grid{margin-bottom:4rem}.item-grid:has(.select-item:checked) .item{padding:.75rem;opacity:.8;filter:var(--filter)}.item-grid .item:has(.select-item:checked){padding:.5rem;filter:none;opacity:1;background-color:var(--action-0)}.grid-view .item>input[type=checkbox]:not(.label-button)+label{padding-left:0;margin:0}.grid-view .item>input[type=checkbox]+label::before{transform:unset;top:.5rem;left:.5rem}.grid-view .item>input[type=checkbox]+label::after{top:.5rem;left:.75rem;transform:translateY(20%) rotate(45deg)}.grid-view .item .item-actions{position:absolute;bottom:0;right:0}.list-view h3,.list-view p{margin:0!important}@media (min-width:768px){.grid-view{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}.grid-view .item .item-actions{bottom:unset;top:0}}.bulk-controls{margin:1rem 0}.bulk-controls .selected-count{font-weight:400;font-size:var(--small);text-transform:none;font-style:italic;display:flex;gap:.25rem;margin-left:2rem}.selected-count::before{content:'{'}.selected-count::after{content:'}'}.bulk-edit-form .selected{display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:4px}.selected label{padding:.5rem;opacity:.6;filter:var(--filter);border:2px solid transparent;transition:filter var(--transition-base),opacity var(--transition-base),border var(--transition-base),padding var(--transition-base)}.selected label:has(:checked){border-color:var(--action-0);padding:0;opacity:1;filter:none;transition:filter var(--transition-base),opacity var(--transition-base),border var(--transition-base),padding var(--transition-base)}form.table img{max-height:4rem}.all-filters{margin:2rem 0;padding:1rem 0;border-top:1px solid var(--base-200);border-bottom:1px solid var(--base-200)}details.uploader+.items-list .all-filters{border-top:none}.all-filters .filters{width:100%}.controls .radio-options,.filters.row.start{--align:center;--justify:flex-start;--gap:.5rem}.all-filters span.label{text-transform:uppercase;font-size:var(--small);font-weight:900;width:15vw;display:inline-flex;align-items:center;padding-right:2rem}.controls .icon{--w:1.4rem}.all-filters .btn+label,.all-filters button{height:fit-content;padding:.5rem!important;min-width:0;min-height:0}.all-filters .btn+label:focus,.all-filters .btn+label:hover,.all-filters button:focus,.all-filters button:hover{background-color:transparent;color:var(--action-0);border-color:var(--action-0)}.search-container:not(.open) .clear-search,.search-container:not(.open) input[type=search]{transform:scaleX(0);transform-origin:left;width:0;padding:0;transition:transform var(--transition-base),width var(--transition-base),padding var(--transition-base)}.search-container button{padding:.5rem}.search-container .icon{--w:1.5rem}.search-container.open .clear-search,.search-container.open input[type=search]{transform:scaleX(1);transform-origin:left;transition:transform var(--transition-base),width var(--transition-base),padding var(--transition-base)}.all-filters>.search,.search-container,input[type=search]{width:100%}form.table textarea{width:250px;padding:.5rem}.multi-select summary{--gap:2rem;padding-right:2.5rem}dialog.bulk-edit[open],dialog.create[open],dialog.edit[open]{height:85vh;top:5vh}.tab-content h2{display:none}.group-fields.hours .group-fields,.group-fields.hours .group-fields .field{display:flex;justify-content:space-between;align-items:center}.group-fields.hours .group-fields{padding:1rem .5rem;gap:1rem}.group-fields.hours .group-fields:nth-of-type(2n+1){background-color:var(--base)}.group-fields.hours .group-fields .field{margin:0}.group-fields.hours .true-false{flex:1}.group-fields.hours .time{position:relative}.group-fields.hours .time label{margin:0;font-size:var(--small);position:absolute;top:-1rem;left:0;color:var(--contrast-200)}.today_hours{width:min(500px,90vw)}.today_hours .group-fields{width:100%;padding:0;display:flex;justify-content:center;gap:.5rem}@media (min-width:768px){.today_hours .group-fields{padding:2rem}}.today_hours .field{margin:0}.dash .true-false{margin:0}.dash [type=submit]{width:90%}.dashboard.dash h2{text-transform:none;font-size:var(--large)}.dashboard.dash .replace>ul{display:flex;list-style:none;align-items:flex-start;justify-content:flex-start;flex-wrap:wrap;gap:.5rem}.dashboard.settings nav.tabs{--height:3.5rem;--x:var(--offHeight);position:fixed;bottom:var(--height);left:var(--x);right:var(--x);z-index:99;width:calc(100% - var(--x) - var(--x));background-color:var(--base)}nav.integrations,nav.integrations a,nav.integrations li,nav.integrations ul{height:auto}.replace{overflow:hidden}body.dash form#options{display:flex;flex-flow:column nowrap;justify-content:center;align-items:center}.item-grid.integrations{grid-template-columns:repeat(2,1fr);gap:2rem}.integration{background:var(--base);border:2px solid var(--base-200);border-radius:var(--outerRadius);padding:1rem;position:relative;transition:all var(--transition-base);box-shadow:var(--shadow)}.integration.connected{border-color:var(--success)}.integration.disconnected,.integration.error{border-color:var(--error)}.integration.hasChanges{border-color:var(--warning)}.integration .header{margin-bottom:.75rem;padding-bottom:.75rem;border-bottom:2px solid var(--base-200)}.integration h3{letter-spacing:1px;font-size:var(--medium);margin:0}.integration .meta{margin-bottom:1rem;text-align:right;color:var(--contrast-200);font-size:var(--small)}.integration .setup{font-size:var(--small);font-weight:700;text-transform:uppercase}.integration .setup .indicator{font-size:var(--medium)}.integration .connected .indicator,.integration .setup .connected{color:var(--success)}.integration .disconnected .indicator,.integration .setup .disconnected{color:var(--error)}.integration.hasChanges .disconnected{color:var(--warning)}.connection-status.connected{background-color:var(--successBack);color:var(--successText)}.connection-status.disconnected{background-color:var(--errorBack);color:var(--errorText)}.integration code{display:inline-block;width:90%;margin:0 .5rem;user-select:all;padding:.75rem;border:2px solid var(--base);background-color:var(--base-200);word-break:break-all}.integration details+details{margin-top:1rem}.integration .actions{margin-top:1rem}.hint{line-height:1.2;font-style:italic;font-size:var(--small)}.hasChanges button[data-action=save_credentials]{border-color:var(--warning);animation:pulse-color 1s infinite;animation-delay:1s}.flash{animation:flash .5s}.flash.connected{--b:var(--success)}.flash.disconnected{--b:var(--error)}.flash.syncing{--b:var(--success)}.flash.error,.flash.hasChanges{--b:var(--warning)}@keyframes flash{0%,100%{border-color:inherit}50%{border-color:var(--b)}}.location.field{width:80vw}.location.field>p{text-align:center}.location.field>p+p{margin:0 .5rem 0 0}.location.field .location-map{height:20vh}.location.field .location-links{padding:.5rem 0;display:flex;justify-content:space-evenly}.field.upload [data-upload-id],.item-grid .item{touch-action:none}.empty-state{grid-column:1/-1;padding:1rem 10vw;margin:0 10vw;border-radius:var(--outerRadius);background-color:var(--base-100)}.jvb-oauth-connect{position:relative;transition:opacity .2s}.jvb-oauth-connect.loading{opacity:.6;pointer-events:none}.jvb-oauth-connect.loading::after{content:'';position:absolute;right:-30px;top:50%;transform:translateY(-50%);width:16px;height:16px;border:2px solid #ccc;border-top-color:#0073aa;border-radius:50%;animation:oauth-spin .8s linear infinite}@keyframes oauth-spin{to{transform:translateY(-50%) rotate(360deg)}}.integration-status-message{padding:12px 16px;margin:16px 0;border-radius:4px;display:none;font-size:14px;line-height:1.5}.integration-status-message.success{display:block;background:#d4edda;color:#155724;border-left:4px solid #28a745}.integration-status-message.error{display:block;background:#f8d7da;color:#721c24;border-left:4px solid #dc3545}.integration-status-message.info{display:block;background:#d1ecf1;color:#0c5460;border-left:4px solid #17a2b8}.connection-status{display:inline-flex;align-items:center;gap:8px;padding:6px 12px;border-radius:4px;font-size:13px;font-weight:500}.connection-status.connected{background:#d4edda;color:#155724}.connection-status.disconnected{background:#f8d7da;color:#721c24}.status-indicator{font-size:10px;line-height:1}.connection-status.connected .status-indicator{color:#28a745}.connection-status.disconnected .status-indicator{color:#dc3545}
:target{outline:0!important;padding:0!important}.dashboard>header{justify-content:flex-end}.dashboard>header img{width:var(--height)}.dashboard h1:first-of-type{margin-top:4rem!important}main>footer{max-width:100%!important;position:fixed;z-index:var(--z-top);bottom:0;left:0;right:0;width:100%;margin:4rem 0 0 0!important;height:var(--height);padding:0!important;background-color:var(--base);box-shadow:var(--shadow)}main>*{max-width:min(768px,90vw)!important;margin:0 auto!important}main h1{margin:0!important;font-size:var(--large)}.item-grid .item{position:relative}img{width:100%;height:auto;aspect-ratio:1;object-fit:cover}.replace{margin-bottom:var(--offHeight)!important}.item-grid{margin-bottom:4rem}.item-grid:has(.select-item:checked) .item{padding:.75rem;opacity:.8;filter:var(--filter)}.item-grid .item:has(.select-item:checked){padding:.5rem;filter:none;opacity:1;background-color:var(--action-0)}.grid-view .item>input[type=checkbox]:not(.label-button)+label{padding-left:0;margin:0}.grid-view .item>input[type=checkbox]+label::before{transform:unset;top:.5rem;left:.5rem}.grid-view .item>input[type=checkbox]+label::after{top:.5rem;left:.75rem;transform:translateY(20%) rotate(45deg)}.grid-view .item .item-actions{position:absolute;bottom:0;right:0}.list-view h3,.list-view p{margin:0!important}@media (min-width:768px){.grid-view{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}.grid-view .item .item-actions{bottom:unset;top:0}}.bulk-controls{margin:1rem 0}.bulk-controls .selected-count{font-weight:400;font-size:var(--small);text-transform:none;font-style:italic;display:flex;gap:.25rem;margin-left:2rem}.selected-count::before{content:'{'}.selected-count::after{content:'}'}.bulk-edit-form .selected{display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:4px}.selected label{padding:.5rem;opacity:.6;filter:var(--filter);border:2px solid transparent;transition:filter var(--transition-base),opacity var(--transition-base),border var(--transition-base),padding var(--transition-base)}.selected label:has(:checked){border-color:var(--action-0);padding:0;opacity:1;filter:none;transition:filter var(--transition-base),opacity var(--transition-base),border var(--transition-base),padding var(--transition-base)}form.table img{max-height:4rem}.all-filters{margin:2rem 0;padding:1rem 0;border-top:1px solid var(--base-200);border-bottom:1px solid var(--base-200)}details.uploader+.items-list .all-filters{border-top:none}.all-filters .filters{width:100%}.controls .radio-options,.filters.row.start{--align:center;--justify:flex-start;--gap:.5rem}.all-filters span.label{text-transform:uppercase;font-size:var(--small);font-weight:900;width:15vw;display:inline-flex;align-items:center;padding-right:2rem}.controls .icon{--w:1.4rem}.all-filters .btn+label,.all-filters button{height:fit-content;padding:.5rem!important;min-width:0;min-height:0}.all-filters .btn+label:focus,.all-filters .btn+label:hover,.all-filters button:focus,.all-filters button:hover{background-color:transparent;color:var(--action-0);border-color:var(--action-0)}.search-container:not(.open) .clear-search,.search-container:not(.open) input[type=search]{transform:scaleX(0);transform-origin:left;width:0;padding:0;transition:transform var(--transition-base),width var(--transition-base),padding var(--transition-base)}.search-container button{padding:.5rem}.search-container .icon{--w:1.5rem}.search-container.open .clear-search,.search-container.open input[type=search]{transform:scaleX(1);transform-origin:left;transition:transform var(--transition-base),width var(--transition-base),padding var(--transition-base)}.all-filters>.search,.search-container,input[type=search]{width:100%}form.table textarea{width:250px;padding:.5rem}.multi-select summary{--gap:2rem;padding-right:2.5rem}dialog.bulk-edit[open],dialog.create[open],dialog.edit[open]{height:85vh;top:5vh}.tab-content h2{display:none}.group-fields.hours .group-fields,.group-fields.hours .group-fields .field{display:flex;justify-content:space-between;align-items:center}.group-fields.hours .group-fields{padding:1rem .5rem;gap:1rem}.group-fields.hours .group-fields:nth-of-type(2n+1){background-color:var(--base)}.group-fields.hours .group-fields .field{margin:0}.group-fields.hours .true-false{flex:1}.group-fields.hours .time{position:relative}.group-fields.hours .time label{margin:0;font-size:var(--small);position:absolute;top:-1rem;left:0;color:var(--contrast-200)}.today_hours{width:min(500px,90vw)}.today_hours .group-fields{width:100%;padding:0;display:flex;justify-content:center;gap:.5rem}@media (min-width:768px){.today_hours .group-fields{padding:2rem}}.today_hours .field{margin:0}.dash .true-false{margin:0}.dash [type=submit]{width:90%}.dashboard.dash h2{text-transform:none;font-size:var(--large)}.dashboard.dash .replace>ul{display:flex;list-style:none;align-items:flex-start;justify-content:flex-start;flex-wrap:wrap;gap:.5rem}.dashboard.settings nav.tabs{--height:3.5rem;--x:var(--offHeight);position:fixed;bottom:var(--height);left:var(--x);right:var(--x);z-index:99;width:calc(100% - var(--x) - var(--x));background-color:var(--base)}nav.integrations,nav.integrations a,nav.integrations li,nav.integrations ul{height:auto}.replace{overflow:hidden}body.dash form#options{display:flex;flex-flow:column nowrap;justify-content:center;align-items:center}.item-grid.integrations{grid-template-columns:repeat(2,1fr);gap:2rem}.integration{background:var(--base);border:2px solid var(--base-200);border-radius:var(--outerRadius);padding:1rem;position:relative;transition:all var(--transition-base);box-shadow:var(--shadow)}.integration.connected{border-color:var(--success)}.integration.disconnected,.integration.error{border-color:var(--error)}.integration.hasChanges{border-color:var(--warning)}.integration .header{margin-bottom:.75rem;padding-bottom:.75rem;border-bottom:2px solid var(--base-200)}.integration h3{letter-spacing:1px;font-size:var(--medium);margin:0}.integration .meta{margin-bottom:1rem;text-align:right;color:var(--contrast-200);font-size:var(--small)}.integration .setup{font-size:var(--small);font-weight:700;text-transform:uppercase}.integration .setup .indicator{font-size:var(--medium)}.integration .connected .indicator,.integration .setup .connected{color:var(--success)}.integration .disconnected .indicator,.integration .setup .disconnected{color:var(--error)}.integration.hasChanges .disconnected{color:var(--warning)}.connection-status.connected{background-color:var(--successBack);color:var(--successText)}.connection-status.disconnected{background-color:var(--errorBack);color:var(--errorText)}.integration code{display:inline-block;width:90%;margin:0 .5rem;user-select:all;padding:.75rem;border:2px solid var(--base);background-color:var(--base-200);word-break:break-all}.integration details+details{margin-top:1rem}.integration .actions{margin-top:1rem}.hint{line-height:1.2;font-style:italic;font-size:var(--small)}.hasChanges button[data-action=save_credentials]{border-color:var(--warning);animation:pulse-color 1s infinite;animation-delay:1s}.flash{animation:flash .5s}.flash.connected{--b:var(--success)}.flash.disconnected{--b:var(--error)}.flash.syncing{--b:var(--success)}.flash.error,.flash.hasChanges{--b:var(--warning)}@keyframes flash{0%,100%{border-color:inherit}50%{border-color:var(--b)}}.location.field{width:80vw}.location.field>p{text-align:center}.location.field>p+p{margin:0 .5rem 0 0}.location.field .location-map{height:20vh}.location.field .location-links{padding:.5rem 0;display:flex;justify-content:space-evenly}.field.upload [data-upload-id],.item-grid .item{touch-action:none}.empty-state{grid-column:1/-1;padding:1rem 10vw;margin:0 10vw;border-radius:var(--outerRadius);background-color:var(--base-100)}.jvb-oauth-connect{position:relative;transition:opacity .2s}.jvb-oauth-connect.loading{opacity:.6;pointer-events:none}.jvb-oauth-connect.loading::after{content:'';position:absolute;right:-30px;top:50%;transform:translateY(-50%);width:16px;height:16px;border:2px solid #ccc;border-top-color:#0073aa;border-radius:50%;animation:oauth-spin .8s linear infinite}@keyframes oauth-spin{to{transform:translateY(-50%) rotate(360deg)}}.integration-status-message{padding:12px 16px;margin:16px 0;border-radius:4px;display:none;font-size:14px;line-height:1.5}.integration-status-message.success{display:block;background:#d4edda;color:#155724;border-left:4px solid #28a745}.integration-status-message.error{display:block;background:#f8d7da;color:#721c24;border-left:4px solid #dc3545}.integration-status-message.info{display:block;background:#d1ecf1;color:#0c5460;border-left:4px solid #17a2b8}.connection-status{display:inline-flex;align-items:center;gap:8px;padding:6px 12px;border-radius:4px;font-size:13px;font-weight:500}.connection-status.connected{background:#d4edda;color:#155724}.connection-status.disconnected{background:#f8d7da;color:#721c24}.status-indicator{font-size:10px;line-height:1}.connection-status.connected .status-indicator{color:#28a745}.connection-status.disconnected .status-indicator{color:#dc3545}
assets/css/forms.min.css
@@ -1 +1 @@
details.uploader .file-upload-container{margin:1rem 0;max-width:100%}@media (min-width:768px){details.uploader .file-upload-container{margin:1rem var(--mr) 1rem var(--ml);max-width:var(--maxWidth)}}.file-upload-wrapper{border:2px dashed var(--action-0);border-radius:4px;padding:2rem;text-align:center;transition:all .3s ease;background:rgba(var(--action-rgb),var(--rgb-subtle));position:relative;cursor:pointer}.file-upload-wrapper h2{margin:0!important;font-size:var(--large)}.dragover,.file-upload-wrapper:hover{background:rgba(var(--action-rgb),var(--rgb-subtle-hover));border-color:var(--action-0)!important}.file-upload-wrapper input[type=file]{position:absolute;left:0;top:0;width:100%;height:100%;opacity:0;cursor:pointer}.file-upload-text{color:var(--contrast);margin:0;font-family:var(--body)}.file-upload-text strong{color:var(--action-0);text-decoration:underline}.field.upload:has(.upload-item) .file-upload-container{display:none}.field.upload{position:relative}.field.upload:not(.uploading) .progress{display:none}.field.upload .actions{position:absolute;top:0;right:0}.item-grid.group,.item-grid.preview,.item-grid.restore{grid-template-columns:repeat(3,1fr)}.item-grid.group .item,.item-grid.preview .item,.item-grid.restore .item{display:block}.item-grid.group button,.item-grid.preview button,.item-grid.restore button{padding:.25rem .5rem}.item-grid.group button .icon,.item-grid.preview button .icon,.item-grid.restore button .icon{--w:1.1em}.item-grid.group .item .preview>input[type=checkbox]:not(.label-button)+label,.item-grid.preview .item .preview>input[type=checkbox]:not(.label-button)+label,.item-grid.restore .item .preview>input[type=checkbox]:not(.label-button)+label{padding-left:0;margin:0}.item-grid.group .item .preview>input[type=checkbox]+label:before,.item-grid.preview .item .preview>input[type=checkbox]+label:before,.item-grid.restore .item .preview>input[type=checkbox]+label:before{transform:unset;top:.5rem;left:.5rem}.item-grid.group .item .preview>input[type=checkbox]+label::after,.item-grid.preview .item .preview>input[type=checkbox]+label::after,.item-grid.restore .item .preview>input[type=checkbox]+label::after{top:.5rem;left:.75rem;transform:translateY(20%) rotate(45deg)}.item-grid.group .item .item-actions,.item-grid.preview .item .item-actions,.item-grid.restore .item .item-actions{position:absolute;top:0;right:0}.item-grid.group summary,.item-grid.preview summary,.item-grid.restore summary{padding:.5rem}.item-grid.group:has([type=checkbox]:checked),.item-grid.preview:has([type=checkbox]:checked),.item-grid.restore:has([type=checkbox]:checked){padding:1rem;background-color:rgba(var(--contrast-rgb),var(--rgb-subtle))}.item-grid.group:has([type=checkbox]:checked) .item,.item-grid.preview:has([type=checkbox]:checked) .item,.item-grid.restore:has([type=checkbox]:checked) .item{padding:.75rem;opacity:.8}.item-grid.group:has([type=checkbox]:checked) .item img,.item-grid.preview:has([type=checkbox]:checked) .item img,.item-grid.restore:has([type=checkbox]:checked) .item img{filter:var(--filter)}.item-grid.group:has([type=checkbox]:checked) details,.item-grid.preview:has([type=checkbox]:checked) details,.item-grid.restore:has([type=checkbox]:checked) details{display:none}.item-grid.group .item:has([type=checkbox]:checked),.item-grid.preview .item:has([type=checkbox]:checked),.item-grid.restore .item:has([type=checkbox]:checked){padding:.5rem;background-color:rgba(var(--action-rgb),var(--rgb-medium));opacity:1}.item-grid.group .item:has([type=checkbox]:checked) img,.item-grid.preview .item:has([type=checkbox]:checked) img,.item-grid.restore .item:has([type=checkbox]:checked) img{filter:none}[type=radio].featured+label .star+.star,[type=radio].featured:checked+label .star{display:none}[type=radio].featured+label .star,[type=radio].featured:checked+label .star+.star{display:inline-block}.restore.item,.upload.item{border-radius:var(--innerRadius);overflow:hidden;background:var(--base);border:1px solid var(--base-200)}.restore.item img,.upload.item img{transition:transform var(--transition-base)}.restore.item:hover img,.upload.item:hover img{transform:scale(1.02);transition:transform var(--transition-base)}.upload-group{background-image:var(--dashed-action);padding:5px;border-radius:var(--innerRadius);background-color:rgba(var(--action-rgb),var(--rgb-subtle))}.submit-uploads{position:fixed;bottom:var(--offHeight);right:var(--offHeight);z-index:var(--z-6);height:var(--height);box-shadow:var(--shadow);border-radius:var(--innerRadius);animation:pulse-color 5s infinite;animation-delay:1s;background-color:var(--action-0);color:var(--action-contrast)}.submit-uploads:hover{background-color:var(--base-200);color:var(--contrast-200)}.empty-group{grid-column:1/-1;padding:20px;background-image:var(--dashed-action);border-radius:var(--innerRadius);margin:10px 0;cursor:pointer;transition:all var(--transition-base);text-align:center;background-color:rgba(var(--action-rgb),var(--rgb-subtle))}.group-display:not([hidden])~.file-upload-container{display:none}.dragging,.upload.item.dragging{opacity:.7;transform:scale(.95) rotate(3deg);z-index:var(--z-top);box-shadow:0 8px 25px rgba(0,0,0,.3)}.dragover{background:rgba(var(--action-rgb),var(--rgb-light))!important;border-color:var(--action-0)!important;transform:scale(1.05);animation:drop-pulse .8s infinite ease-in-out}.drag-preview{position:fixed;z-index:var(--zz-top);width:fit-content;overflow:visible;pointer-events:none;opacity:.9;transform:scale(1.05);transition:transform .2s ease}.drag-preview .drag-items{width:max-content;height:max-content;position:relative}.drag-preview .drag-items .drag-item{width:120px;height:120px;position:absolute;top:0;left:0;background:var(--base);border-radius:var(--outerRadius);box-shadow:var(--shadow)}.drag-preview .drag-items .drag-item:nth-child(1){transform:rotate(-3deg);z-index:3}.drag-preview .drag-items .drag-item:nth-child(2){left:8px;top:-4px;transform:rotate(4deg);z-index:2;transition-delay:30ms}.drag-preview .drag-items .drag-item:nth-child(3){left:-6px;top:-8px;transform:rotate(-5deg);z-index:1;transition-delay:60ms}.drag-preview .drag-items .drag-item:nth-child(4){left:12px;top:-12px;transform:rotate(3deg);z-index:0;transition-delay:90ms}.drag-preview .drag-items .drag-item:nth-child(n+5){left:-10px;top:-16px;transform:rotate(-4deg);z-index:0;opacity:.8}.drag-preview .drag-items img,.drag-preview .drag-items video{width:100%;height:100%;object-fit:cover;display:block}.drag-preview .drag-count{position:absolute;top:-8px;right:-8px;background:var(--base-200);color:var(--contrast);border-radius:50%;width:24px;height:24px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;box-shadow:var(--shadow);z-index:var(--z-3)}.item.dragging{opacity:.5;transform:scale(.95);filter:grayscale(50%);transition:opacity .2s ease,transform .2s ease,filter .2s ease}@keyframes drop-pulse{0%,100%{background-color:rgba(var(--action-rgb),var(--rgb-light));transform:scale(1.02)}50%{background-color:var(rgba(var(--action-rgb),var(--rgb-medium)));transform:scale(1.04)}}.group-actions{display:flex;gap:.25rem}@media (max-width:767px){body:not(.uploading):has(.group-display:not([hidden])){overflow:hidden}body:not(.uploading):has(.group-display:not([hidden])) .qtoggle{z-index:var(--z-1)}.group-display.group-display{position:fixed;top:var(--height);bottom:var(--height);left:0;right:0;max-height:var(--maxHeight);overflow:hidden;z-index:var(--z-6);width:calc(100% - 1rem);height:calc(100% - 1rem);padding:0 0 3rem;--justify:flex-start;--align:flex-start;--gap:0}.group-display::before{content:'';display:block;z-index:-1;top:-.5rem;bottom:-.5rem;left:-.5rem;right:-.5rem;position:absolute;background-color:rgba(var(--base-rgb),var(--rgb-heavy));filter:blur(5px)}.group-display .preview-wrap,.group-display .sidebar{height:50%;overflow:hidden auto;position:relative;padding:.5rem}.group-display .preview-wrap{top:0}.group-display .preview-wrap .selected{display:flex;justify-content:space-between;align-items:center}.group-display .sidebar{bottom:0;flex-wrap:nowrap;overflow:hidden auto;background-color:var(--contrast-200);color:var(--base)}.group-display .sidebar>.hint{color:var(--contrast)}.group-display .sidebar .header{display:none}.group-display .preview-actions{top:0;flex-shrink:0}.group-display .preview-wrap>.hint,.group-display .sidebar>.hint{bottom:0;margin:0;text-align:center}.group-display .preview-actions,.group-display .preview-wrap>.hint,.group-display .sidebar>.hint{position:absolute;left:0;right:0;background-color:rgba(var(--base-rgb),var(--rgb-heavy));z-index:var(--z-3);box-shadow:var(--shadow)}.group-display .item-grid{height:100%;overflow:hidden auto;grid-template-columns:repeat(3,1fr);padding:2rem 0}.group-display .sidebar>.item-grid{grid-template-columns:repeat(1,1fr);gap:1rem;padding:0}.group-display .sidebar .empty-group{order:0;position:sticky;height:fit-content;top:0;z-index:var(--z-3);background-color:rgba(var(--action-rgb),var(--rgb-heavy))}.group-display .sidebar .upload-group{order:1}.group-display .sidebar .empty-group p{margin:0}.group-display .field,.group-display .field label{margin:0;padding:0}.group-display .sidebar h4{margin:.25rem}.group-display .item{width:100%;height:max-content}.submit-uploads{bottom:var(--height);left:0;right:0;width:100%;height:3rem}body.uploading .group-display.group-display{position:relative;top:unset;bottom:unset;right:unset;left:unset}}@media (min-width:768px){.group-display.group-display{--wrap:nowrap;--dir:row;--gap:1rem;--align:flex-start}.group-display .preview-wrap,.group-display .sidebar{--justify:flex-start;max-height:calc(100vh - var(--doubleHeight));overflow:hidden auto}.group-display .preview-wrap,.group-display .sidebar{width:50%}.preview-actions,.preview-wrap .hint{position:sticky;z-index:var(--z-3);box-shadow:var(--shadow);background-color:var(--base);width:100%}.preview-actions{top:0;left:0;right:0}.preview-actions .field{margin:0}.preview-wrap .hint,.sidebar>.hint{bottom:-1rem;padding-bottom:1rem;margin:0;left:0;right:0;text-align:center}}.restore-uploads{position:fixed;top:var(--offHeight);bottom:var(--offHeight);left:1rem;right:1rem;border-radius:var(--outerRadius);padding:1rem;z-index:var(--z-top);box-shadow:var(--shadow);background-color:var(--base-200);overflow:hidden auto}dialog nav.tabs{position:sticky;top:0;background-color:var(--base-50);z-index:var(--z-6);box-shadow:var(--shadow-down);margin-bottom:2rem}.editor-container .ql-toolbar{display:flex;background-color:var(--base-50);justify-content:flex-start;flex-wrap:wrap;padding:.25rem;gap:.5rem 1rem;border-top-left-radius:var(--innerRadius);border-top-right-radius:var(--innerRadius);border-bottom:4px solid var(--base-50)}.ql-toolbar .ql-formats{display:flex;gap:.25rem}.editor-container .ql-container{--padding:1rem;background-color:var(--base);border-bottom-left-radius:var(--innerRadius);border-bottom-right-radius:var(--innerRadius);height:fit-content;padding:2px;border:1px solid var(--base-200)}.editor-container .ql-container .ql-editor{padding:var(--padding);width:100%;height:100%}.ql-editor img{max-width:50%;height:auto}.ql-clipboard{left:-100000px;height:1px;overflow-y:hidden;position:absolute;top:50%}.ql-hidden{display:none}.ql-tooltip{position:absolute;transform:translateY(10px);background-color:var(--base-100);border:1px solid var(--base);box-shadow:0 0 5px var(--overlay-heavy);color:var(--contrast);padding:5px 12px;white-space:nowrap}[data-type=single] .item-grid{display:flex}.repeater-row details summary::after{margin-left:0}.repeater-row details summary button{margin-left:auto}/*!* Group actions buttons - more visible *!*//*!* Group item grid - distinct from preview grid *!*//*!* Group count hint *!*//*!* ============================================================================*//*!* Base drag preview *!*//*!* Single item drag preview *!*//*!* Multi-item drag preview container *!*//*!* Items being dragged - reduce opacity on originals *!*//*!* Count badge on multi-item preview *!*//*!* ============================================================================*//*!* Ensure progress bar is visible when needed *!*//*!* Progress bar track *!*//*!* Progress bar fill *!*//*!* Progress details - styled for row layout with text and count *!*//*!* Individual item progress - overlay style *!*//*!* Item progress icon and status text *!*//*!* ============================================================================*//*!* Hide uploader when we have uploads *!*//*!* Show group display when we have uploads *!*//*!* ============================================================================*//*!* Selected items - more obvious *!*//*!* Selection checkbox - always visible on hover or when checked *!*//*!* Selection controls - more prominent *!*//*!* ============================================================================*//*!* Smooth dragover animation *!*//*!* ============================================================================*//*!* ============================================================================*//*!* Notification container - fixed overlay *!*//*!* Content card *!*//*!* Message section *!*//*!* Scrollable field list *!*//*!* Item grid for restore preview *!*//*!* Restore item *!*//*!* Checked state *!*//*!* Preview section *!*//*!* Item info *!*//*!* Checkbox controls *!*//*!* Actions section *!*//*!* Selection controls *!*//*!* Action buttons *!*//*!* Restore button - primary action *!*//*!* Scrap cache button - destructive action *!*//*!* Dismiss button - secondary action *!*//*!* Mobile responsive *!*//*!* Animation *!*//*!* Scrollbar styling for restore field list *!*/form{--step-size:2.5rem}.form-progress{padding:0 1rem}.form-progress .progress{background:var(--base-100);border-radius:var(--innerRadius);padding:1rem}.form-progress .bar{height:6px;background:var(--base-200);border-radius:3px;overflow:hidden;margin-bottom:.5rem}.form-progress .fill{height:100%;background:linear-gradient(90deg,var(--action-0),var(--action-200));width:0%;transition:width .4s ease;border-radius:3px}.form-progress .step-text{font-size:var(--small);font-weight:600;color:var(--contrast-200)}form nav.tabs{position:relative;top:0;left:0;right:0;padding:1rem 0;gap:0;z-index:0}form nav.tabs button{position:relative;background:0 0;border:none;padding:.5rem 1rem .5rem 3rem;z-index:1}form nav.tabs .step-number{width:2.5rem;height:100%;border-radius:50% 0 0 50%;position:absolute;left:0;top:0;background:var(--base-200);color:var(--contrast-50);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:var(--small);border:3px solid var(--base)}form nav.tabs button.pending .step-number{background:var(--base-100);color:var(--contrast-200)}form nav.tabs button.active .step-number,form nav.tabs button.current .step-number{background:var(--action-0);color:var(--action-contrast);border-color:var(--action-200)}form nav.tabs button.completed .step-number{background:var(--successBack);color:var(--successBack);border-color:var(--successText)}form nav.tabs button.completed .step-number::before{content:'✓';font-size:1.2rem;color:var(--successText);position:absolute}form nav.tabs button.completed h2{color:var(--contrast-200)}.step-navigation{margin-top:2rem;padding-top:2rem;border-top:1px solid var(--base-200);gap:1rem}.step-navigation .prev-step{background:var(--base-100)}.step-navigation .next-step,.step-navigation button[type=submit]{margin-left:auto}.field input.error,.field select.error,.field textarea.error{border-color:var(--errorBack)}.error-message{color:var(--errorText);font-size:var(--small);margin-top:.25rem;display:block}@media (max-width:768px){form nav.tabs button{min-width:80px;font-size:var(--small)}form nav.tabs button h2{font-size:var(--small)}form{--step-size:2rem}}.field-input-wrapper{position:relative;display:flex;align-items:center;gap:.5rem}.field-input-wrapper input,.field-input-wrapper select,.field-input-wrapper textarea{flex:1}.validation-icon{display:flex;align-items:center;justify-content:center;font-size:1.25rem;animation:scaleIn .3s ease;--w:1.25rem}.validation-icon.error{color:var(--error)}.validation-icon.success{color:var(--success)}@keyframes scaleIn{from{transform:scale(0);opacity:0}to{transform:scale(1);opacity:1}}.validation-message{color:var(--error-0);font-size:var(--small);margin-top:.25rem;display:block;animation:slideDown .2s ease}@keyframes slideDown{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.field.has-error input,.field.has-error select,.field.has-error textarea{border-color:var(--error);background-color:var(--errorBack)}.field.has-error input:focus,.field.has-error select:focus,.field.has-error textarea:focus{outline-color:var(--error);box-shadow:0 0 0 3px rgba(var(--error-rgb),.2)}.field.has-success input,.field.has-success select,.field.has-success textarea{border-color:var(--success)}.field label .required{color:var(--error);margin-left:.25rem}.form-summary{padding:2rem;border-radius:8px;margin-top:2rem;border:2px dashed var(--contrast-200)}.form-summary .message{margin-bottom:2rem}.form-summary .result+.result{position:relative;margin-top:1.5rem;padding-top:1.5rem}.form-summary .result+.result::before{position:absolute;top:0;left:16.5%;content:'';width:67%;height:1px;border-bottom:1px solid var(--base-200)}.form-summary h2{margin:1rem 0}.form-summary h4{background-color:var(--base-100);padding:.5rem 2rem;position:relative;left:-2rem;color:var(--contrast-200);font-size:.875rem;text-transform:uppercase;letter-spacing:.05em;margin-bottom:.75rem}.form-summary p{color:var(--text);margin:0}.group-summary,.repeater-summary{background:var(--base-100);padding:1rem;border-radius:4px;margin-top:.5rem}.repeater-row{margin-bottom:1rem}.repeater-row:last-child{margin-bottom:0}.ql-toolbar button{--height:fit-content;padding:.5rem}
details.uploader .file-upload-container{margin:1rem 0;max-width:100%}@media (min-width:768px){details.uploader .file-upload-container{margin:1rem var(--mr) 1rem var(--ml);max-width:var(--maxWidth)}}.file-upload-wrapper{border:2px dashed var(--action-0);border-radius:4px;padding:2rem;text-align:center;transition:all .3s ease;background:rgba(var(--action-rgb),var(--rgb-subtle));position:relative;cursor:pointer}.file-upload-wrapper h2{margin:0!important;font-size:var(--large)}.dragover,.file-upload-wrapper:hover{background:rgba(var(--action-rgb),var(--rgb-subtle-hover));border-color:var(--action-0)!important}.file-upload-wrapper input[type=file]{position:absolute;left:0;top:0;width:100%;height:100%;opacity:0;cursor:pointer}.file-upload-text{color:var(--contrast);margin:0;font-family:var(--body)}.file-upload-text strong{color:var(--action-0);text-decoration:underline}.field.upload:has(.upload-item) .file-upload-container{display:none}.field.upload{position:relative}.field.upload:not(.uploading) .progress{display:none}.field.upload .actions{position:absolute;top:0;right:0}.item-grid.group,.item-grid.preview,.item-grid.restore{grid-template-columns:repeat(3,1fr)}.item-grid.group .item,.item-grid.preview .item,.item-grid.restore .item{display:block}.item-grid.group button,.item-grid.preview button,.item-grid.restore button{padding:.25rem .5rem}.item-grid.group button .icon,.item-grid.preview button .icon,.item-grid.restore button .icon{--w:1.1em}.item-grid.group .item .preview>input[type=checkbox]:not(.label-button)+label,.item-grid.preview .item .preview>input[type=checkbox]:not(.label-button)+label,.item-grid.restore .item .preview>input[type=checkbox]:not(.label-button)+label{padding-left:0;margin:0}.item-grid.group .item .preview>input[type=checkbox]+label:before,.item-grid.preview .item .preview>input[type=checkbox]+label:before,.item-grid.restore .item .preview>input[type=checkbox]+label:before{transform:unset;top:.5rem;left:.5rem}.item-grid.group .item .preview>input[type=checkbox]+label::after,.item-grid.preview .item .preview>input[type=checkbox]+label::after,.item-grid.restore .item .preview>input[type=checkbox]+label::after{top:.5rem;left:.75rem;transform:translateY(20%) rotate(45deg)}.item-grid.group .item .item-actions,.item-grid.preview .item .item-actions,.item-grid.restore .item .item-actions{position:absolute;top:0;right:0}.item-grid.group summary,.item-grid.preview summary,.item-grid.restore summary{padding:.5rem}.item-grid.group:has([type=checkbox]:checked),.item-grid.preview:has([type=checkbox]:checked),.item-grid.restore:has([type=checkbox]:checked){padding:1rem;background-color:rgba(var(--contrast-rgb),var(--rgb-subtle))}.item-grid.group:has([type=checkbox]:checked) .item,.item-grid.preview:has([type=checkbox]:checked) .item,.item-grid.restore:has([type=checkbox]:checked) .item{padding:.75rem;opacity:.8}.item-grid.group:has([type=checkbox]:checked) .item img,.item-grid.preview:has([type=checkbox]:checked) .item img,.item-grid.restore:has([type=checkbox]:checked) .item img{filter:var(--filter)}.item-grid.group:has([type=checkbox]:checked) details,.item-grid.preview:has([type=checkbox]:checked) details,.item-grid.restore:has([type=checkbox]:checked) details{display:none}.item-grid.group .item:has([type=checkbox]:checked),.item-grid.preview .item:has([type=checkbox]:checked),.item-grid.restore .item:has([type=checkbox]:checked){padding:.5rem;background-color:rgba(var(--action-rgb),var(--rgb-medium));opacity:1}.item-grid.group .item:has([type=checkbox]:checked) img,.item-grid.preview .item:has([type=checkbox]:checked) img,.item-grid.restore .item:has([type=checkbox]:checked) img{filter:none}[type=radio].featured+label .star+.star,[type=radio].featured:checked+label .star{display:none}[type=radio].featured+label .star,[type=radio].featured:checked+label .star+.star{display:inline-block}.restore.restore.item,.upload.upload.item{border-radius:var(--innerRadius);aspect-ratio:unset;overflow:hidden;background:var(--base);border:1px solid var(--base-200)}.restore-item [for=select-item],.upload.item [for=select-item]{aspect-ratio:1}.upload.item:has(details[open]){grid-column:1/-1}.restore.item img,.upload.item img{transition:transform var(--transition-base)}.restore.item:hover img,.upload.item:hover img{transform:scale(1.02);transition:transform var(--transition-base)}.upload-group{background-image:var(--dashed-action);padding:5px;border-radius:var(--innerRadius);background-color:rgba(var(--action-rgb),var(--rgb-subtle))}.submit-uploads{position:fixed;bottom:var(--offHeight);right:var(--offHeight);z-index:var(--z-6);height:var(--height);box-shadow:var(--shadow);border-radius:var(--innerRadius);animation:pulse-color 5s infinite;animation-delay:1s;background-color:var(--action-0);color:var(--action-contrast)}.submit-uploads:hover{background-color:var(--base-200);color:var(--contrast-200)}.empty-group{grid-column:1/-1;padding:20px;background-image:var(--dashed-action);border-radius:var(--innerRadius);margin:10px 0;cursor:pointer;transition:all var(--transition-base);text-align:center;background-color:rgba(var(--action-rgb),var(--rgb-subtle))}.group-display:not([hidden])~.file-upload-container{display:none}.dragging,.upload.item.dragging{opacity:.7;transform:scale(.95) rotate(3deg);z-index:var(--z-top);box-shadow:0 8px 25px rgba(0,0,0,.3)}.dragover{background:rgba(var(--action-rgb),var(--rgb-light))!important;border-color:var(--action-0)!important;transform:scale(1.05);animation:drop-pulse .8s infinite ease-in-out}.drag-preview{position:fixed;z-index:var(--zz-top);width:fit-content;overflow:visible;pointer-events:none;opacity:.9;transform:scale(1.05);transition:transform .2s ease}.drag-preview .drag-items{width:max-content;height:max-content;position:relative}.drag-preview .drag-items .drag-item{width:120px;height:120px;position:absolute;top:0;left:0;background:var(--base);border-radius:var(--outerRadius);box-shadow:var(--shadow)}.drag-preview .drag-items .drag-item:nth-child(1){transform:rotate(-3deg);z-index:3}.drag-preview .drag-items .drag-item:nth-child(2){left:8px;top:-4px;transform:rotate(4deg);z-index:2;transition-delay:30ms}.drag-preview .drag-items .drag-item:nth-child(3){left:-6px;top:-8px;transform:rotate(-5deg);z-index:1;transition-delay:60ms}.drag-preview .drag-items .drag-item:nth-child(4){left:12px;top:-12px;transform:rotate(3deg);z-index:0;transition-delay:90ms}.drag-preview .drag-items .drag-item:nth-child(n+5){left:-10px;top:-16px;transform:rotate(-4deg);z-index:0;opacity:.8}.drag-preview .drag-items img,.drag-preview .drag-items video{width:100%;height:100%;object-fit:cover;display:block}.drag-preview .drag-count{position:absolute;top:-8px;right:-8px;background:var(--base-200);color:var(--contrast);border-radius:50%;width:24px;height:24px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;box-shadow:var(--shadow);z-index:var(--z-3)}.item.dragging{opacity:.5;transform:scale(.95);filter:grayscale(50%);transition:opacity .2s ease,transform .2s ease,filter .2s ease}@keyframes drop-pulse{0%,100%{background-color:rgba(var(--action-rgb),var(--rgb-light));transform:scale(1.02)}50%{background-color:var(rgba(var(--action-rgb),var(--rgb-medium)));transform:scale(1.04)}}.group-actions{display:flex;gap:.25rem}@media (max-width:767px){body:not(.uploading):has(.group-display:not([hidden])){overflow:hidden}body:not(.uploading):has(.group-display:not([hidden])) .qtoggle{z-index:var(--z-1)}.group-display.group-display{position:fixed;top:var(--height);bottom:var(--height);left:0;right:0;max-height:var(--maxHeight);overflow:hidden;z-index:var(--z-6);width:calc(100% - 1rem);height:calc(100% - 1rem);padding:0 0 3rem;--justify:flex-start;--align:flex-start;--gap:0}.group-display::before{content:'';display:block;z-index:-1;top:-.5rem;bottom:-.5rem;left:-.5rem;right:-.5rem;position:absolute;background-color:rgba(var(--base-rgb),var(--rgb-heavy));filter:blur(5px)}.group-display .preview-wrap,.group-display .sidebar{height:50%;overflow:hidden auto;position:relative;padding:.5rem}.group-display .preview-wrap{top:0}.group-display .preview-wrap .selected{display:flex;justify-content:space-between;align-items:center}.group-display .sidebar{bottom:0;flex-wrap:nowrap;overflow:hidden auto;background-color:var(--contrast-200);color:var(--base)}.group-display .sidebar>.hint{color:var(--contrast)}.group-display .sidebar .header{display:none}.group-display .preview-actions{top:0;flex-shrink:0}.group-display .preview-wrap>.hint,.group-display .sidebar>.hint{bottom:0;margin:0;text-align:center}.group-display .preview-actions,.group-display .preview-wrap>.hint,.group-display .sidebar>.hint{position:absolute;left:0;right:0;background-color:rgba(var(--base-rgb),var(--rgb-heavy));z-index:var(--z-3);box-shadow:var(--shadow)}.group-display .item-grid{height:100%;overflow:hidden auto;grid-template-columns:repeat(3,1fr);padding:2rem 0}.group-display .sidebar>.item-grid{grid-template-columns:repeat(1,1fr);gap:1rem;padding:0}.group-display .sidebar .empty-group{order:0;position:sticky;height:fit-content;top:0;z-index:var(--z-3);background-color:rgba(var(--action-rgb),var(--rgb-heavy))}.group-display .sidebar .upload-group{order:1}.group-display .sidebar .empty-group p{margin:0}.group-display .field,.group-display .field label{margin:0;padding:0}.group-display .sidebar h4{margin:.25rem}.group-display .item{width:100%;height:max-content}.submit-uploads{bottom:var(--height);left:0;right:0;width:100%;height:3rem}body.uploading .group-display.group-display{position:relative;top:unset;bottom:unset;right:unset;left:unset}}@media (min-width:768px){.group-display.group-display{--wrap:nowrap;--dir:row;--gap:1rem;--align:flex-start}.group-display .preview-wrap,.group-display .sidebar{--justify:flex-start;max-height:calc(100vh - var(--doubleHeight));overflow:hidden auto}.group-display .preview-wrap,.group-display .sidebar{width:50%}.preview-actions,.preview-wrap .hint{position:sticky;z-index:var(--z-3);box-shadow:var(--shadow);background-color:var(--base);width:100%}.preview-actions{top:0;left:0;right:0}.preview-actions .field{margin:0}.preview-wrap .hint,.sidebar>.hint{bottom:-1rem;padding-bottom:1rem;margin:0;left:0;right:0;text-align:center}}.restore-uploads{position:fixed;top:var(--offHeight);bottom:var(--offHeight);left:1rem;right:1rem;border-radius:var(--outerRadius);padding:1rem;z-index:var(--z-top);box-shadow:var(--shadow);background-color:var(--base-200);overflow:hidden auto}dialog nav.tabs{position:sticky;top:0;background-color:var(--base-50);z-index:var(--z-6);box-shadow:var(--shadow-down);margin-bottom:2rem}.editor-container .ql-toolbar{display:flex;background-color:var(--base-50);justify-content:flex-start;flex-wrap:wrap;padding:.25rem;gap:.5rem 1rem;border-top-left-radius:var(--innerRadius);border-top-right-radius:var(--innerRadius);border-bottom:4px solid var(--base-50)}.ql-toolbar .ql-formats{display:flex;gap:.25rem}.editor-container .ql-container{--padding:1rem;background-color:var(--base);border-bottom-left-radius:var(--innerRadius);border-bottom-right-radius:var(--innerRadius);height:fit-content;padding:2px;border:1px solid var(--base-200)}.editor-container .ql-container .ql-editor{padding:var(--padding);width:100%;height:100%}.ql-editor img{max-width:50%;height:auto}.ql-clipboard{left:-100000px;height:1px;overflow-y:hidden;position:absolute;top:50%}.ql-hidden{display:none}.ql-tooltip{position:absolute;transform:translateY(10px);background-color:var(--base-100);border:1px solid var(--base);box-shadow:0 0 5px var(--overlay-heavy);color:var(--contrast);padding:5px 12px;white-space:nowrap}[data-type=single] .item-grid{display:flex}.repeater-row details summary::after{margin-left:0}.repeater-row details summary button{margin-left:auto}/*!* Group actions buttons - more visible *!*//*!* Group item grid - distinct from preview grid *!*//*!* Group count hint *!*//*!* ============================================================================*//*!* Base drag preview *!*//*!* Single item drag preview *!*//*!* Multi-item drag preview container *!*//*!* Items being dragged - reduce opacity on originals *!*//*!* Count badge on multi-item preview *!*//*!* ============================================================================*//*!* Ensure progress bar is visible when needed *!*//*!* Progress bar track *!*//*!* Progress bar fill *!*//*!* Progress details - styled for row layout with text and count *!*//*!* Individual item progress - overlay style *!*//*!* Item progress icon and status text *!*//*!* ============================================================================*//*!* Hide uploader when we have uploads *!*//*!* Show group display when we have uploads *!*//*!* ============================================================================*//*!* Selected items - more obvious *!*//*!* Selection checkbox - always visible on hover or when checked *!*//*!* Selection controls - more prominent *!*//*!* ============================================================================*//*!* Smooth dragover animation *!*//*!* ============================================================================*//*!* ============================================================================*//*!* Notification container - fixed overlay *!*//*!* Content card *!*//*!* Message section *!*//*!* Scrollable field list *!*//*!* Item grid for restore preview *!*//*!* Restore item *!*//*!* Checked state *!*//*!* Preview section *!*//*!* Item info *!*//*!* Checkbox controls *!*//*!* Actions section *!*//*!* Selection controls *!*//*!* Action buttons *!*//*!* Restore button - primary action *!*//*!* Scrap cache button - destructive action *!*//*!* Dismiss button - secondary action *!*//*!* Mobile responsive *!*//*!* Animation *!*//*!* Scrollbar styling for restore field list *!*/form{--step-size:2.5rem}.form-progress{padding:0 1rem}.form-progress .progress{background:var(--base-100);border-radius:var(--innerRadius);padding:1rem}.form-progress .bar{height:6px;background:var(--base-200);border-radius:3px;overflow:hidden;margin-bottom:.5rem}.form-progress .fill{height:100%;background:linear-gradient(90deg,var(--action-0),var(--action-200));width:0%;transition:width .4s ease;border-radius:3px}.form-progress .step-text{font-size:var(--small);font-weight:600;color:var(--contrast-200)}form nav.tabs{position:relative;top:0;left:0;right:0;padding:1rem 0;gap:0;z-index:0}form nav.tabs button{position:relative;background:0 0;border:none;padding:.5rem 1rem .5rem 3rem;z-index:1}form nav.tabs .step-number{width:2.5rem;height:100%;border-radius:50% 0 0 50%;position:absolute;left:0;top:0;background:var(--base-200);color:var(--contrast-50);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:var(--small);border:3px solid var(--base)}form nav.tabs button.pending .step-number{background:var(--base-100);color:var(--contrast-200)}form nav.tabs button.active .step-number,form nav.tabs button.current .step-number{background:var(--action-0);color:var(--action-contrast);border-color:var(--action-200)}form nav.tabs button.completed .step-number{background:var(--successBack);color:var(--successBack);border-color:var(--successText)}form nav.tabs button.completed .step-number::before{content:'✓';font-size:1.2rem;color:var(--successText);position:absolute}form nav.tabs button.completed h2{color:var(--contrast-200)}.step-navigation{margin-top:2rem;padding-top:2rem;border-top:1px solid var(--base-200);gap:1rem}.step-navigation .prev-step{background:var(--base-100)}.step-navigation .next-step,.step-navigation button[type=submit]{margin-left:auto}.field input.error,.field select.error,.field textarea.error{border-color:var(--errorBack)}.error-message{color:var(--errorText);font-size:var(--small);margin-top:.25rem;display:block}@media (max-width:768px){form nav.tabs button{min-width:80px;font-size:var(--small)}form nav.tabs button h2{font-size:var(--small)}form{--step-size:2rem}}.field-input-wrapper{position:relative;display:flex;align-items:center;gap:.5rem}.field-input-wrapper input,.field-input-wrapper select,.field-input-wrapper textarea{flex:1}.validation-icon{display:flex;align-items:center;justify-content:center;font-size:1.25rem;animation:scaleIn .3s ease;--w:1.25rem}.validation-icon.error{color:var(--error)}.validation-icon.success{color:var(--success)}@keyframes scaleIn{from{transform:scale(0);opacity:0}to{transform:scale(1);opacity:1}}.validation-message{color:var(--error-0);font-size:var(--small);margin-top:.25rem;display:block;animation:slideDown .2s ease}@keyframes slideDown{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.field.has-error input,.field.has-error select,.field.has-error textarea{border-color:var(--error);background-color:var(--errorBack)}.field.has-error input:focus,.field.has-error select:focus,.field.has-error textarea:focus{outline-color:var(--error);box-shadow:0 0 0 3px rgba(var(--error-rgb),.2)}.field.has-success input,.field.has-success select,.field.has-success textarea{border-color:var(--success)}.field label .required{color:var(--error);margin-left:.25rem}.form-summary{padding:2rem;border-radius:8px;margin-top:2rem;border:2px dashed var(--contrast-200)}.form-summary .message{margin-bottom:2rem}.form-summary .result+.result{position:relative;margin-top:1.5rem;padding-top:1.5rem}.form-summary .result+.result::before{position:absolute;top:0;left:16.5%;content:'';width:67%;height:1px;border-bottom:1px solid var(--base-200)}.form-summary h2{margin:1rem 0}.form-summary h4{background-color:var(--base-100);padding:.5rem 2rem;position:relative;left:-2rem;color:var(--contrast-200);font-size:.875rem;text-transform:uppercase;letter-spacing:.05em;margin-bottom:.75rem}.form-summary p{color:var(--text);margin:0}.group-summary,.repeater-summary{background:var(--base-100);padding:1rem;border-radius:4px;margin-top:.5rem}.repeater-row{margin-bottom:1rem}.repeater-row:last-child{margin-bottom:0}.ql-toolbar button{--height:fit-content;padding:.5rem}.success-message{color:var(--success,#16a34a);background-color:var(--success-bg,#f0fdf4);border:1px solid var(--success,#16a34a);padding:.75rem 1rem;border-radius:var(--radius);margin-bottom:1rem;display:flex;align-items:center;gap:.5rem}.success-message .success-icon{width:1.25rem;height:1.25rem;flex-shrink:0}.success-box{background-color:var(--success-bg,#f0fdf4);border:2px solid var(--success,#16a34a);padding:1.5rem;border-radius:var(--outerRadius);margin-bottom:1rem;text-align:center}.success-box h3{color:var(--success,#16a34a);margin-bottom:.5rem}.success-box p{margin:.5rem 0}.form-success{opacity:.9}.form-success .field:not(.form-success-message):not(.success-box){display:none}.form-success button[type=submit]{opacity:.6;pointer-events:none}.field-error input,.field-error select,.field-error textarea{border-color:var(--error,#dc2626)}.error-message{color:var(--error,#dc2626);font-size:var(--small);margin-top:.25rem;display:block}.form-error{background-color:var(--error-bg,#fee);border:1px solid var(--error,#dc2626);padding:.75rem;border-radius:var(--radius);margin-bottom:1rem}.has-success input,.has-success select,.has-success textarea{border-color:var(--success,#16a34a)}.form-error{display:flex;align-items:center;gap:.5rem}.form-error .error-icon{width:1.25rem;height:1.25rem;flex-shrink:0}.autocomplete-dropdown{width:100%;background-color:var(--base-100);padding:.5rem;box-shadow:var(--shadow)}
assets/css/icons.css
New file
@@ -0,0 +1 @@
.icon-house{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTcuOSwxMTAuMWwtODAtODBhMTQsMTQsMCwwLDAtMTkuOCwwbC04MCw4MEExMy45MiwxMy45MiwwLDAsMCwzNCwxMjB2OTZhNiw2LDAsMCwwLDYsNmg2NGE2LDYsMCwwLDAsNi02VjE1OGgzNnY1OGE2LDYsMCwwLDAsNiw2aDY0YTYsNiwwLDAsMCw2LTZWMTIwQTEzLjkyLDEzLjkyLDAsMCwwLDIxNy45LDExMC4xWk0yMTAsMjEwSDE1OFYxNTJhNiw2LDAsMCwwLTYtNkgxMDRhNiw2LDAsMCwwLTYsNnY1OEg0NlYxMjBhMiwyLDAsMCwxLC41OC0xLjQybDgwLTgwYTIsMiwwLDAsMSwyLjg0LDBsODAsODBBMiwyLDAsMCwxLDIxMCwxMjBaIi8+PC9zdmc+');}.icon-sun-dim{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjIsNDBWMzJhNiw2LDAsMCwxLDEyLDB2OGE2LDYsMCwwLDEtMTIsMFptNjgsODhhNjIsNjIsMCwxLDEtNjItNjJBNjIuMDcsNjIuMDcsMCwwLDEsMTkwLDEyOFptLTEyLDBhNTAsNTAsMCwxLDAtNTAsNTBBNTAuMDYsNTAuMDYsMCwwLDAsMTc4LDEyOFpNNTkuNzYsNjguMjRhNiw2LDAsMSwwLDguNDgtOC40OGwtOC04YTYsNiwwLDAsMC04LjQ4LDguNDhabTAsMTE5LjUyLTgsOGE2LDYsMCwxLDAsOC40OCw4LjQ4bDgtOGE2LDYsMCwxLDAtOC40OC04LjQ4Wm0xMzYtMTM2LTgsOGE2LDYsMCwxLDAsOC40OCw4LjQ4bDgtOGE2LDYsMCwwLDAtOC40OC04LjQ4Wm0uNDgsMTM2YTYsNiwwLDAsMC04LjQ4LDguNDhsOCw4YTYsNiwwLDAsMCw4LjQ4LTguNDhaTTQwLDEyMkgzMmE2LDYsMCwwLDAsMCwxMmg4YTYsNiwwLDAsMCwwLTEyWm04OCw4OGE2LDYsMCwwLDAtNiw2djhhNiw2LDAsMCwwLDEyLDB2LThBNiw2LDAsMCwwLDEyOCwyMTBabTk2LTg4aC04YTYsNiwwLDAsMCwwLDEyaDhhNiw2LDAsMCwwLDAtMTJaIi8+PC9zdmc+');}.icon-moon{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzIuMTMsMTQzLjY0YTYsNiwwLDAsMC02LTEuNDlBOTAuMDcsOTAuMDcsMCwwLDEsMTEzLjg2LDI5Ljg1YTYsNiwwLDAsMC03LjQ5LTcuNDhBMTAyLjg4LDEwMi44OCwwLDAsMCw1NC40OCw1OC42OCwxMDIsMTAyLDAsMCwwLDE5Ny4zMiwyMDEuNTJhMTAyLjg4LDEwMi44OCwwLDAsMCwzNi4zMS01MS44OUE2LDYsMCwwLDAsMjMyLjEzLDE0My42NFptLTQyLDQ4LjI5YTkwLDkwLDAsMCwxLTEyNi0xMjZBOTAuOSw5MC45LDAsMCwxLDk5LjY1LDM3LjY2LDEwMi4wNiwxMDIuMDYsMCwwLDAsMjE4LjM0LDE1Ni4zNSw5MC45LDkwLjksMCwwLDEsMTkwLjEsMTkxLjkzWiIvPjwvc3ZnPg==');}.icon-sign-out{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMTgsMjE2YTYsNiwwLDAsMS02LDZINDhhNiw2LDAsMCwxLTYtNlY0MGE2LDYsMCwwLDEsNi02aDY0YTYsNiwwLDAsMSwwLDEySDU0VjIxMGg1OEE2LDYsMCwwLDEsMTE4LDIxNlptMTEwLjI0LTkyLjI0LTQwLTQwYTYsNiwwLDAsMC04LjQ4LDguNDhMMjA5LjUxLDEyMkgxMTJhNiw2LDAsMCwwLDAsMTJoOTcuNTFsLTI5Ljc1LDI5Ljc2YTYsNiwwLDEsMCw4LjQ4LDguNDhsNDAtNDBBNiw2LDAsMCwwLDIyOC4yNCwxMjMuNzZaIi8+PC9zdmc+');}.icon-star{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzcuMjgsOTcuODdBMTQuMTgsMTQuMTgsMCwwLDAsMjI0Ljc2LDg4bC02MC4yNS00Ljg3LTIzLjIyLTU2LjJhMTQuMzcsMTQuMzcsMCwwLDAtMjYuNTgsMEw5MS40OSw4My4xMSwzMS4yNCw4OGExNC4xOCwxNC4xOCwwLDAsMC0xMi41Miw5Ljg5QTE0LjQzLDE0LjQzLDAsMCwwLDIzLDExMy4zMkw2OSwxNTIuOTNsLTE0LDU5LjI1YTE0LjQsMTQuNCwwLDAsMCw1LjU5LDE1LDE0LjEsMTQuMSwwLDAsMCwxNS45MS42TDEyOCwxOTYuMTJsNTEuNTgsMzEuNzFhMTQuMSwxNC4xLDAsMCwwLDE1LjkxLS42LDE0LjQsMTQuNCwwLDAsMCw1LjU5LTE1bC0xNC01OS4yNUwyMzMsMTEzLjMyQTE0LjQzLDE0LjQzLDAsMCwwLDIzNy4yOCw5Ny44N1ptLTEyLjE0LDYuMzctNDguNjksNDJhNiw2LDAsMCwwLTEuOTIsNS45MmwxNC44OCw2Mi43OWEyLjM1LDIuMzUsMCwwLDEtLjk1LDIuNTcsMi4yNCwyLjI0LDAsMCwxLTIuNi4xTDEzMS4xNCwxODRhNiw2LDAsMCwwLTYuMjgsMEw3MC4xNCwyMTcuNjFhMi4yNCwyLjI0LDAsMCwxLTIuNi0uMSwyLjM1LDIuMzUsMCwwLDEtMS0yLjU3bDE0Ljg4LTYyLjc5YTYsNiwwLDAsMC0xLjkyLTUuOTJsLTQ4LjY5LTQyYTIuMzcsMi4zNywwLDAsMS0uNzMtMi42NSwyLjI4LDIuMjgsMCwwLDEsMi4wNy0xLjY1bDYzLjkyLTUuMTZhNiw2LDAsMCwwLDUuMDYtMy42OWwyNC42My01OS42YTIuMzUsMi4zNSwwLDAsMSw0LjM4LDBsMjQuNjMsNTkuNmE2LDYsMCwwLDAsNS4wNiwzLjY5bDYzLjkyLDUuMTZhMi4yOCwyLjI4LDAsMCwxLDIuMDcsMS42NUEyLjM3LDIuMzcsMCwwLDEsMjI1LjE0LDEwNC4yNFoiLz48L3N2Zz4=');}.icon-plus-square{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsMzRINDhBMTQsMTQsMCwwLDAsMzQsNDhWMjA4YTE0LDE0LDAsMCwwLDE0LDE0SDIwOGExNCwxNCwwLDAsMCwxNC0xNFY0OEExNCwxNCwwLDAsMCwyMDgsMzRabTIsMTc0YTIsMiwwLDAsMS0yLDJINDhhMiwyLDAsMCwxLTItMlY0OGEyLDIsMCwwLDEsMi0ySDIwOGEyLDIsMCwwLDEsMiwyWm0tMzYtODBhNiw2LDAsMCwxLTYsNkgxMzR2MzRhNiw2LDAsMCwxLTEyLDBWMTM0SDg4YTYsNiwwLDAsMSwwLTEyaDM0Vjg4YTYsNiwwLDAsMSwxMiwwdjM0aDM0QTYsNiwwLDAsMSwxNzQsMTI4WiIvPjwvc3ZnPg==');}.icon-trash{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNTBIMTc0VjQwYTIyLDIyLDAsMCwwLTIyLTIySDEwNEEyMiwyMiwwLDAsMCw4Miw0MFY1MEg0MGE2LDYsMCwwLDAsMCwxMkg1MFYyMDhhMTQsMTQsMCwwLDAsMTQsMTRIMTkyYTE0LDE0LDAsMCwwLDE0LTE0VjYyaDEwYTYsNiwwLDAsMCwwLTEyWk05NCw0MGExMCwxMCwwLDAsMSwxMC0xMGg0OGExMCwxMCwwLDAsMSwxMCwxMFY1MEg5NFpNMTk0LDIwOGEyLDIsMCwwLDEtMiwySDY0YTIsMiwwLDAsMS0yLTJWNjJIMTk0Wk0xMTAsMTA0djY0YTYsNiwwLDAsMS0xMiwwVjEwNGE2LDYsMCwwLDEsMTIsMFptNDgsMHY2NGE2LDYsMCwwLDEtMTIsMFYxMDRhNiw2LDAsMCwxLDEyLDBaIi8+PC9zdmc+');}.icon-cloud-arrow-up{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xODguMjQsMTY0LjI0YTYsNiwwLDAsMS04LjQ4LDBMMTU4LDE0Mi40OVYyMDhhNiw2LDAsMCwxLTEyLDBWMTQyLjQ5bC0yMS43NiwyMS43NWE2LDYsMCwwLDEtOC40OC04LjQ4bDMyLTMyYTYsNiwwLDAsMSw4LjQ4LDBsMzIsMzJBNiw2LDAsMCwxLDE4OC4yNCwxNjQuMjRaTTE2MCw0MkE4Ni4xLDg2LjEsMCwwLDAsODIuNDMsOTAuODgsNjIsNjIsMCwxLDAsNzIsMjE0aDQwYTYsNiwwLDAsMCwwLTEySDcyYTUwLDUwLDAsMCwxLDAtMTAwLDUwLjY4LDUwLjY4LDAsMCwxLDUuOTEuMzZBODUuNTQsODUuNTQsMCwwLDAsNzQsMTI4YTYsNiwwLDAsMCwxMiwwLDc0LDc0LDAsMSwxLDEwMy42LDY3Ljg1LDYsNiwwLDAsMCw0LjgsMTFBODYsODYsMCwwLDAsMTYwLDQyWiIvPjwvc3ZnPg==');}.icon-arrow-elbow-left-up{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzgsMTkyYTYsNiwwLDAsMS02LDZIODhhNiw2LDAsMCwxLTYtNlY2Mi40OUw0NC4yNCwxMDAuMjRhNiw2LDAsMCwxLTguNDgtOC40OGw0OC00OGE2LDYsMCwwLDEsOC40OCwwbDQ4LDQ4YTYsNiwwLDEsMS04LjQ4LDguNDhMOTQsNjIuNDlWMTg2SDIzMkE2LDYsMCwwLDEsMjM4LDE5MloiLz48L3N2Zz4=');}.icon-arrow-elbow-right-up{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjAuMjQsMTAwLjI0YTYsNiwwLDAsMS04LjQ4LDBMMTc0LDYyLjQ5VjE5MmE2LDYsMCwwLDEtNiw2SDI0YTYsNiwwLDAsMSwwLTEySDE2MlY2Mi40OWwtMzcuNzYsMzcuNzVhNiw2LDAsMCwxLTguNDgtOC40OGw0OC00OGE2LDYsMCwwLDEsOC40OCwwbDQ4LDQ4QTYsNiwwLDAsMSwyMjAuMjQsMTAwLjI0WiIvPjwvc3ZnPg==');}.icon-x{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDQuMjQsMTk1Ljc2YTYsNiwwLDEsMS04LjQ4LDguNDhMMTI4LDEzNi40OSw2MC4yNCwyMDQuMjRhNiw2LDAsMCwxLTguNDgtOC40OEwxMTkuNTEsMTI4LDUxLjc2LDYwLjI0YTYsNiwwLDAsMSw4LjQ4LTguNDhMMTI4LDExOS41MWw2Ny43Ni02Ny43NWE2LDYsMCwwLDEsOC40OCw4LjQ4TDEzNi40OSwxMjhaIi8+PC9zdmc+');}.icon-magnifying-glass{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjguMjQsMjE5Ljc2bC01MS4zOC01MS4zOGE4Ni4xNSw4Ni4xNSwwLDEsMC04LjQ4LDguNDhsNTEuMzgsNTEuMzhhNiw2LDAsMCwwLDguNDgtOC40OFpNMzgsMTEyYTc0LDc0LDAsMSwxLDc0LDc0QTc0LjA5LDc0LjA5LDAsMCwxLDM4LDExMloiLz48L3N2Zz4=');}.icon-squares-four{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMDQsNDJINTZBMTQsMTQsMCwwLDAsNDIsNTZ2NDhhMTQsMTQsMCwwLDAsMTQsMTRoNDhhMTQsMTQsMCwwLDAsMTQtMTRWNTZBMTQsMTQsMCwwLDAsMTA0LDQyWm0yLDYyYTIsMiwwLDAsMS0yLDJINTZhMiwyLDAsMCwxLTItMlY1NmEyLDIsMCwwLDEsMi0yaDQ4YTIsMiwwLDAsMSwyLDJabTk0LTYySDE1MmExNCwxNCwwLDAsMC0xNCwxNHY0OGExNCwxNCwwLDAsMCwxNCwxNGg0OGExNCwxNCwwLDAsMCwxNC0xNFY1NkExNCwxNCwwLDAsMCwyMDAsNDJabTIsNjJhMiwyLDAsMCwxLTIsMkgxNTJhMiwyLDAsMCwxLTItMlY1NmEyLDIsMCwwLDEsMi0yaDQ4YTIsMiwwLDAsMSwyLDJabS05OCwzNEg1NmExNCwxNCwwLDAsMC0xNCwxNHY0OGExNCwxNCwwLDAsMCwxNCwxNGg0OGExNCwxNCwwLDAsMCwxNC0xNFYxNTJBMTQsMTQsMCwwLDAsMTA0LDEzOFptMiw2MmEyLDIsMCwwLDEtMiwySDU2YTIsMiwwLDAsMS0yLTJWMTUyYTIsMiwwLDAsMSwyLTJoNDhhMiwyLDAsMCwxLDIsMlptOTQtNjJIMTUyYTE0LDE0LDAsMCwwLTE0LDE0djQ4YTE0LDE0LDAsMCwwLDE0LDE0aDQ4YTE0LDE0LDAsMCwwLDE0LTE0VjE1MkExNCwxNCwwLDAsMCwyMDAsMTM4Wm0yLDYyYTIsMiwwLDAsMS0yLDJIMTUyYTIsMiwwLDAsMS0yLTJWMTUyYTIsMiwwLDAsMSwyLTJoNDhhMiwyLDAsMCwxLDIsMloiLz48L3N2Zz4=');}.icon-rows{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsMTM4SDQ4YTE0LDE0LDAsMCwwLTE0LDE0djQwYTE0LDE0LDAsMCwwLDE0LDE0SDIwOGExNCwxNCwwLDAsMCwxNC0xNFYxNTJBMTQsMTQsMCwwLDAsMjA4LDEzOFptMiw1NGEyLDIsMCwwLDEtMiwySDQ4YTIsMiwwLDAsMS0yLTJWMTUyYTIsMiwwLDAsMSwyLTJIMjA4YTIsMiwwLDAsMSwyLDJaTTIwOCw1MEg0OEExNCwxNCwwLDAsMCwzNCw2NHY0MGExNCwxNCwwLDAsMCwxNCwxNEgyMDhhMTQsMTQsMCwwLDAsMTQtMTRWNjRBMTQsMTQsMCwwLDAsMjA4LDUwWm0yLDU0YTIsMiwwLDAsMS0yLDJINDhhMiwyLDAsMCwxLTItMlY2NGEyLDIsMCwwLDEsMi0ySDIwOGEyLDIsMCwwLDEsMiwyWiIvPjwvc3ZnPg==');}.icon-table{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsNTBIMzJhNiw2LDAsMCwwLTYsNlYxOTJhMTQsMTQsMCwwLDAsMTQsMTRIMjE2YTE0LDE0LDAsMCwwLDE0LTE0VjU2QTYsNiwwLDAsMCwyMjQsNTBaTTM4LDExMEg4MnYzNkgzOFptNTYsMEgyMTh2MzZIOTRaTTIxOCw2MlY5OEgzOFY2MlpNMzgsMTkyVjE1OEg4MnYzNkg0MEEyLDIsMCwwLDEsMzgsMTkyWm0xNzgsMkg5NFYxNThIMjE4djM0QTIsMiwwLDAsMSwyMTYsMTk0WiIvPjwvc3ZnPg==');}.icon-infinity{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDYsMTI4YTU0LDU0LDAsMCwxLTkyLjE4LDM4LjE4LDMuMDcsMy4wNywwLDAsMS0uMjUtLjI2bC02MC02Ny43NGE0Miw0MiwwLDEsMCwwLDU5LjY0bDguNTctOS42N2E2LDYsMCwxLDEsOSw4bC04LjY5LDkuODFhMy4wNywzLjA3LDAsMCwxLS4yNS4yNiw1NCw1NCwwLDEsMSwwLTc2LjM2LDMuMDcsMy4wNywwLDAsMSwuMjUuMjZsNjAsNjcuNzRhNDIsNDIsMCwxLDAsMC01OS42NGwtOC41Nyw5LjY3YTYsNiwwLDEsMS05LThsOC42OS05LjgxYTMuMDcsMy4wNywwLDAsMSwuMjUtLjI2QTU0LDU0LDAsMCwxLDI0NiwxMjhaIi8+PC9zdmc+');}.icon-eye{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDUuNDgsMTI1LjU3Yy0uMzQtLjc4LTguNjYtMTkuMjMtMjcuMjQtMzcuODFDMjAxLDcwLjU0LDE3MS4zOCw1MCwxMjgsNTBTNTUsNzAuNTQsMzcuNzYsODcuNzZjLTE4LjU4LDE4LjU4LTI2LjksMzctMjcuMjQsMzcuODFhNiw2LDAsMCwwLDAsNC44OGMuMzQuNzcsOC42NiwxOS4yMiwyNy4yNCwzNy44QzU1LDE4NS40Nyw4NC42MiwyMDYsMTI4LDIwNnM3My0yMC41Myw5MC4yNC0zNy43NWMxOC41OC0xOC41OCwyNi45LTM3LDI3LjI0LTM3LjhBNiw2LDAsMCwwLDI0NS40OCwxMjUuNTdaTTEyOCwxOTRjLTMxLjM4LDAtNTguNzgtMTEuNDItODEuNDUtMzMuOTNBMTM0Ljc3LDEzNC43NywwLDAsMSwyMi42OSwxMjgsMTM0LjU2LDEzNC41NiwwLDAsMSw0Ni41NSw5NS45NEM2OS4yMiw3My40Miw5Ni42Miw2MiwxMjgsNjJzNTguNzgsMTEuNDIsODEuNDUsMzMuOTRBMTM0LjU2LDEzNC41NiwwLDAsMSwyMzMuMzEsMTI4QzIyNi45NCwxNDAuMjEsMTk1LDE5NCwxMjgsMTk0Wm0wLTExMmE0Niw0NiwwLDEsMCw0Niw0NkE0Ni4wNiw0Ni4wNiwwLDAsMCwxMjgsODJabTAsODBhMzQsMzQsMCwxLDEsMzQtMzRBMzQsMzQsMCwwLDEsMTI4LDE2MloiLz48L3N2Zz4=');}.icon-eye-closed{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjkuMjEsMTY1YTYsNiwwLDAsMS0xMC40Miw2bC0yMC0zNS4wOGExMjIsMTIyLDAsMCwxLTM5LDE4LjA5bDYuMTcsMzdhNiw2LDAsMCwxLTQuOTMsNi45MSw2Ljg1LDYuODUsMCwwLDEtMSwuMDgsNiw2LDAsMCwxLTUuOTEtNUwxNDgsMTU2LjQ0YTEyOC44NiwxMjguODYsMCwwLDEtNDAsMEwxMDEuOTIsMTkzQTYsNiwwLDAsMSw5NiwxOThhNi44NSw2Ljg1LDAsMCwxLTEtLjA4QTYsNiwwLDAsMSw5MC4wOCwxOTFsNi4xNy0zN2ExMjIsMTIyLDAsMCwxLTM5LTE4LjA5TDM3LjIxLDE3MWE2LDYsMCwxLDEtMTAuNDItNmwyMC44NS0zNi40OGExNTIsMTUyLDAsMCwxLTIwLjMxLTIwLjc3LDYsNiwwLDAsMSw5LjM0LTcuNTRDNTMuNTQsMTIxLjExLDgzLjA3LDE0NiwxMjgsMTQ2czc0LjQ2LTI0Ljg5LDkxLjMzLTQ1Ljc3YTYsNiwwLDAsMSw5LjM0LDcuNTQsMTUyLDE1MiwwLDAsMS0yMC4zMSwyMC43N1oiLz48L3N2Zz4=');}.icon-calendar{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsMzRIMTgyVjI0YTYsNiwwLDAsMC0xMiwwVjM0SDg2VjI0YTYsNiwwLDAsMC0xMiwwVjM0SDQ4QTE0LDE0LDAsMCwwLDM0LDQ4VjIwOGExNCwxNCwwLDAsMCwxNCwxNEgyMDhhMTQsMTQsMCwwLDAsMTQtMTRWNDhBMTQsMTQsMCwwLDAsMjA4LDM0Wk00OCw0Nkg3NFY1NmE2LDYsMCwwLDAsMTIsMFY0Nmg4NFY1NmE2LDYsMCwwLDAsMTIsMFY0NmgyNmEyLDIsMCwwLDEsMiwyVjgySDQ2VjQ4QTIsMiwwLDAsMSw0OCw0NlpNMjA4LDIxMEg0OGEyLDIsMCwwLDEtMi0yVjk0SDIxMFYyMDhBMiwyLDAsMCwxLDIwOCwyMTBabS05OC05MHY2NGE2LDYsMCwwLDEtMTIsMFYxMjkuNzFsLTcuMzIsMy42NmE2LDYsMCwxLDEtNS4zNi0xMC43NGwxNi04QTYsNiwwLDAsMSwxMTAsMTIwWm01OS41NywyOS4yNUwxNDgsMTc4aDIwYTYsNiwwLDAsMSwwLDEySDEzNmE2LDYsMCwwLDEtNC44LTkuNkwxNjAsMTQyYTEwLDEwLDAsMSwwLTE2LjY1LTExQTYsNiwwLDEsMSwxMzMsMTI1YTIyLDIyLDAsMSwxLDM2LjYyLDI0LjI2WiIvPjwvc3ZnPg==');}.icon-alphabetical{--icon:url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIGZpbGw9ImN1cnJlbnRDb2xvciIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIDAgMTgzLjc4IDE4NC4wNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtNTkuNTg2IDY5Ljc0MmMtMC44NTEzIDAtMS40NjEgMC4xOTY1Ni0xLjgzNjYgMC41OTcxOC0wLjM1MDU0IDAuMzc1NTgtMC41Mjk1OCAxLjAyMjktMC41Mjk1OCAxLjk0OTNzMC4xNzkwMyAxLjU5MzcgMC41Mjk1OCAxLjk5NDRjMC4zNzU1OCAwLjM3NTU4IDAuOTg1MjkgMC41NjMzOCAxLjgzNjYgMC41NjMzOGg3LjAxOTdsLTEyLjQyOCAzNC4zNjZoLTIuMTA3Yy0wLjg1MTMgMC0xLjQ2MSAwLjE5NjU2LTEuODM2NiAwLjU5NzE4LTAuMzUwNTQgMC4zNzU1OC0wLjUyOTU3IDEuMDM0MS0wLjUyOTU3IDEuOTYwNiAwIDAuOTI2NDQgMC4xNzkwMyAxLjU4MjUgMC41Mjk1NyAxLjk4MyAwLjM3NTU4IDAuMzc1NTkgMC45ODUyOSAwLjU2MzM4IDEuODM2NiAwLjU2MzM4aDEyLjU1MmMwLjg1MTMgMCAxLjQ1MjItMC4xODc3OSAxLjgwMjgtMC41NjMzOCAwLjM3NTU4LTAuNDAwNjIgMC41NjMzNy0xLjA1NjYgMC41NjMzNy0xLjk4MyAwLTAuOTI2NDUtMC4xODc3OS0xLjU4NS0wLjU2MzM3LTEuOTYwNi0wLjM1MDU0LTAuNDAwNjItMC45NTE0Ny0wLjU5NzE4LTEuODAyOC0wLjU5NzE4aC00LjU1MjFsMy4xMjExLTguOTM0OWgxOC4yMmwzLjA3NiA4LjkzNDloLTUuMDcwNGMtMC44NTEzIDAtMS40NjEgMC4xOTY1Ni0xLjgzNjYgMC41OTcxOC0wLjM1MDU0IDAuMzc1NTgtMC41Mjk1OCAxLjAzNDEtMC41Mjk1OCAxLjk2MDYgMCAwLjkyNjQ0IDAuMTc5MDMgMS41ODI1IDAuNTI5NTggMS45ODMgMC4zNzU1OCAwLjM3NTU5IDAuOTg1MjkgMC41NjMzOCAxLjgzNjYgMC41NjMzOGgxMy4yOTZjMC44NTEzIDAgMS40NTIyLTAuMTg3NzkgMS44MDI4LTAuNTYzMzggMC4zNzU1OC0wLjQwMDYyIDAuNTYzMzctMS4wNTY2IDAuNTYzMzctMS45ODMgMC0wLjkyNjQ1LTAuMTg3NzktMS41ODUtMC41NjMzNy0xLjk2MDYtMC4zNTA1NC0wLjQwMDYyLTAuOTUxNDctMC41OTcxOC0xLjgwMjgtMC41OTcxOGgtMi4yODczbC0xMy4yNjItMzcuMDM2Yy0wLjMwMDQ3LTAuODUxMy0wLjc1OTk0LTEuNDYxLTEuMzg1OS0xLjgzNjYtMC42MDA5My0wLjQwMDYyLTEuNDA5Ny0wLjU5NzE4LTIuNDExMy0wLjU5NzE4em00NC4xNDYgMGMtMC44NTEzIDAtMS40NzIzIDAuMTk2NTYtMS44NDc4IDAuNTk3MTgtMC4zNTA1NSAwLjM3NTU4LTAuNTE4MyAxLjAyMjktMC41MTgzIDEuOTQ5M3YxMS45MWMwIDAuODc2MzMgMC4yMDUzMiAxLjUwNjEgMC42MzA5OCAxLjg4MTcgMC40MjU2NiAwLjM3NTU4IDEuMTU5MyAwLjU2MzM3IDIuMTg1OSAwLjU2MzM3czEuNzQ5LTAuMTg3NzkgMi4xNzQ3LTAuNTYzMzdjMC40MjU2OS0wLjM3NTU4IDAuNjQyMjYtMS4wMDUzIDAuNjQyMjYtMS44ODE3di05LjM1MTdoMTguODUxbC0yNC43NTQgMzUuMzAxYy0wLjM1MDU0IDAuNTI1ODItMC41MTgzMSAxLjA3MTctMC41MTgzMSAxLjYyMjYgMCAwLjkyNjQ1IDAuMTY3NzcgMS41ODI1IDAuNTE4MzEgMS45ODMxIDAuMzc1NTggMC4zNzU1OCAwLjk5NjU0IDAuNTYzMzggMS44NDc4IDAuNTYzMzhoMjguNzY2YzAuODUxMyAwIDEuNDUyMi0wLjE4NzggMS44MDI4LTAuNTYzMzggMC4zNzU1OC0wLjQwMDYyIDAuNTYzMzgtMS4wNTY2IDAuNTYzMzgtMS45ODMxdi0xMi42NjVjMC0wLjg3NjMzLTAuMjE2NTgtMS40OTQ4LTAuNjQyMjUtMS44NzA0LTAuNDI1NjYtMC4zNzU1OC0xLjE0OC0wLjU2MzM4LTIuMTc0Ny0wLjU2MzM4LTEuMDI2NiAwLTEuNzQ5IDAuMTg3NzktMi4xNzQ3IDAuNTYzMzgtMC40MjU2NiAwLjM3NTU4LTAuNjQyMjQgMC45OTQwMi0wLjY0MjI0IDEuODcwNHYxMC4xMDdoLTE5Ljk3OGwyNC45MDEtMzUuNDU5YzAuMjUwMzktMC4zNTA1NCAwLjM3MTgzLTAuODM4ODMgMC4zNzE4My0xLjQ2NDggMC0wLjkyNjQ1LTAuMTg3OC0xLjU3MzctMC41NjMzOC0xLjk0OTMtMC4zNTA1NS0wLjQwMDYyLTAuOTUxNDctMC41OTcxOC0xLjgwMjgtMC41OTcxOHptLTMxLjc1MiA1LjEwNDJoMC43MDk4NWw2Ljk4NTkgMjAuMzE1aC0xNC43MTZ6bS0zNy43MjMtNDkuMTgzYy00LjczNDIgMC04LjYzMTMgMy44OTctOC42MzEzIDguNjMxM3YxMTUuNDdjMCA0LjczNDIgMy44OTcgOC42MzEzIDguNjMxMyA4LjYzMTNoMTE1LjI2YzQuNzM0MiAwIDguNjQyMS0zLjg5NyA4LjY0MjEtOC42MzEzdi0xMTUuNDdjMC00LjczNDItMy45MDgyLTguNjMxMy04LjY0MjEtOC42MzEzem0wIDUuNzI0aDExNS4yNmMxLjY1OCAwIDIuOTA3IDEuMjQ5MSAyLjkwNyAyLjkwNzF2MTE1LjQ3YzAgMS42NTgtMS4yNDkxIDIuOTA3LTIuOTA3IDIuOTA3aC0xMTUuMjZjLTEuNjU4IDAtMi44OTU4LTEuMjQ5MS0yLjg5NTgtMi45MDd2LTExNS40N2MwLTEuNjU4IDEuMjM3OC0yLjkwNzEgMi44OTU4LTIuOTA3MXoiIGZpbGw9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIuNzIxMTQiLz48L3N2Zz4=');}.icon-sort-ascending{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjYsMTI4YTYsNiwwLDAsMS02LDZINDhhNiw2LDAsMCwxLDAtMTJoNzJBNiw2LDAsMCwxLDEyNiwxMjhaTTQ4LDcwSDE4NGE2LDYsMCwwLDAsMC0xMkg0OGE2LDYsMCwwLDAsMCwxMlptNTYsMTE2SDQ4YTYsNiwwLDAsMCwwLDEyaDU2YTYsNiwwLDAsMCwwLTEyWm0xMjQuMjQtMjIuMjRhNiw2LDAsMCwwLTguNDgsMEwxOTAsMTkzLjUxVjExMmE2LDYsMCwwLDAtMTIsMHY4MS41MWwtMjkuNzYtMjkuNzVhNiw2LDAsMCwwLTguNDgsOC40OGw0MCw0MGE2LDYsMCwwLDAsOC40OCwwbDQwLTQwQTYsNiwwLDAsMCwyMjguMjQsMTYzLjc2WiIvPjwvc3ZnPg==');}.icon-sort-descending{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik00MiwxMjhhNiw2LDAsMCwxLDYtNmg3MmE2LDYsMCwwLDEsMCwxMkg0OEE2LDYsMCwwLDEsNDIsMTI4Wm02LTU4aDU2YTYsNiwwLDAsMCwwLTEySDQ4YTYsNiwwLDAsMCwwLDEyWk0xODQsMTg2SDQ4YTYsNiwwLDAsMCwwLDEySDE4NGE2LDYsMCwwLDAsMC0xMlpNMjI4LjI0LDgzLjc2bC00MC00MGE2LDYsMCwwLDAtOC40OCwwbC00MCw0MGE2LDYsMCwwLDAsOC40OCw4LjQ4TDE3OCw2Mi40OVYxNDRhNiw2LDAsMCwwLDEyLDBWNjIuNDlsMjkuNzYsMjkuNzVhNiw2LDAsMCwwLDguNDgtOC40OFoiLz48L3N2Zz4=');}.icon-floppy-disk{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTcuOSw3My40MiwxODIuNTgsMzguMWExMy45LDEzLjksMCwwLDAtOS44OS00LjFINDhBMTQsMTQsMCwwLDAsMzQsNDhWMjA4YTE0LDE0LDAsMCwwLDE0LDE0SDIwOGExNCwxNCwwLDAsMCwxNC0xNFY4My4zMUExMy45LDEzLjksMCwwLDAsMjE3LjksNzMuNDJaTTE3MCwyMTBIODZWMTUyYTIsMiwwLDAsMSwyLTJoODBhMiwyLDAsMCwxLDIsMlptNDAtMmEyLDIsMCwwLDEtMiwySDE4MlYxNTJhMTQsMTQsMCwwLDAtMTQtMTRIODhhMTQsMTQsMCwwLDAtMTQsMTR2NThINDhhMiwyLDAsMCwxLTItMlY0OGEyLDIsMCwwLDEsMi0ySDE3Mi42OWEyLDIsMCwwLDEsMS40MS41OEwyMDkuNDIsODEuOWEyLDIsMCwwLDEsLjU4LDEuNDFaTTE1OCw3MmE2LDYsMCwwLDEtNiw2SDk2YTYsNiwwLDAsMSwwLTEyaDU2QTYsNiwwLDAsMSwxNTgsNzJaIi8+PC9zdmc+');}.icon-columns{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMDQsMzRINjRBMTQsMTQsMCwwLDAsNTAsNDhWMjA4YTE0LDE0LDAsMCwwLDE0LDE0aDQwYTE0LDE0LDAsMCwwLDE0LTE0VjQ4QTE0LDE0LDAsMCwwLDEwNCwzNFptMiwxNzRhMiwyLDAsMCwxLTIsMkg2NGEyLDIsMCwwLDEtMi0yVjQ4YTIsMiwwLDAsMSwyLTJoNDBhMiwyLDAsMCwxLDIsMlpNMTkyLDM0SDE1MmExNCwxNCwwLDAsMC0xNCwxNFYyMDhhMTQsMTQsMCwwLDAsMTQsMTRoNDBhMTQsMTQsMCwwLDAsMTQtMTRWNDhBMTQsMTQsMCwwLDAsMTkyLDM0Wm0yLDE3NGEyLDIsMCwwLDEtMiwySDE1MmEyLDIsMCwwLDEtMi0yVjQ4YTIsMiwwLDAsMSwyLTJoNDBhMiwyLDAsMCwxLDIsMloiLz48L3N2Zz4=');}.icon-arrows-left-right{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTIuMjQsMTcxLjc2YTYsNiwwLDAsMSwwLDguNDhsLTMyLDMyYTYsNiwwLDAsMS04LjQ4LTguNDhMMTkzLjUxLDE4Mkg0OGE2LDYsMCwwLDEsMC0xMkgxOTMuNTFsLTIxLjc1LTIxLjc2YTYsNiwwLDAsMSw4LjQ4LTguNDhaTTc1Ljc2LDExNi4yNGE2LDYsMCwwLDAsOC40OC04LjQ4TDYyLjQ5LDg2SDIwOGE2LDYsMCwwLDAsMC0xMkg2Mi40OUw4NC4yNCw1Mi4yNGE2LDYsMCwwLDAtOC40OC04LjQ4bC0zMiwzMmE2LDYsMCwwLDAsMCw4LjQ4WiIvPjwvc3ZnPg==');}.icon-arrows-clockwise{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsNDhWOTZhNiw2LDAsMCwxLTYsNkgxNjhhNiw2LDAsMCwxLDAtMTJoMzMuNTJMMTgzLjQ3LDcyYTgxLjUxLDgxLjUxLDAsMCwwLTU3LjUzLTI0aC0uNDZBODEuNSw4MS41LDAsMCwwLDY4LjE5LDcxLjI4YTYsNiwwLDEsMS04LjM4LTguNTgsOTMuMzgsOTMuMzgsMCwwLDEsNjUuNjctMjYuNzZIMTI2YTkzLjQ1LDkzLjQ1LDAsMCwxLDY2LDI3LjUzbDE4LDE4VjQ4YTYsNiwwLDAsMSwxMiwwWk0xODcuODEsMTg0LjcyYTgxLjUsODEuNSwwLDAsMS01Ny4yOSwyMy4zNGgtLjQ2YTgxLjUxLDgxLjUxLDAsMCwxLTU3LjUzLTI0TDU0LjQ4LDE2Nkg4OGE2LDYsMCwwLDAsMC0xMkg0MGE2LDYsMCwwLDAtNiw2djQ4YTYsNiwwLDAsMCwxMiwwVjE3NC40OGwxOCwxOC4wNWE5My40NSw5My40NSwwLDAsMCw2NiwyNy41M2guNTJhOTMuMzgsOTMuMzgsMCwwLDAsNjUuNjctMjYuNzYsNiw2LDAsMSwwLTguMzgtOC41OFoiLz48L3N2Zz4=');}.icon-check-circle{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNzIuMjQsOTkuNzZhNiw2LDAsMCwxLDAsOC40OGwtNTYsNTZhNiw2LDAsMCwxLTguNDgsMGwtMjQtMjRhNiw2LDAsMCwxLDguNDgtOC40OEwxMTIsMTUxLjUxbDUxLjc2LTUxLjc1QTYsNiwwLDAsMSwxNzIuMjQsOTkuNzZaTTIzMCwxMjhBMTAyLDEwMiwwLDEsMSwxMjgsMjYsMTAyLjEyLDEwMi4xMiwwLDAsMSwyMzAsMTI4Wm0tMTIsMGE5MCw5MCwwLDEsMC05MCw5MEE5MC4xLDkwLjEsMCwwLDAsMjE4LDEyOFoiLz48L3N2Zz4=');}.icon-x-circle{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjQuMjQsMTAwLjI0LDEzNi40OCwxMjhsMjcuNzYsMjcuNzZhNiw2LDAsMSwxLTguNDgsOC40OEwxMjgsMTM2LjQ4bC0yNy43NiwyNy43NmE2LDYsMCwwLDEtOC40OC04LjQ4TDExOS41MiwxMjgsOTEuNzYsMTAwLjI0YTYsNiwwLDAsMSw4LjQ4LTguNDhMMTI4LDExOS41MmwyNy43Ni0yNy43NmE2LDYsMCwwLDEsOC40OCw4LjQ4Wk0yMzAsMTI4QTEwMiwxMDIsMCwxLDEsMTI4LDI2LDEwMi4xMiwxMDIuMTIsMCwwLDEsMjMwLDEyOFptLTEyLDBhOTAsOTAsMCwxLDAtOTAsOTBBOTAuMSw5MC4xLDAsMCwwLDIxOCwxMjhaIi8+PC9zdmc+');}.icon-user{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjkuMTksMjEzYy0xNS44MS0yNy4zMi00MC42My00Ni40OS02OS40Ny01NC42MmE3MCw3MCwwLDEsMC02My40NCwwQzY3LjQ0LDE2Ni41LDQyLjYyLDE4NS42NywyNi44MSwyMTNhNiw2LDAsMSwwLDEwLjM4LDZDNTYuNCwxODUuODEsOTAuMzQsMTY2LDEyOCwxNjZzNzEuNiwxOS44MSw5MC44MSw1M2E2LDYsMCwxLDAsMTAuMzgtNlpNNzAsOTZhNTgsNTgsMCwxLDEsNTgsNThBNTguMDcsNTguMDcsMCwwLDEsNzAsOTZaIi8+PC9zdmc+');}.icon-crosshair{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzIsMTIySDIyMS44QTk0LjEzLDk0LjEzLDAsMCwwLDEzNCwzNC4yVjI0YTYsNiwwLDAsMC0xMiwwVjM0LjJBOTQuMTMsOTQuMTMsMCwwLDAsMzQuMiwxMjJIMjRhNiw2LDAsMCwwLDAsMTJIMzQuMkE5NC4xMyw5NC4xMywwLDAsMCwxMjIsMjIxLjhWMjMyYTYsNiwwLDAsMCwxMiwwVjIyMS44QTk0LjEzLDk0LjEzLDAsMCwwLDIyMS44LDEzNEgyMzJhNiw2LDAsMCwwLDAtMTJabS05OCw4Ny43NlYyMDBhNiw2LDAsMCwwLTEyLDB2OS43NkE4Mi4wOSw4Mi4wOSwwLDAsMSw0Ni4yNCwxMzRINTZhNiw2LDAsMCwwLDAtMTJINDYuMjRBODIuMDksODIuMDksMCwwLDEsMTIyLDQ2LjI0VjU2YTYsNiwwLDAsMCwxMiwwVjQ2LjI0QTgyLjA5LDgyLjA5LDAsMCwxLDIwOS43NiwxMjJIMjAwYTYsNiwwLDAsMCwwLDEyaDkuNzZBODIuMDksODIuMDksMCwwLDEsMTM0LDIwOS43NlpNMTI4LDkwYTM4LDM4LDAsMSwwLDM4LDM4QTM4LDM4LDAsMCwwLDEyOCw5MFptMCw2NGEyNiwyNiwwLDEsMSwyNi0yNkEyNiwyNiwwLDAsMSwxMjgsMTU0WiIvPjwvc3ZnPg==');}.icon-person{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsNzBBMzAsMzAsMCwxLDAsOTgsNDAsMzAsMzAsMCwwLDAsMTI4LDcwWm0wLTQ4YTE4LDE4LDAsMSwxLTE4LDE4QTE4LDE4LDAsMCwxLDEyOCwyMlptODguODgsMTEzLjQyTDE3MS42Nyw4NC4xNkEzMCwzMCwwLDAsMCwxNDkuMTcsNzRIMTA2LjgzYTMwLDMwLDAsMCwwLTIyLjUsMTAuMTVMMzkuMTIsMTM1LjQyQTE4LDE4LDAsMCwwLDY0LjQ2LDE2MWwyMS4xMS0xNi45M0w2Ny40NCwyMTIuOTJhMTgsMTgsMCwwLDAsMzIuNzUsMTQuOTRMMTI4LDE4MGwyNy44MSw0Ny45MWExOCwxOCwwLDAsMCwzMi43NS0xNC45NGwtMTguMTMtNjguODdMMTkxLjU0LDE2MWExOCwxOCwwLDAsMCwyNS4zNC0yNS41NlptLTguNjMsMTYuODJhNiw2LDAsMCwxLTguNDksMCw0LjE1LDQuMTUsMCwwLDAtLjQ5LS40NGwtMzUuNTEtMjguNDhhNiw2LDAsMCwwLTkuNTYsNi4ybDIyLjg3LDg2LjkzYTcuNjYsNy42NiwwLDAsMCwuMzcsMSw2LDYsMCwwLDEtMTAuODgsNS4wNyw0LjM3LDQuMzcsMCwwLDAtLjI1LS40OEwxMzMuMTksMTY1YTYsNiwwLDAsMC0xMC4zOCwwTDg5LjY5LDIyMi4wNWE0LjM3LDQuMzcsMCwwLDAtLjI1LjQ4LDYsNiwwLDAsMS0xMC44OC01LjA3LDcuNjYsNy42NiwwLDAsMCwuMzctMWwyMi44Ny04Ni45M0E2LDYsMCwwLDAsOTkuMjcsMTIzLDYuMDcsNi4wNywwLDAsMCw5NiwxMjJhNiw2LDAsMCwwLTMuNzYsMS4zMkw1Ni43MywxNTEuOGE0LjE1LDQuMTUsMCwwLDAtLjQ5LjQ0LDYsNiwwLDAsMS04LjQ5LTguNDlsLjI2LS4yN0w5My4zMyw5Mi4wOUExOCwxOCwwLDAsMSwxMDYuODMsODZoNDIuMzRhMTgsMTgsMCwwLDEsMTMuNSw2LjA5TDIwOCwxNDMuNDhsLjI2LjI3QTYsNiwwLDAsMSwyMDguMjUsMTUyLjI0WiIvPjwvc3ZnPg==');}.icon-folder{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNzRIMTMwLjQ5bC0yNy45LTI3LjlhMTMuOTQsMTMuOTQsMCwwLDAtOS45LTQuMUg0MEExNCwxNCwwLDAsMCwyNiw1NlYyMDAuNjJBMTMuMzksMTMuMzksMCwwLDAsMzkuMzgsMjE0SDIxNi44OUExMy4xMiwxMy4xMiwwLDAsMCwyMzAsMjAwLjg5Vjg4QTE0LDE0LDAsMCwwLDIxNiw3NFpNNDAsNTRIOTIuNjlhMiwyLDAsMCwxLDEuNDEuNTlMMTEzLjUxLDc0SDM4VjU2QTIsMiwwLDAsMSw0MCw1NFpNMjE4LDIwMC44OWExLjExLDEuMTEsMCwwLDEtMS4xMSwxLjExSDM5LjM4QTEuNCwxLjQsMCwwLDEsMzgsMjAwLjYyVjg2SDIxNmEyLDIsMCwwLDEsMiwyWiIvPjwvc3ZnPg==');}.icon-dots-six{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik03MCw5MkExMCwxMCwwLDEsMSw2MCw4MiwxMCwxMCwwLDAsMSw3MCw5MlptNTgtMTBhMTAsMTAsMCwxLDAsMTAsMTBBMTAsMTAsMCwwLDAsMTI4LDgyWm02OCwyMGExMCwxMCwwLDEsMC0xMC0xMEExMCwxMCwwLDAsMCwxOTYsMTAyWk02MCwxNTRhMTAsMTAsMCwxLDAsMTAsMTBBMTAsMTAsMCwwLDAsNjAsMTU0Wm02OCwwYTEwLDEwLDAsMSwwLDEwLDEwQTEwLDEwLDAsMCwwLDEyOCwxNTRabTY4LDBhMTAsMTAsMCwxLDAsMTAsMTBBMTAsMTAsMCwwLDAsMTk2LDE1NFoiLz48L3N2Zz4=');}.icon-cake{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzAsMTEyYTIyLDIyLDAsMCwwLTIyLTIySDEzNFY3Ny40QTMwLjA1LDMwLjA1LDAsMCwwLDE1OCw0OGMwLTI2LjktMjUuNTgtNDQuMjctMjYuNjctNDVhNiw2LDAsMCwwLTYuNjYsMEMxMjMuNTgsMy43Myw5OCwyMS4xLDk4LDQ4YTMwLjA1LDMwLjA1LDAsMCwwLDI0LDI5LjRWOTBINDhhMjIsMjIsMCwwLDAtMjIsMjJ2MjMuMzNhMzguODEsMzguODEsMCwwLDAsOCwyMy41N1YyMDBhMjIsMjIsMCwwLDAsMjIsMjJIMjAwYTIyLDIyLDAsMCwwLDIyLTIyVjE1OC45YTM4LjgxLDM4LjgxLDAsMCwwLDgtMjMuNTdaTTExMCw0OGMwLTE1LjQsMTItMjcuNDEsMTgtMzIuNDQsNiw1LDE4LDE3LDE4LDMyLjQ0YTE4LDE4LDAsMCwxLTM2LDBaTTM4LDExMmExMCwxMCwwLDAsMSwxMC0xMEgyMDhhMTAsMTAsMCwwLDEsMTAsMTB2MjMuMzNjMCwxNC4zMi0xMS4zMywyNi4yOC0yNS4yNiwyNi42NkEyNiwyNiwwLDAsMSwxNjYsMTM2YTYsNiwwLDAsMC0xMiwwLDI2LDI2LDAsMCwxLTUyLDAsNiw2LDAsMCwwLTEyLDAsMjYsMjYsMCwwLDEtMjYuNzQsMjZDNDkuMzMsMTYxLjYxLDM4LDE0OS42NSwzOCwxMzUuMzNabTE2Miw5OEg1NmExMCwxMCwwLDAsMS0xMC0xMFYxNjkuMzFBMzYuOCwzNi44LDAsMCwwLDYyLjk0LDE3NCwzNy44MiwzNy44MiwwLDAsMCw5MC41LDE2My4yNCwzOC44MywzOC44MywwLDAsMCw5NiwxNTYuNWEzOCwzOCwwLDAsMCw2NCwwLDM4LjgzLDM4LjgzLDAsMCwwLDUuNTIsNi43NEEzNy43MywzNy43MywwLDAsMCwxOTIsMTc0aDEuMDdBMzYuOCwzNi44LDAsMCwwLDIxMCwxNjkuMzFWMjAwQTEwLDEwLDAsMCwxLDIwMCwyMTBaIi8+PC9zdmc+');}.icon-hourglass{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xOTgsNzUuNjRWNDBhMTQsMTQsMCwwLDAtMTQtMTRINzJBMTQsMTQsMCwwLDAsNTgsNDBWNzZhMTQuMDYsMTQuMDYsMCwwLDAsNS42LDExLjJMMTE4LDEyOCw2My42LDE2OC44QTE0LjA2LDE0LjA2LDAsMCwwLDU4LDE4MHYzNmExNCwxNCwwLDAsMCwxNCwxNEgxODRhMTQsMTQsMCwwLDAsMTQtMTRWMTgwLjM2YTE0LjA4LDE0LjA4LDAsMCwwLTUuNTYtMTEuMTdMMTM4LDEyOGw1NC40OS00MS4xOUExNC4wOCwxNC4wOCwwLDAsMCwxOTgsNzUuNjRaTTE4NiwxODAuMzZWMjE2YTIsMiwwLDAsMS0yLDJINzJhMiwyLDAsMCwxLTItMlYxODBhMiwyLDAsMCwxLC44LTEuNkwxMjgsMTM1LjUxbDU3LjIyLDQzLjI1QTIsMiwwLDAsMSwxODYsMTgwLjM2Wm0wLTEwNC43MmEyLDIsMCwwLDEtLjc5LDEuNkwxMjgsMTIwLjQ5LDcwLjgsNzcuNkEyLDIsMCwwLDEsNzAsNzZWNDBhMiwyLDAsMCwxLDItMkgxODRhMiwyLDAsMCwxLDIsMloiLz48L3N2Zz4=');}.icon-hash{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsOTBIMTczbDguODktNDguOTNhNiw2LDAsMSwwLTExLjgtMi4xNEwxNjAuODEsOTBIMTA5bDguODktNDguOTNhNiw2LDAsMCwwLTExLjgtMi4xNEw5Ni44MSw5MEg0OGE2LDYsMCwwLDAsMCwxMkg5NC42M2wtOS40Niw1MkgzMmE2LDYsMCwwLDAsMCwxMkg4M0w3NC4xLDIxNC45M2E2LDYsMCwwLDAsNC44Myw3QTUuNjQsNS42NCwwLDAsMCw4MCwyMjJhNiw2LDAsMCwwLDUuODktNC45M0w5NS4xOSwxNjZIMTQ3bC04Ljg5LDQ4LjkzYTYsNiwwLDAsMCw0LjgzLDcsNS42NCw1LjY0LDAsMCwwLDEuMDguMSw2LDYsMCwwLDAsNS44OS00LjkzTDE1OS4xOSwxNjZIMjA4YTYsNiwwLDAsMCwwLTEySDE2MS4zN2w5LjQ2LTUySDIyNGE2LDYsMCwwLDAsMC0xMlptLTc0LjgzLDY0SDk3LjM3bDkuNDYtNTJoNTEuOFoiLz48L3N2Zz4=');}.icon-pencil-simple{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjUuOSw3NC43OCwxODEuMjEsMzAuMDlhMTQsMTQsMCwwLDAtMTkuOCwwTDM4LjEsMTUzLjQxYTEzLjk0LDEzLjk0LDAsMCwwLTQuMSw5LjlWMjA4YTE0LDE0LDAsMCwwLDE0LDE0SDkyLjY5YTEzLjk0LDEzLjk0LDAsMCwwLDkuOS00LjFMMjI1LjksOTQuNThhMTQsMTQsMCwwLDAsMC0xOS44Wk05NC4xLDIwOS40MWEyLDIsMCwwLDEtMS40MS41OUg0OGEyLDIsMCwwLDEtMi0yVjE2My4zMWEyLDIsMCwwLDEsLjU5LTEuNDFMMTM2LDcyLjQ4LDE4My41MSwxMjBaTTIxNy40MSw4Ni4xLDE5MiwxMTEuNTEsMTQ0LjQ5LDY0LDE2OS45LDM4LjU4YTIsMiwwLDAsMSwyLjgzLDBsNDQuNjgsNDQuNjlhMiwyLDAsMCwxLDAsMi44M1oiLz48L3N2Zz4=');}.icon-caret-double-down{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTIuMjQsMTMxLjc2YTYsNiwwLDAsMSwwLDguNDhsLTgwLDgwYTYsNiwwLDAsMS04LjQ4LDBsLTgwLTgwYTYsNiwwLDAsMSw4LjQ4LTguNDhMMTI4LDIwNy41MWw3NS43Ni03NS43NUE2LDYsMCwwLDEsMjEyLjI0LDEzMS43NlptLTg4LjQ4LDguNDhhNiw2LDAsMCwwLDguNDgsMGw4MC04MGE2LDYsMCwwLDAtOC40OC04LjQ4TDEyOCwxMjcuNTEsNTIuMjQsNTEuNzZhNiw2LDAsMCwwLTguNDgsOC40OFoiLz48L3N2Zz4=');}.icon-caret-double-right{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNDAuMjQsMTMyLjI0bC04MCw4MGE2LDYsMCwwLDEtOC40OC04LjQ4TDEyNy41MSwxMjgsNTEuNzYsNTIuMjRhNiw2LDAsMCwxLDguNDgtOC40OGw4MCw4MEE2LDYsMCwwLDEsMTQwLjI0LDEzMi4yNFptODAtOC40OC04MC04MGE2LDYsMCwwLDAtOC40OCw4LjQ4TDIwNy41MSwxMjhsLTc1Ljc1LDc1Ljc2YTYsNiwwLDEsMCw4LjQ4LDguNDhsODAtODBBNiw2LDAsMCwwLDIyMC4yNCwxMjMuNzZaIi8+PC9zdmc+');}.icon-dots-six-vertical{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMDIsNjBBMTAsMTAsMCwxLDEsOTIsNTAsMTAsMTAsMCwwLDEsMTAyLDYwWm02MiwxMGExMCwxMCwwLDEsMC0xMC0xMEExMCwxMCwwLDAsMCwxNjQsNzBaTTkyLDExOGExMCwxMCwwLDEsMCwxMCwxMEExMCwxMCwwLDAsMCw5MiwxMThabTcyLDBhMTAsMTAsMCwxLDAsMTAsMTBBMTAsMTAsMCwwLDAsMTY0LDExOFpNOTIsMTg2YTEwLDEwLDAsMSwwLDEwLDEwQTEwLDEwLDAsMCwwLDkyLDE4NlptNzIsMGExMCwxMCwwLDEsMCwxMCwxMEExMCwxMCwwLDAsMCwxNjQsMTg2WiIvPjwvc3ZnPg==');}.icon-drop-simple{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNzIuNTMsNDkuMDZhMjUxLjQyLDI1MS40MiwwLDAsMC00MS4wOS0zOCw2LDYsMCwwLDAtNi44OCwwLDI1MS40MiwyNTEuNDIsMCwwLDAtNDEuMDksMzhDNTYuMzQsODAuMjYsNDIsMTEzLjA5LDQyLDE0NGE4Niw4NiwwLDAsMCwxNzIsMEMyMTQsMTEzLjA5LDE5OS42Niw4MC4yNiwxNzIuNTMsNDkuMDZaTTEyOCwyMThhNzQuMDksNzQuMDksMCwwLDEtNzQtNzRjMC01OS42Miw1OS0xMDguOTMsNzQtMTIwLjUxQzE0MywzNS4wNywyMDIsODQuMzgsMjAyLDE0NEE3NC4wOSw3NC4wOSwwLDAsMSwxMjgsMjE4WiIvPjwvc3ZnPg==');}.icon-door{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzIsMjE4SDIwNlY0MGExNCwxNCwwLDAsMC0xNC0xNEg2NEExNCwxNCwwLDAsMCw1MCw0MFYyMThIMjRhNiw2LDAsMCwwLDAsMTJIMjMyYTYsNiwwLDAsMCwwLTEyWk02Miw0MGEyLDIsMCwwLDEsMi0ySDE5MmEyLDIsMCwwLDEsMiwyVjIxOEg2MlptMTA0LDkyYTEwLDEwLDAsMSwxLTEwLTEwQTEwLDEwLDAsMCwxLDE2NiwxMzJaIi8+PC9zdmc+');}.icon-hand-heart{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjkuMTIsMTQyLjY1YTIyLjQzLDIyLjQzLDAsMCwwLTE5LjU1LTMuODhsLTQuMzIsMUMyMjcsMTE5LjU1LDIzOCw5OS41MSwyMzgsODBjMC0yNS4zNi0yMC4zOS00Ni00NS40Ni00NkE0NS41MSw0NS41MSwwLDAsMCwxNTYsNTJhNDUuNTEsNDUuNTEsMCwwLDAtMzYuNTQtMThDOTQuMzksMzQsNzQsNTQuNjQsNzQsODBjMCwxMS4zOCwzLjYzLDIyLjQ5LDExLjI5LDM0LjM2YTI5LjczLDI5LjczLDAsMCwwLTE2LjU2LDguNDNMNDUuNTIsMTQ2SDE2QTE0LDE0LDAsMCwwLDIsMTYwdjQwYTE0LDE0LDAsMCwwLDE0LDE0SDEyMGE2LDYsMCwwLDAsMS40Ni0uMThsNjQtMTZhNy4xNiw3LjE2LDAsMCwwLC44OS0uM0wyMjUuMTcsMTgxbC4zMy0uMTVhMjIuNiwyMi42LDAsMCwwLDMuNjItMzguMThaTTExOS40Niw0NmEzMy4xNiwzMy4xNiwwLDAsMSwzMSwyMC4yOCw2LDYsMCwwLDAsMTEuMSwwLDMzLjE2LDMzLjE2LDAsMCwxLDMxLTIwLjI4QzIxMC42OCw0NiwyMjYsNjEuNTcsMjI2LDgwYzAsMjAuMjQtMTYuMTgsNDMtNDYuOCw2NS43NWwtMTQuODcsMy40MkEyNiwyNiwwLDAsMCwxNDAsMTE0SDk5LjY3QzkwLjM2LDEwMS42Nyw4Niw5MC44MSw4Niw4MCw4Niw2MS41NywxMDEuMzIsNDYsMTE5LjQ2LDQ2Wk0xNCwyMDBWMTYwYTIsMiwwLDAsMSwyLTJINDJ2NDRIMTZBMiwyLDAsMCwxLDE0LDIwMFptMjA2LjI4LTMwLTM4LjIsMTYuMjdMMTE5LjI2LDIwMkg1NFYxNTQuNDlsMjMuMjEtMjMuMjJBMTcuODgsMTcuODgsMCwwLDEsODkuOTQsMTI2SDE0MGExNCwxNCwwLDAsMSwwLDI4SDExMmE2LDYsMCwwLDAsMCwxMmgzMmE2LDYsMCwwLDAsMS4zNC0uMTVsNjctMTUuNDEuMjQtLjA2QTEwLjYsMTAuNiwwLDAsMSwyMjAuMjgsMTcwWiIvPjwvc3ZnPg==');}.icon-plugs-connected{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzYuMjQsMTkuNzZhNiw2LDAsMCwwLTguNDgsMEwxNzMuOTQsNzMuNTdsLTYuNzktNi43OGEzMCwzMCwwLDAsMC00Mi40MiwwTDEwMCw5MS41MWwtNy43Ni03Ljc1YTYsNiwwLDAsMC04LjQ4LDguNDhMOTEuNTEsMTAwLDY2Ljc5LDEyNC43M2EzMCwzMCwwLDAsMCwwLDQyLjQybDYuNzgsNi43OUwxOS43NiwyMjcuNzZhNiw2LDAsMSwwLDguNDgsOC40OGw1My44Mi01My44MSw2Ljc5LDYuNzhhMzAsMzAsMCwwLDAsNDIuNDIsMEwxNTYsMTY0LjQ5bDcuNzYsNy43NWE2LDYsMCwwLDAsOC40OC04LjQ4TDE2NC40OSwxNTZsMjQuNzItMjQuNzNhMzAsMzAsMCwwLDAsMC00Mi40MmwtNi43OC02Ljc5LDUzLjgxLTUzLjgyQTYsNiwwLDAsMCwyMzYuMjQsMTkuNzZabS0xMTMuNDUsMTYxYTE4LDE4LDAsMCwxLTI1LjQ2LDBMNzUuMjcsMTU4LjY3YTE4LDE4LDAsMCwxLDAtMjUuNDZMMTAwLDEwOC40OSwxNDcuNTEsMTU2Wm01Ny45NC01Ny45NEwxNTYsMTQ3LjUxLDEwOC40OSwxMDBsMjQuNzItMjQuNzNhMTgsMTgsMCwwLDEsMjUuNDYsMGwyMi4wNiwyMi4wNmExOCwxOCwwLDAsMSwwLDI1LjQ2Wk05MC40MywzNC4yM2E2LDYsMCwwLDEsMTEuMTQtNC40Nmw4LDIwYTYsNiwwLDEsMS0xMS4xNCw0LjQ2Wm0tNjQsNTkuNTRhNiw2LDAsMCwxLDcuOC0zLjM0bDIwLDhhNiw2LDAsMSwxLTQuNDYsMTEuMTRsLTIwLThBNiw2LDAsMCwxLDI2LjQzLDkzLjc3Wm0yMDMuMTQsNjguNDZhNiw2LDAsMCwxLTcuOCwzLjM0bC0yMC04YTYsNiwwLDAsMSw0LjQ2LTExLjE0bDIwLDhBNiw2LDAsMCwxLDIyOS41NywxNjIuMjNabS02NCw1OS41NGE2LDYsMCwxLDEtMTEuMTQsNC40NmwtOC0yMGE2LDYsMCwwLDEsMTEuMTQtNC40NloiLz48L3N2Zz4=');}.icon-question{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMzgsMTgwYTEwLDEwLDAsMSwxLTEwLTEwQTEwLDEwLDAsMCwxLDEzOCwxODBaTTEyOCw3NGMtMjEsMC0zOCwxNS4yNS0zOCwzNHY0YTYsNiwwLDAsMCwxMiwwdi00YzAtMTIuMTMsMTEuNjYtMjIsMjYtMjJzMjYsOS44NywyNiwyMi0xMS42NiwyMi0yNiwyMmE2LDYsMCwwLDAtNiw2djhhNiw2LDAsMCwwLDEyLDB2LTIuNDJjMTguMTEtMi41OCwzMi0xNi42NiwzMi0zMy41OEMxNjYsODkuMjUsMTQ5LDc0LDEyOCw3NFptMTAyLDU0QTEwMiwxMDIsMCwxLDEsMTI4LDI2LDEwMi4xMiwxMDIuMTIsMCwwLDEsMjMwLDEyOFptLTEyLDBhOTAsOTAsMCwxLDAtOTAsOTBBOTAuMSw5MC4xLDAsMCwwLDIxOCwxMjhaIi8+PC9zdmc+');}.icon-asterisk{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTMuMTQsMTc5LjA5YTYsNiwwLDAsMS04LjIzLDIuMDZMMTM0LDEzOC42VjIxNmE2LDYsMCwwLDEtMTIsMFYxMzguNkw1MS4wOSwxODEuMTVBNi4wNyw2LjA3LDAsMCwxLDQ4LDE4MmE2LDYsMCwwLDEtMy4xLTExLjE1TDExNi4zNCwxMjgsNDQuOTEsODUuMTVhNiw2LDAsMCwxLDYuMTgtMTAuM0wxMjIsMTE3LjRWNDBhNiw2LDAsMCwxLDEyLDB2NzcuNGw3MC45MS00Mi41NWE2LDYsMCwwLDEsNi4xOCwxMC4zTDEzOS42NiwxMjhsNzEuNDMsNDIuODVBNiw2LDAsMCwxLDIxMy4xNCwxNzkuMDlaIi8+PC9zdmc+');}.icon-arrow-counter-clockwise{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTk0LDk0LDAsMCwxLTkyLjc0LDk0SDEyOGE5My40Myw5My40MywwLDAsMS02NC41LTI1LjY1LDYsNiwwLDEsMSw4LjI0LTguNzJBODIsODIsMCwxLDAsNzAsNzBsLS4xOS4xOUwzOS40NCw5OEg3MmE2LDYsMCwwLDEsMCwxMkgyNGE2LDYsMCwwLDEtNi02VjU2YTYsNiwwLDAsMSwxMiwwVjkwLjM0TDYxLjYzLDYxLjRBOTQsOTQsMCwwLDEsMjIyLDEyOFoiLz48L3N2Zz4=');}.icon-clock{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsMjZBMTAyLDEwMiwwLDEsMCwyMzAsMTI4LDEwMi4xMiwxMDIuMTIsMCwwLDAsMTI4LDI2Wm0wLDE5MmE5MCw5MCwwLDEsMSw5MC05MEE5MC4xLDkwLjEsMCwwLDEsMTI4LDIxOFptNjItOTBhNiw2LDAsMCwxLTYsNkgxMjhhNiw2LDAsMCwxLTYtNlY3MmE2LDYsMCwwLDEsMTIsMHY1MGg1MEE2LDYsMCwwLDEsMTkwLDEyOFoiLz48L3N2Zz4=');}.icon-envelope{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsNTBIMzJhNiw2LDAsMCwwLTYsNlYxOTJhMTQsMTQsMCwwLDAsMTQsMTRIMjE2YTE0LDE0LDAsMCwwLDE0LTE0VjU2QTYsNiwwLDAsMCwyMjQsNTBabS05Niw4NS44Nkw0Ny40Miw2MkgyMDguNThaTTEwMS42NywxMjgsMzgsMTg2LjM2VjY5LjY0Wm04Ljg4LDguMTRMMTI0LDE0OC40MmE2LDYsMCwwLDAsOC4xLDBsMTMuNC0xMi4yOEwyMDguNTgsMTk0SDQ3LjQzWk0xNTQuMzMsMTI4LDIxOCw2OS42NFYxODYuMzZaIi8+PC9zdmc+');}.icon-facebook-logo{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsMjZBMTAyLDEwMiwwLDEsMCwyMzAsMTI4LDEwMi4xMiwxMDIuMTIsMCwwLDAsMTI4LDI2Wm02LDE5MS44VjE1MGgyNmE2LDYsMCwwLDAsMC0xMkgxMzRWMTEyYTE4LDE4LDAsMCwxLDE4LTE4aDE2YTYsNiwwLDAsMCwwLTEySDE1MmEzMCwzMCwwLDAsMC0zMCwzMHYyNkg5NmE2LDYsMCwwLDAsMCwxMmgyNnY2Ny44YTkwLDkwLDAsMSwxLDEyLDBaIi8+PC9zdmc+');}.icon-twitter-logo{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDUuNTQsNjkuNzFBNiw2LDAsMCwwLDI0MCw2NkgyMDguNGE0Ni42LDQ2LjYsMCwwLDAtNDAuMzMtMjQsNDQuOTMsNDQuOTMsMCwwLDAtMzIuMzEsMTMuMTJBNDUuOTIsNDUuOTIsMCwwLDAsMTIyLDg4djguNjZjLTQyLTEwLTc2LjYtNDQuNTItNzctNDQuODhBNiw2LDAsMCwwLDM0LjgsNTUuNDZjLTQuMjUsNDcuMjIsOS40Miw3OC43NSwyMS42NCw5Ni44OWExMDcuNzEsMTA3LjcxLDAsMCwwLDIzLjA3LDI1Yy0xNS40OSwxOS00MS4zNCwyOC44OS00MS42MiwyOWE2LDYsMCwwLDAtMi44OCw5QzM2LDIxNi44Myw0NS43OCwyMzAsODAsMjMwYzY5Ljg4LDAsMTI4LjIzLTU0LDEzMy44Mi0xMjMuMzRsMzAuNDItMzAuNDFBNiw2LDAsMCwwLDI0NS41NCw2OS43MVpNMjAzLjc2LDk5Ljc2YTYsNiwwLDAsMC0xLjc1LDMuODZDMTk3LjkzLDE2Ny43NiwxNDQuMzMsMjE4LDgwLDIxOGMtMTQsMC0yMi43Ni0yLjQxLTI4LjA2LTQuOCwxMS4zLTUuNjgsMjkuNzItMTYuODgsNDEuMDUtMzMuODdhNiw2LDAsMCwwLC44NS00LjY3QTYsNiwwLDAsMCw5MSwxNzAuODJjLS4xMy0uMDgtMTMuMTMtNy44Ni0yNS0yNS43MkM1MiwxMjQsNDUuMzEsOTguNDMsNDYsNjguODhjMTQuODgsMTIuNiw0NS41NywzNS4wOSw4MSw0MWE2LDYsMCwwLDAsNy01LjkyVjg4YTM0LDM0LDAsMCwxLDEwLjE5LTI0LjM0QTMzLjA1LDMzLjA1LDAsMCwxLDE2Ny45Myw1NGMxMy40My4xNywyNiw4LjM3LDMxLjI0LDIwLjRhNiw2LDAsMCwwLDUuNSwzLjZoMjAuODRaIi8+PC9zdmc+');}.icon-caret-down{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTIuMjQsMTAwLjI0bC04MCw4MGE2LDYsMCwwLDEtOC40OCwwbC04MC04MGE2LDYsMCwwLDEsOC40OC04LjQ4TDEyOCwxNjcuNTFsNzUuNzYtNzUuNzVhNiw2LDAsMCwxLDguNDgsOC40OFoiLz48L3N2Zz4=');}.icon-list{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTYsNiwwLDAsMS02LDZINDBhNiw2LDAsMCwxLDAtMTJIMjE2QTYsNiwwLDAsMSwyMjIsMTI4Wk00MCw3MEgyMTZhNiw2LDAsMCwwLDAtMTJINDBhNiw2LDAsMCwwLDAsMTJaTTIxNiwxODZINDBhNiw2LDAsMCwwLDAsMTJIMjE2YTYsNiwwLDAsMCwwLTEyWiIvPjwvc3ZnPg==');}.icon-instagram-logo{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsODJhNDYsNDYsMCwxLDAsNDYsNDZBNDYuMDYsNDYuMDYsMCwwLDAsMTI4LDgyWm0wLDgwYTM0LDM0LDAsMSwxLDM0LTM0QTM0LDM0LDAsMCwxLDEyOCwxNjJaTTE3NiwyNkg4MEE1NC4wNiw1NC4wNiwwLDAsMCwyNiw4MHY5NmE1NC4wNiw1NC4wNiwwLDAsMCw1NCw1NGg5NmE1NC4wNiw1NC4wNiwwLDAsMCw1NC01NFY4MEE1NC4wNiw1NC4wNiwwLDAsMCwxNzYsMjZabTQyLDE1MGE0Miw0MiwwLDAsMS00Miw0Mkg4MGE0Miw0MiwwLDAsMS00Mi00MlY4MEE0Miw0MiwwLDAsMSw4MCwzOGg5NmE0Miw0MiwwLDAsMSw0Miw0MlpNMTkwLDc2YTEwLDEwLDAsMSwxLTEwLTEwQTEwLDEwLDAsMCwxLDE5MCw3NloiLz48L3N2Zz4=');}.icon-butterfly{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzEuMSw1MS43MUMyMjYuMDksNDUuMjcsMjE4LjY0LDQyLDIwOSw0MmMtMTYuMzMsMC0zNy40MSwxMS4wNi01Ni40LDI5LjU5QTEzMiwxMzIsMCwwLDAsMTM0LDkzLjk0VjU2YTYsNiwwLDAsMC0xMiwwVjkzLjk0YTEzMiwxMzIsMCwwLDAtMTguNTQtMjIuMzVDODQuNDksNTMuMDYsNjMuNCw0Miw0Ny4wNyw0MmMtOS42OSwwLTE3LjE0LDMuMjctMjIuMTUsOS43MS01LjUzLDcuMTEtNy43MSwxNy42OS02LjY2LDMyLjM0LjkxLDEyLjczLDQuMTIsMjYuNTMsNi44MSwzNy4xMyw2LjI4LDI0Ljc0LDIwLjc3LDMzLDMxLjc4LDM1LjY4QTQyLDQyLDAsMSwwLDEyOCwyMDEuNjJhNDIsNDIsMCwxLDAsNzEuMTYtNDQuNzZjMTEtMi42MywyNS41LTEwLjk0LDMxLjc4LTM1LjY4QzIzNy41NSw5NS4yMSwyNDIuNzIsNjYuNjUsMjMxLjEsNTEuNzFaTTkyLDIxMGEzMC4xMiwzMC4xMiwwLDAsMS0zLjM0LTYwQTYsNiwwLDAsMCw4Ny4zNSwxMzhhNDEuNzEsNDEuNzEsMCwwLDAtMjAuMjgsOGMtOSwuMzEtMjQuMTItMy4xNi0zMC4zNy0yNy43Ni0zLjI1LTEyLjgxLTExLjg5LTQ2LjgzLTIuMzEtNTkuMTVDMzcuMDUsNTUuNjYsNDEuMiw1NCw0Ny4wNyw1NGMxMi44OCwwLDMxLjcyLDEwLjI4LDQ4LDI2LjE4QzExMS42OSw5Ni4zOSwxMjIsMTE0LjU5LDEyMiwxMjcuNjdWMTgwQTMwLDMwLDAsMCwxLDkyLDIxMFptMTI3LjMxLTkxLjc3QzIxMy4wNywxNDIuODMsMTk4LDE0Ni4yOSwxODksMTQ2YTQxLjYyLDQxLjYyLDAsMCwwLTIwLjI4LThBNiw2LDAsMSwwLDE2Ny4zNiwxNTAsMzAuMTEsMzAuMTEsMCwxLDEsMTM0LDE4MFYxMjcuNjdjMC0xMy4wOCwxMC4zMi0zMS4yOCwyNi45My00Ny40OUMxNzcuMjMsNjQuMjgsMTk2LjA3LDU0LDIwOSw1NGM1Ljg3LDAsMTAsMS42NiwxMi42OCw1LjA4QzIzMS4yMSw3MS40LDIyMi41NywxMDUuNDIsMjE5LjMyLDExOC4yM1oiLz48L3N2Zz4=');}.icon-phone{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjEuNTksMTYwLjNsLTQ3LjI0LTIxLjE3YTE0LDE0LDAsMCwwLTEzLjI4LDEuMjIsNC44MSw0LjgxLDAsMCwwLS41Ni40MmwtMjQuNjksMjFhMS44OCwxLjg4LDAsMCwxLTEuNjguMDZjLTE1Ljg3LTcuNjYtMzIuMzEtMjQtNDAtMzkuNjVhMS45MSwxLjkxLDAsMCwxLDAtMS42OGwyMS4wNy0yNWE2LjEzLDYuMTMsMCwwLDAsLjQyLS41OCwxNCwxNCwwLDAsMCwxLjEyLTEzLjI3TDk1LjczLDM0LjQ5YTE0LDE0LDAsMCwwLTE0LjU2LTguMzhBNTQuMjQsNTQuMjQsMCwwLDAsMzQsODBjMCw3OC4zLDYzLjcsMTQyLDE0MiwxNDJhNTQuMjUsNTQuMjUsMCwwLDAsNTMuODktNDcuMTdBMTQsMTQsMCwwLDAsMjIxLjU5LDE2MC4zWk0xNzYsMjEwQzEwNC4zMiwyMTAsNDYsMTUxLjY4LDQ2LDgwQTQyLjIzLDQyLjIzLDAsMCwxLDgyLjY3LDM4aC4yM2EyLDIsMCwwLDEsMS44NCwxLjMxbDIxLjEsNDcuMTFhMiwyLDAsMCwxLDAsMS42N0w4NC43MywxMTMuMTVhNC43Myw0LjczLDAsMCwwLS40My41NywxNCwxNCwwLDAsMC0uOTEsMTMuNzNjOC44NywxOC4xNiwyNy4xNywzNi4zMiw0NS41Myw0NS4xOWExNCwxNCwwLDAsMCwxMy43Ny0xYy4xOS0uMTMuMzgtLjI3LjU2LS40MmwyNC42OC0yMWExLjkyLDEuOTIsMCwwLDEsMS42LS4xbDQ3LjI1LDIxLjE3YTIsMiwwLDAsMSwxLjIxLDJBNDIuMjQsNDIuMjQsMCwwLDEsMTc2LDIxMFoiLz48L3N2Zz4=');}.icon-chat{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNTBINDBBMTQsMTQsMCwwLDAsMjYsNjRWMjI0YTEzLjg4LDEzLjg4LDAsMCwwLDguMDksMTIuNjlBMTQuMTEsMTQuMTEsMCwwLDAsNDAsMjM4YTEzLjg3LDEzLjg3LDAsMCwwLDktMy4zMWwuMDYtLjA1TDgyLjIzLDIwNkgyMTZhMTQsMTQsMCwwLDAsMTQtMTRWNjRBMTQsMTQsMCwwLDAsMjE2LDUwWm0yLDE0MmEyLDIsMCwwLDEtMiwySDgwYTYsNiwwLDAsMC0zLjkyLDEuNDZMNDEuMjYsMjI1LjUzQTIsMiwwLDAsMSwzOCwyMjRWNjRhMiwyLDAsMCwxLDItMkgyMTZhMiwyLDAsMCwxLDIsMloiLz48L3N2Zz4=');}.icon-google-logo{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTk0LDk0LDAsMSwxLTIxLjQ5LTU5LjgyLDYsNiwwLDEsMS05LjI1LDcuNjRBODIsODIsMCwxLDAsMjA5Ljc4LDEzNEgxMjhhNiw2LDAsMCwxLDAtMTJoODhBNiw2LDAsMCwxLDIyMiwxMjhaIi8+PC9zdmc+');}.icon-apple-logo{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTkuNCwxNjcuODRDMjAxLjcxLDE1NS42OSwxOTgsMTM1LjEyLDE5OCwxMjBjMC0xOC40MiwxMy44Ni0zNC4yOSwyMi4xMi00Mi4xMmE2LDYsMCwwLDAsMC04LjcxQzIwOCw1Ny43LDE4Ny4wNyw1MCwxNjgsNTBhNzAuMjMsNzAuMjMsMCwwLDAtNDAsMTIuNTUsNjkuNiw2OS42LDAsMCwwLTg5LjMxLDguMDhBNzIuNjMsNzIuNjMsMCwwLDAsMTgsMTIzLjM1YTEyNS4xMSwxMjUuMTEsMCwwLDAsMzkuNTMsODguMzNBMzcuODUsMzcuODUsMCwwLDAsODMuNiwyMjJoODcuN0EzNy44MywzNy44MywwLDAsMCwxOTksMjEwLjA3YTEyMi42LDEyMi42LDAsMCwwLDE3LjU0LTI0LjJjNi41NS0xMiw1Ljc3LTEzLjc1LDUtMTUuNDhBNi4wNyw2LjA3LDAsMCwwLDIxOS40LDE2Ny44NFptLTI5LjIzLDM0QTI1LjgyLDI1LjgyLDAsMCwxLDE3MS4zLDIxMEg4My42QTI1Ljg1LDI1Ljg1LDAsMCwxLDY1Ljc4LDIwMywxMTMuMjEsMTEzLjIxLDAsMCwxLDMwLDEyM2E2MC41NSw2MC41NSwwLDAsMSwxNy4yMS00NEE1Ni44Miw1Ni44MiwwLDAsMSw4OCw2MmguODFhNTcuMzUsNTcuMzUsMCwwLDEsMzUuNDQsMTIuNzEsNiw2LDAsMCwwLDcuNSwwQTU3LjM5LDU3LjM5LDAsMCwxLDE2OCw2MmMxMy44OSwwLDI4LjgxLDQuNjgsMzkuMTEsMTItOS40NCwxMC4xNC0yMS4xLDI2LjU5LTIxLjEsNDYsMCwyMy43OCw3LjgxLDQyLjYsMjIuNjYsNTQuNzdBMTA3LjMzLDEwNy4zMywwLDAsMSwxOTAuMTcsMjAxLjg5Wm0tNjAtMTcxLjM5QTM4LDM4LDAsMCwxLDE2NywyaDFhNiw2LDAsMCwxLDAsMTJoLTFhMjYsMjYsMCwwLDAtMjUuMTgsMTkuNSw2LDYsMCwxLDEtMTEuNjItM1oiLz48L3N2Zz4=');}.icon-tiktok-logo{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsNzRhNTAuMDYsNTAuMDYsMCwwLDEtNTAtNTAsNiw2LDAsMCwwLTYtNkgxMjhhNiw2LDAsMCwwLTYsNlYxNTZhMjIsMjIsMCwxLDEtMzEuNDMtMTkuODlBNiw2LDAsMCwwLDk0LDEzMC42OVY4OGE2LDYsMCwwLDAtNy01LjkxQzUyLjIsODguMjgsMjYsMTIwLjA1LDI2LDE1NmE3NCw3NCwwLDAsMCwxNDgsMFYxMTIuOTNBMTAxLjI4LDEwMS4yOCwwLDAsMCwyMjQsMTI2YTYsNiwwLDAsMCw2LTZWODBBNiw2LDAsMCwwLDIyNCw3NFptLTYsMzkuOGE4OS4xMyw4OS4xMywwLDAsMS00Ni41LTE2LjY5QTYsNiwwLDAsMCwxNjIsMTAydjU0YTYyLDYyLDAsMCwxLTEyNCwwYzAtMjcuNzIsMTguNDctNTIuNDgsNDQtNjAuMzh2MzEuNTNBMzQsMzQsMCwxLDAsMTM0LDE1NlYzMGgyOC4yOUE2Mi4wOSw2Mi4wOSwwLDAsMCwyMTgsODUuNzFaIi8+PC9zdmc+');}.icon-link{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzgsODguMThhNTIuNDIsNTIuNDIsMCwwLDEtMTUuNCwzNS42NmwtMzQuNzUsMzQuNzVBNTIuMjgsNTIuMjgsMCwwLDEsMTUwLjYyLDE3NGgtLjA1QTUyLjYzLDUyLjYzLDAsMCwxLDk4LDExOS45YTYsNiwwLDAsMSw2LTUuODRoLjE3YTYsNiwwLDAsMSw1LjgzLDYuMTZBNDAuNjIsNDAuNjIsMCwwLDAsMTUwLjU4LDE2MmgwYTQwLjQsNDAuNCwwLDAsMCwyOC43My0xMS45bDM0Ljc1LTM0Ljc0QTQwLjYzLDQwLjYzLDAsMCwwLDE1Ni42Myw1Ny45bC0xMSwxMWE2LDYsMCwwLDEtOC40OS04LjQ5bDExLTExYTUyLjYyLDUyLjYyLDAsMCwxLDc0LjQzLDBBNTIuODMsNTIuODMsMCwwLDEsMjM4LDg4LjE4Wm0tMTI3LjYyLDk4LjktMTEsMTFBNDAuMzYsNDAuMzYsMCwwLDEsNzAuNiwyMTBoMGE0MC42Myw0MC42MywwLDAsMS0yOC43LTY5LjM2TDc2LjYyLDEwNS45QTQwLjYzLDQwLjYzLDAsMCwxLDE0NiwxMzUuNzdhNiw2LDAsMCwwLDUuODMsNi4xNkgxNTJhNiw2LDAsMCwwLDYtNS44NEE1Mi42Myw1Mi42MywwLDAsMCw2OC4xNCw5Ny40MkwzMy4zOCwxMzIuMTZBNTIuNjMsNTIuNjMsMCwwLDAsNzAuNTYsMjIyaDBhNTIuMjYsNTIuMjYsMCwwLDAsMzcuMjItMTUuNDJsMTEtMTFhNiw2LDAsMSwwLTguNDktOC40OFoiLz48L3N2Zz4=');}.icon-star-fi{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzQuMjksMTE0Ljg1bC00NSwzOC44M0wyMDMsMjExLjc1YTE2LjQsMTYuNCwwLDAsMS0yNC41LDE3LjgyTDEyOCwxOTguNDksNzcuNDcsMjI5LjU3QTE2LjQsMTYuNCwwLDAsMSw1MywyMTEuNzVsMTMuNzYtNTguMDctNDUtMzguODNBMTYuNDYsMTYuNDYsMCwwLDEsMzEuMDgsODZsNTktNC43NiwyMi43Ni01NS4wOGExNi4zNiwxNi4zNiwwLDAsMSwzMC4yNywwbDIyLjc1LDU1LjA4LDU5LDQuNzZhMTYuNDYsMTYuNDYsMCwwLDEsOS4zNywyOC44NloiLz48L3N2Zz4=');}
assets/css/icons.min.css
New file
@@ -0,0 +1 @@
.icon-align-left{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik00Niw0MFYyMTZhNiw2LDAsMCwxLTEyLDBWNDBhNiw2LDAsMCwxLDEyLDBabTIwLDY0VjY0QTE0LDE0LDAsMCwxLDgwLDUwaDk2YTE0LDE0LDAsMCwxLDE0LDE0djQwYTE0LDE0LDAsMCwxLTE0LDE0SDgwQTE0LDE0LDAsMCwxLDY2LDEwNFptMTIsMGEyLDIsMCwwLDAsMiwyaDk2YTIsMiwwLDAsMCwyLTJWNjRhMiwyLDAsMCwwLTItMkg4MGEyLDIsMCwwLDAtMiwyWm0xNTIsNDh2NDBhMTQsMTQsMCwwLDEtMTQsMTRIODBhMTQsMTQsMCwwLDEtMTQtMTRWMTUyYTE0LDE0LDAsMCwxLDE0LTE0SDIxNkExNCwxNCwwLDAsMSwyMzAsMTUyWm0tMTIsMGEyLDIsMCwwLDAtMi0ySDgwYTIsMiwwLDAsMC0yLDJ2NDBhMiwyLDAsMCwwLDIsMkgyMTZhMiwyLDAsMCwwLDItMloiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik00Niw0MFYyMTZhNiw2LDAsMCwxLTEyLDBWNDBhNiw2LDAsMCwxLDEyLDBabTIwLDY0VjY0QTE0LDE0LDAsMCwxLDgwLDUwaDk2YTE0LDE0LDAsMCwxLDE0LDE0djQwYTE0LDE0LDAsMCwxLTE0LDE0SDgwQTE0LDE0LDAsMCwxLDY2LDEwNFptMTIsMGEyLDIsMCwwLDAsMiwyaDk2YTIsMiwwLDAsMCwyLTJWNjRhMiwyLDAsMCwwLTItMkg4MGEyLDIsMCwwLDAtMiwyWm0xNTIsNDh2NDBhMTQsMTQsMCwwLDEtMTQsMTRIODBhMTQsMTQsMCwwLDEtMTQtMTRWMTUyYTE0LDE0LDAsMCwxLDE0LTE0SDIxNkExNCwxNCwwLDAsMSwyMzAsMTUyWm0tMTIsMGEyLDIsMCwwLDAtMi0ySDgwYTIsMiwwLDAsMC0yLDJ2NDBhMiwyLDAsMCwwLDIsMkgyMTZhMiwyLDAsMCwwLDItMloiLz48L3N2Zz4=)}.icon-align-right{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsNDBWMjE2YTYsNiwwLDAsMS0xMiwwVjQwYTYsNiwwLDAsMSwxMiwwWk0xOTAsNjR2NDBhMTQsMTQsMCwwLDEtMTQsMTRIODBhMTQsMTQsMCwwLDEtMTQtMTRWNjRBMTQsMTQsMCwwLDEsODAsNTBoOTZBMTQsMTQsMCwwLDEsMTkwLDY0Wm0tMTIsMGEyLDIsMCwwLDAtMi0ySDgwYTIsMiwwLDAsMC0yLDJ2NDBhMiwyLDAsMCwwLDIsMmg5NmEyLDIsMCwwLDAsMi0yWm0xMiw4OHY0MGExNCwxNCwwLDAsMS0xNCwxNEg0MGExNCwxNCwwLDAsMS0xNC0xNFYxNTJhMTQsMTQsMCwwLDEsMTQtMTRIMTc2QTE0LDE0LDAsMCwxLDE5MCwxNTJabS0xMiwwYTIsMiwwLDAsMC0yLTJINDBhMiwyLDAsMCwwLTIsMnY0MGEyLDIsMCwwLDAsMiwySDE3NmEyLDIsMCwwLDAsMi0yWiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsNDBWMjE2YTYsNiwwLDAsMS0xMiwwVjQwYTYsNiwwLDAsMSwxMiwwWk0xOTAsNjR2NDBhMTQsMTQsMCwwLDEtMTQsMTRIODBhMTQsMTQsMCwwLDEtMTQtMTRWNjRBMTQsMTQsMCwwLDEsODAsNTBoOTZBMTQsMTQsMCwwLDEsMTkwLDY0Wm0tMTIsMGEyLDIsMCwwLDAtMi0ySDgwYTIsMiwwLDAsMC0yLDJ2NDBhMiwyLDAsMCwwLDIsMmg5NmEyLDIsMCwwLDAsMi0yWm0xMiw4OHY0MGExNCwxNCwwLDAsMS0xNCwxNEg0MGExNCwxNCwwLDAsMS0xNC0xNFYxNTJhMTQsMTQsMCwwLDEsMTQtMTRIMTc2QTE0LDE0LDAsMCwxLDE5MCwxNTJabS0xMiwwYTIsMiwwLDAsMC0yLTJINDBhMiwyLDAsMCwwLTIsMnY0MGEyLDIsMCwwLDAsMiwySDE3NmEyLDIsMCwwLDAsMi0yWiIvPjwvc3ZnPg==)}.icon-bell{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjAuMDcsMTc2Ljk0QzIxNC40MSwxNjcuMiwyMDYsMTM5LjczLDIwNiwxMDRhNzgsNzgsMCwxLDAtMTU2LDBjMCwzNS43NC04LjQyLDYzLjItMTQuMDgsNzIuOTRBMTQsMTQsMCwwLDAsNDgsMTk4SDkwLjQ4YTM4LDM4LDAsMCwwLDc1LDBIMjA4YTE0LDE0LDAsMCwwLDEyLjA2LTIxLjA2Wk0xMjgsMjE4YTI2LDI2LDAsMCwxLTI1LjI5LTIwaDUwLjU4QTI2LDI2LDAsMCwxLDEyOCwyMThabTgxLjcxLTMzYTEuOSwxLjksMCwwLDEtMS43LDFINDhhMS45LDEuOSwwLDAsMS0xLjctMSwyLDIsMCwwLDEsMC0yQzUzLjg3LDE3MCw2MiwxMzkuNjksNjIsMTA0YTY2LDY2LDAsMSwxLDEzMiwwYzAsMzUuNjgsOC4xNCw2NS45NSwxNS43MSw3OUEyLDIsMCwwLDEsMjA5LjcxLDE4NVoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjAuMDcsMTc2Ljk0QzIxNC40MSwxNjcuMiwyMDYsMTM5LjczLDIwNiwxMDRhNzgsNzgsMCwxLDAtMTU2LDBjMCwzNS43NC04LjQyLDYzLjItMTQuMDgsNzIuOTRBMTQsMTQsMCwwLDAsNDgsMTk4SDkwLjQ4YTM4LDM4LDAsMCwwLDc1LDBIMjA4YTE0LDE0LDAsMCwwLDEyLjA2LTIxLjA2Wk0xMjgsMjE4YTI2LDI2LDAsMCwxLTI1LjI5LTIwaDUwLjU4QTI2LDI2LDAsMCwxLDEyOCwyMThabTgxLjcxLTMzYTEuOSwxLjksMCwwLDEtMS43LDFINDhhMS45LDEuOSwwLDAsMS0xLjctMSwyLDIsMCwwLDEsMC0yQzUzLjg3LDE3MCw2MiwxMzkuNjksNjIsMTA0YTY2LDY2LDAsMSwxLDEzMiwwYzAsMzUuNjgsOC4xNCw2NS45NSwxNS43MSw3OUEyLDIsMCwwLDEsMjA5LjcxLDE4NVoiLz48L3N2Zz4=)}.icon-bell-ringing{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjMuMDUsNjkuMzNBNiw2LDAsMCwxLDIxNSw2Ni43Niw5Niw5NiwwLDAsMCwxODAuOCwyOS4wOGE2LDYsMCwwLDEsNi40LTEwLjE1LDEwOS4yNiwxMDkuMjYsMCwwLDEsMzguNDEsNDIuMzFBNiw2LDAsMCwxLDIyMy4wNSw2OS4zM1pNNDEsNjYuNzZBOTYsOTYsMCwwLDEsNzUuMiwyOS4wOGE2LDYsMCwwLDAtNi40LTEwLjE1QTEwOS4yNiwxMDkuMjYsMCwwLDAsMzAuMzksNjEuMjQsNiw2LDAsMSwwLDQxLDY2Ljc2Wm0xNzksMTEwLjE4QTE0LDE0LDAsMCwxLDIwOCwxOThoLTQyLjVhMzgsMzgsMCwwLDEtNzUsMEg0OGExNCwxNCwwLDAsMS0xMi4wNi0yMS4wNkM0NS4xMywxNjEuMDksNTAsMTM4LjYzLDUwLDExMmE3OCw3OCwwLDAsMSwxNTYsMEMyMDYsMTM5LDIxMC43NCwxNjAuODYsMjIwLjA4LDE3Ni45NFpNMTUzLjI5LDE5OEgxMDIuNzFhMjYsMjYsMCwwLDAsNTAuNTgsMFptNTYuNDItMTVDMTk5LjI5LDE2NSwxOTQsMTQxLjE1LDE5NCwxMTJhNjYsNjYsMCwwLDAtMTMyLDBjMCwyOS4xNi01LjI5LDUzLTE1LjcxLDcxYTIsMiwwLDAsMCwwLDIsMS45LDEuOSwwLDAsMCwxLjcsMUgyMDhhMS45LDEuOSwwLDAsMCwxLjctMUEyLDIsMCwwLDAsMjA5LjcxLDE4M1oiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjMuMDUsNjkuMzNBNiw2LDAsMCwxLDIxNSw2Ni43Niw5Niw5NiwwLDAsMCwxODAuOCwyOS4wOGE2LDYsMCwwLDEsNi40LTEwLjE1LDEwOS4yNiwxMDkuMjYsMCwwLDEsMzguNDEsNDIuMzFBNiw2LDAsMCwxLDIyMy4wNSw2OS4zM1pNNDEsNjYuNzZBOTYsOTYsMCwwLDEsNzUuMiwyOS4wOGE2LDYsMCwwLDAtNi40LTEwLjE1QTEwOS4yNiwxMDkuMjYsMCwwLDAsMzAuMzksNjEuMjQsNiw2LDAsMSwwLDQxLDY2Ljc2Wm0xNzksMTEwLjE4QTE0LDE0LDAsMCwxLDIwOCwxOThoLTQyLjVhMzgsMzgsMCwwLDEtNzUsMEg0OGExNCwxNCwwLDAsMS0xMi4wNi0yMS4wNkM0NS4xMywxNjEuMDksNTAsMTM4LjYzLDUwLDExMmE3OCw3OCwwLDAsMSwxNTYsMEMyMDYsMTM5LDIxMC43NCwxNjAuODYsMjIwLjA4LDE3Ni45NFpNMTUzLjI5LDE5OEgxMDIuNzFhMjYsMjYsMCwwLDAsNTAuNTgsMFptNTYuNDItMTVDMTk5LjI5LDE2NSwxOTQsMTQxLjE1LDE5NCwxMTJhNjYsNjYsMCwwLDAtMTMyLDBjMCwyOS4xNi01LjI5LDUzLTE1LjcxLDcxYTIsMiwwLDAsMCwwLDIsMS45LDEuOSwwLDAsMCwxLjcsMUgyMDhhMS45LDEuOSwwLDAsMCwxLjctMUEyLDIsMCwwLDAsMjA5LjcxLDE4M1oiLz48L3N2Zz4=)}.icon-calendar{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsMzRIMTgyVjI0YTYsNiwwLDAsMC0xMiwwVjM0SDg2VjI0YTYsNiwwLDAsMC0xMiwwVjM0SDQ4QTE0LDE0LDAsMCwwLDM0LDQ4VjIwOGExNCwxNCwwLDAsMCwxNCwxNEgyMDhhMTQsMTQsMCwwLDAsMTQtMTRWNDhBMTQsMTQsMCwwLDAsMjA4LDM0Wk00OCw0Nkg3NFY1NmE2LDYsMCwwLDAsMTIsMFY0Nmg4NFY1NmE2LDYsMCwwLDAsMTIsMFY0NmgyNmEyLDIsMCwwLDEsMiwyVjgySDQ2VjQ4QTIsMiwwLDAsMSw0OCw0NlpNMjA4LDIxMEg0OGEyLDIsMCwwLDEtMi0yVjk0SDIxMFYyMDhBMiwyLDAsMCwxLDIwOCwyMTBabS05OC05MHY2NGE2LDYsMCwwLDEtMTIsMFYxMjkuNzFsLTcuMzIsMy42NmE2LDYsMCwxLDEtNS4zNi0xMC43NGwxNi04QTYsNiwwLDAsMSwxMTAsMTIwWm01OS41NywyOS4yNUwxNDgsMTc4aDIwYTYsNiwwLDAsMSwwLDEySDEzNmE2LDYsMCwwLDEtNC44LTkuNkwxNjAsMTQyYTEwLDEwLDAsMSwwLTE2LjY1LTExQTYsNiwwLDEsMSwxMzMsMTI1YTIyLDIyLDAsMSwxLDM2LjYyLDI0LjI2WiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsMzRIMTgyVjI0YTYsNiwwLDAsMC0xMiwwVjM0SDg2VjI0YTYsNiwwLDAsMC0xMiwwVjM0SDQ4QTE0LDE0LDAsMCwwLDM0LDQ4VjIwOGExNCwxNCwwLDAsMCwxNCwxNEgyMDhhMTQsMTQsMCwwLDAsMTQtMTRWNDhBMTQsMTQsMCwwLDAsMjA4LDM0Wk00OCw0Nkg3NFY1NmE2LDYsMCwwLDAsMTIsMFY0Nmg4NFY1NmE2LDYsMCwwLDAsMTIsMFY0NmgyNmEyLDIsMCwwLDEsMiwyVjgySDQ2VjQ4QTIsMiwwLDAsMSw0OCw0NlpNMjA4LDIxMEg0OGEyLDIsMCwwLDEtMi0yVjk0SDIxMFYyMDhBMiwyLDAsMCwxLDIwOCwyMTBabS05OC05MHY2NGE2LDYsMCwwLDEtMTIsMFYxMjkuNzFsLTcuMzIsMy42NmE2LDYsMCwxLDEtNS4zNi0xMC43NGwxNi04QTYsNiwwLDAsMSwxMTAsMTIwWm01OS41NywyOS4yNUwxNDgsMTc4aDIwYTYsNiwwLDAsMSwwLDEySDEzNmE2LDYsMCwwLDEtNC44LTkuNkwxNjAsMTQyYTEwLDEwLDAsMSwwLTE2LjY1LTExQTYsNiwwLDEsMSwxMzMsMTI1YTIyLDIyLDAsMSwxLDM2LjYyLDI0LjI2WiIvPjwvc3ZnPg==)}.icon-check{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjguMjQsNzYuMjRsLTEyOCwxMjhhNiw2LDAsMCwxLTguNDgsMGwtNTYtNTZhNiw2LDAsMCwxLDguNDgtOC40OEw5NiwxOTEuNTEsMjE5Ljc2LDY3Ljc2YTYsNiwwLDAsMSw4LjQ4LDguNDhaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjguMjQsNzYuMjRsLTEyOCwxMjhhNiw2LDAsMCwxLTguNDgsMGwtNTYtNTZhNiw2LDAsMCwxLDguNDgtOC40OEw5NiwxOTEuNTEsMjE5Ljc2LDY3Ljc2YTYsNiwwLDAsMSw4LjQ4LDguNDhaIi8+PC9zdmc+)}.icon-clock{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsMjZBMTAyLDEwMiwwLDEsMCwyMzAsMTI4LDEwMi4xMiwxMDIuMTIsMCwwLDAsMTI4LDI2Wm0wLDE5MmE5MCw5MCwwLDEsMSw5MC05MEE5MC4xLDkwLjEsMCwwLDEsMTI4LDIxOFptNjItOTBhNiw2LDAsMCwxLTYsNkgxMjhhNiw2LDAsMCwxLTYtNlY3MmE2LDYsMCwwLDEsMTIsMHY1MGg1MEE2LDYsMCwwLDEsMTkwLDEyOFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsMjZBMTAyLDEwMiwwLDEsMCwyMzAsMTI4LDEwMi4xMiwxMDIuMTIsMCwwLDAsMTI4LDI2Wm0wLDE5MmE5MCw5MCwwLDEsMSw5MC05MEE5MC4xLDkwLjEsMCwwLDEsMTI4LDIxOFptNjItOTBhNiw2LDAsMCwxLTYsNkgxMjhhNiw2LDAsMCwxLTYtNlY3MmE2LDYsMCwwLDEsMTIsMHY1MGg1MEE2LDYsMCwwLDEsMTkwLDEyOFoiLz48L3N2Zz4=)}.icon-cloud{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjAsNDJBODYuMTEsODYuMTEsMCwwLDAsODIuNDMsOTAuODgsNjIsNjIsMCwxLDAsNzIsMjE0aDg4YTg2LDg2LDAsMCwwLDAtMTcyWm0wLDE2MEg3MmE1MCw1MCwwLDAsMSwwLTEwMCw1MC42Nyw1MC42NywwLDAsMSw1LjkxLjM1QTg1LjYxLDg1LjYxLDAsMCwwLDc0LDEyOGE2LDYsMCwwLDAsMTIsMCw3NCw3NCwwLDEsMSw3NCw3NFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjAsNDJBODYuMTEsODYuMTEsMCwwLDAsODIuNDMsOTAuODgsNjIsNjIsMCwxLDAsNzIsMjE0aDg4YTg2LDg2LDAsMCwwLDAtMTcyWm0wLDE2MEg3MmE1MCw1MCwwLDAsMSwwLTEwMCw1MC42Nyw1MC42NywwLDAsMSw1LjkxLjM1QTg1LjYxLDg1LjYxLDAsMCwwLDc0LDEyOGE2LDYsMCwwLDAsMTIsMCw3NCw3NCwwLDEsMSw3NCw3NFoiLz48L3N2Zz4=)}.icon-copy{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsMzRIODhhNiw2LDAsMCwwLTYsNlY4Mkg0MGE2LDYsMCwwLDAtNiw2VjIxNmE2LDYsMCwwLDAsNiw2SDE2OGE2LDYsMCwwLDAsNi02VjE3NGg0MmE2LDYsMCwwLDAsNi02VjQwQTYsNiwwLDAsMCwyMTYsMzRaTTE2MiwyMTBINDZWOTRIMTYyWm00OC00OEgxNzRWODhhNiw2LDAsMCwwLTYtNkg5NFY0NkgyMTBaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsMzRIODhhNiw2LDAsMCwwLTYsNlY4Mkg0MGE2LDYsMCwwLDAtNiw2VjIxNmE2LDYsMCwwLDAsNiw2SDE2OGE2LDYsMCwwLDAsNi02VjE3NGg0MmE2LDYsMCwwLDAsNi02VjQwQTYsNiwwLDAsMCwyMTYsMzRaTTE2MiwyMTBINDZWOTRIMTYyWm00OC00OEgxNzRWODhhNiw2LDAsMCwwLTYtNkg5NFY0NkgyMTBaIi8+PC9zdmc+)}.icon-download{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzgsMTM2djY0YTE0LDE0LDAsMCwxLTE0LDE0SDMyYTE0LDE0LDAsMCwxLTE0LTE0VjEzNmExNCwxNCwwLDAsMSwxNC0xNEg3MmE2LDYsMCwwLDEsMCwxMkgzMmEyLDIsMCwwLDAtMiwydjY0YTIsMiwwLDAsMCwyLDJIMjI0YTIsMiwwLDAsMCwyLTJWMTM2YTIsMiwwLDAsMC0yLTJIMTg0YTYsNiwwLDAsMSwwLTEyaDQwQTE0LDE0LDAsMCwxLDIzOCwxMzZabS0xMTQuMjQtMy43NmE2LDYsMCwwLDAsOC40OCwwbDQ4LTQ4YTYsNiwwLDAsMC04LjQ4LTguNDhMMTM0LDExMy41MVYyNGE2LDYsMCwwLDAtMTIsMHY4OS41MUw4NC4yNCw3NS43NmE2LDYsMCwwLDAtOC40OCw4LjQ4Wk0xOTgsMTY4YTEwLDEwLDAsMSwwLTEwLDEwQTEwLDEwLDAsMCwwLDE5OCwxNjhaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzgsMTM2djY0YTE0LDE0LDAsMCwxLTE0LDE0SDMyYTE0LDE0LDAsMCwxLTE0LTE0VjEzNmExNCwxNCwwLDAsMSwxNC0xNEg3MmE2LDYsMCwwLDEsMCwxMkgzMmEyLDIsMCwwLDAtMiwydjY0YTIsMiwwLDAsMCwyLDJIMjI0YTIsMiwwLDAsMCwyLTJWMTM2YTIsMiwwLDAsMC0yLTJIMTg0YTYsNiwwLDAsMSwwLTEyaDQwQTE0LDE0LDAsMCwxLDIzOCwxMzZabS0xMTQuMjQtMy43NmE2LDYsMCwwLDAsOC40OCwwbDQ4LTQ4YTYsNiwwLDAsMC04LjQ4LTguNDhMMTM0LDExMy41MVYyNGE2LDYsMCwwLDAtMTIsMHY4OS41MUw4NC4yNCw3NS43NmE2LDYsMCwwLDAtOC40OCw4LjQ4Wk0xOTgsMTY4YTEwLDEwLDAsMSwwLTEwLDEwQTEwLDEwLDAsMCwwLDE5OCwxNjhaIi8+PC9zdmc+)}.icon-exclamation-mark{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNDIsMjAwYTE0LDE0LDAsMSwxLTE0LTE0QTE0LDE0LDAsMCwxLDE0MiwyMDBabS0xNC00MmE2LDYsMCwwLDAsNi02VjQ4YTYsNiwwLDAsMC0xMiwwVjE1MkE2LDYsMCwwLDAsMTI4LDE1OFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNDIsMjAwYTE0LDE0LDAsMSwxLTE0LTE0QTE0LDE0LDAsMCwxLDE0MiwyMDBabS0xNC00MmE2LDYsMCwwLDAsNi02VjQ4YTYsNiwwLDAsMC0xMiwwVjE1MkE2LDYsMCwwLDAsMTI4LDE1OFoiLz48L3N2Zz4=)}.icon-hamburger{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik00OC4wNywxMDJIMjA3LjkzYTE0LDE0LDAsMCwwLDEzLjc2LTE3QzIxNC40NSw1MC44MywxNzUuMDUsMjYsMTI4LDI2UzQxLjU1LDUwLjgzLDM0LjMxLDg1YTE0LDE0LDAsMCwwLDEzLjc2LDE3Wm0tMi0xNC40OEM1Miw1OS4yOSw4Ny4yNSwzOCwxMjgsMzhTMjA0LDU5LjI5LDIxMCw4Ny41MmEyLDIsMCwwLDEtMiwyLjQ4SDQ4LjA3YTIsMiwwLDAsMS0yLTIuNDhaTTIzMCwxNTQuMzYsMTg4LjEsMTY5LjU4bC0zNy44Ny0xNS4xNWE2LDYsMCwwLDAtNC40NiwwTDEwOCwxNjkuNTQsNzAuMjMsMTU0LjQzYTYsNiwwLDAsMC00LjI4LS4wN2wtNDQsMTZBNiw2LDAsMCwwLDI0LDE4MmE2LjExLDYuMTEsMCwwLDAsMi4wNS0uMzZsMTYtNS44VjE4NGEzOCwzOCwwLDAsMCwzOCwzOGg5NmEzOCwzOCwwLDAsMCwzOC0zOFYxNzIuOTNsMjAuMDUtNy4yOWE2LDYsMCwwLDAtNC4xLTExLjI4Wk0yMDIsMTg0YTI2LDI2LDAsMCwxLTI2LDI2SDgwYTI2LDI2LDAsMCwxLTI2LTI2VjE3MS40OGwxMy45LTUuMDYsMzcuODcsMTUuMTVhNiw2LDAsMCwwLDQuNDYsMEwxNDgsMTY2LjQ2bDM3Ljc3LDE1LjExYTYsNiwwLDAsMCw0LjI4LjA3TDIwMiwxNzcuMjlaTTE4LDEyOGE2LDYsMCwwLDEsNi02SDIzMmE2LDYsMCwwLDEsMCwxMkgyNEE2LDYsMCwwLDEsMTgsMTI4WiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik00OC4wNywxMDJIMjA3LjkzYTE0LDE0LDAsMCwwLDEzLjc2LTE3QzIxNC40NSw1MC44MywxNzUuMDUsMjYsMTI4LDI2UzQxLjU1LDUwLjgzLDM0LjMxLDg1YTE0LDE0LDAsMCwwLDEzLjc2LDE3Wm0tMi0xNC40OEM1Miw1OS4yOSw4Ny4yNSwzOCwxMjgsMzhTMjA0LDU5LjI5LDIxMCw4Ny41MmEyLDIsMCwwLDEtMiwyLjQ4SDQ4LjA3YTIsMiwwLDAsMS0yLTIuNDhaTTIzMCwxNTQuMzYsMTg4LjEsMTY5LjU4bC0zNy44Ny0xNS4xNWE2LDYsMCwwLDAtNC40NiwwTDEwOCwxNjkuNTQsNzAuMjMsMTU0LjQzYTYsNiwwLDAsMC00LjI4LS4wN2wtNDQsMTZBNiw2LDAsMCwwLDI0LDE4MmE2LjExLDYuMTEsMCwwLDAsMi4wNS0uMzZsMTYtNS44VjE4NGEzOCwzOCwwLDAsMCwzOCwzOGg5NmEzOCwzOCwwLDAsMCwzOC0zOFYxNzIuOTNsMjAuMDUtNy4yOWE2LDYsMCwwLDAtNC4xLTExLjI4Wk0yMDIsMTg0YTI2LDI2LDAsMCwxLTI2LDI2SDgwYTI2LDI2LDAsMCwxLTI2LTI2VjE3MS40OGwxMy45LTUuMDYsMzcuODcsMTUuMTVhNiw2LDAsMCwwLDQuNDYsMEwxNDgsMTY2LjQ2bDM3Ljc3LDE1LjExYTYsNiwwLDAsMCw0LjI4LjA3TDIwMiwxNzcuMjlaTTE4LDEyOGE2LDYsMCwwLDEsNi02SDIzMmE2LDYsMCwwLDEsMCwxMkgyNEE2LDYsMCwwLDEsMTgsMTI4WiIvPjwvc3ZnPg==)}.icon-heart{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNzgsNDJjLTIxLDAtMzkuMjYsOS40Ny01MCwyNS4zNEMxMTcuMjYsNTEuNDcsOTksNDIsNzgsNDJhNjAuMDcsNjAuMDcsMCwwLDAtNjAsNjBjMCwyOS4yLDE4LjIsNTkuNTksNTQuMSw5MC4zMWEzMzQuNjgsMzM0LjY4LDAsMCwwLDUzLjA2LDM3LDYsNiwwLDAsMCw1LjY4LDAsMzM0LjY4LDMzNC42OCwwLDAsMCw1My4wNi0zN0MyMTkuOCwxNjEuNTksMjM4LDEzMS4yLDIzOCwxMDJBNjAuMDcsNjAuMDcsMCwwLDAsMTc4LDQyWk0xMjgsMjE3LjExQzExMS41OSwyMDcuNjQsMzAsMTU3LjcyLDMwLDEwMkE0OC4wNSw0OC4wNSwwLDAsMSw3OCw1NGMyMC4yOCwwLDM3LjMxLDEwLjgzLDQ0LjQ1LDI4LjI3YTYsNiwwLDAsMCwxMS4xLDBDMTQwLjY5LDY0LjgzLDE1Ny43Miw1NCwxNzgsNTRhNDguMDUsNDguMDUsMCwwLDEsNDgsNDhDMjI2LDE1Ny43MiwxNDQuNDEsMjA3LjY0LDEyOCwyMTcuMTFaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNzgsNDJjLTIxLDAtMzkuMjYsOS40Ny01MCwyNS4zNEMxMTcuMjYsNTEuNDcsOTksNDIsNzgsNDJhNjAuMDcsNjAuMDcsMCwwLDAtNjAsNjBjMCwyOS4yLDE4LjIsNTkuNTksNTQuMSw5MC4zMWEzMzQuNjgsMzM0LjY4LDAsMCwwLDUzLjA2LDM3LDYsNiwwLDAsMCw1LjY4LDAsMzM0LjY4LDMzNC42OCwwLDAsMCw1My4wNi0zN0MyMTkuOCwxNjEuNTksMjM4LDEzMS4yLDIzOCwxMDJBNjAuMDcsNjAuMDcsMCwwLDAsMTc4LDQyWk0xMjgsMjE3LjExQzExMS41OSwyMDcuNjQsMzAsMTU3LjcyLDMwLDEwMkE0OC4wNSw0OC4wNSwwLDAsMSw3OCw1NGMyMC4yOCwwLDM3LjMxLDEwLjgzLDQ0LjQ1LDI4LjI3YTYsNiwwLDAsMCwxMS4xLDBDMTQwLjY5LDY0LjgzLDE1Ny43Miw1NCwxNzgsNTRhNDguMDUsNDguMDUsMCwwLDEsNDgsNDhDMjI2LDE1Ny43MiwxNDQuNDEsMjA3LjY0LDEyOCwyMTcuMTFaIi8+PC9zdmc+)}.icon-image{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNDJINDBBMTQsMTQsMCwwLDAsMjYsNTZWMjAwYTE0LDE0LDAsMCwwLDE0LDE0SDIxNmExNCwxNCwwLDAsMCwxNC0xNFY1NkExNCwxNCwwLDAsMCwyMTYsNDJaTTQwLDU0SDIxNmEyLDIsMCwwLDEsMiwyVjE2My41N0wxODguNTMsMTM0LjFhMTQsMTQsMCwwLDAtMTkuOCwwbC0yMS40MiwyMS40MkwxMDEuOSwxMTAuMWExNCwxNCwwLDAsMC0xOS44LDBMMzgsMTU0LjJWNTZBMiwyLDAsMCwxLDQwLDU0Wk0zOCwyMDBWMTcxLjE3bDUyLjU4LTUyLjU4YTIsMiwwLDAsMSwyLjg0LDBMMTc2LjgzLDIwMkg0MEEyLDIsMCwwLDEsMzgsMjAwWm0xNzgsMkgxOTMuOGwtMzgtMzgsMjEuNDEtMjEuNDJhMiwyLDAsMCwxLDIuODMsMGwzOCwzOFYyMDBBMiwyLDAsMCwxLDIxNiwyMDJaTTE0NiwxMDBhMTAsMTAsMCwxLDEsMTAsMTBBMTAsMTAsMCwwLDEsMTQ2LDEwMFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNDJINDBBMTQsMTQsMCwwLDAsMjYsNTZWMjAwYTE0LDE0LDAsMCwwLDE0LDE0SDIxNmExNCwxNCwwLDAsMCwxNC0xNFY1NkExNCwxNCwwLDAsMCwyMTYsNDJaTTQwLDU0SDIxNmEyLDIsMCwwLDEsMiwyVjE2My41N0wxODguNTMsMTM0LjFhMTQsMTQsMCwwLDAtMTkuOCwwbC0yMS40MiwyMS40MkwxMDEuOSwxMTAuMWExNCwxNCwwLDAsMC0xOS44LDBMMzgsMTU0LjJWNTZBMiwyLDAsMCwxLDQwLDU0Wk0zOCwyMDBWMTcxLjE3bDUyLjU4LTUyLjU4YTIsMiwwLDAsMSwyLjg0LDBMMTc2LjgzLDIwMkg0MEEyLDIsMCwwLDEsMzgsMjAwWm0xNzgsMkgxOTMuOGwtMzgtMzgsMjEuNDEtMjEuNDJhMiwyLDAsMCwxLDIuODMsMGwzOCwzOFYyMDBBMiwyLDAsMCwxLDIxNiwyMDJaTTE0NiwxMDBhMTAsMTAsMCwxLDEsMTAsMTBBMTAsMTAsMCwwLDEsMTQ2LDEwMFoiLz48L3N2Zz4=)}.icon-link{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzgsODguMThhNTIuNDIsNTIuNDIsMCwwLDEtMTUuNCwzNS42NmwtMzQuNzUsMzQuNzVBNTIuMjgsNTIuMjgsMCwwLDEsMTUwLjYyLDE3NGgtLjA1QTUyLjYzLDUyLjYzLDAsMCwxLDk4LDExOS45YTYsNiwwLDAsMSw2LTUuODRoLjE3YTYsNiwwLDAsMSw1LjgzLDYuMTZBNDAuNjIsNDAuNjIsMCwwLDAsMTUwLjU4LDE2MmgwYTQwLjQsNDAuNCwwLDAsMCwyOC43My0xMS45bDM0Ljc1LTM0Ljc0QTQwLjYzLDQwLjYzLDAsMCwwLDE1Ni42Myw1Ny45bC0xMSwxMWE2LDYsMCwwLDEtOC40OS04LjQ5bDExLTExYTUyLjYyLDUyLjYyLDAsMCwxLDc0LjQzLDBBNTIuODMsNTIuODMsMCwwLDEsMjM4LDg4LjE4Wm0tMTI3LjYyLDk4LjktMTEsMTFBNDAuMzYsNDAuMzYsMCwwLDEsNzAuNiwyMTBoMGE0MC42Myw0MC42MywwLDAsMS0yOC43LTY5LjM2TDc2LjYyLDEwNS45QTQwLjYzLDQwLjYzLDAsMCwxLDE0NiwxMzUuNzdhNiw2LDAsMCwwLDUuODMsNi4xNkgxNTJhNiw2LDAsMCwwLDYtNS44NEE1Mi42Myw1Mi42MywwLDAsMCw2OC4xNCw5Ny40MkwzMy4zOCwxMzIuMTZBNTIuNjMsNTIuNjMsMCwwLDAsNzAuNTYsMjIyaDBhNTIuMjYsNTIuMjYsMCwwLDAsMzcuMjItMTUuNDJsMTEtMTFhNiw2LDAsMSwwLTguNDktOC40OFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzgsODguMThhNTIuNDIsNTIuNDIsMCwwLDEtMTUuNCwzNS42NmwtMzQuNzUsMzQuNzVBNTIuMjgsNTIuMjgsMCwwLDEsMTUwLjYyLDE3NGgtLjA1QTUyLjYzLDUyLjYzLDAsMCwxLDk4LDExOS45YTYsNiwwLDAsMSw2LTUuODRoLjE3YTYsNiwwLDAsMSw1LjgzLDYuMTZBNDAuNjIsNDAuNjIsMCwwLDAsMTUwLjU4LDE2MmgwYTQwLjQsNDAuNCwwLDAsMCwyOC43My0xMS45bDM0Ljc1LTM0Ljc0QTQwLjYzLDQwLjYzLDAsMCwwLDE1Ni42Myw1Ny45bC0xMSwxMWE2LDYsMCwwLDEtOC40OS04LjQ5bDExLTExYTUyLjYyLDUyLjYyLDAsMCwxLDc0LjQzLDBBNTIuODMsNTIuODMsMCwwLDEsMjM4LDg4LjE4Wm0tMTI3LjYyLDk4LjktMTEsMTFBNDAuMzYsNDAuMzYsMCwwLDEsNzAuNiwyMTBoMGE0MC42Myw0MC42MywwLDAsMS0yOC43LTY5LjM2TDc2LjYyLDEwNS45QTQwLjYzLDQwLjYzLDAsMCwxLDE0NiwxMzUuNzdhNiw2LDAsMCwwLDUuODMsNi4xNkgxNTJhNiw2LDAsMCwwLDYtNS44NEE1Mi42Myw1Mi42MywwLDAsMCw2OC4xNCw5Ny40MkwzMy4zOCwxMzIuMTZBNTIuNjMsNTIuNjMsMCwwLDAsNzAuNTYsMjIyaDBhNTIuMjYsNTIuMjYsMCwwLDAsMzcuMjItMTUuNDJsMTEtMTFhNiw2LDAsMSwwLTguNDktOC40OFoiLz48L3N2Zz4=)}.icon-list{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTYsNiwwLDAsMS02LDZINDBhNiw2LDAsMCwxLDAtMTJIMjE2QTYsNiwwLDAsMSwyMjIsMTI4Wk00MCw3MEgyMTZhNiw2LDAsMCwwLDAtMTJINDBhNiw2LDAsMCwwLDAsMTJaTTIxNiwxODZINDBhNiw2LDAsMCwwLDAsMTJIMjE2YTYsNiwwLDAsMCwwLTEyWiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTYsNiwwLDAsMS02LDZINDBhNiw2LDAsMCwxLDAtMTJIMjE2QTYsNiwwLDAsMSwyMjIsMTI4Wk00MCw3MEgyMTZhNiw2LDAsMCwwLDAtMTJINDBhNiw2LDAsMCwwLDAsMTJaTTIxNiwxODZINDBhNiw2LDAsMCwwLDAsMTJIMjE2YTYsNiwwLDAsMCwwLTEyWiIvPjwvc3ZnPg==)}.icon-list-bullets{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik04Miw2NGE2LDYsMCwwLDEsNi02SDIxNmE2LDYsMCwwLDEsMCwxMkg4OEE2LDYsMCwwLDEsODIsNjRabTEzNCw1OEg4OGE2LDYsMCwwLDAsMCwxMkgyMTZhNiw2LDAsMCwwLDAtMTJabTAsNjRIODhhNiw2LDAsMCwwLDAsMTJIMjE2YTYsNiwwLDAsMCwwLTEyWk00NCw1NEExMCwxMCwwLDEsMCw1NCw2NCwxMCwxMCwwLDAsMCw0NCw1NFptMCwxMjhhMTAsMTAsMCwxLDAsMTAsMTBBMTAsMTAsMCwwLDAsNDQsMTgyWm0wLTY0YTEwLDEwLDAsMSwwLDEwLDEwQTEwLDEwLDAsMCwwLDQ0LDExOFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik04Miw2NGE2LDYsMCwwLDEsNi02SDIxNmE2LDYsMCwwLDEsMCwxMkg4OEE2LDYsMCwwLDEsODIsNjRabTEzNCw1OEg4OGE2LDYsMCwwLDAsMCwxMkgyMTZhNiw2LDAsMCwwLDAtMTJabTAsNjRIODhhNiw2LDAsMCwwLDAsMTJIMjE2YTYsNiwwLDAsMCwwLTEyWk00NCw1NEExMCwxMCwwLDEsMCw1NCw2NCwxMCwxMCwwLDAsMCw0NCw1NFptMCwxMjhhMTAsMTAsMCwxLDAsMTAsMTBBMTAsMTAsMCwwLDAsNDQsMTgyWm0wLTY0YTEwLDEwLDAsMSwwLDEwLDEwQTEwLDEwLDAsMCwwLDQ0LDExOFoiLz48L3N2Zz4=)}.icon-list-heart{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0zNCw2NGE2LDYsMCwwLDEsNi02SDIxNmE2LDYsMCwwLDEsMCwxMkg0MEE2LDYsMCwwLDEsMzQsNjRabTYsNzBoNjRhNiw2LDAsMCwwLDAtMTJINDBhNiw2LDAsMCwwLDAsMTJabTgwLDUySDQwYTYsNiwwLDAsMCwwLDEyaDgwYTYsNiwwLDAsMCwwLTEyWm0xMjYtNDJjMCwzNS4zLTQ5LjIyLDYwLjMyLTUxLjMyLDYxLjM3YTYsNiwwLDAsMS01LjM2LDBDMTg3LjIyLDIwNC4zMiwxMzgsMTc5LjMsMTM4LDE0NGEzMCwzMCwwLDAsMSw1NC0xOCwzMCwzMCwwLDAsMSw1NCwxOFptLTEyLDBhMTgsMTgsMCwwLDAtMzYsMCw2LDYsMCwwLDEtMTIsMCwxOCwxOCwwLDAsMC0zNiwwYzAsMTQuMTUsMTEuNzgsMjcsMjEuNjcsMzUuMjVBMTM4LjgyLDEzOC44MiwwLDAsMCwxOTIsMTkzLjE5YTEzOCwxMzgsMCwwLDAsMjAuMzMtMTMuOTRDMjIyLjIyLDE3MSwyMzQsMTU4LjE1LDIzNCwxNDRaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0zNCw2NGE2LDYsMCwwLDEsNi02SDIxNmE2LDYsMCwwLDEsMCwxMkg0MEE2LDYsMCwwLDEsMzQsNjRabTYsNzBoNjRhNiw2LDAsMCwwLDAtMTJINDBhNiw2LDAsMCwwLDAsMTJabTgwLDUySDQwYTYsNiwwLDAsMCwwLDEyaDgwYTYsNiwwLDAsMCwwLTEyWm0xMjYtNDJjMCwzNS4zLTQ5LjIyLDYwLjMyLTUxLjMyLDYxLjM3YTYsNiwwLDAsMS01LjM2LDBDMTg3LjIyLDIwNC4zMiwxMzgsMTc5LjMsMTM4LDE0NGEzMCwzMCwwLDAsMSw1NC0xOCwzMCwzMCwwLDAsMSw1NCwxOFptLTEyLDBhMTgsMTgsMCwwLDAtMzYsMCw2LDYsMCwwLDEtMTIsMCwxOCwxOCwwLDAsMC0zNiwwYzAsMTQuMTUsMTEuNzgsMjcsMjEuNjcsMzUuMjVBMTM4LjgyLDEzOC44MiwwLDAsMCwxOTIsMTkzLjE5YTEzOCwxMzgsMCwwLDAsMjAuMzMtMTMuOTRDMjIyLjIyLDE3MSwyMzQsMTU4LjE1LDIzNCwxNDRaIi8+PC9zdmc+)}.icon-list-numbers{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTYsNiwwLDAsMS02LDZIMTA0YTYsNiwwLDAsMSwwLTEySDIxNkE2LDYsMCwwLDEsMjIyLDEyOFpNMTA0LDcwSDIxNmE2LDYsMCwwLDAsMC0xMkgxMDRhNiw2LDAsMCwwLDAsMTJaTTIxNiwxODZIMTA0YTYsNiwwLDAsMCwwLDEySDIxNmE2LDYsMCwwLDAsMC0xMlpNNDIuNjgsNTMuMzcsNTAsNDkuNzFWMTA0YTYsNiwwLDAsMCwxMiwwVjQwYTYsNiwwLDAsMC04LjY4LTUuMzdsLTE2LDhhNiw2LDAsMCwwLDUuMzYsMTAuNzRaTTcyLDIwMkg1MmwyMS40OC0yOC43NEEyMS41LDIxLjUsMCwwLDAsNzcuNzksMTU3LDIxLjc1LDIxLjc1LDAsMCwwLDY5LDE0Mi4zOGEyMi44NiwyMi44NiwwLDAsMC0zMS4zNSw0LjMxLDIyLjE4LDIyLjE4LDAsMCwwLTMuMjgsNS45Miw2LDYsMCwwLDAsMTEuMjgsNC4xMSw5Ljg3LDkuODcsMCwwLDEsMS40OC0yLjY3LDEwLjc4LDEwLjc4LDAsMCwxLDE0Ljc4LTIsOS44OSw5Ljg5LDAsMCwxLDQsNi42MSw5LjY0LDkuNjQsMCwwLDEtMiw3LjI4bC0uMDYuMDlMMzUuMiwyMDQuNDFBNiw2LDAsMCwwLDQwLDIxNEg3MmE2LDYsMCwwLDAsMC0xMloiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTYsNiwwLDAsMS02LDZIMTA0YTYsNiwwLDAsMSwwLTEySDIxNkE2LDYsMCwwLDEsMjIyLDEyOFpNMTA0LDcwSDIxNmE2LDYsMCwwLDAsMC0xMkgxMDRhNiw2LDAsMCwwLDAsMTJaTTIxNiwxODZIMTA0YTYsNiwwLDAsMCwwLDEySDIxNmE2LDYsMCwwLDAsMC0xMlpNNDIuNjgsNTMuMzcsNTAsNDkuNzFWMTA0YTYsNiwwLDAsMCwxMiwwVjQwYTYsNiwwLDAsMC04LjY4LTUuMzdsLTE2LDhhNiw2LDAsMCwwLDUuMzYsMTAuNzRaTTcyLDIwMkg1MmwyMS40OC0yOC43NEEyMS41LDIxLjUsMCwwLDAsNzcuNzksMTU3LDIxLjc1LDIxLjc1LDAsMCwwLDY5LDE0Mi4zOGEyMi44NiwyMi44NiwwLDAsMC0zMS4zNSw0LjMxLDIyLjE4LDIyLjE4LDAsMCwwLTMuMjgsNS45Miw2LDYsMCwwLDAsMTEuMjgsNC4xMSw5Ljg3LDkuODcsMCwwLDEsMS40OC0yLjY3LDEwLjc4LDEwLjc4LDAsMCwxLDE0Ljc4LTIsOS44OSw5Ljg5LDAsMCwxLDQsNi42MSw5LjY0LDkuNjQsMCwwLDEtMiw3LjI4bC0uMDYuMDlMMzUuMiwyMDQuNDFBNiw2LDAsMCwwLDQwLDIxNEg3MmE2LDYsMCwwLDAsMC0xMloiLz48L3N2Zz4=)}.icon-minus{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTYsNiwwLDAsMS02LDZINDBhNiw2LDAsMCwxLDAtMTJIMjE2QTYsNiwwLDAsMSwyMjIsMTI4WiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTYsNiwwLDAsMS02LDZINDBhNiw2LDAsMCwxLDAtMTJIMjE2QTYsNiwwLDAsMSwyMjIsMTI4WiIvPjwvc3ZnPg==)}.icon-note{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik05MCw5NmE2LDYsMCwwLDEsNi02aDY0YTYsNiwwLDAsMSwwLDEySDk2QTYsNiwwLDAsMSw5MCw5NlptNiwzOGg2NGE2LDYsMCwwLDAsMC0xMkg5NmE2LDYsMCwwLDAsMCwxMlptMzIsMjBIOTZhNiw2LDAsMCwwLDAsMTJoMzJhNiw2LDAsMCwwLDAtMTJaTTIyMiw0OFYxNTYuNjlhMTMuOTQsMTMuOTQsMCwwLDEtNC4xLDkuOUwxNjYuNTksMjE3LjlhMTMuOTQsMTMuOTQsMCwwLDEtOS45LDQuMUg0OGExNCwxNCwwLDAsMS0xNC0xNFY0OEExNCwxNCwwLDAsMSw0OCwzNEgyMDhBMTQsMTQsMCwwLDEsMjIyLDQ4Wk00OCwyMTBIMTU0VjE2MGE2LDYsMCwwLDEsNi02aDUwVjQ4YTIsMiwwLDAsMC0yLTJINDhhMiwyLDAsMCwwLTIsMlYyMDhBMiwyLDAsMCwwLDQ4LDIxMFptMTUzLjUyLTQ0SDE2NnYzNS41MloiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik05MCw5NmE2LDYsMCwwLDEsNi02aDY0YTYsNiwwLDAsMSwwLDEySDk2QTYsNiwwLDAsMSw5MCw5NlptNiwzOGg2NGE2LDYsMCwwLDAsMC0xMkg5NmE2LDYsMCwwLDAsMCwxMlptMzIsMjBIOTZhNiw2LDAsMCwwLDAsMTJoMzJhNiw2LDAsMCwwLDAtMTJaTTIyMiw0OFYxNTYuNjlhMTMuOTQsMTMuOTQsMCwwLDEtNC4xLDkuOUwxNjYuNTksMjE3LjlhMTMuOTQsMTMuOTQsMCwwLDEtOS45LDQuMUg0OGExNCwxNCwwLDAsMS0xNC0xNFY0OEExNCwxNCwwLDAsMSw0OCwzNEgyMDhBMTQsMTQsMCwwLDEsMjIyLDQ4Wk00OCwyMTBIMTU0VjE2MGE2LDYsMCwwLDEsNi02aDUwVjQ4YTIsMiwwLDAsMC0yLTJINDhhMiwyLDAsMCwwLTIsMlYyMDhBMiwyLDAsMCwwLDQ4LDIxMFptMTUzLjUyLTQ0SDE2NnYzNS41MloiLz48L3N2Zz4=)}.icon-paragraph{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsNDJIOTZhNjIsNjIsMCwwLDAsMCwxMjRoNDJ2NDJhNiw2LDAsMCwwLDEyLDBWNTRoMjhWMjA4YTYsNiwwLDAsMCwxMiwwVjU0aDE4YTYsNiwwLDAsMCwwLTEyWk0xMzgsMTU0SDk2QTUwLDUwLDAsMCwxLDk2LDU0aDQyWiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsNDJIOTZhNjIsNjIsMCwwLDAsMCwxMjRoNDJ2NDJhNiw2LDAsMCwwLDEyLDBWNTRoMjhWMjA4YTYsNiwwLDAsMCwxMiwwVjU0aDE4YTYsNiwwLDAsMCwwLTEyWk0xMzgsMTU0SDk2QTUwLDUwLDAsMCwxLDk2LDU0aDQyWiIvPjwvc3ZnPg==)}.icon-password{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik00Niw1NlYyMDBhNiw2LDAsMCwxLTEyLDBWNTZhNiw2LDAsMCwxLDEyLDBabTk0LjU4LDU2LjQxTDExOCwxMTkuNzRWOTZhNiw2LDAsMCwwLTEyLDB2MjMuNzRsLTIyLjU4LTcuMzNhNiw2LDAsMSwwLTMuNzEsMTEuNDFsMjIuNTgsNy4zMy0xNCwxOS4yMWE2LDYsMCwxLDAsOS43LDcuMDZsMTQtMTkuMjEsMTQsMTkuMjFhNiw2LDAsMCwwLDkuNy03LjA2bC0xNC0xOS4yMSwyMi41OC03LjMzYTYsNiwwLDEsMC0zLjcxLTExLjQxWm0xMDMuNTYsMy44NWE2LDYsMCwwLDAtNy41Ni0zLjg1TDIxNCwxMTkuNzRWOTZhNiw2LDAsMCwwLTEyLDB2MjMuNzRsLTIyLjU4LTcuMzNhNiw2LDAsMSwwLTMuNzEsMTEuNDFsMjIuNTgsNy4zMy0xMy45NSwxOS4yMWE2LDYsMCwxLDAsOS43LDcuMDZsMTQtMTkuMjEsMTQsMTkuMjFhNiw2LDAsMCwwLDkuNy03LjA2bC0xMy45NS0xOS4yMSwyMi41OC03LjMzQTYsNiwwLDAsMCwyNDQuMTQsMTE2LjI2WiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik00Niw1NlYyMDBhNiw2LDAsMCwxLTEyLDBWNTZhNiw2LDAsMCwxLDEyLDBabTk0LjU4LDU2LjQxTDExOCwxMTkuNzRWOTZhNiw2LDAsMCwwLTEyLDB2MjMuNzRsLTIyLjU4LTcuMzNhNiw2LDAsMSwwLTMuNzEsMTEuNDFsMjIuNTgsNy4zMy0xNCwxOS4yMWE2LDYsMCwxLDAsOS43LDcuMDZsMTQtMTkuMjEsMTQsMTkuMjFhNiw2LDAsMCwwLDkuNy03LjA2bC0xNC0xOS4yMSwyMi41OC03LjMzYTYsNiwwLDEsMC0zLjcxLTExLjQxWm0xMDMuNTYsMy44NWE2LDYsMCwwLDAtNy41Ni0zLjg1TDIxNCwxMTkuNzRWOTZhNiw2LDAsMCwwLTEyLDB2MjMuNzRsLTIyLjU4LTcuMzNhNiw2LDAsMSwwLTMuNzEsMTEuNDFsMjIuNTgsNy4zMy0xMy45NSwxOS4yMWE2LDYsMCwxLDAsOS43LDcuMDZsMTQtMTkuMjEsMTQsMTkuMjFhNiw2LDAsMCwwLDkuNy03LjA2bC0xMy45NS0xOS4yMSwyMi41OC03LjMzQTYsNiwwLDAsMCwyNDQuMTQsMTE2LjI2WiIvPjwvc3ZnPg==)}.icon-share{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjguMjQsMTA4LjI0bC00OCw0OGE2LDYsMCwwLDEtOC40OC04LjQ4TDIwOS41MSwxMTBIMTY1YTg5Ljk0LDg5Ljk0LDAsMCwwLTg3LjE3LDY3LjUsNiw2LDAsMCwxLTExLjYyLTNBMTAxLjk0LDEwMS45NCwwLDAsMSwxNjUsOThoNDQuNTNMMTcxLjc2LDYwLjI0YTYsNiwwLDAsMSw4LjQ4LTguNDhsNDgsNDhBNiw2LDAsMCwxLDIyOC4yNCwxMDguMjRaTTE5MiwyMTBIMzhWODhhNiw2LDAsMCwwLTEyLDBWMjE2YTYsNiwwLDAsMCw2LDZIMTkyYTYsNiwwLDAsMCwwLTEyWiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjguMjQsMTA4LjI0bC00OCw0OGE2LDYsMCwwLDEtOC40OC04LjQ4TDIwOS41MSwxMTBIMTY1YTg5Ljk0LDg5Ljk0LDAsMCwwLTg3LjE3LDY3LjUsNiw2LDAsMCwxLTExLjYyLTNBMTAxLjk0LDEwMS45NCwwLDAsMSwxNjUsOThoNDQuNTNMMTcxLjc2LDYwLjI0YTYsNiwwLDAsMSw4LjQ4LTguNDhsNDgsNDhBNiw2LDAsMCwxLDIyOC4yNCwxMDguMjRaTTE5MiwyMTBIMzhWODhhNiw2LDAsMCwwLTEyLDBWMjE2YTYsNiwwLDAsMCw2LDZIMTkyYTYsNiwwLDAsMCwwLTEyWiIvPjwvc3ZnPg==)}.icon-star{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzcuMjgsOTcuODdBMTQuMTgsMTQuMTgsMCwwLDAsMjI0Ljc2LDg4bC02MC4yNS00Ljg3LTIzLjIyLTU2LjJhMTQuMzcsMTQuMzcsMCwwLDAtMjYuNTgsMEw5MS40OSw4My4xMSwzMS4yNCw4OGExNC4xOCwxNC4xOCwwLDAsMC0xMi41Miw5Ljg5QTE0LjQzLDE0LjQzLDAsMCwwLDIzLDExMy4zMkw2OSwxNTIuOTNsLTE0LDU5LjI1YTE0LjQsMTQuNCwwLDAsMCw1LjU5LDE1LDE0LjEsMTQuMSwwLDAsMCwxNS45MS42TDEyOCwxOTYuMTJsNTEuNTgsMzEuNzFhMTQuMSwxNC4xLDAsMCwwLDE1LjkxLS42LDE0LjQsMTQuNCwwLDAsMCw1LjU5LTE1bC0xNC01OS4yNUwyMzMsMTEzLjMyQTE0LjQzLDE0LjQzLDAsMCwwLDIzNy4yOCw5Ny44N1ptLTEyLjE0LDYuMzctNDguNjksNDJhNiw2LDAsMCwwLTEuOTIsNS45MmwxNC44OCw2Mi43OWEyLjM1LDIuMzUsMCwwLDEtLjk1LDIuNTcsMi4yNCwyLjI0LDAsMCwxLTIuNi4xTDEzMS4xNCwxODRhNiw2LDAsMCwwLTYuMjgsMEw3MC4xNCwyMTcuNjFhMi4yNCwyLjI0LDAsMCwxLTIuNi0uMSwyLjM1LDIuMzUsMCwwLDEtMS0yLjU3bDE0Ljg4LTYyLjc5YTYsNiwwLDAsMC0xLjkyLTUuOTJsLTQ4LjY5LTQyYTIuMzcsMi4zNywwLDAsMS0uNzMtMi42NSwyLjI4LDIuMjgsMCwwLDEsMi4wNy0xLjY1bDYzLjkyLTUuMTZhNiw2LDAsMCwwLDUuMDYtMy42OWwyNC42My01OS42YTIuMzUsMi4zNSwwLDAsMSw0LjM4LDBsMjQuNjMsNTkuNmE2LDYsMCwwLDAsNS4wNiwzLjY5bDYzLjkyLDUuMTZhMi4yOCwyLjI4LDAsMCwxLDIuMDcsMS42NUEyLjM3LDIuMzcsMCwwLDEsMjI1LjE0LDEwNC4yNFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzcuMjgsOTcuODdBMTQuMTgsMTQuMTgsMCwwLDAsMjI0Ljc2LDg4bC02MC4yNS00Ljg3LTIzLjIyLTU2LjJhMTQuMzcsMTQuMzcsMCwwLDAtMjYuNTgsMEw5MS40OSw4My4xMSwzMS4yNCw4OGExNC4xOCwxNC4xOCwwLDAsMC0xMi41Miw5Ljg5QTE0LjQzLDE0LjQzLDAsMCwwLDIzLDExMy4zMkw2OSwxNTIuOTNsLTE0LDU5LjI1YTE0LjQsMTQuNCwwLDAsMCw1LjU5LDE1LDE0LjEsMTQuMSwwLDAsMCwxNS45MS42TDEyOCwxOTYuMTJsNTEuNTgsMzEuNzFhMTQuMSwxNC4xLDAsMCwwLDE1LjkxLS42LDE0LjQsMTQuNCwwLDAsMCw1LjU5LTE1bC0xNC01OS4yNUwyMzMsMTEzLjMyQTE0LjQzLDE0LjQzLDAsMCwwLDIzNy4yOCw5Ny44N1ptLTEyLjE0LDYuMzctNDguNjksNDJhNiw2LDAsMCwwLTEuOTIsNS45MmwxNC44OCw2Mi43OWEyLjM1LDIuMzUsMCwwLDEtLjk1LDIuNTcsMi4yNCwyLjI0LDAsMCwxLTIuNi4xTDEzMS4xNCwxODRhNiw2LDAsMCwwLTYuMjgsMEw3MC4xNCwyMTcuNjFhMi4yNCwyLjI0LDAsMCwxLTIuNi0uMSwyLjM1LDIuMzUsMCwwLDEtMS0yLjU3bDE0Ljg4LTYyLjc5YTYsNiwwLDAsMC0xLjkyLTUuOTJsLTQ4LjY5LTQyYTIuMzcsMi4zNywwLDAsMS0uNzMtMi42NSwyLjI4LDIuMjgsMCwwLDEsMi4wNy0xLjY1bDYzLjkyLTUuMTZhNiw2LDAsMCwwLDUuMDYtMy42OWwyNC42My01OS42YTIuMzUsMi4zNSwwLDAsMSw0LjM4LDBsMjQuNjMsNTkuNmE2LDYsMCwwLDAsNS4wNiwzLjY5bDYzLjkyLDUuMTZhMi4yOCwyLjI4LDAsMCwxLDIuMDcsMS42NUEyLjM3LDIuMzcsMCwwLDEsMjI1LjE0LDEwNC4yNFoiLz48L3N2Zz4=)}.icon-star-half{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzMsMTEzLjMybC01LjA2LDQuMzZhNiw2LDAsMSwxLTcuODMtOS4wOGw1LjA2LTQuMzZhMi4zOCwyLjM4LDAsMCwwLC43Mi0yLjY1LDIuMjgsMi4yOCwwLDAsMC0yLjA3LTEuNjVsLTguMjgtLjY3YTYsNiwwLDEsMSwxLTEybDguMjcuNjdhMTQuMTcsMTQuMTcsMCwwLDEsMTIuNTIsOS44OUExNC40MywxNC40MywwLDAsMSwyMzMsMTEzLjMyWm0tNTguNDUsMzguODMsNC4wOCwxNy4yMmE2LDYsMCwwLDAsNS44Myw0LjYyLDUuNTEsNS41MSwwLDAsMCwxLjM5LS4xNyw2LDYsMCwwLDAsNC40Ni03LjIybC0zLjI0LTEzLjY3LDguODctNy42NWE2LDYsMCwxLDAtNy44NC05LjA5bC0xMS42MywxMEE2LDYsMCwwLDAsMTc0LjUzLDE1Mi4xNVptMjUuMjMsNTQuNDVhNiw2LDAsMSwwLTExLjY3LDIuNzdsMS4zMiw1LjU3YTIuMzUsMi4zNSwwLDAsMS0uOTUsMi41NywyLjI0LDIuMjQsMCwwLDEtMi42LjFsLTYuNzEtNC4xM2E2LDYsMCwxLDAtNi4yOSwxMC4yMmw2LjcyLDQuMTNhMTQuMzksMTQuMzksMCwwLDAsMjEuNS0xNS42NVpNMTc1LjUyLDk2bC0xNS42NC0xLjI2YTYsNiwwLDAsMS01LjA2LTMuNjlMMTM0LDQwLjd2MTQ1bDguMzksNS4xNWE2LDYsMCwwLDEtNi4yOSwxMC4yM2wtOC4xLTVMNzYuNDIsMjI3LjgzYTE0LjM5LDE0LjM5LDAsMCwxLTIxLjUtMTUuNjVsMTQtNTkuMjVMMjMsMTEzLjMyYTE0LjQzLDE0LjQzLDAsMCwxLTQuMy0xNS40NUExNC4xNywxNC4xNywwLDAsMSwzMS4yNCw4OGw2MC4yNS00Ljg3LDIzLjIyLTU2LjJhMTQuMzcsMTQuMzcsMCwwLDEsMjYuNTgsMGwyMy4yMiw1Ni4yLDEyLDFhNiw2LDAsMCwxLTEsMTJaTTEyMiwxODUuNzJWNDAuN0wxMDEuMTgsOTEuMDlhNiw2LDAsMCwxLTUuMDYsMy42OUwzMi4yLDk5Ljk0YTIuMjgsMi4yOCwwLDAsMC0yLjA3LDEuNjUsMi4zOCwyLjM4LDAsMCwwLC43MiwyLjY1bDQ4LjcsNDJhNiw2LDAsMCwxLDEuOTIsNS45Mkw2Ni41OSwyMTQuOTRhMi4zNSwyLjM1LDAsMCwwLDEsMi41NywyLjI0LDIuMjQsMCwwLDAsMi42LjFaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzMsMTEzLjMybC01LjA2LDQuMzZhNiw2LDAsMSwxLTcuODMtOS4wOGw1LjA2LTQuMzZhMi4zOCwyLjM4LDAsMCwwLC43Mi0yLjY1LDIuMjgsMi4yOCwwLDAsMC0yLjA3LTEuNjVsLTguMjgtLjY3YTYsNiwwLDEsMSwxLTEybDguMjcuNjdhMTQuMTcsMTQuMTcsMCwwLDEsMTIuNTIsOS44OUExNC40MywxNC40MywwLDAsMSwyMzMsMTEzLjMyWm0tNTguNDUsMzguODMsNC4wOCwxNy4yMmE2LDYsMCwwLDAsNS44Myw0LjYyLDUuNTEsNS41MSwwLDAsMCwxLjM5LS4xNyw2LDYsMCwwLDAsNC40Ni03LjIybC0zLjI0LTEzLjY3LDguODctNy42NWE2LDYsMCwxLDAtNy44NC05LjA5bC0xMS42MywxMEE2LDYsMCwwLDAsMTc0LjUzLDE1Mi4xNVptMjUuMjMsNTQuNDVhNiw2LDAsMSwwLTExLjY3LDIuNzdsMS4zMiw1LjU3YTIuMzUsMi4zNSwwLDAsMS0uOTUsMi41NywyLjI0LDIuMjQsMCwwLDEtMi42LjFsLTYuNzEtNC4xM2E2LDYsMCwxLDAtNi4yOSwxMC4yMmw2LjcyLDQuMTNhMTQuMzksMTQuMzksMCwwLDAsMjEuNS0xNS42NVpNMTc1LjUyLDk2bC0xNS42NC0xLjI2YTYsNiwwLDAsMS01LjA2LTMuNjlMMTM0LDQwLjd2MTQ1bDguMzksNS4xNWE2LDYsMCwwLDEtNi4yOSwxMC4yM2wtOC4xLTVMNzYuNDIsMjI3LjgzYTE0LjM5LDE0LjM5LDAsMCwxLTIxLjUtMTUuNjVsMTQtNTkuMjVMMjMsMTEzLjMyYTE0LjQzLDE0LjQzLDAsMCwxLTQuMy0xNS40NUExNC4xNywxNC4xNywwLDAsMSwzMS4yNCw4OGw2MC4yNS00Ljg3LDIzLjIyLTU2LjJhMTQuMzcsMTQuMzcsMCwwLDEsMjYuNTgsMGwyMy4yMiw1Ni4yLDEyLDFhNiw2LDAsMCwxLTEsMTJaTTEyMiwxODUuNzJWNDAuN0wxMDEuMTgsOTEuMDlhNiw2LDAsMCwxLTUuMDYsMy42OUwzMi4yLDk5Ljk0YTIuMjgsMi4yOCwwLDAsMC0yLjA3LDEuNjUsMi4zOCwyLjM4LDAsMCwwLC43MiwyLjY1bDQ4LjcsNDJhNiw2LDAsMCwxLDEuOTIsNS45Mkw2Ni41OSwyMTQuOTRhMi4zNSwyLjM1LDAsMCwwLDEsMi41NywyLjI0LDIuMjQsMCwwLDAsMi42LjFaIi8+PC9zdmc+)}.icon-upload{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzgsMTM2djY0YTE0LDE0LDAsMCwxLTE0LDE0SDMyYTE0LDE0LDAsMCwxLTE0LTE0VjEzNmExNCwxNCwwLDAsMSwxNC0xNEg4MGE2LDYsMCwwLDEsMCwxMkgzMmEyLDIsMCwwLDAtMiwydjY0YTIsMiwwLDAsMCwyLDJIMjI0YTIsMiwwLDAsMCwyLTJWMTM2YTIsMiwwLDAsMC0yLTJIMTc2YTYsNiwwLDAsMSwwLTEyaDQ4QTE0LDE0LDAsMCwxLDIzOCwxMzZaTTg0LjI0LDc2LjI0LDEyMiwzOC40OVYxMjhhNiw2LDAsMCwwLDEyLDBWMzguNDlsMzcuNzYsMzcuNzVhNiw2LDAsMCwwLDguNDgtOC40OGwtNDgtNDhhNiw2LDAsMCwwLTguNDgsMGwtNDgsNDhhNiw2LDAsMCwwLDguNDgsOC40OFpNMTk4LDE2OGExMCwxMCwwLDEsMC0xMCwxMEExMCwxMCwwLDAsMCwxOTgsMTY4WiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzgsMTM2djY0YTE0LDE0LDAsMCwxLTE0LDE0SDMyYTE0LDE0LDAsMCwxLTE0LTE0VjEzNmExNCwxNCwwLDAsMSwxNC0xNEg4MGE2LDYsMCwwLDEsMCwxMkgzMmEyLDIsMCwwLDAtMiwydjY0YTIsMiwwLDAsMCwyLDJIMjI0YTIsMiwwLDAsMCwyLTJWMTM2YTIsMiwwLDAsMC0yLTJIMTc2YTYsNiwwLDAsMSwwLTEyaDQ4QTE0LDE0LDAsMCwxLDIzOCwxMzZaTTg0LjI0LDc2LjI0LDEyMiwzOC40OVYxMjhhNiw2LDAsMCwwLDEyLDBWMzguNDlsMzcuNzYsMzcuNzVhNiw2LDAsMCwwLDguNDgtOC40OGwtNDgtNDhhNiw2LDAsMCwwLTguNDgsMGwtNDgsNDhhNiw2LDAsMCwwLDguNDgsOC40OFpNMTk4LDE2OGExMCwxMCwwLDEsMC0xMCwxMEExMCwxMCwwLDAsMCwxOTgsMTY4WiIvPjwvc3ZnPg==)}.icon-sun{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjIsNDBWMTZhNiw2LDAsMCwxLDEyLDBWNDBhNiw2LDAsMCwxLTEyLDBabTY4LDg4YTYyLDYyLDAsMSwxLTYyLTYyQTYyLjA3LDYyLjA3LDAsMCwxLDE5MCwxMjhabS0xMiwwYTUwLDUwLDAsMSwwLTUwLDUwQTUwLjA2LDUwLjA2LDAsMCwwLDE3OCwxMjhaTTU5Ljc2LDY4LjI0YTYsNiwwLDEsMCw4LjQ4LTguNDhsLTE2LTE2YTYsNiwwLDAsMC04LjQ4LDguNDhabTAsMTE5LjUyLTE2LDE2YTYsNiwwLDEsMCw4LjQ4LDguNDhsMTYtMTZhNiw2LDAsMSwwLTguNDgtOC40OFpNMTkyLDcwYTYsNiwwLDAsMCw0LjI0LTEuNzZsMTYtMTZhNiw2LDAsMCwwLTguNDgtOC40OGwtMTYsMTZBNiw2LDAsMCwwLDE5Miw3MFptNC4yNCwxMTcuNzZhNiw2LDAsMCwwLTguNDgsOC40OGwxNiwxNmE2LDYsMCwwLDAsOC40OC04LjQ4Wk00NiwxMjhhNiw2LDAsMCwwLTYtNkgxNmE2LDYsMCwwLDAsMCwxMkg0MEE2LDYsMCwwLDAsNDYsMTI4Wm04Miw4MmE2LDYsMCwwLDAtNiw2djI0YTYsNiwwLDAsMCwxMiwwVjIxNkE2LDYsMCwwLDAsMTI4LDIxMFptMTEyLTg4SDIxNmE2LDYsMCwwLDAsMCwxMmgyNGE2LDYsMCwwLDAsMC0xMloiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjIsNDBWMTZhNiw2LDAsMCwxLDEyLDBWNDBhNiw2LDAsMCwxLTEyLDBabTY4LDg4YTYyLDYyLDAsMSwxLTYyLTYyQTYyLjA3LDYyLjA3LDAsMCwxLDE5MCwxMjhabS0xMiwwYTUwLDUwLDAsMSwwLTUwLDUwQTUwLjA2LDUwLjA2LDAsMCwwLDE3OCwxMjhaTTU5Ljc2LDY4LjI0YTYsNiwwLDEsMCw4LjQ4LTguNDhsLTE2LTE2YTYsNiwwLDAsMC04LjQ4LDguNDhabTAsMTE5LjUyLTE2LDE2YTYsNiwwLDEsMCw4LjQ4LDguNDhsMTYtMTZhNiw2LDAsMSwwLTguNDgtOC40OFpNMTkyLDcwYTYsNiwwLDAsMCw0LjI0LTEuNzZsMTYtMTZhNiw2LDAsMCwwLTguNDgtOC40OGwtMTYsMTZBNiw2LDAsMCwwLDE5Miw3MFptNC4yNCwxMTcuNzZhNiw2LDAsMCwwLTguNDgsOC40OGwxNiwxNmE2LDYsMCwwLDAsOC40OC04LjQ4Wk00NiwxMjhhNiw2LDAsMCwwLTYtNkgxNmE2LDYsMCwwLDAsMCwxMkg0MEE2LDYsMCwwLDAsNDYsMTI4Wm04Miw4MmE2LDYsMCwwLDAtNiw2djI0YTYsNiwwLDAsMCwxMiwwVjIxNkE2LDYsMCwwLDAsMTI4LDIxMFptMTEyLTg4SDIxNmE2LDYsMCwwLDAsMCwxMmgyNGE2LDYsMCwwLDAsMC0xMloiLz48L3N2Zz4=)}.icon-moon{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzIuMTMsMTQzLjY0YTYsNiwwLDAsMC02LTEuNDlBOTAuMDcsOTAuMDcsMCwwLDEsMTEzLjg2LDI5Ljg1YTYsNiwwLDAsMC03LjQ5LTcuNDhBMTAyLjg4LDEwMi44OCwwLDAsMCw1NC40OCw1OC42OCwxMDIsMTAyLDAsMCwwLDE5Ny4zMiwyMDEuNTJhMTAyLjg4LDEwMi44OCwwLDAsMCwzNi4zMS01MS44OUE2LDYsMCwwLDAsMjMyLjEzLDE0My42NFptLTQyLDQ4LjI5YTkwLDkwLDAsMCwxLTEyNi0xMjZBOTAuOSw5MC45LDAsMCwxLDk5LjY1LDM3LjY2LDEwMi4wNiwxMDIuMDYsMCwwLDAsMjE4LjM0LDE1Ni4zNSw5MC45LDkwLjksMCwwLDEsMTkwLjEsMTkxLjkzWiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzIuMTMsMTQzLjY0YTYsNiwwLDAsMC02LTEuNDlBOTAuMDcsOTAuMDcsMCwwLDEsMTEzLjg2LDI5Ljg1YTYsNiwwLDAsMC03LjQ5LTcuNDhBMTAyLjg4LDEwMi44OCwwLDAsMCw1NC40OCw1OC42OCwxMDIsMTAyLDAsMCwwLDE5Ny4zMiwyMDEuNTJhMTAyLjg4LDEwMi44OCwwLDAsMCwzNi4zMS01MS44OUE2LDYsMCwwLDAsMjMyLjEzLDE0My42NFptLTQyLDQ4LjI5YTkwLDkwLDAsMCwxLTEyNi0xMjZBOTAuOSw5MC45LDAsMCwxLDk5LjY1LDM3LjY2LDEwMi4wNiwxMDIuMDYsMCwwLDAsMjE4LjM0LDE1Ni4zNSw5MC45LDkwLjksMCwwLDEsMTkwLjEsMTkxLjkzWiIvPjwvc3ZnPg==)}.icon-x{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDQuMjQsMTk1Ljc2YTYsNiwwLDEsMS04LjQ4LDguNDhMMTI4LDEzNi40OSw2MC4yNCwyMDQuMjRhNiw2LDAsMCwxLTguNDgtOC40OEwxMTkuNTEsMTI4LDUxLjc2LDYwLjI0YTYsNiwwLDAsMSw4LjQ4LTguNDhMMTI4LDExOS41MWw2Ny43Ni02Ny43NWE2LDYsMCwwLDEsOC40OCw4LjQ4TDEzNi40OSwxMjhaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDQuMjQsMTk1Ljc2YTYsNiwwLDEsMS04LjQ4LDguNDhMMTI4LDEzNi40OSw2MC4yNCwyMDQuMjRhNiw2LDAsMCwxLTguNDgtOC40OEwxMTkuNTEsMTI4LDUxLjc2LDYwLjI0YTYsNiwwLDAsMSw4LjQ4LTguNDhMMTI4LDExOS41MWw2Ny43Ni02Ny43NWE2LDYsMCwwLDEsOC40OCw4LjQ4TDEzNi40OSwxMjhaIi8+PC9zdmc+)}.icon-question{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMzgsMTgwYTEwLDEwLDAsMSwxLTEwLTEwQTEwLDEwLDAsMCwxLDEzOCwxODBaTTEyOCw3NGMtMjEsMC0zOCwxNS4yNS0zOCwzNHY0YTYsNiwwLDAsMCwxMiwwdi00YzAtMTIuMTMsMTEuNjYtMjIsMjYtMjJzMjYsOS44NywyNiwyMi0xMS42NiwyMi0yNiwyMmE2LDYsMCwwLDAtNiw2djhhNiw2LDAsMCwwLDEyLDB2LTIuNDJjMTguMTEtMi41OCwzMi0xNi42NiwzMi0zMy41OEMxNjYsODkuMjUsMTQ5LDc0LDEyOCw3NFptMTAyLDU0QTEwMiwxMDIsMCwxLDEsMTI4LDI2LDEwMi4xMiwxMDIuMTIsMCwwLDEsMjMwLDEyOFptLTEyLDBhOTAsOTAsMCwxLDAtOTAsOTBBOTAuMSw5MC4xLDAsMCwwLDIxOCwxMjhaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMzgsMTgwYTEwLDEwLDAsMSwxLTEwLTEwQTEwLDEwLDAsMCwxLDEzOCwxODBaTTEyOCw3NGMtMjEsMC0zOCwxNS4yNS0zOCwzNHY0YTYsNiwwLDAsMCwxMiwwdi00YzAtMTIuMTMsMTEuNjYtMjIsMjYtMjJzMjYsOS44NywyNiwyMi0xMS42NiwyMi0yNiwyMmE2LDYsMCwwLDAtNiw2djhhNiw2LDAsMCwwLDEyLDB2LTIuNDJjMTguMTEtMi41OCwzMi0xNi42NiwzMi0zMy41OEMxNjYsODkuMjUsMTQ5LDc0LDEyOCw3NFptMTAyLDU0QTEwMiwxMDIsMCwxLDEsMTI4LDI2LDEwMi4xMiwxMDIuMTIsMCwwLDEsMjMwLDEyOFptLTEyLDBhOTAsOTAsMCwxLDAtOTAsOTBBOTAuMSw5MC4xLDAsMCwwLDIxOCwxMjhaIi8+PC9zdmc+)}.icon-arrows-left-right{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTIuMjQsMTcxLjc2YTYsNiwwLDAsMSwwLDguNDhsLTMyLDMyYTYsNiwwLDAsMS04LjQ4LTguNDhMMTkzLjUxLDE4Mkg0OGE2LDYsMCwwLDEsMC0xMkgxOTMuNTFsLTIxLjc1LTIxLjc2YTYsNiwwLDAsMSw4LjQ4LTguNDhaTTc1Ljc2LDExNi4yNGE2LDYsMCwwLDAsOC40OC04LjQ4TDYyLjQ5LDg2SDIwOGE2LDYsMCwwLDAsMC0xMkg2Mi40OUw4NC4yNCw1Mi4yNGE2LDYsMCwwLDAtOC40OC04LjQ4bC0zMiwzMmE2LDYsMCwwLDAsMCw4LjQ4WiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTIuMjQsMTcxLjc2YTYsNiwwLDAsMSwwLDguNDhsLTMyLDMyYTYsNiwwLDAsMS04LjQ4LTguNDhMMTkzLjUxLDE4Mkg0OGE2LDYsMCwwLDEsMC0xMkgxOTMuNTFsLTIxLjc1LTIxLjc2YTYsNiwwLDAsMSw4LjQ4LTguNDhaTTc1Ljc2LDExNi4yNGE2LDYsMCwwLDAsOC40OC04LjQ4TDYyLjQ5LDg2SDIwOGE2LDYsMCwwLDAsMC0xMkg2Mi40OUw4NC4yNCw1Mi4yNGE2LDYsMCwwLDAtOC40OC04LjQ4bC0zMiwzMmE2LDYsMCwwLDAsMCw4LjQ4WiIvPjwvc3ZnPg==)}.icon-asterisk{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTMuMTQsMTc5LjA5YTYsNiwwLDAsMS04LjIzLDIuMDZMMTM0LDEzOC42VjIxNmE2LDYsMCwwLDEtMTIsMFYxMzguNkw1MS4wOSwxODEuMTVBNi4wNyw2LjA3LDAsMCwxLDQ4LDE4MmE2LDYsMCwwLDEtMy4xLTExLjE1TDExNi4zNCwxMjgsNDQuOTEsODUuMTVhNiw2LDAsMCwxLDYuMTgtMTAuM0wxMjIsMTE3LjRWNDBhNiw2LDAsMCwxLDEyLDB2NzcuNGw3MC45MS00Mi41NWE2LDYsMCwwLDEsNi4xOCwxMC4zTDEzOS42NiwxMjhsNzEuNDMsNDIuODVBNiw2LDAsMCwxLDIxMy4xNCwxNzkuMDlaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTMuMTQsMTc5LjA5YTYsNiwwLDAsMS04LjIzLDIuMDZMMTM0LDEzOC42VjIxNmE2LDYsMCwwLDEtMTIsMFYxMzguNkw1MS4wOSwxODEuMTVBNi4wNyw2LjA3LDAsMCwxLDQ4LDE4MmE2LDYsMCwwLDEtMy4xLTExLjE1TDExNi4zNCwxMjgsNDQuOTEsODUuMTVhNiw2LDAsMCwxLDYuMTgtMTAuM0wxMjIsMTE3LjRWNDBhNiw2LDAsMCwxLDEyLDB2NzcuNGw3MC45MS00Mi41NWE2LDYsMCwwLDEsNi4xOCwxMC4zTDEzOS42NiwxMjhsNzEuNDMsNDIuODVBNiw2LDAsMCwxLDIxMy4xNCwxNzkuMDlaIi8+PC9zdmc+)}.icon-cake{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzAsMTEyYTIyLDIyLDAsMCwwLTIyLTIySDEzNFY3Ny40QTMwLjA1LDMwLjA1LDAsMCwwLDE1OCw0OGMwLTI2LjktMjUuNTgtNDQuMjctMjYuNjctNDVhNiw2LDAsMCwwLTYuNjYsMEMxMjMuNTgsMy43Myw5OCwyMS4xLDk4LDQ4YTMwLjA1LDMwLjA1LDAsMCwwLDI0LDI5LjRWOTBINDhhMjIsMjIsMCwwLDAtMjIsMjJ2MjMuMzNhMzguODEsMzguODEsMCwwLDAsOCwyMy41N1YyMDBhMjIsMjIsMCwwLDAsMjIsMjJIMjAwYTIyLDIyLDAsMCwwLDIyLTIyVjE1OC45YTM4LjgxLDM4LjgxLDAsMCwwLDgtMjMuNTdaTTExMCw0OGMwLTE1LjQsMTItMjcuNDEsMTgtMzIuNDQsNiw1LDE4LDE3LDE4LDMyLjQ0YTE4LDE4LDAsMCwxLTM2LDBaTTM4LDExMmExMCwxMCwwLDAsMSwxMC0xMEgyMDhhMTAsMTAsMCwwLDEsMTAsMTB2MjMuMzNjMCwxNC4zMi0xMS4zMywyNi4yOC0yNS4yNiwyNi42NkEyNiwyNiwwLDAsMSwxNjYsMTM2YTYsNiwwLDAsMC0xMiwwLDI2LDI2LDAsMCwxLTUyLDAsNiw2LDAsMCwwLTEyLDAsMjYsMjYsMCwwLDEtMjYuNzQsMjZDNDkuMzMsMTYxLjYxLDM4LDE0OS42NSwzOCwxMzUuMzNabTE2Miw5OEg1NmExMCwxMCwwLDAsMS0xMC0xMFYxNjkuMzFBMzYuOCwzNi44LDAsMCwwLDYyLjk0LDE3NCwzNy44MiwzNy44MiwwLDAsMCw5MC41LDE2My4yNCwzOC44MywzOC44MywwLDAsMCw5NiwxNTYuNWEzOCwzOCwwLDAsMCw2NCwwLDM4LjgzLDM4LjgzLDAsMCwwLDUuNTIsNi43NEEzNy43MywzNy43MywwLDAsMCwxOTIsMTc0aDEuMDdBMzYuOCwzNi44LDAsMCwwLDIxMCwxNjkuMzFWMjAwQTEwLDEwLDAsMCwxLDIwMCwyMTBaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzAsMTEyYTIyLDIyLDAsMCwwLTIyLTIySDEzNFY3Ny40QTMwLjA1LDMwLjA1LDAsMCwwLDE1OCw0OGMwLTI2LjktMjUuNTgtNDQuMjctMjYuNjctNDVhNiw2LDAsMCwwLTYuNjYsMEMxMjMuNTgsMy43Myw5OCwyMS4xLDk4LDQ4YTMwLjA1LDMwLjA1LDAsMCwwLDI0LDI5LjRWOTBINDhhMjIsMjIsMCwwLDAtMjIsMjJ2MjMuMzNhMzguODEsMzguODEsMCwwLDAsOCwyMy41N1YyMDBhMjIsMjIsMCwwLDAsMjIsMjJIMjAwYTIyLDIyLDAsMCwwLDIyLTIyVjE1OC45YTM4LjgxLDM4LjgxLDAsMCwwLDgtMjMuNTdaTTExMCw0OGMwLTE1LjQsMTItMjcuNDEsMTgtMzIuNDQsNiw1LDE4LDE3LDE4LDMyLjQ0YTE4LDE4LDAsMCwxLTM2LDBaTTM4LDExMmExMCwxMCwwLDAsMSwxMC0xMEgyMDhhMTAsMTAsMCwwLDEsMTAsMTB2MjMuMzNjMCwxNC4zMi0xMS4zMywyNi4yOC0yNS4yNiwyNi42NkEyNiwyNiwwLDAsMSwxNjYsMTM2YTYsNiwwLDAsMC0xMiwwLDI2LDI2LDAsMCwxLTUyLDAsNiw2LDAsMCwwLTEyLDAsMjYsMjYsMCwwLDEtMjYuNzQsMjZDNDkuMzMsMTYxLjYxLDM4LDE0OS42NSwzOCwxMzUuMzNabTE2Miw5OEg1NmExMCwxMCwwLDAsMS0xMC0xMFYxNjkuMzFBMzYuOCwzNi44LDAsMCwwLDYyLjk0LDE3NCwzNy44MiwzNy44MiwwLDAsMCw5MC41LDE2My4yNCwzOC44MywzOC44MywwLDAsMCw5NiwxNTYuNWEzOCwzOCwwLDAsMCw2NCwwLDM4LjgzLDM4LjgzLDAsMCwwLDUuNTIsNi43NEEzNy43MywzNy43MywwLDAsMCwxOTIsMTc0aDEuMDdBMzYuOCwzNi44LDAsMCwwLDIxMCwxNjkuMzFWMjAwQTEwLDEwLDAsMCwxLDIwMCwyMTBaIi8+PC9zdmc+)}.icon-person{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsNzBBMzAsMzAsMCwxLDAsOTgsNDAsMzAsMzAsMCwwLDAsMTI4LDcwWm0wLTQ4YTE4LDE4LDAsMSwxLTE4LDE4QTE4LDE4LDAsMCwxLDEyOCwyMlptODguODgsMTEzLjQyTDE3MS42Nyw4NC4xNkEzMCwzMCwwLDAsMCwxNDkuMTcsNzRIMTA2LjgzYTMwLDMwLDAsMCwwLTIyLjUsMTAuMTVMMzkuMTIsMTM1LjQyQTE4LDE4LDAsMCwwLDY0LjQ2LDE2MWwyMS4xMS0xNi45M0w2Ny40NCwyMTIuOTJhMTgsMTgsMCwwLDAsMzIuNzUsMTQuOTRMMTI4LDE4MGwyNy44MSw0Ny45MWExOCwxOCwwLDAsMCwzMi43NS0xNC45NGwtMTguMTMtNjguODdMMTkxLjU0LDE2MWExOCwxOCwwLDAsMCwyNS4zNC0yNS41NlptLTguNjMsMTYuODJhNiw2LDAsMCwxLTguNDksMCw0LjE1LDQuMTUsMCwwLDAtLjQ5LS40NGwtMzUuNTEtMjguNDhhNiw2LDAsMCwwLTkuNTYsNi4ybDIyLjg3LDg2LjkzYTcuNjYsNy42NiwwLDAsMCwuMzcsMSw2LDYsMCwwLDEtMTAuODgsNS4wNyw0LjM3LDQuMzcsMCwwLDAtLjI1LS40OEwxMzMuMTksMTY1YTYsNiwwLDAsMC0xMC4zOCwwTDg5LjY5LDIyMi4wNWE0LjM3LDQuMzcsMCwwLDAtLjI1LjQ4LDYsNiwwLDAsMS0xMC44OC01LjA3LDcuNjYsNy42NiwwLDAsMCwuMzctMWwyMi44Ny04Ni45M0E2LDYsMCwwLDAsOTkuMjcsMTIzLDYuMDcsNi4wNywwLDAsMCw5NiwxMjJhNiw2LDAsMCwwLTMuNzYsMS4zMkw1Ni43MywxNTEuOGE0LjE1LDQuMTUsMCwwLDAtLjQ5LjQ0LDYsNiwwLDAsMS04LjQ5LTguNDlsLjI2LS4yN0w5My4zMyw5Mi4wOUExOCwxOCwwLDAsMSwxMDYuODMsODZoNDIuMzRhMTgsMTgsMCwwLDEsMTMuNSw2LjA5TDIwOCwxNDMuNDhsLjI2LjI3QTYsNiwwLDAsMSwyMDguMjUsMTUyLjI0WiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsNzBBMzAsMzAsMCwxLDAsOTgsNDAsMzAsMzAsMCwwLDAsMTI4LDcwWm0wLTQ4YTE4LDE4LDAsMSwxLTE4LDE4QTE4LDE4LDAsMCwxLDEyOCwyMlptODguODgsMTEzLjQyTDE3MS42Nyw4NC4xNkEzMCwzMCwwLDAsMCwxNDkuMTcsNzRIMTA2LjgzYTMwLDMwLDAsMCwwLTIyLjUsMTAuMTVMMzkuMTIsMTM1LjQyQTE4LDE4LDAsMCwwLDY0LjQ2LDE2MWwyMS4xMS0xNi45M0w2Ny40NCwyMTIuOTJhMTgsMTgsMCwwLDAsMzIuNzUsMTQuOTRMMTI4LDE4MGwyNy44MSw0Ny45MWExOCwxOCwwLDAsMCwzMi43NS0xNC45NGwtMTguMTMtNjguODdMMTkxLjU0LDE2MWExOCwxOCwwLDAsMCwyNS4zNC0yNS41NlptLTguNjMsMTYuODJhNiw2LDAsMCwxLTguNDksMCw0LjE1LDQuMTUsMCwwLDAtLjQ5LS40NGwtMzUuNTEtMjguNDhhNiw2LDAsMCwwLTkuNTYsNi4ybDIyLjg3LDg2LjkzYTcuNjYsNy42NiwwLDAsMCwuMzcsMSw2LDYsMCwwLDEtMTAuODgsNS4wNyw0LjM3LDQuMzcsMCwwLDAtLjI1LS40OEwxMzMuMTksMTY1YTYsNiwwLDAsMC0xMC4zOCwwTDg5LjY5LDIyMi4wNWE0LjM3LDQuMzcsMCwwLDAtLjI1LjQ4LDYsNiwwLDAsMS0xMC44OC01LjA3LDcuNjYsNy42NiwwLDAsMCwuMzctMWwyMi44Ny04Ni45M0E2LDYsMCwwLDAsOTkuMjcsMTIzLDYuMDcsNi4wNywwLDAsMCw5NiwxMjJhNiw2LDAsMCwwLTMuNzYsMS4zMkw1Ni43MywxNTEuOGE0LjE1LDQuMTUsMCwwLDAtLjQ5LjQ0LDYsNiwwLDAsMS04LjQ5LTguNDlsLjI2LS4yN0w5My4zMyw5Mi4wOUExOCwxOCwwLDAsMSwxMDYuODMsODZoNDIuMzRhMTgsMTgsMCwwLDEsMTMuNSw2LjA5TDIwOCwxNDMuNDhsLjI2LjI3QTYsNiwwLDAsMSwyMDguMjUsMTUyLjI0WiIvPjwvc3ZnPg==)}.icon-crosshair{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzIsMTIySDIyMS44QTk0LjEzLDk0LjEzLDAsMCwwLDEzNCwzNC4yVjI0YTYsNiwwLDAsMC0xMiwwVjM0LjJBOTQuMTMsOTQuMTMsMCwwLDAsMzQuMiwxMjJIMjRhNiw2LDAsMCwwLDAsMTJIMzQuMkE5NC4xMyw5NC4xMywwLDAsMCwxMjIsMjIxLjhWMjMyYTYsNiwwLDAsMCwxMiwwVjIyMS44QTk0LjEzLDk0LjEzLDAsMCwwLDIyMS44LDEzNEgyMzJhNiw2LDAsMCwwLDAtMTJabS05OCw4Ny43NlYyMDBhNiw2LDAsMCwwLTEyLDB2OS43NkE4Mi4wOSw4Mi4wOSwwLDAsMSw0Ni4yNCwxMzRINTZhNiw2LDAsMCwwLDAtMTJINDYuMjRBODIuMDksODIuMDksMCwwLDEsMTIyLDQ2LjI0VjU2YTYsNiwwLDAsMCwxMiwwVjQ2LjI0QTgyLjA5LDgyLjA5LDAsMCwxLDIwOS43NiwxMjJIMjAwYTYsNiwwLDAsMCwwLDEyaDkuNzZBODIuMDksODIuMDksMCwwLDEsMTM0LDIwOS43NlpNMTI4LDkwYTM4LDM4LDAsMSwwLDM4LDM4QTM4LDM4LDAsMCwwLDEyOCw5MFptMCw2NGEyNiwyNiwwLDEsMSwyNi0yNkEyNiwyNiwwLDAsMSwxMjgsMTU0WiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzIsMTIySDIyMS44QTk0LjEzLDk0LjEzLDAsMCwwLDEzNCwzNC4yVjI0YTYsNiwwLDAsMC0xMiwwVjM0LjJBOTQuMTMsOTQuMTMsMCwwLDAsMzQuMiwxMjJIMjRhNiw2LDAsMCwwLDAsMTJIMzQuMkE5NC4xMyw5NC4xMywwLDAsMCwxMjIsMjIxLjhWMjMyYTYsNiwwLDAsMCwxMiwwVjIyMS44QTk0LjEzLDk0LjEzLDAsMCwwLDIyMS44LDEzNEgyMzJhNiw2LDAsMCwwLDAtMTJabS05OCw4Ny43NlYyMDBhNiw2LDAsMCwwLTEyLDB2OS43NkE4Mi4wOSw4Mi4wOSwwLDAsMSw0Ni4yNCwxMzRINTZhNiw2LDAsMCwwLDAtMTJINDYuMjRBODIuMDksODIuMDksMCwwLDEsMTIyLDQ2LjI0VjU2YTYsNiwwLDAsMCwxMiwwVjQ2LjI0QTgyLjA5LDgyLjA5LDAsMCwxLDIwOS43NiwxMjJIMjAwYTYsNiwwLDAsMCwwLDEyaDkuNzZBODIuMDksODIuMDksMCwwLDEsMTM0LDIwOS43NlpNMTI4LDkwYTM4LDM4LDAsMSwwLDM4LDM4QTM4LDM4LDAsMCwwLDEyOCw5MFptMCw2NGEyNiwyNiwwLDEsMSwyNi0yNkEyNiwyNiwwLDAsMSwxMjgsMTU0WiIvPjwvc3ZnPg==)}.icon-dots-six{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik03MCw5MkExMCwxMCwwLDEsMSw2MCw4MiwxMCwxMCwwLDAsMSw3MCw5MlptNTgtMTBhMTAsMTAsMCwxLDAsMTAsMTBBMTAsMTAsMCwwLDAsMTI4LDgyWm02OCwyMGExMCwxMCwwLDEsMC0xMC0xMEExMCwxMCwwLDAsMCwxOTYsMTAyWk02MCwxNTRhMTAsMTAsMCwxLDAsMTAsMTBBMTAsMTAsMCwwLDAsNjAsMTU0Wm02OCwwYTEwLDEwLDAsMSwwLDEwLDEwQTEwLDEwLDAsMCwwLDEyOCwxNTRabTY4LDBhMTAsMTAsMCwxLDAsMTAsMTBBMTAsMTAsMCwwLDAsMTk2LDE1NFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik03MCw5MkExMCwxMCwwLDEsMSw2MCw4MiwxMCwxMCwwLDAsMSw3MCw5MlptNTgtMTBhMTAsMTAsMCwxLDAsMTAsMTBBMTAsMTAsMCwwLDAsMTI4LDgyWm02OCwyMGExMCwxMCwwLDEsMC0xMC0xMEExMCwxMCwwLDAsMCwxOTYsMTAyWk02MCwxNTRhMTAsMTAsMCwxLDAsMTAsMTBBMTAsMTAsMCwwLDAsNjAsMTU0Wm02OCwwYTEwLDEwLDAsMSwwLDEwLDEwQTEwLDEwLDAsMCwwLDEyOCwxNTRabTY4LDBhMTAsMTAsMCwxLDAsMTAsMTBBMTAsMTAsMCwwLDAsMTk2LDE1NFoiLz48L3N2Zz4=)}.icon-hourglass{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xOTgsNzUuNjRWNDBhMTQsMTQsMCwwLDAtMTQtMTRINzJBMTQsMTQsMCwwLDAsNTgsNDBWNzZhMTQuMDYsMTQuMDYsMCwwLDAsNS42LDExLjJMMTE4LDEyOCw2My42LDE2OC44QTE0LjA2LDE0LjA2LDAsMCwwLDU4LDE4MHYzNmExNCwxNCwwLDAsMCwxNCwxNEgxODRhMTQsMTQsMCwwLDAsMTQtMTRWMTgwLjM2YTE0LjA4LDE0LjA4LDAsMCwwLTUuNTYtMTEuMTdMMTM4LDEyOGw1NC40OS00MS4xOUExNC4wOCwxNC4wOCwwLDAsMCwxOTgsNzUuNjRaTTE4NiwxODAuMzZWMjE2YTIsMiwwLDAsMS0yLDJINzJhMiwyLDAsMCwxLTItMlYxODBhMiwyLDAsMCwxLC44LTEuNkwxMjgsMTM1LjUxbDU3LjIyLDQzLjI1QTIsMiwwLDAsMSwxODYsMTgwLjM2Wm0wLTEwNC43MmEyLDIsMCwwLDEtLjc5LDEuNkwxMjgsMTIwLjQ5LDcwLjgsNzcuNkEyLDIsMCwwLDEsNzAsNzZWNDBhMiwyLDAsMCwxLDItMkgxODRhMiwyLDAsMCwxLDIsMloiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xOTgsNzUuNjRWNDBhMTQsMTQsMCwwLDAtMTQtMTRINzJBMTQsMTQsMCwwLDAsNTgsNDBWNzZhMTQuMDYsMTQuMDYsMCwwLDAsNS42LDExLjJMMTE4LDEyOCw2My42LDE2OC44QTE0LjA2LDE0LjA2LDAsMCwwLDU4LDE4MHYzNmExNCwxNCwwLDAsMCwxNCwxNEgxODRhMTQsMTQsMCwwLDAsMTQtMTRWMTgwLjM2YTE0LjA4LDE0LjA4LDAsMCwwLTUuNTYtMTEuMTdMMTM4LDEyOGw1NC40OS00MS4xOUExNC4wOCwxNC4wOCwwLDAsMCwxOTgsNzUuNjRaTTE4NiwxODAuMzZWMjE2YTIsMiwwLDAsMS0yLDJINzJhMiwyLDAsMCwxLTItMlYxODBhMiwyLDAsMCwxLC44LTEuNkwxMjgsMTM1LjUxbDU3LjIyLDQzLjI1QTIsMiwwLDAsMSwxODYsMTgwLjM2Wm0wLTEwNC43MmEyLDIsMCwwLDEtLjc5LDEuNkwxMjgsMTIwLjQ5LDcwLjgsNzcuNkEyLDIsMCwwLDEsNzAsNzZWNDBhMiwyLDAsMCwxLDItMkgxODRhMiwyLDAsMCwxLDIsMloiLz48L3N2Zz4=)}.icon-folder{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNzRIMTMwLjQ5bC0yNy45LTI3LjlhMTMuOTQsMTMuOTQsMCwwLDAtOS45LTQuMUg0MEExNCwxNCwwLDAsMCwyNiw1NlYyMDAuNjJBMTMuMzksMTMuMzksMCwwLDAsMzkuMzgsMjE0SDIxNi44OUExMy4xMiwxMy4xMiwwLDAsMCwyMzAsMjAwLjg5Vjg4QTE0LDE0LDAsMCwwLDIxNiw3NFpNNDAsNTRIOTIuNjlhMiwyLDAsMCwxLDEuNDEuNTlMMTEzLjUxLDc0SDM4VjU2QTIsMiwwLDAsMSw0MCw1NFpNMjE4LDIwMC44OWExLjExLDEuMTEsMCwwLDEtMS4xMSwxLjExSDM5LjM4QTEuNCwxLjQsMCwwLDEsMzgsMjAwLjYyVjg2SDIxNmEyLDIsMCwwLDEsMiwyWiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNzRIMTMwLjQ5bC0yNy45LTI3LjlhMTMuOTQsMTMuOTQsMCwwLDAtOS45LTQuMUg0MEExNCwxNCwwLDAsMCwyNiw1NlYyMDAuNjJBMTMuMzksMTMuMzksMCwwLDAsMzkuMzgsMjE0SDIxNi44OUExMy4xMiwxMy4xMiwwLDAsMCwyMzAsMjAwLjg5Vjg4QTE0LDE0LDAsMCwwLDIxNiw3NFpNNDAsNTRIOTIuNjlhMiwyLDAsMCwxLDEuNDEuNTlMMTEzLjUxLDc0SDM4VjU2QTIsMiwwLDAsMSw0MCw1NFpNMjE4LDIwMC44OWExLjExLDEuMTEsMCwwLDEtMS4xMSwxLjExSDM5LjM4QTEuNCwxLjQsMCwwLDEsMzgsMjAwLjYyVjg2SDIxNmEyLDIsMCwwLDEsMiwyWiIvPjwvc3ZnPg==)}.icon-envelope{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsNTBIMzJhNiw2LDAsMCwwLTYsNlYxOTJhMTQsMTQsMCwwLDAsMTQsMTRIMjE2YTE0LDE0LDAsMCwwLDE0LTE0VjU2QTYsNiwwLDAsMCwyMjQsNTBabS05Niw4NS44Nkw0Ny40Miw2MkgyMDguNThaTTEwMS42NywxMjgsMzgsMTg2LjM2VjY5LjY0Wm04Ljg4LDguMTRMMTI0LDE0OC40MmE2LDYsMCwwLDAsOC4xLDBsMTMuNC0xMi4yOEwyMDguNTgsMTk0SDQ3LjQzWk0xNTQuMzMsMTI4LDIxOCw2OS42NFYxODYuMzZaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsNTBIMzJhNiw2LDAsMCwwLTYsNlYxOTJhMTQsMTQsMCwwLDAsMTQsMTRIMjE2YTE0LDE0LDAsMCwwLDE0LTE0VjU2QTYsNiwwLDAsMCwyMjQsNTBabS05Niw4NS44Nkw0Ny40Miw2MkgyMDguNThaTTEwMS42NywxMjgsMzgsMTg2LjM2VjY5LjY0Wm04Ljg4LDguMTRMMTI0LDE0OC40MmE2LDYsMCwwLDAsOC4xLDBsMTMuNC0xMi4yOEwyMDguNTgsMTk0SDQ3LjQzWk0xNTQuMzMsMTI4LDIxOCw2OS42NFYxODYuMzZaIi8+PC9zdmc+)}.icon-hand-heart{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjkuMTIsMTQyLjY1YTIyLjQzLDIyLjQzLDAsMCwwLTE5LjU1LTMuODhsLTQuMzIsMUMyMjcsMTE5LjU1LDIzOCw5OS41MSwyMzgsODBjMC0yNS4zNi0yMC4zOS00Ni00NS40Ni00NkE0NS41MSw0NS41MSwwLDAsMCwxNTYsNTJhNDUuNTEsNDUuNTEsMCwwLDAtMzYuNTQtMThDOTQuMzksMzQsNzQsNTQuNjQsNzQsODBjMCwxMS4zOCwzLjYzLDIyLjQ5LDExLjI5LDM0LjM2YTI5LjczLDI5LjczLDAsMCwwLTE2LjU2LDguNDNMNDUuNTIsMTQ2SDE2QTE0LDE0LDAsMCwwLDIsMTYwdjQwYTE0LDE0LDAsMCwwLDE0LDE0SDEyMGE2LDYsMCwwLDAsMS40Ni0uMThsNjQtMTZhNy4xNiw3LjE2LDAsMCwwLC44OS0uM0wyMjUuMTcsMTgxbC4zMy0uMTVhMjIuNiwyMi42LDAsMCwwLDMuNjItMzguMThaTTExOS40Niw0NmEzMy4xNiwzMy4xNiwwLDAsMSwzMSwyMC4yOCw2LDYsMCwwLDAsMTEuMSwwLDMzLjE2LDMzLjE2LDAsMCwxLDMxLTIwLjI4QzIxMC42OCw0NiwyMjYsNjEuNTcsMjI2LDgwYzAsMjAuMjQtMTYuMTgsNDMtNDYuOCw2NS43NWwtMTQuODcsMy40MkEyNiwyNiwwLDAsMCwxNDAsMTE0SDk5LjY3QzkwLjM2LDEwMS42Nyw4Niw5MC44MSw4Niw4MCw4Niw2MS41NywxMDEuMzIsNDYsMTE5LjQ2LDQ2Wk0xNCwyMDBWMTYwYTIsMiwwLDAsMSwyLTJINDJ2NDRIMTZBMiwyLDAsMCwxLDE0LDIwMFptMjA2LjI4LTMwLTM4LjIsMTYuMjdMMTE5LjI2LDIwMkg1NFYxNTQuNDlsMjMuMjEtMjMuMjJBMTcuODgsMTcuODgsMCwwLDEsODkuOTQsMTI2SDE0MGExNCwxNCwwLDAsMSwwLDI4SDExMmE2LDYsMCwwLDAsMCwxMmgzMmE2LDYsMCwwLDAsMS4zNC0uMTVsNjctMTUuNDEuMjQtLjA2QTEwLjYsMTAuNiwwLDAsMSwyMjAuMjgsMTcwWiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjkuMTIsMTQyLjY1YTIyLjQzLDIyLjQzLDAsMCwwLTE5LjU1LTMuODhsLTQuMzIsMUMyMjcsMTE5LjU1LDIzOCw5OS41MSwyMzgsODBjMC0yNS4zNi0yMC4zOS00Ni00NS40Ni00NkE0NS41MSw0NS41MSwwLDAsMCwxNTYsNTJhNDUuNTEsNDUuNTEsMCwwLDAtMzYuNTQtMThDOTQuMzksMzQsNzQsNTQuNjQsNzQsODBjMCwxMS4zOCwzLjYzLDIyLjQ5LDExLjI5LDM0LjM2YTI5LjczLDI5LjczLDAsMCwwLTE2LjU2LDguNDNMNDUuNTIsMTQ2SDE2QTE0LDE0LDAsMCwwLDIsMTYwdjQwYTE0LDE0LDAsMCwwLDE0LDE0SDEyMGE2LDYsMCwwLDAsMS40Ni0uMThsNjQtMTZhNy4xNiw3LjE2LDAsMCwwLC44OS0uM0wyMjUuMTcsMTgxbC4zMy0uMTVhMjIuNiwyMi42LDAsMCwwLDMuNjItMzguMThaTTExOS40Niw0NmEzMy4xNiwzMy4xNiwwLDAsMSwzMSwyMC4yOCw2LDYsMCwwLDAsMTEuMSwwLDMzLjE2LDMzLjE2LDAsMCwxLDMxLTIwLjI4QzIxMC42OCw0NiwyMjYsNjEuNTcsMjI2LDgwYzAsMjAuMjQtMTYuMTgsNDMtNDYuOCw2NS43NWwtMTQuODcsMy40MkEyNiwyNiwwLDAsMCwxNDAsMTE0SDk5LjY3QzkwLjM2LDEwMS42Nyw4Niw5MC44MSw4Niw4MCw4Niw2MS41NywxMDEuMzIsNDYsMTE5LjQ2LDQ2Wk0xNCwyMDBWMTYwYTIsMiwwLDAsMSwyLTJINDJ2NDRIMTZBMiwyLDAsMCwxLDE0LDIwMFptMjA2LjI4LTMwLTM4LjIsMTYuMjdMMTE5LjI2LDIwMkg1NFYxNTQuNDlsMjMuMjEtMjMuMjJBMTcuODgsMTcuODgsMCwwLDEsODkuOTQsMTI2SDE0MGExNCwxNCwwLDAsMSwwLDI4SDExMmE2LDYsMCwwLDAsMCwxMmgzMmE2LDYsMCwwLDAsMS4zNC0uMTVsNjctMTUuNDEuMjQtLjA2QTEwLjYsMTAuNiwwLDAsMSwyMjAuMjgsMTcwWiIvPjwvc3ZnPg==)}.icon-butterfly{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzEuMSw1MS43MUMyMjYuMDksNDUuMjcsMjE4LjY0LDQyLDIwOSw0MmMtMTYuMzMsMC0zNy40MSwxMS4wNi01Ni40LDI5LjU5QTEzMiwxMzIsMCwwLDAsMTM0LDkzLjk0VjU2YTYsNiwwLDAsMC0xMiwwVjkzLjk0YTEzMiwxMzIsMCwwLDAtMTguNTQtMjIuMzVDODQuNDksNTMuMDYsNjMuNCw0Miw0Ny4wNyw0MmMtOS42OSwwLTE3LjE0LDMuMjctMjIuMTUsOS43MS01LjUzLDcuMTEtNy43MSwxNy42OS02LjY2LDMyLjM0LjkxLDEyLjczLDQuMTIsMjYuNTMsNi44MSwzNy4xMyw2LjI4LDI0Ljc0LDIwLjc3LDMzLDMxLjc4LDM1LjY4QTQyLDQyLDAsMSwwLDEyOCwyMDEuNjJhNDIsNDIsMCwxLDAsNzEuMTYtNDQuNzZjMTEtMi42MywyNS41LTEwLjk0LDMxLjc4LTM1LjY4QzIzNy41NSw5NS4yMSwyNDIuNzIsNjYuNjUsMjMxLjEsNTEuNzFaTTkyLDIxMGEzMC4xMiwzMC4xMiwwLDAsMS0zLjM0LTYwQTYsNiwwLDAsMCw4Ny4zNSwxMzhhNDEuNzEsNDEuNzEsMCwwLDAtMjAuMjgsOGMtOSwuMzEtMjQuMTItMy4xNi0zMC4zNy0yNy43Ni0zLjI1LTEyLjgxLTExLjg5LTQ2LjgzLTIuMzEtNTkuMTVDMzcuMDUsNTUuNjYsNDEuMiw1NCw0Ny4wNyw1NGMxMi44OCwwLDMxLjcyLDEwLjI4LDQ4LDI2LjE4QzExMS42OSw5Ni4zOSwxMjIsMTE0LjU5LDEyMiwxMjcuNjdWMTgwQTMwLDMwLDAsMCwxLDkyLDIxMFptMTI3LjMxLTkxLjc3QzIxMy4wNywxNDIuODMsMTk4LDE0Ni4yOSwxODksMTQ2YTQxLjYyLDQxLjYyLDAsMCwwLTIwLjI4LThBNiw2LDAsMSwwLDE2Ny4zNiwxNTAsMzAuMTEsMzAuMTEsMCwxLDEsMTM0LDE4MFYxMjcuNjdjMC0xMy4wOCwxMC4zMi0zMS4yOCwyNi45My00Ny40OUMxNzcuMjMsNjQuMjgsMTk2LjA3LDU0LDIwOSw1NGM1Ljg3LDAsMTAsMS42NiwxMi42OCw1LjA4QzIzMS4yMSw3MS40LDIyMi41NywxMDUuNDIsMjE5LjMyLDExOC4yM1oiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzEuMSw1MS43MUMyMjYuMDksNDUuMjcsMjE4LjY0LDQyLDIwOSw0MmMtMTYuMzMsMC0zNy40MSwxMS4wNi01Ni40LDI5LjU5QTEzMiwxMzIsMCwwLDAsMTM0LDkzLjk0VjU2YTYsNiwwLDAsMC0xMiwwVjkzLjk0YTEzMiwxMzIsMCwwLDAtMTguNTQtMjIuMzVDODQuNDksNTMuMDYsNjMuNCw0Miw0Ny4wNyw0MmMtOS42OSwwLTE3LjE0LDMuMjctMjIuMTUsOS43MS01LjUzLDcuMTEtNy43MSwxNy42OS02LjY2LDMyLjM0LjkxLDEyLjczLDQuMTIsMjYuNTMsNi44MSwzNy4xMyw2LjI4LDI0Ljc0LDIwLjc3LDMzLDMxLjc4LDM1LjY4QTQyLDQyLDAsMSwwLDEyOCwyMDEuNjJhNDIsNDIsMCwxLDAsNzEuMTYtNDQuNzZjMTEtMi42MywyNS41LTEwLjk0LDMxLjc4LTM1LjY4QzIzNy41NSw5NS4yMSwyNDIuNzIsNjYuNjUsMjMxLjEsNTEuNzFaTTkyLDIxMGEzMC4xMiwzMC4xMiwwLDAsMS0zLjM0LTYwQTYsNiwwLDAsMCw4Ny4zNSwxMzhhNDEuNzEsNDEuNzEsMCwwLDAtMjAuMjgsOGMtOSwuMzEtMjQuMTItMy4xNi0zMC4zNy0yNy43Ni0zLjI1LTEyLjgxLTExLjg5LTQ2LjgzLTIuMzEtNTkuMTVDMzcuMDUsNTUuNjYsNDEuMiw1NCw0Ny4wNyw1NGMxMi44OCwwLDMxLjcyLDEwLjI4LDQ4LDI2LjE4QzExMS42OSw5Ni4zOSwxMjIsMTE0LjU5LDEyMiwxMjcuNjdWMTgwQTMwLDMwLDAsMCwxLDkyLDIxMFptMTI3LjMxLTkxLjc3QzIxMy4wNywxNDIuODMsMTk4LDE0Ni4yOSwxODksMTQ2YTQxLjYyLDQxLjYyLDAsMCwwLTIwLjI4LThBNiw2LDAsMSwwLDE2Ny4zNiwxNTAsMzAuMTEsMzAuMTEsMCwxLDEsMTM0LDE4MFYxMjcuNjdjMC0xMy4wOCwxMC4zMi0zMS4yOCwyNi45My00Ny40OUMxNzcuMjMsNjQuMjgsMTk2LjA3LDU0LDIwOSw1NGM1Ljg3LDAsMTAsMS42NiwxMi42OCw1LjA4QzIzMS4yMSw3MS40LDIyMi41NywxMDUuNDIsMjE5LjMyLDExOC4yM1oiLz48L3N2Zz4=)}.icon-phone{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjEuNTksMTYwLjNsLTQ3LjI0LTIxLjE3YTE0LDE0LDAsMCwwLTEzLjI4LDEuMjIsNC44MSw0LjgxLDAsMCwwLS41Ni40MmwtMjQuNjksMjFhMS44OCwxLjg4LDAsMCwxLTEuNjguMDZjLTE1Ljg3LTcuNjYtMzIuMzEtMjQtNDAtMzkuNjVhMS45MSwxLjkxLDAsMCwxLDAtMS42OGwyMS4wNy0yNWE2LjEzLDYuMTMsMCwwLDAsLjQyLS41OCwxNCwxNCwwLDAsMCwxLjEyLTEzLjI3TDk1LjczLDM0LjQ5YTE0LDE0LDAsMCwwLTE0LjU2LTguMzhBNTQuMjQsNTQuMjQsMCwwLDAsMzQsODBjMCw3OC4zLDYzLjcsMTQyLDE0MiwxNDJhNTQuMjUsNTQuMjUsMCwwLDAsNTMuODktNDcuMTdBMTQsMTQsMCwwLDAsMjIxLjU5LDE2MC4zWk0xNzYsMjEwQzEwNC4zMiwyMTAsNDYsMTUxLjY4LDQ2LDgwQTQyLjIzLDQyLjIzLDAsMCwxLDgyLjY3LDM4aC4yM2EyLDIsMCwwLDEsMS44NCwxLjMxbDIxLjEsNDcuMTFhMiwyLDAsMCwxLDAsMS42N0w4NC43MywxMTMuMTVhNC43Myw0LjczLDAsMCwwLS40My41NywxNCwxNCwwLDAsMC0uOTEsMTMuNzNjOC44NywxOC4xNiwyNy4xNywzNi4zMiw0NS41Myw0NS4xOWExNCwxNCwwLDAsMCwxMy43Ny0xYy4xOS0uMTMuMzgtLjI3LjU2LS40MmwyNC42OC0yMWExLjkyLDEuOTIsMCwwLDEsMS42LS4xbDQ3LjI1LDIxLjE3YTIsMiwwLDAsMSwxLjIxLDJBNDIuMjQsNDIuMjQsMCwwLDEsMTc2LDIxMFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjEuNTksMTYwLjNsLTQ3LjI0LTIxLjE3YTE0LDE0LDAsMCwwLTEzLjI4LDEuMjIsNC44MSw0LjgxLDAsMCwwLS41Ni40MmwtMjQuNjksMjFhMS44OCwxLjg4LDAsMCwxLTEuNjguMDZjLTE1Ljg3LTcuNjYtMzIuMzEtMjQtNDAtMzkuNjVhMS45MSwxLjkxLDAsMCwxLDAtMS42OGwyMS4wNy0yNWE2LjEzLDYuMTMsMCwwLDAsLjQyLS41OCwxNCwxNCwwLDAsMCwxLjEyLTEzLjI3TDk1LjczLDM0LjQ5YTE0LDE0LDAsMCwwLTE0LjU2LTguMzhBNTQuMjQsNTQuMjQsMCwwLDAsMzQsODBjMCw3OC4zLDYzLjcsMTQyLDE0MiwxNDJhNTQuMjUsNTQuMjUsMCwwLDAsNTMuODktNDcuMTdBMTQsMTQsMCwwLDAsMjIxLjU5LDE2MC4zWk0xNzYsMjEwQzEwNC4zMiwyMTAsNDYsMTUxLjY4LDQ2LDgwQTQyLjIzLDQyLjIzLDAsMCwxLDgyLjY3LDM4aC4yM2EyLDIsMCwwLDEsMS44NCwxLjMxbDIxLjEsNDcuMTFhMiwyLDAsMCwxLDAsMS42N0w4NC43MywxMTMuMTVhNC43Myw0LjczLDAsMCwwLS40My41NywxNCwxNCwwLDAsMC0uOTEsMTMuNzNjOC44NywxOC4xNiwyNy4xNywzNi4zMiw0NS41Myw0NS4xOWExNCwxNCwwLDAsMCwxMy43Ny0xYy4xOS0uMTMuMzgtLjI3LjU2LS40MmwyNC42OC0yMWExLjkyLDEuOTIsMCwwLDEsMS42LS4xbDQ3LjI1LDIxLjE3YTIsMiwwLDAsMSwxLjIxLDJBNDIuMjQsNDIuMjQsMCwwLDEsMTc2LDIxMFoiLz48L3N2Zz4=)}.icon-chat{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNTBINDBBMTQsMTQsMCwwLDAsMjYsNjRWMjI0YTEzLjg4LDEzLjg4LDAsMCwwLDguMDksMTIuNjlBMTQuMTEsMTQuMTEsMCwwLDAsNDAsMjM4YTEzLjg3LDEzLjg3LDAsMCwwLDktMy4zMWwuMDYtLjA1TDgyLjIzLDIwNkgyMTZhMTQsMTQsMCwwLDAsMTQtMTRWNjRBMTQsMTQsMCwwLDAsMjE2LDUwWm0yLDE0MmEyLDIsMCwwLDEtMiwySDgwYTYsNiwwLDAsMC0zLjkyLDEuNDZMNDEuMjYsMjI1LjUzQTIsMiwwLDAsMSwzOCwyMjRWNjRhMiwyLDAsMCwxLDItMkgyMTZhMiwyLDAsMCwxLDIsMloiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNTBINDBBMTQsMTQsMCwwLDAsMjYsNjRWMjI0YTEzLjg4LDEzLjg4LDAsMCwwLDguMDksMTIuNjlBMTQuMTEsMTQuMTEsMCwwLDAsNDAsMjM4YTEzLjg3LDEzLjg3LDAsMCwwLDktMy4zMWwuMDYtLjA1TDgyLjIzLDIwNkgyMTZhMTQsMTQsMCwwLDAsMTQtMTRWNjRBMTQsMTQsMCwwLDAsMjE2LDUwWm0yLDE0MmEyLDIsMCwwLDEtMiwySDgwYTYsNiwwLDAsMC0zLjkyLDEuNDZMNDEuMjYsMjI1LjUzQTIsMiwwLDAsMSwzOCwyMjRWNjRhMiwyLDAsMCwxLDItMkgyMTZhMiwyLDAsMCwxLDIsMloiLz48L3N2Zz4=)}.icon-arrows-clockwise{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsNDhWOTZhNiw2LDAsMCwxLTYsNkgxNjhhNiw2LDAsMCwxLDAtMTJoMzMuNTJMMTgzLjQ3LDcyYTgxLjUxLDgxLjUxLDAsMCwwLTU3LjUzLTI0aC0uNDZBODEuNSw4MS41LDAsMCwwLDY4LjE5LDcxLjI4YTYsNiwwLDEsMS04LjM4LTguNTgsOTMuMzgsOTMuMzgsMCwwLDEsNjUuNjctMjYuNzZIMTI2YTkzLjQ1LDkzLjQ1LDAsMCwxLDY2LDI3LjUzbDE4LDE4VjQ4YTYsNiwwLDAsMSwxMiwwWk0xODcuODEsMTg0LjcyYTgxLjUsODEuNSwwLDAsMS01Ny4yOSwyMy4zNGgtLjQ2YTgxLjUxLDgxLjUxLDAsMCwxLTU3LjUzLTI0TDU0LjQ4LDE2Nkg4OGE2LDYsMCwwLDAsMC0xMkg0MGE2LDYsMCwwLDAtNiw2djQ4YTYsNiwwLDAsMCwxMiwwVjE3NC40OGwxOCwxOC4wNWE5My40NSw5My40NSwwLDAsMCw2NiwyNy41M2guNTJhOTMuMzgsOTMuMzgsMCwwLDAsNjUuNjctMjYuNzYsNiw2LDAsMSwwLTguMzgtOC41OFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsNDhWOTZhNiw2LDAsMCwxLTYsNkgxNjhhNiw2LDAsMCwxLDAtMTJoMzMuNTJMMTgzLjQ3LDcyYTgxLjUxLDgxLjUxLDAsMCwwLTU3LjUzLTI0aC0uNDZBODEuNSw4MS41LDAsMCwwLDY4LjE5LDcxLjI4YTYsNiwwLDEsMS04LjM4LTguNTgsOTMuMzgsOTMuMzgsMCwwLDEsNjUuNjctMjYuNzZIMTI2YTkzLjQ1LDkzLjQ1LDAsMCwxLDY2LDI3LjUzbDE4LDE4VjQ4YTYsNiwwLDAsMSwxMiwwWk0xODcuODEsMTg0LjcyYTgxLjUsODEuNSwwLDAsMS01Ny4yOSwyMy4zNGgtLjQ2YTgxLjUxLDgxLjUxLDAsMCwxLTU3LjUzLTI0TDU0LjQ4LDE2Nkg4OGE2LDYsMCwwLDAsMC0xMkg0MGE2LDYsMCwwLDAtNiw2djQ4YTYsNiwwLDAsMCwxMiwwVjE3NC40OGwxOCwxOC4wNWE5My40NSw5My40NSwwLDAsMCw2NiwyNy41M2guNTJhOTMuMzgsOTMuMzgsMCwwLDAsNjUuNjctMjYuNzYsNiw2LDAsMSwwLTguMzgtOC41OFoiLz48L3N2Zz4=)}.icon-check-circle{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNzIuMjQsOTkuNzZhNiw2LDAsMCwxLDAsOC40OGwtNTYsNTZhNiw2LDAsMCwxLTguNDgsMGwtMjQtMjRhNiw2LDAsMCwxLDguNDgtOC40OEwxMTIsMTUxLjUxbDUxLjc2LTUxLjc1QTYsNiwwLDAsMSwxNzIuMjQsOTkuNzZaTTIzMCwxMjhBMTAyLDEwMiwwLDEsMSwxMjgsMjYsMTAyLjEyLDEwMi4xMiwwLDAsMSwyMzAsMTI4Wm0tMTIsMGE5MCw5MCwwLDEsMC05MCw5MEE5MC4xLDkwLjEsMCwwLDAsMjE4LDEyOFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNzIuMjQsOTkuNzZhNiw2LDAsMCwxLDAsOC40OGwtNTYsNTZhNiw2LDAsMCwxLTguNDgsMGwtMjQtMjRhNiw2LDAsMCwxLDguNDgtOC40OEwxMTIsMTUxLjUxbDUxLjc2LTUxLjc1QTYsNiwwLDAsMSwxNzIuMjQsOTkuNzZaTTIzMCwxMjhBMTAyLDEwMiwwLDEsMSwxMjgsMjYsMTAyLjEyLDEwMi4xMiwwLDAsMSwyMzAsMTI4Wm0tMTIsMGE5MCw5MCwwLDEsMC05MCw5MEE5MC4xLDkwLjEsMCwwLDAsMjE4LDEyOFoiLz48L3N2Zz4=)}.icon-x-circle{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjQuMjQsMTAwLjI0LDEzNi40OCwxMjhsMjcuNzYsMjcuNzZhNiw2LDAsMSwxLTguNDgsOC40OEwxMjgsMTM2LjQ4bC0yNy43NiwyNy43NmE2LDYsMCwwLDEtOC40OC04LjQ4TDExOS41MiwxMjgsOTEuNzYsMTAwLjI0YTYsNiwwLDAsMSw4LjQ4LTguNDhMMTI4LDExOS41MmwyNy43Ni0yNy43NmE2LDYsMCwwLDEsOC40OCw4LjQ4Wk0yMzAsMTI4QTEwMiwxMDIsMCwxLDEsMTI4LDI2LDEwMi4xMiwxMDIuMTIsMCwwLDEsMjMwLDEyOFptLTEyLDBhOTAsOTAsMCwxLDAtOTAsOTBBOTAuMSw5MC4xLDAsMCwwLDIxOCwxMjhaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjQuMjQsMTAwLjI0LDEzNi40OCwxMjhsMjcuNzYsMjcuNzZhNiw2LDAsMSwxLTguNDgsOC40OEwxMjgsMTM2LjQ4bC0yNy43NiwyNy43NmE2LDYsMCwwLDEtOC40OC04LjQ4TDExOS41MiwxMjgsOTEuNzYsMTAwLjI0YTYsNiwwLDAsMSw4LjQ4LTguNDhMMTI4LDExOS41MmwyNy43Ni0yNy43NmE2LDYsMCwwLDEsOC40OCw4LjQ4Wk0yMzAsMTI4QTEwMiwxMDIsMCwxLDEsMTI4LDI2LDEwMi4xMiwxMDIuMTIsMCwwLDEsMjMwLDEyOFptLTEyLDBhOTAsOTAsMCwxLDAtOTAsOTBBOTAuMSw5MC4xLDAsMCwwLDIxOCwxMjhaIi8+PC9zdmc+)}.icon-arrow-fat-down{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjkuNjYsMTQxLjY2bC05Niw5NmE4LDgsMCwwLDEtMTEuMzIsMGwtOTYtOTZBOCw4LDAsMCwxLDMyLDEyOEg3MlY0OEExNiwxNiwwLDAsMSw4OCwzMmg4MGExNiwxNiwwLDAsMSwxNiwxNnY4MGg0MGE4LDgsMCwwLDEsNS42NiwxMy42NloiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjkuNjYsMTQxLjY2bC05Niw5NmE4LDgsMCwwLDEtMTEuMzIsMGwtOTYtOTZBOCw4LDAsMCwxLDMyLDEyOEg3MlY0OEExNiwxNiwwLDAsMSw4OCwzMmg4MGExNiwxNiwwLDAsMSwxNiwxNnY4MGg0MGE4LDgsMCwwLDEsNS42NiwxMy42NloiLz48L3N2Zz4=)}.icon-arrow-fat-up{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzEuMzksMTIzLjA2QTgsOCwwLDAsMSwyMjQsMTI4SDE4NHY4MGExNiwxNiwwLDAsMS0xNiwxNkg4OGExNiwxNiwwLDAsMS0xNi0xNlYxMjhIMzJhOCw4LDAsMCwxLTUuNjYtMTMuNjZsOTYtOTZhOCw4LDAsMCwxLDExLjMyLDBsOTYsOTZBOCw4LDAsMCwxLDIzMS4zOSwxMjMuMDZaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzEuMzksMTIzLjA2QTgsOCwwLDAsMSwyMjQsMTI4SDE4NHY4MGExNiwxNiwwLDAsMS0xNiwxNkg4OGExNiwxNiwwLDAsMS0xNi0xNlYxMjhIMzJhOCw4LDAsMCwxLTUuNjYtMTMuNjZsOTYtOTZhOCw4LDAsMCwxLDExLjMyLDBsOTYsOTZBOCw4LDAsMCwxLDIzMS4zOSwxMjMuMDZaIi8+PC9zdmc+)}.icon-heart{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDAsMTAyYzAsNzAtMTAzLjc5LDEyNi42Ni0xMDguMjEsMTI5YTgsOCwwLDAsMS03LjU4LDBDMTE5Ljc5LDIyOC42NiwxNiwxNzIsMTYsMTAyQTYyLjA3LDYyLjA3LDAsMCwxLDc4LDQwYzIwLjY1LDAsMzguNzMsOC44OCw1MCwyMy44OUMxMzkuMjcsNDguODgsMTU3LjM1LDQwLDE3OCw0MEE2Mi4wNyw2Mi4wNywwLDAsMSwyNDAsMTAyWiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDAsMTAyYzAsNzAtMTAzLjc5LDEyNi42Ni0xMDguMjEsMTI5YTgsOCwwLDAsMS03LjU4LDBDMTE5Ljc5LDIyOC42NiwxNiwxNzIsMTYsMTAyQTYyLjA3LDYyLjA3LDAsMCwxLDc4LDQwYzIwLjY1LDAsMzguNzMsOC44OCw1MCwyMy44OUMxMzkuMjcsNDguODgsMTU3LjM1LDQwLDE3OCw0MEE2Mi4wNyw2Mi4wNywwLDAsMSwyNDAsMTAyWiIvPjwvc3ZnPg==)}.icon-star{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzQuMjksMTE0Ljg1bC00NSwzOC44M0wyMDMsMjExLjc1YTE2LjQsMTYuNCwwLDAsMS0yNC41LDE3LjgyTDEyOCwxOTguNDksNzcuNDcsMjI5LjU3QTE2LjQsMTYuNCwwLDAsMSw1MywyMTEuNzVsMTMuNzYtNTguMDctNDUtMzguODNBMTYuNDYsMTYuNDYsMCwwLDEsMzEuMDgsODZsNTktNC43NiwyMi43Ni01NS4wOGExNi4zNiwxNi4zNiwwLDAsMSwzMC4yNywwbDIyLjc1LDU1LjA4LDU5LDQuNzZhMTYuNDYsMTYuNDYsMCwwLDEsOS4zNywyOC44NloiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzQuMjksMTE0Ljg1bC00NSwzOC44M0wyMDMsMjExLjc1YTE2LjQsMTYuNCwwLDAsMS0yNC41LDE3LjgyTDEyOCwxOTguNDksNzcuNDcsMjI5LjU3QTE2LjQsMTYuNCwwLDAsMSw1MywyMTEuNzVsMTMuNzYtNTguMDctNDUtMzguODNBMTYuNDYsMTYuNDYsMCwwLDEsMzEuMDgsODZsNTktNC43NiwyMi43Ni01NS4wOGExNi4zNiwxNi4zNiwwLDAsMSwzMC4yNywwbDIyLjc1LDU1LjA4LDU5LDQuNzZhMTYuNDYsMTYuNDYsMCwwLDEsOS4zNywyOC44NloiLz48L3N2Zz4=)}.icon-apple-logo{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjMuMywxNjkuNTlhOC4wNyw4LjA3LDAsMCwwLTIuOC0zLjRDMjAzLjUzLDE1NC41MywyMDAsMTM0LjY0LDIwMCwxMjBjMC0xNy42NywxMy40Ny0zMy4wNiwyMS41LTQwLjY3YTgsOCwwLDAsMCwwLTExLjYyQzIwOC44Miw1NS43NCwxODcuODIsNDgsMTY4LDQ4YTcyLjIsNzIuMiwwLDAsMC00MCwxMi4xMyw3MS41Niw3MS41NiwwLDAsMC05MC43MSw5LjA5QTc0LjYzLDc0LjYzLDAsMCwwLDE2LDEyMy40YTEyNy4wNiwxMjcuMDYsMCwwLDAsNDAuMTQsODkuNzNBMzkuOCwzOS44LDAsMCwwLDgzLjU5LDIyNGg4Ny42OGEzOS44NCwzOS44NCwwLDAsMCwyOS4xMi0xMi41NywxMjUsMTI1LDAsMCwwLDE3LjgyLTI0LjZDMjI1LjIzLDE3NCwyMjQuMzMsMTcyLDIyMy4zLDE2OS41OVptLTM0LjYzLDMwLjk0YTIzLjc2LDIzLjc2LDAsMCwxLTE3LjQsNy40N0g4My41OWEyMy44MiwyMy44MiwwLDAsMS0xNi40NC02LjUxQTExMS4xNCwxMTEuMTQsMCwwLDEsMzIsMTIzLDU4LjUsNTguNSwwLDAsMSw0OC42NSw4MC40Nyw1NC44MSw1NC44MSwwLDAsMSw4OCw2NGguNzhBNTUuNDUsNTUuNDUsMCwwLDEsMTIzLDc2LjI4YTgsOCwwLDAsMCwxMCwwQTU1LjQ0LDU1LjQ0LDAsMCwxLDE2OCw2NGE3MC42NCw3MC42NCwwLDAsMSwzNiwxMC4zNWMtMTMsMTQuNTItMjAsMzAuNDctMjAsNDUuNjUsMCwyMy43Nyw3LjY0LDQyLjczLDIyLjE4LDU1LjNBMTA1LjgyLDEwNS44MiwwLDAsMSwxODguNjcsMjAwLjUzWk0xMjguMjMsMzBBNDAsNDAsMCwwLDEsMTY3LDBoMWE4LDgsMCwwLDEsMCwxNmgtMWEyNCwyNCwwLDAsMC0yMy4yNCwxOCw4LDgsMCwxLDEtMTUuNS00WiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjMuMywxNjkuNTlhOC4wNyw4LjA3LDAsMCwwLTIuOC0zLjRDMjAzLjUzLDE1NC41MywyMDAsMTM0LjY0LDIwMCwxMjBjMC0xNy42NywxMy40Ny0zMy4wNiwyMS41LTQwLjY3YTgsOCwwLDAsMCwwLTExLjYyQzIwOC44Miw1NS43NCwxODcuODIsNDgsMTY4LDQ4YTcyLjIsNzIuMiwwLDAsMC00MCwxMi4xMyw3MS41Niw3MS41NiwwLDAsMC05MC43MSw5LjA5QTc0LjYzLDc0LjYzLDAsMCwwLDE2LDEyMy40YTEyNy4wNiwxMjcuMDYsMCwwLDAsNDAuMTQsODkuNzNBMzkuOCwzOS44LDAsMCwwLDgzLjU5LDIyNGg4Ny42OGEzOS44NCwzOS44NCwwLDAsMCwyOS4xMi0xMi41NywxMjUsMTI1LDAsMCwwLDE3LjgyLTI0LjZDMjI1LjIzLDE3NCwyMjQuMzMsMTcyLDIyMy4zLDE2OS41OVptLTM0LjYzLDMwLjk0YTIzLjc2LDIzLjc2LDAsMCwxLTE3LjQsNy40N0g4My41OWEyMy44MiwyMy44MiwwLDAsMS0xNi40NC02LjUxQTExMS4xNCwxMTEuMTQsMCwwLDEsMzIsMTIzLDU4LjUsNTguNSwwLDAsMSw0OC42NSw4MC40Nyw1NC44MSw1NC44MSwwLDAsMSw4OCw2NGguNzhBNTUuNDUsNTUuNDUsMCwwLDEsMTIzLDc2LjI4YTgsOCwwLDAsMCwxMCwwQTU1LjQ0LDU1LjQ0LDAsMCwxLDE2OCw2NGE3MC42NCw3MC42NCwwLDAsMSwzNiwxMC4zNWMtMTMsMTQuNTItMjAsMzAuNDctMjAsNDUuNjUsMCwyMy43Nyw3LjY0LDQyLjczLDIyLjE4LDU1LjNBMTA1LjgyLDEwNS44MiwwLDAsMSwxODguNjcsMjAwLjUzWk0xMjguMjMsMzBBNDAsNDAsMCwwLDEsMTY3LDBoMWE4LDgsMCwwLDEsMCwxNmgtMWEyNCwyNCwwLDAsMC0yMy4yNCwxOCw4LDgsMCwxLDEtMTUuNS00WiIvPjwvc3ZnPg==)}.icon-arrows-clockwise{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsNDhWOTZhOCw4LDAsMCwxLTgsOEgxNjhhOCw4LDAsMCwxLDAtMTZoMjguNjlMMTgyLjA2LDczLjM3YTc5LjU2LDc5LjU2LDAsMCwwLTU2LjEzLTIzLjQzaC0uNDVBNzkuNTIsNzkuNTIsMCwwLDAsNjkuNTksNzIuNzEsOCw4LDAsMCwxLDU4LjQxLDYxLjI3YTk2LDk2LDAsMCwxLDEzNSwuNzlMMjA4LDc2LjY5VjQ4YTgsOCwwLDAsMSwxNiwwWk0xODYuNDEsMTgzLjI5YTgwLDgwLDAsMCwxLTExMi40Ny0uNjZMNTkuMzEsMTY4SDg4YTgsOCwwLDAsMCwwLTE2SDQwYTgsOCwwLDAsMC04LDh2NDhhOCw4LDAsMCwwLDE2LDBWMTc5LjMxbDE0LjYzLDE0LjYzQTk1LjQzLDk1LjQzLDAsMCwwLDEzMCwyMjIuMDZoLjUzYTk1LjM2LDk1LjM2LDAsMCwwLDY3LjA3LTI3LjMzLDgsOCwwLDAsMC0xMS4xOC0xMS40NFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsNDhWOTZhOCw4LDAsMCwxLTgsOEgxNjhhOCw4LDAsMCwxLDAtMTZoMjguNjlMMTgyLjA2LDczLjM3YTc5LjU2LDc5LjU2LDAsMCwwLTU2LjEzLTIzLjQzaC0uNDVBNzkuNTIsNzkuNTIsMCwwLDAsNjkuNTksNzIuNzEsOCw4LDAsMCwxLDU4LjQxLDYxLjI3YTk2LDk2LDAsMCwxLDEzNSwuNzlMMjA4LDc2LjY5VjQ4YTgsOCwwLDAsMSwxNiwwWk0xODYuNDEsMTgzLjI5YTgwLDgwLDAsMCwxLTExMi40Ny0uNjZMNTkuMzEsMTY4SDg4YTgsOCwwLDAsMCwwLTE2SDQwYTgsOCwwLDAsMC04LDh2NDhhOCw4LDAsMCwwLDE2LDBWMTc5LjMxbDE0LjYzLDE0LjYzQTk1LjQzLDk1LjQzLDAsMCwwLDEzMCwyMjIuMDZoLjUzYTk1LjM2LDk1LjM2LDAsMCwwLDY3LjA3LTI3LjMzLDgsOCwwLDAsMC0xMS4xOC0xMS40NFoiLz48L3N2Zz4=)}.icon-arrows-left-right{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTMuNjYsMTgxLjY2bC0zMiwzMmE4LDgsMCwwLDEtMTEuMzItMTEuMzJMMTg4LjY5LDE4NEg0OGE4LDgsMCwwLDEsMC0xNkgxODguNjlsLTE4LjM1LTE4LjM0YTgsOCwwLDAsMSwxMS4zMi0xMS4zMmwzMiwzMkE4LDgsMCwwLDEsMjEzLjY2LDE4MS42NlptLTEzOS4zMi02NGE4LDgsMCwwLDAsMTEuMzItMTEuMzJMNjcuMzEsODhIMjA4YTgsOCwwLDAsMCwwLTE2SDY3LjMxTDg1LjY2LDUzLjY2QTgsOCwwLDAsMCw3NC4zNCw0Mi4zNGwtMzIsMzJhOCw4LDAsMCwwLDAsMTEuMzJaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTMuNjYsMTgxLjY2bC0zMiwzMmE4LDgsMCwwLDEtMTEuMzItMTEuMzJMMTg4LjY5LDE4NEg0OGE4LDgsMCwwLDEsMC0xNkgxODguNjlsLTE4LjM1LTE4LjM0YTgsOCwwLDAsMSwxMS4zMi0xMS4zMmwzMiwzMkE4LDgsMCwwLDEsMjEzLjY2LDE4MS42NlptLTEzOS4zMi02NGE4LDgsMCwwLDAsMTEuMzItMTEuMzJMNjcuMzEsODhIMjA4YTgsOCwwLDAsMCwwLTE2SDY3LjMxTDg1LjY2LDUzLjY2QTgsOCwwLDAsMCw3NC4zNCw0Mi4zNGwtMzIsMzJhOCw4LDAsMCwwLDAsMTEuMzJaIi8+PC9zdmc+)}.icon-asterisk{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTQuODYsMTgwLjEyYTgsOCwwLDAsMS0xMSwyLjc0TDEzNiwxNDIuMTNWMjE2YTgsOCwwLDAsMS0xNiwwVjE0Mi4xM0w1Mi4xMiwxODIuODZhOCw4LDAsMSwxLTguMjMtMTMuNzJMMTEyLjQ1LDEyOCw0My44OSw4Ni44NmE4LDgsMCwxLDEsOC4yMy0xMy43MkwxMjAsMTEzLjg3VjQwYTgsOCwwLDAsMSwxNiwwdjczLjg3bDY3Ljg4LTQwLjczYTgsOCwwLDEsMSw4LjIzLDEzLjcyTDE0My41NSwxMjhsNjguNTYsNDEuMTRBOCw4LDAsMCwxLDIxNC44NiwxODAuMTJaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTQuODYsMTgwLjEyYTgsOCwwLDAsMS0xMSwyLjc0TDEzNiwxNDIuMTNWMjE2YTgsOCwwLDAsMS0xNiwwVjE0Mi4xM0w1Mi4xMiwxODIuODZhOCw4LDAsMSwxLTguMjMtMTMuNzJMMTEyLjQ1LDEyOCw0My44OSw4Ni44NmE4LDgsMCwxLDEsOC4yMy0xMy43MkwxMjAsMTEzLjg3VjQwYTgsOCwwLDAsMSwxNiwwdjczLjg3bDY3Ljg4LTQwLjczYTgsOCwwLDEsMSw4LjIzLDEzLjcyTDE0My41NSwxMjhsNjguNTYsNDEuMTRBOCw4LDAsMCwxLDIxNC44NiwxODAuMTJaIi8+PC9zdmc+)}.icon-butterfly{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzIuNyw1MC40OEMyMjksNDUuNywyMjEuODQsNDAsMjA5LDQwYy0xNi44NSwwLTM4LjQ2LDExLjI4LTU3LjgxLDMwLjE2QTE0MC4wNywxNDAuMDcsMCwwLDAsMTM2LDg3LjUzVjU2YTgsOCwwLDAsMC0xNiwwVjg3LjUzYTE0MC4wNywxNDAuMDcsMCwwLDAtMTUuMTUtMTcuMzdDODUuNDksNTEuMjgsNjMuODgsNDAsNDcsNDAsMzQuMTYsNDAsMjcsNDUuNywyMy4zLDUwLjQ4Yy02LjgyLDguNzctMTIuMTgsMjQuMDgtLjIxLDcxLjIsNi4wNSwyMy44MywxOS41MSwzMywzMC42MywzNi40MkE0NCw0NCwwLDAsMCwxMjgsMjA1LjI3YTQ0LDQ0LDAsMCwwLDc0LjI4LTQ3LjE3YzExLjEyLTMuNCwyNC41Ny0xMi41OSwzMC42My0zNi40MkMyMzkuNjMsOTUuMjQsMjQ0Ljg1LDY2LjEsMjMyLjcsNTAuNDhaTTkyLDIwOEEyOC4xMiwyOC4xMiwwLDAsMSw4OC44NiwxNTJhOCw4LDAsMSwwLTEuNzYtMTUuOUE0My42NCw0My42NCwwLDAsMCw2Ni4zNiwxNDRjLTguNDMuMDktMjItMy41Ny0yNy43Ni0yNi4yNkMzNS43MiwxMDYuMzksMjcsNzEuODYsMzUuOTQsNjAuMywzNy4zNyw1OC40Niw0MC4wOSw1Niw0Nyw1NmMyNy4yNywwLDczLDQ0Ljg4LDczLDcxLjY3VjE4MEEyOCwyOCwwLDAsMSw5MiwyMDhaTTIxNy40LDExNy43NGMtNS43NywyMi42OS0xOS4zMywyNi4zNC0yNy43NywyNi4yNmE0My42LDQzLjYsMCwwLDAtMjAuNzQtNy45NSw4LDgsMCwxLDAtMS43NiwxNS45QTI4LjExLDI4LjExLDAsMSwxLDEzNiwxODBWMTI3LjY3QzEzNiwxMDAuODgsMTgxLjY5LDU2LDIwOSw1NmM2Ljk1LDAsOS42NiwyLjQ2LDExLjEsNC4zQzIyOS4wNSw3MS44NiwyMjAuMjgsMTA2LjM5LDIxNy40LDExNy43NFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzIuNyw1MC40OEMyMjksNDUuNywyMjEuODQsNDAsMjA5LDQwYy0xNi44NSwwLTM4LjQ2LDExLjI4LTU3LjgxLDMwLjE2QTE0MC4wNywxNDAuMDcsMCwwLDAsMTM2LDg3LjUzVjU2YTgsOCwwLDAsMC0xNiwwVjg3LjUzYTE0MC4wNywxNDAuMDcsMCwwLDAtMTUuMTUtMTcuMzdDODUuNDksNTEuMjgsNjMuODgsNDAsNDcsNDAsMzQuMTYsNDAsMjcsNDUuNywyMy4zLDUwLjQ4Yy02LjgyLDguNzctMTIuMTgsMjQuMDgtLjIxLDcxLjIsNi4wNSwyMy44MywxOS41MSwzMywzMC42MywzNi40MkE0NCw0NCwwLDAsMCwxMjgsMjA1LjI3YTQ0LDQ0LDAsMCwwLDc0LjI4LTQ3LjE3YzExLjEyLTMuNCwyNC41Ny0xMi41OSwzMC42My0zNi40MkMyMzkuNjMsOTUuMjQsMjQ0Ljg1LDY2LjEsMjMyLjcsNTAuNDhaTTkyLDIwOEEyOC4xMiwyOC4xMiwwLDAsMSw4OC44NiwxNTJhOCw4LDAsMSwwLTEuNzYtMTUuOUE0My42NCw0My42NCwwLDAsMCw2Ni4zNiwxNDRjLTguNDMuMDktMjItMy41Ny0yNy43Ni0yNi4yNkMzNS43MiwxMDYuMzksMjcsNzEuODYsMzUuOTQsNjAuMywzNy4zNyw1OC40Niw0MC4wOSw1Niw0Nyw1NmMyNy4yNywwLDczLDQ0Ljg4LDczLDcxLjY3VjE4MEEyOCwyOCwwLDAsMSw5MiwyMDhaTTIxNy40LDExNy43NGMtNS43NywyMi42OS0xOS4zMywyNi4zNC0yNy43NywyNi4yNmE0My42LDQzLjYsMCwwLDAtMjAuNzQtNy45NSw4LDgsMCwxLDAtMS43NiwxNS45QTI4LjExLDI4LjExLDAsMSwxLDEzNiwxODBWMTI3LjY3QzEzNiwxMDAuODgsMTgxLjY5LDU2LDIwOSw1NmM2Ljk1LDAsOS42NiwyLjQ2LDExLjEsNC4zQzIyOS4wNSw3MS44NiwyMjAuMjgsMTA2LjM5LDIxNy40LDExNy43NFoiLz48L3N2Zz4=)}.icon-cake{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzIsMTEyYTI0LDI0LDAsMCwwLTI0LTI0SDEzNlY3OWEzMi4wNiwzMi4wNiwwLDAsMCwyNC0zMWMwLTI4LTI2LjQ0LTQ1LjkxLTI3LjU2LTQ2LjY2YTgsOCwwLDAsMC04Ljg4LDBDMTIyLjQ0LDIuMDksOTYsMjAsOTYsNDhhMzIuMDYsMzIuMDYsMCwwLDAsMjQsMzF2OUg0OGEyNCwyNCwwLDAsMC0yNCwyNHYyMy4zM2E0MC44NCw0MC44NCwwLDAsMCw4LDI0LjI0VjIwMGEyNCwyNCwwLDAsMCwyNCwyNEgyMDBhMjQsMjQsMCwwLDAsMjQtMjRWMTU5LjU3YTQwLjg0LDQwLjg0LDAsMCwwLDgtMjQuMjRaTTExMiw0OGMwLTEzLjU3LDEwLTI0LjQ2LDE2LTI5Ljc5LDYsNS4zMywxNiwxNi4yMiwxNiwyOS43OWExNiwxNiwwLDAsMS0zMiwwWk00MCwxMTJhOCw4LDAsMCwxLDgtOEgyMDhhOCw4LDAsMCwxLDgsOHYyMy4zM2MwLDEzLjI1LTEwLjQ2LDI0LjMxLTIzLjMyLDI0LjY2QTI0LDI0LDAsMCwxLDE2OCwxMzZhOCw4LDAsMCwwLTE2LDAsMjQsMjQsMCwwLDEtNDgsMCw4LDgsMCwwLDAtMTYsMCwyNCwyNCwwLDAsMS0yNC42OCwyNEM1MC40NiwxNTkuNjQsNDAsMTQ4LjU4LDQwLDEzNS4zM1ptMTYwLDk2SDU2YTgsOCwwLDAsMS04LThWMTcyLjU2QTM4Ljc3LDM4Ljc3LDAsMCwwLDYyLjg4LDE3NmEzOS42OSwzOS42OSwwLDAsMCwyOS0xMS4zMUE0MC4zNiw0MC4zNiwwLDAsMCw5NiwxNjBhNDAsNDAsMCwwLDAsNjQsMCw0MC4zNiw0MC4zNiwwLDAsMCw0LjEzLDQuNjdBMzkuNjcsMzkuNjcsMCwwLDAsMTkyLDE3NmMuMzgsMCwuNzYsMCwxLjE0LDBBMzguNzcsMzguNzcsMCwwLDAsMjA4LDE3Mi41NlYyMDBBOCw4LDAsMCwxLDIwMCwyMDhaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzIsMTEyYTI0LDI0LDAsMCwwLTI0LTI0SDEzNlY3OWEzMi4wNiwzMi4wNiwwLDAsMCwyNC0zMWMwLTI4LTI2LjQ0LTQ1LjkxLTI3LjU2LTQ2LjY2YTgsOCwwLDAsMC04Ljg4LDBDMTIyLjQ0LDIuMDksOTYsMjAsOTYsNDhhMzIuMDYsMzIuMDYsMCwwLDAsMjQsMzF2OUg0OGEyNCwyNCwwLDAsMC0yNCwyNHYyMy4zM2E0MC44NCw0MC44NCwwLDAsMCw4LDI0LjI0VjIwMGEyNCwyNCwwLDAsMCwyNCwyNEgyMDBhMjQsMjQsMCwwLDAsMjQtMjRWMTU5LjU3YTQwLjg0LDQwLjg0LDAsMCwwLDgtMjQuMjRaTTExMiw0OGMwLTEzLjU3LDEwLTI0LjQ2LDE2LTI5Ljc5LDYsNS4zMywxNiwxNi4yMiwxNiwyOS43OWExNiwxNiwwLDAsMS0zMiwwWk00MCwxMTJhOCw4LDAsMCwxLDgtOEgyMDhhOCw4LDAsMCwxLDgsOHYyMy4zM2MwLDEzLjI1LTEwLjQ2LDI0LjMxLTIzLjMyLDI0LjY2QTI0LDI0LDAsMCwxLDE2OCwxMzZhOCw4LDAsMCwwLTE2LDAsMjQsMjQsMCwwLDEtNDgsMCw4LDgsMCwwLDAtMTYsMCwyNCwyNCwwLDAsMS0yNC42OCwyNEM1MC40NiwxNTkuNjQsNDAsMTQ4LjU4LDQwLDEzNS4zM1ptMTYwLDk2SDU2YTgsOCwwLDAsMS04LThWMTcyLjU2QTM4Ljc3LDM4Ljc3LDAsMCwwLDYyLjg4LDE3NmEzOS42OSwzOS42OSwwLDAsMCwyOS0xMS4zMUE0MC4zNiw0MC4zNiwwLDAsMCw5NiwxNjBhNDAsNDAsMCwwLDAsNjQsMCw0MC4zNiw0MC4zNiwwLDAsMCw0LjEzLDQuNjdBMzkuNjcsMzkuNjcsMCwwLDAsMTkyLDE3NmMuMzgsMCwuNzYsMCwxLjE0LDBBMzguNzcsMzguNzcsMCwwLDAsMjA4LDE3Mi41NlYyMDBBOCw4LDAsMCwxLDIwMCwyMDhaIi8+PC9zdmc+)}.icon-chat{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNDhINDBBMTYsMTYsMCwwLDAsMjQsNjRWMjI0YTE1Ljg0LDE1Ljg0LDAsMCwwLDkuMjUsMTQuNUExNi4wNSwxNi4wNSwwLDAsMCw0MCwyNDBhMTUuODksMTUuODksMCwwLDAsMTAuMjUtMy43OGwuMDktLjA3TDgzLDIwOEgyMTZhMTYsMTYsMCwwLDAsMTYtMTZWNjRBMTYsMTYsMCwwLDAsMjE2LDQ4Wk00MCwyMjRoMFpNMjE2LDE5Mkg4MGE4LDgsMCwwLDAtNS4yMywxLjk1TDQwLDIyNFY2NEgyMTZaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNDhINDBBMTYsMTYsMCwwLDAsMjQsNjRWMjI0YTE1Ljg0LDE1Ljg0LDAsMCwwLDkuMjUsMTQuNUExNi4wNSwxNi4wNSwwLDAsMCw0MCwyNDBhMTUuODksMTUuODksMCwwLDAsMTAuMjUtMy43OGwuMDktLjA3TDgzLDIwOEgyMTZhMTYsMTYsMCwwLDAsMTYtMTZWNjRBMTYsMTYsMCwwLDAsMjE2LDQ4Wk00MCwyMjRoMFpNMjE2LDE5Mkg4MGE4LDgsMCwwLDAtNS4yMywxLjk1TDQwLDIyNFY2NEgyMTZaIi8+PC9zdmc+)}.icon-check-circle{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNzMuNjYsOTguMzRhOCw4LDAsMCwxLDAsMTEuMzJsLTU2LDU2YTgsOCwwLDAsMS0xMS4zMiwwbC0yNC0yNGE4LDgsMCwwLDEsMTEuMzItMTEuMzJMMTEyLDE0OC42OWw1MC4zNC01MC4zNUE4LDgsMCwwLDEsMTczLjY2LDk4LjM0Wk0yMzIsMTI4QTEwNCwxMDQsMCwxLDEsMTI4LDI0LDEwNC4xMSwxMDQuMTEsMCwwLDEsMjMyLDEyOFptLTE2LDBhODgsODgsMCwxLDAtODgsODhBODguMSw4OC4xLDAsMCwwLDIxNiwxMjhaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNzMuNjYsOTguMzRhOCw4LDAsMCwxLDAsMTEuMzJsLTU2LDU2YTgsOCwwLDAsMS0xMS4zMiwwbC0yNC0yNGE4LDgsMCwwLDEsMTEuMzItMTEuMzJMMTEyLDE0OC42OWw1MC4zNC01MC4zNUE4LDgsMCwwLDEsMTczLjY2LDk4LjM0Wk0yMzIsMTI4QTEwNCwxMDQsMCwxLDEsMTI4LDI0LDEwNC4xMSwxMDQuMTEsMCwwLDEsMjMyLDEyOFptLTE2LDBhODgsODgsMCwxLDAtODgsODhBODguMSw4OC4xLDAsMCwwLDIxNiwxMjhaIi8+PC9zdmc+)}.icon-clock{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsMjRBMTA0LDEwNCwwLDEsMCwyMzIsMTI4LDEwNC4xMSwxMDQuMTEsMCwwLDAsMTI4LDI0Wm0wLDE5MmE4OCw4OCwwLDEsMSw4OC04OEE4OC4xLDg4LjEsMCwwLDEsMTI4LDIxNlptNjQtODhhOCw4LDAsMCwxLTgsOEgxMjhhOCw4LDAsMCwxLTgtOFY3MmE4LDgsMCwwLDEsMTYsMHY0OGg0OEE4LDgsMCwwLDEsMTkyLDEyOFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsMjRBMTA0LDEwNCwwLDEsMCwyMzIsMTI4LDEwNC4xMSwxMDQuMTEsMCwwLDAsMTI4LDI0Wm0wLDE5MmE4OCw4OCwwLDEsMSw4OC04OEE4OC4xLDg4LjEsMCwwLDEsMTI4LDIxNlptNjQtODhhOCw4LDAsMCwxLTgsOEgxMjhhOCw4LDAsMCwxLTgtOFY3MmE4LDgsMCwwLDEsMTYsMHY0OGg0OEE4LDgsMCwwLDEsMTkyLDEyOFoiLz48L3N2Zz4=)}.icon-crosshair{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzIsMTIwaC04LjM0QTk2LjE0LDk2LjE0LDAsMCwwLDEzNiwzMi4zNFYyNGE4LDgsMCwwLDAtMTYsMHY4LjM0QTk2LjE0LDk2LjE0LDAsMCwwLDMyLjM0LDEyMEgyNGE4LDgsMCwwLDAsMCwxNmg4LjM0QTk2LjE0LDk2LjE0LDAsMCwwLDEyMCwyMjMuNjZWMjMyYTgsOCwwLDAsMCwxNiwwdi04LjM0QTk2LjE0LDk2LjE0LDAsMCwwLDIyMy42NiwxMzZIMjMyYTgsOCwwLDAsMCwwLTE2Wm0tOTYsODcuNlYyMDBhOCw4LDAsMCwwLTE2LDB2Ny42QTgwLjE1LDgwLjE1LDAsMCwxLDQ4LjQsMTM2SDU2YTgsOCwwLDAsMCwwLTE2SDQ4LjRBODAuMTUsODAuMTUsMCwwLDEsMTIwLDQ4LjRWNTZhOCw4LDAsMCwwLDE2LDBWNDguNEE4MC4xNSw4MC4xNSwwLDAsMSwyMDcuNiwxMjBIMjAwYTgsOCwwLDAsMCwwLDE2aDcuNkE4MC4xNSw4MC4xNSwwLDAsMSwxMzYsMjA3LjZaTTEyOCw4OGE0MCw0MCwwLDEsMCw0MCw0MEE0MCw0MCwwLDAsMCwxMjgsODhabTAsNjRhMjQsMjQsMCwxLDEsMjQtMjRBMjQsMjQsMCwwLDEsMTI4LDE1MloiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzIsMTIwaC04LjM0QTk2LjE0LDk2LjE0LDAsMCwwLDEzNiwzMi4zNFYyNGE4LDgsMCwwLDAtMTYsMHY4LjM0QTk2LjE0LDk2LjE0LDAsMCwwLDMyLjM0LDEyMEgyNGE4LDgsMCwwLDAsMCwxNmg4LjM0QTk2LjE0LDk2LjE0LDAsMCwwLDEyMCwyMjMuNjZWMjMyYTgsOCwwLDAsMCwxNiwwdi04LjM0QTk2LjE0LDk2LjE0LDAsMCwwLDIyMy42NiwxMzZIMjMyYTgsOCwwLDAsMCwwLTE2Wm0tOTYsODcuNlYyMDBhOCw4LDAsMCwwLTE2LDB2Ny42QTgwLjE1LDgwLjE1LDAsMCwxLDQ4LjQsMTM2SDU2YTgsOCwwLDAsMCwwLTE2SDQ4LjRBODAuMTUsODAuMTUsMCwwLDEsMTIwLDQ4LjRWNTZhOCw4LDAsMCwwLDE2LDBWNDguNEE4MC4xNSw4MC4xNSwwLDAsMSwyMDcuNiwxMjBIMjAwYTgsOCwwLDAsMCwwLDE2aDcuNkE4MC4xNSw4MC4xNSwwLDAsMSwxMzYsMjA3LjZaTTEyOCw4OGE0MCw0MCwwLDEsMCw0MCw0MEE0MCw0MCwwLDAsMCwxMjgsODhabTAsNjRhMjQsMjQsMCwxLDEsMjQtMjRBMjQsMjQsMCwwLDEsMTI4LDE1MloiLz48L3N2Zz4=)}.icon-dots-six{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik03Miw5MkExMiwxMiwwLDEsMSw2MCw4MCwxMiwxMiwwLDAsMSw3Miw5MlptNTYtMTJhMTIsMTIsMCwxLDAsMTIsMTJBMTIsMTIsMCwwLDAsMTI4LDgwWm02OCwyNGExMiwxMiwwLDEsMC0xMi0xMkExMiwxMiwwLDAsMCwxOTYsMTA0Wk02MCwxNTJhMTIsMTIsMCwxLDAsMTIsMTJBMTIsMTIsMCwwLDAsNjAsMTUyWm02OCwwYTEyLDEyLDAsMSwwLDEyLDEyQTEyLDEyLDAsMCwwLDEyOCwxNTJabTY4LDBhMTIsMTIsMCwxLDAsMTIsMTJBMTIsMTIsMCwwLDAsMTk2LDE1MloiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik03Miw5MkExMiwxMiwwLDEsMSw2MCw4MCwxMiwxMiwwLDAsMSw3Miw5MlptNTYtMTJhMTIsMTIsMCwxLDAsMTIsMTJBMTIsMTIsMCwwLDAsMTI4LDgwWm02OCwyNGExMiwxMiwwLDEsMC0xMi0xMkExMiwxMiwwLDAsMCwxOTYsMTA0Wk02MCwxNTJhMTIsMTIsMCwxLDAsMTIsMTJBMTIsMTIsMCwwLDAsNjAsMTUyWm02OCwwYTEyLDEyLDAsMSwwLDEyLDEyQTEyLDEyLDAsMCwwLDEyOCwxNTJabTY4LDBhMTIsMTIsMCwxLDAsMTIsMTJBMTIsMTIsMCwwLDAsMTk2LDE1MloiLz48L3N2Zz4=)}.icon-envelope{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsNDhIMzJhOCw4LDAsMCwwLTgsOFYxOTJhMTYsMTYsMCwwLDAsMTYsMTZIMjE2YTE2LDE2LDAsMCwwLDE2LTE2VjU2QTgsOCwwLDAsMCwyMjQsNDhabS05Niw4NS4xNUw1Mi41Nyw2NEgyMDMuNDNaTTk4LjcxLDEyOCw0MCwxODEuODFWNzQuMTlabTExLjg0LDEwLjg1LDEyLDExLjA1YTgsOCwwLDAsMCwxMC44MiwwbDEyLTExLjA1LDU4LDUzLjE1SDUyLjU3Wk0xNTcuMjksMTI4LDIxNiw3NC4xOFYxODEuODJaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsNDhIMzJhOCw4LDAsMCwwLTgsOFYxOTJhMTYsMTYsMCwwLDAsMTYsMTZIMjE2YTE2LDE2LDAsMCwwLDE2LTE2VjU2QTgsOCwwLDAsMCwyMjQsNDhabS05Niw4NS4xNUw1Mi41Nyw2NEgyMDMuNDNaTTk4LjcxLDEyOCw0MCwxODEuODFWNzQuMTlabTExLjg0LDEwLjg1LDEyLDExLjA1YTgsOCwwLDAsMCwxMC44MiwwbDEyLTExLjA1LDU4LDUzLjE1SDUyLjU3Wk0xNTcuMjksMTI4LDIxNiw3NC4xOFYxODEuODJaIi8+PC9zdmc+)}.icon-facebook-logo{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsMjRBMTA0LDEwNCwwLDEsMCwyMzIsMTI4LDEwNC4xMSwxMDQuMTEsMCwwLDAsMTI4LDI0Wm04LDE5MS42M1YxNTJoMjRhOCw4LDAsMCwwLDAtMTZIMTM2VjExMmExNiwxNiwwLDAsMSwxNi0xNmgxNmE4LDgsMCwwLDAsMC0xNkgxNTJhMzIsMzIsMCwwLDAtMzIsMzJ2MjRIOTZhOCw4LDAsMCwwLDAsMTZoMjR2NjMuNjNhODgsODgsMCwxLDEsMTYsMFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsMjRBMTA0LDEwNCwwLDEsMCwyMzIsMTI4LDEwNC4xMSwxMDQuMTEsMCwwLDAsMTI4LDI0Wm04LDE5MS42M1YxNTJoMjRhOCw4LDAsMCwwLDAtMTZIMTM2VjExMmExNiwxNiwwLDAsMSwxNi0xNmgxNmE4LDgsMCwwLDAsMC0xNkgxNTJhMzIsMzIsMCwwLDAtMzIsMzJ2MjRIOTZhOCw4LDAsMCwwLDAsMTZoMjR2NjMuNjNhODgsODgsMCwxLDEsMTYsMFoiLz48L3N2Zz4=)}.icon-folder{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNzJIMTMxLjMxTDEwNCw0NC42OUExNS44NiwxNS44NiwwLDAsMCw5Mi42OSw0MEg0MEExNiwxNiwwLDAsMCwyNCw1NlYyMDAuNjJBMTUuNCwxNS40LDAsMCwwLDM5LjM4LDIxNkgyMTYuODlBMTUuMTMsMTUuMTMsMCwwLDAsMjMyLDIwMC44OVY4OEExNiwxNiwwLDAsMCwyMTYsNzJaTTQwLDU2SDkyLjY5bDE2LDE2SDQwWk0yMTYsMjAwSDQwVjg4SDIxNloiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNzJIMTMxLjMxTDEwNCw0NC42OUExNS44NiwxNS44NiwwLDAsMCw5Mi42OSw0MEg0MEExNiwxNiwwLDAsMCwyNCw1NlYyMDAuNjJBMTUuNCwxNS40LDAsMCwwLDM5LjM4LDIxNkgyMTYuODlBMTUuMTMsMTUuMTMsMCwwLDAsMjMyLDIwMC44OVY4OEExNiwxNiwwLDAsMCwyMTYsNzJaTTQwLDU2SDkyLjY5bDE2LDE2SDQwWk0yMTYsMjAwSDQwVjg4SDIxNloiLz48L3N2Zz4=)}.icon-google-logo{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsMTI4YTk2LDk2LDAsMSwxLTIxLjk1LTYxLjA5LDgsOCwwLDEsMS0xMi4zMywxMC4xOEE4MCw4MCwwLDEsMCwyMDcuNiwxMzZIMTI4YTgsOCwwLDAsMSwwLTE2aDg4QTgsOCwwLDAsMSwyMjQsMTI4WiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsMTI4YTk2LDk2LDAsMSwxLTIxLjk1LTYxLjA5LDgsOCwwLDEsMS0xMi4zMywxMC4xOEE4MCw4MCwwLDEsMCwyMDcuNiwxMzZIMTI4YTgsOCwwLDAsMSwwLTE2aDg4QTgsOCwwLDAsMSwyMjQsMTI4WiIvPjwvc3ZnPg==)}.icon-hand-heart{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzAuMzMsMTQxLjA2YTI0LjM0LDI0LjM0LDAsMCwwLTE4LjYxLTQuNzdDMjMwLjUsMTE3LjMzLDI0MCw5OC40OCwyNDAsODBjMC0yNi40Ny0yMS4yOS00OC00Ny40Ni00OEE0Ny41OCw0Ny41OCwwLDAsMCwxNTYsNDguNzUsNDcuNTgsNDcuNTgsMCwwLDAsMTE5LjQ2LDMyQzkzLjI5LDMyLDcyLDUzLjUzLDcyLDgwYzAsMTEsMy4yNCwyMS42OSwxMC4wNiwzM2EzMS44NywzMS44NywwLDAsMC0xNC43NSw4LjRMNDQuNjksMTQ0SDE2QTE2LDE2LDAsMCwwLDAsMTYwdjQwYTE2LDE2LDAsMCwwLDE2LDE2SDEyMGE3LjkzLDcuOTMsMCwwLDAsMS45NC0uMjRsNjQtMTZhNi45NCw2Ljk0LDAsMCwwLDEuMTktLjRMMjI2LDE4Mi44MmwuNDQtLjJhMjQuNiwyNC42LDAsMCwwLDMuOTMtNDEuNTZaTTExOS40Niw0OEEzMS4xNSwzMS4xNSwwLDAsMSwxNDguNiw2N2E4LDgsMCwwLDAsMTQuOCwwLDMxLjE1LDMxLjE1LDAsMCwxLDI5LjE0LTE5QzIwOS41OSw0OCwyMjQsNjIuNjUsMjI0LDgwYzAsMTkuNTEtMTUuNzksNDEuNTgtNDUuNjYsNjMuOWwtMTEuMDksMi41NUEyOCwyOCwwLDAsMCwxNDAsMTEySDEwMC42OEM5Mi4wNSwxMDAuMzYsODgsOTAuMTIsODgsODAsODgsNjIuNjUsMTAyLjQxLDQ4LDExOS40Niw0OFpNMTYsMTYwSDQwdjQwSDE2Wm0yMDMuNDMsOC4yMS0zOCwxNi4xOEwxMTksMjAwSDU2VjE1NS4zMWwyMi42My0yMi42MkExNS44NiwxNS44NiwwLDAsMSw4OS45NCwxMjhIMTQwYTEyLDEyLDAsMCwxLDAsMjRIMTEyYTgsOCwwLDAsMCwwLDE2aDMyYTguMzIsOC4zMiwwLDAsMCwxLjc5LS4ybDY3LTE1LjQxLjMxLS4wOGE4LjYsOC42LDAsMCwxLDYuMywxNS45WiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzAuMzMsMTQxLjA2YTI0LjM0LDI0LjM0LDAsMCwwLTE4LjYxLTQuNzdDMjMwLjUsMTE3LjMzLDI0MCw5OC40OCwyNDAsODBjMC0yNi40Ny0yMS4yOS00OC00Ny40Ni00OEE0Ny41OCw0Ny41OCwwLDAsMCwxNTYsNDguNzUsNDcuNTgsNDcuNTgsMCwwLDAsMTE5LjQ2LDMyQzkzLjI5LDMyLDcyLDUzLjUzLDcyLDgwYzAsMTEsMy4yNCwyMS42OSwxMC4wNiwzM2EzMS44NywzMS44NywwLDAsMC0xNC43NSw4LjRMNDQuNjksMTQ0SDE2QTE2LDE2LDAsMCwwLDAsMTYwdjQwYTE2LDE2LDAsMCwwLDE2LDE2SDEyMGE3LjkzLDcuOTMsMCwwLDAsMS45NC0uMjRsNjQtMTZhNi45NCw2Ljk0LDAsMCwwLDEuMTktLjRMMjI2LDE4Mi44MmwuNDQtLjJhMjQuNiwyNC42LDAsMCwwLDMuOTMtNDEuNTZaTTExOS40Niw0OEEzMS4xNSwzMS4xNSwwLDAsMSwxNDguNiw2N2E4LDgsMCwwLDAsMTQuOCwwLDMxLjE1LDMxLjE1LDAsMCwxLDI5LjE0LTE5QzIwOS41OSw0OCwyMjQsNjIuNjUsMjI0LDgwYzAsMTkuNTEtMTUuNzksNDEuNTgtNDUuNjYsNjMuOWwtMTEuMDksMi41NUEyOCwyOCwwLDAsMCwxNDAsMTEySDEwMC42OEM5Mi4wNSwxMDAuMzYsODgsOTAuMTIsODgsODAsODgsNjIuNjUsMTAyLjQxLDQ4LDExOS40Niw0OFpNMTYsMTYwSDQwdjQwSDE2Wm0yMDMuNDMsOC4yMS0zOCwxNi4xOEwxMTksMjAwSDU2VjE1NS4zMWwyMi42My0yMi42MkExNS44NiwxNS44NiwwLDAsMSw4OS45NCwxMjhIMTQwYTEyLDEyLDAsMCwxLDAsMjRIMTEyYTgsOCwwLDAsMCwwLDE2aDMyYTguMzIsOC4zMiwwLDAsMCwxLjc5LS4ybDY3LTE1LjQxLjMxLS4wOGE4LjYsOC42LDAsMCwxLDYuMywxNS45WiIvPjwvc3ZnPg==)}.icon-hourglass{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDAsNzUuNjRWNDBhMTYsMTYsMCwwLDAtMTYtMTZINzJBMTYsMTYsMCwwLDAsNTYsNDBWNzZhMTYuMDcsMTYuMDcsMCwwLDAsNi40LDEyLjhMMTE0LjY3LDEyOCw2Mi40LDE2Ny4yQTE2LjA3LDE2LjA3LDAsMCwwLDU2LDE4MHYzNmExNiwxNiwwLDAsMCwxNiwxNkgxODRhMTYsMTYsMCwwLDAsMTYtMTZWMTgwLjM2YTE2LjA5LDE2LjA5LDAsMCwwLTYuMzUtMTIuNzdMMTQxLjI3LDEyOGw1Mi4zOC0zOS42QTE2LjA1LDE2LjA1LDAsMCwwLDIwMCw3NS42NFpNMTg0LDIxNkg3MlYxODBsNTYtNDIsNTYsNDIuMzVabTAtMTQwLjM2TDEyOCwxMTgsNzIsNzZWNDBIMTg0WiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDAsNzUuNjRWNDBhMTYsMTYsMCwwLDAtMTYtMTZINzJBMTYsMTYsMCwwLDAsNTYsNDBWNzZhMTYuMDcsMTYuMDcsMCwwLDAsNi40LDEyLjhMMTE0LjY3LDEyOCw2Mi40LDE2Ny4yQTE2LjA3LDE2LjA3LDAsMCwwLDU2LDE4MHYzNmExNiwxNiwwLDAsMCwxNiwxNkgxODRhMTYsMTYsMCwwLDAsMTYtMTZWMTgwLjM2YTE2LjA5LDE2LjA5LDAsMCwwLTYuMzUtMTIuNzdMMTQxLjI3LDEyOGw1Mi4zOC0zOS42QTE2LjA1LDE2LjA1LDAsMCwwLDIwMCw3NS42NFpNMTg0LDIxNkg3MlYxODBsNTYtNDIsNTYsNDIuMzVabTAtMTQwLjM2TDEyOCwxMTgsNzIsNzZWNDBIMTg0WiIvPjwvc3ZnPg==)}.icon-instagram-logo{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsODBhNDgsNDgsMCwxLDAsNDgsNDhBNDguMDUsNDguMDUsMCwwLDAsMTI4LDgwWm0wLDgwYTMyLDMyLDAsMSwxLDMyLTMyQTMyLDMyLDAsMCwxLDEyOCwxNjBaTTE3NiwyNEg4MEE1Ni4wNiw1Ni4wNiwwLDAsMCwyNCw4MHY5NmE1Ni4wNiw1Ni4wNiwwLDAsMCw1Niw1Nmg5NmE1Ni4wNiw1Ni4wNiwwLDAsMCw1Ni01NlY4MEE1Ni4wNiw1Ni4wNiwwLDAsMCwxNzYsMjRabTQwLDE1MmE0MCw0MCwwLDAsMS00MCw0MEg4MGE0MCw0MCwwLDAsMS00MC00MFY4MEE0MCw0MCwwLDAsMSw4MCw0MGg5NmE0MCw0MCwwLDAsMSw0MCw0MFpNMTkyLDc2YTEyLDEyLDAsMSwxLTEyLTEyQTEyLDEyLDAsMCwxLDE5Miw3NloiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsODBhNDgsNDgsMCwxLDAsNDgsNDhBNDguMDUsNDguMDUsMCwwLDAsMTI4LDgwWm0wLDgwYTMyLDMyLDAsMSwxLDMyLTMyQTMyLDMyLDAsMCwxLDEyOCwxNjBaTTE3NiwyNEg4MEE1Ni4wNiw1Ni4wNiwwLDAsMCwyNCw4MHY5NmE1Ni4wNiw1Ni4wNiwwLDAsMCw1Niw1Nmg5NmE1Ni4wNiw1Ni4wNiwwLDAsMCw1Ni01NlY4MEE1Ni4wNiw1Ni4wNiwwLDAsMCwxNzYsMjRabTQwLDE1MmE0MCw0MCwwLDAsMS00MCw0MEg4MGE0MCw0MCwwLDAsMS00MC00MFY4MEE0MCw0MCwwLDAsMSw4MCw0MGg5NmE0MCw0MCwwLDAsMSw0MCw0MFpNMTkyLDc2YTEyLDEyLDAsMSwxLTEyLTEyQTEyLDEyLDAsMCwxLDE5Miw3NloiLz48L3N2Zz4=)}.icon-link{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDAsODguMjNhNTQuNDMsNTQuNDMsMCwwLDEtMTYsMzdMMTg5LjI1LDE2MGE1NC4yNyw1NC4yNywwLDAsMS0zOC42MywxNmgtLjA1QTU0LjYzLDU0LjYzLDAsMCwxLDk2LDExOS44NGE4LDgsMCwwLDEsMTYsLjQ1QTM4LjYyLDM4LjYyLDAsMCwwLDE1MC41OCwxNjBoMGEzOC4zOSwzOC4zOSwwLDAsMCwyNy4zMS0xMS4zMWwzNC43NS0zNC43NWEzOC42MywzOC42MywwLDAsMC01NC42My01NC42M2wtMTEsMTFBOCw4LDAsMCwxLDEzNS43LDU5bDExLTExQTU0LjY1LDU0LjY1LDAsMCwxLDIyNCw0OCw1NC44Niw1NC44NiwwLDAsMSwyNDAsODguMjNaTTEwOSwxODUuNjZsLTExLDExQTM4LjQxLDM4LjQxLDAsMCwxLDcwLjYsMjA4aDBhMzguNjMsMzguNjMsMCwwLDEtMjcuMjktNjUuOTRMNzgsMTA3LjMxQTM4LjYzLDM4LjYzLDAsMCwxLDE0NCwxMzUuNzFhOCw4LDAsMCwwLDE2LC40NUE1NC44Niw1NC44NiwwLDAsMCwxNDQsOTZhNTQuNjUsNTQuNjUsMCwwLDAtNzcuMjcsMEwzMiwxMzAuNzVBNTQuNjIsNTQuNjIsMCwwLDAsNzAuNTYsMjI0aDBhNTQuMjgsNTQuMjgsMCwwLDAsMzguNjQtMTZsMTEtMTFBOCw4LDAsMCwwLDEwOSwxODUuNjZaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDAsODguMjNhNTQuNDMsNTQuNDMsMCwwLDEtMTYsMzdMMTg5LjI1LDE2MGE1NC4yNyw1NC4yNywwLDAsMS0zOC42MywxNmgtLjA1QTU0LjYzLDU0LjYzLDAsMCwxLDk2LDExOS44NGE4LDgsMCwwLDEsMTYsLjQ1QTM4LjYyLDM4LjYyLDAsMCwwLDE1MC41OCwxNjBoMGEzOC4zOSwzOC4zOSwwLDAsMCwyNy4zMS0xMS4zMWwzNC43NS0zNC43NWEzOC42MywzOC42MywwLDAsMC01NC42My01NC42M2wtMTEsMTFBOCw4LDAsMCwxLDEzNS43LDU5bDExLTExQTU0LjY1LDU0LjY1LDAsMCwxLDIyNCw0OCw1NC44Niw1NC44NiwwLDAsMSwyNDAsODguMjNaTTEwOSwxODUuNjZsLTExLDExQTM4LjQxLDM4LjQxLDAsMCwxLDcwLjYsMjA4aDBhMzguNjMsMzguNjMsMCwwLDEtMjcuMjktNjUuOTRMNzgsMTA3LjMxQTM4LjYzLDM4LjYzLDAsMCwxLDE0NCwxMzUuNzFhOCw4LDAsMCwwLDE2LC40NUE1NC44Niw1NC44NiwwLDAsMCwxNDQsOTZhNTQuNjUsNTQuNjUsMCwwLDAtNzcuMjcsMEwzMiwxMzAuNzVBNTQuNjIsNTQuNjIsMCwwLDAsNzAuNTYsMjI0aDBhNTQuMjgsNTQuMjgsMCwwLDAsMzguNjQtMTZsMTEtMTFBOCw4LDAsMCwwLDEwOSwxODUuNjZaIi8+PC9zdmc+)}.icon-magic-wand{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik00OCw2NGE4LDgsMCwwLDEsOC04SDcyVjQwYTgsOCwwLDAsMSwxNiwwVjU2aDE2YTgsOCwwLDAsMSwwLDE2SDg4Vjg4YTgsOCwwLDAsMS0xNiwwVjcySDU2QTgsOCwwLDAsMSw0OCw2NFpNMTg0LDE5MmgtOHYtOGE4LDgsMCwwLDAtMTYsMHY4aC04YTgsOCwwLDAsMCwwLDE2aDh2OGE4LDgsMCwwLDAsMTYsMHYtOGg4YTgsOCwwLDAsMCwwLTE2Wm01Ni00OEgyMjRWMTI4YTgsOCwwLDAsMC0xNiwwdjE2SDE5MmE4LDgsMCwwLDAsMCwxNmgxNnYxNmE4LDgsMCwwLDAsMTYsMFYxNjBoMTZhOCw4LDAsMCwwLDAtMTZaTTIxOS4zMSw4MCw4MCwyMTkuMzFhMTYsMTYsMCwwLDEtMjIuNjIsMEwzNi42OCwxOTguNjNhMTYsMTYsMCwwLDEsMC0yMi42M0wxNzYsMzYuNjlhMTYsMTYsMCwwLDEsMjIuNjMsMGwyMC42OCwyMC42OEExNiwxNiwwLDAsMSwyMTkuMzEsODBabS01NC42MywzMkwxNDQsOTEuMzFsLTk2LDk2TDY4LjY4LDIwOFpNMjA4LDY4LjY5LDE4Ny4zMSw0OGwtMzIsMzJMMTc2LDEwMC42OVoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik00OCw2NGE4LDgsMCwwLDEsOC04SDcyVjQwYTgsOCwwLDAsMSwxNiwwVjU2aDE2YTgsOCwwLDAsMSwwLDE2SDg4Vjg4YTgsOCwwLDAsMS0xNiwwVjcySDU2QTgsOCwwLDAsMSw0OCw2NFpNMTg0LDE5MmgtOHYtOGE4LDgsMCwwLDAtMTYsMHY4aC04YTgsOCwwLDAsMCwwLDE2aDh2OGE4LDgsMCwwLDAsMTYsMHYtOGg4YTgsOCwwLDAsMCwwLTE2Wm01Ni00OEgyMjRWMTI4YTgsOCwwLDAsMC0xNiwwdjE2SDE5MmE4LDgsMCwwLDAsMCwxNmgxNnYxNmE4LDgsMCwwLDAsMTYsMFYxNjBoMTZhOCw4LDAsMCwwLDAtMTZaTTIxOS4zMSw4MCw4MCwyMTkuMzFhMTYsMTYsMCwwLDEtMjIuNjIsMEwzNi42OCwxOTguNjNhMTYsMTYsMCwwLDEsMC0yMi42M0wxNzYsMzYuNjlhMTYsMTYsMCwwLDEsMjIuNjMsMGwyMC42OCwyMC42OEExNiwxNiwwLDAsMSwyMTkuMzEsODBabS01NC42MywzMkwxNDQsOTEuMzFsLTk2LDk2TDY4LjY4LDIwOFpNMjA4LDY4LjY5LDE4Ny4zMSw0OGwtMzIsMzJMMTc2LDEwMC42OVoiLz48L3N2Zz4=)}.icon-person{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjAsNDBhMzIsMzIsMCwxLDAtMzIsMzJBMzIsMzIsMCwwLDAsMTYwLDQwWk0xMjgsNTZhMTYsMTYsMCwxLDEsMTYtMTZBMTYsMTYsMCwwLDEsMTI4LDU2Wm05MC4zNCw3OC4wNUwxNzMuMTcsODIuODNhMzIsMzIsMCwwLDAtMjQtMTAuODNIMTA2LjgzYTMyLDMyLDAsMCwwLTI0LDEwLjgzTDM3LjY2LDEzNC4wNWEyMCwyMCwwLDAsMCwyOC4xMywyOC40M2wxNi4zLTEzLjA4TDY1LjU1LDIxMi4yOEEyMCwyMCwwLDAsMCwxMDIsMjI4LjhsMjYtNDQuODcsMjYsNDQuODdhMjAsMjAsMCwwLDAsMzYuNDEtMTYuNTJMMTczLjkxLDE0OS40bDE2LjMsMTMuMDhhMjAsMjAsMCwwLDAsMjguMTMtMjguNDNabS0xMS41MSwxNi43N2E0LDQsMCwwLDEtNS42NiwwYy0uMjEtLjItLjQyLS40LS42NS0uNThMMTY1LDEyMS43NkE4LDgsMCwwLDAsMTUyLjI2LDEzMEwxNzUuMTQsMjE3YTcuNzIsNy43MiwwLDAsMCwuNDgsMS4zNSw0LDQsMCwxLDEtNy4yNSwzLjM4LDYuMjUsNi4yNSwwLDAsMC0uMzMtLjYzTDEzNC45MiwxNjRhOCw4LDAsMCwwLTEzLjg0LDBMODgsMjIxLjA1YTYuMjUsNi4yNSwwLDAsMC0uMzMuNjMsNCw0LDAsMCwxLTIuMjYsMi4wNyw0LDQsMCwwLDEtNS01LjQ1LDcuNzIsNy43MiwwLDAsMCwuNDgtMS4zNUwxMDMuNzQsMTMwQTgsOCwwLDAsMCw5MSwxMjEuNzZMNTUuNDgsMTUwLjI0Yy0uMjMuMTgtLjQ0LjM4LS42NS41OGE0LDQsMCwxLDEtNS42Ni01LjY1Yy4xMi0uMTIuMjMtLjI0LjM0LS4zN0w5NC44Myw5My40MWExNiwxNiwwLDAsMSwxMi01LjQxaDQyLjM0YTE2LDE2LDAsMCwxLDEyLDUuNDFsNDUuMzIsNTEuMzljLjExLjEzLjIyLjI1LjM0LjM3QTQsNCwwLDAsMSwyMDYuODMsMTUwLjgyWiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjAsNDBhMzIsMzIsMCwxLDAtMzIsMzJBMzIsMzIsMCwwLDAsMTYwLDQwWk0xMjgsNTZhMTYsMTYsMCwxLDEsMTYtMTZBMTYsMTYsMCwwLDEsMTI4LDU2Wm05MC4zNCw3OC4wNUwxNzMuMTcsODIuODNhMzIsMzIsMCwwLDAtMjQtMTAuODNIMTA2LjgzYTMyLDMyLDAsMCwwLTI0LDEwLjgzTDM3LjY2LDEzNC4wNWEyMCwyMCwwLDAsMCwyOC4xMywyOC40M2wxNi4zLTEzLjA4TDY1LjU1LDIxMi4yOEEyMCwyMCwwLDAsMCwxMDIsMjI4LjhsMjYtNDQuODcsMjYsNDQuODdhMjAsMjAsMCwwLDAsMzYuNDEtMTYuNTJMMTczLjkxLDE0OS40bDE2LjMsMTMuMDhhMjAsMjAsMCwwLDAsMjguMTMtMjguNDNabS0xMS41MSwxNi43N2E0LDQsMCwwLDEtNS42NiwwYy0uMjEtLjItLjQyLS40LS42NS0uNThMMTY1LDEyMS43NkE4LDgsMCwwLDAsMTUyLjI2LDEzMEwxNzUuMTQsMjE3YTcuNzIsNy43MiwwLDAsMCwuNDgsMS4zNSw0LDQsMCwxLDEtNy4yNSwzLjM4LDYuMjUsNi4yNSwwLDAsMC0uMzMtLjYzTDEzNC45MiwxNjRhOCw4LDAsMCwwLTEzLjg0LDBMODgsMjIxLjA1YTYuMjUsNi4yNSwwLDAsMC0uMzMuNjMsNCw0LDAsMCwxLTIuMjYsMi4wNyw0LDQsMCwwLDEtNS01LjQ1LDcuNzIsNy43MiwwLDAsMCwuNDgtMS4zNUwxMDMuNzQsMTMwQTgsOCwwLDAsMCw5MSwxMjEuNzZMNTUuNDgsMTUwLjI0Yy0uMjMuMTgtLjQ0LjM4LS42NS41OGE0LDQsMCwxLDEtNS42Ni01LjY1Yy4xMi0uMTIuMjMtLjI0LjM0LS4zN0w5NC44Myw5My40MWExNiwxNiwwLDAsMSwxMi01LjQxaDQyLjM0YTE2LDE2LDAsMCwxLDEyLDUuNDFsNDUuMzIsNTEuMzljLjExLjEzLjIyLjI1LjM0LjM3QTQsNCwwLDAsMSwyMDYuODMsMTUwLjgyWiIvPjwvc3ZnPg==)}.icon-phone{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIuMzcsMTU4LjQ2bC00Ny4xMS0yMS4xMS0uMTMtLjA2YTE2LDE2LDAsMCwwLTE1LjE3LDEuNCw4LjEyLDguMTIsMCwwLDAtLjc1LjU2TDEzNC44NywxNjBjLTE1LjQyLTcuNDktMzEuMzQtMjMuMjktMzguODMtMzguNTFsMjAuNzgtMjQuNzFjLjItLjI1LjM5LS41LjU3LS43N2ExNiwxNiwwLDAsMCwxLjMyLTE1LjA2bDAtLjEyTDk3LjU0LDMzLjY0YTE2LDE2LDAsMCwwLTE2LjYyLTkuNTJBNTYuMjYsNTYuMjYsMCwwLDAsMzIsODBjMCw3OS40LDY0LjYsMTQ0LDE0NCwxNDRhNTYuMjYsNTYuMjYsMCwwLDAsNTUuODgtNDguOTJBMTYsMTYsMCwwLDAsMjIyLjM3LDE1OC40NlpNMTc2LDIwOEExMjguMTQsMTI4LjE0LDAsMCwxLDQ4LDgwLDQwLjIsNDAuMiwwLDAsMSw4Mi44Nyw0MGEuNjEuNjEsMCwwLDAsMCwuMTJsMjEsNDdMODMuMiwxMTEuODZhNi4xMyw2LjEzLDAsMCwwLS41Ny43NywxNiwxNiwwLDAsMC0xLDE1LjdjOS4wNiwxOC41MywyNy43MywzNy4wNiw0Ni40Niw0Ni4xMWExNiwxNiwwLDAsMCwxNS43NS0xLjE0LDguNDQsOC40NCwwLDAsMCwuNzQtLjU2TDE2OC44OSwxNTJsNDcsMjEuMDVoMHMuMDgsMCwuMTEsMEE0MC4yMSw0MC4yMSwwLDAsMSwxNzYsMjA4WiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIuMzcsMTU4LjQ2bC00Ny4xMS0yMS4xMS0uMTMtLjA2YTE2LDE2LDAsMCwwLTE1LjE3LDEuNCw4LjEyLDguMTIsMCwwLDAtLjc1LjU2TDEzNC44NywxNjBjLTE1LjQyLTcuNDktMzEuMzQtMjMuMjktMzguODMtMzguNTFsMjAuNzgtMjQuNzFjLjItLjI1LjM5LS41LjU3LS43N2ExNiwxNiwwLDAsMCwxLjMyLTE1LjA2bDAtLjEyTDk3LjU0LDMzLjY0YTE2LDE2LDAsMCwwLTE2LjYyLTkuNTJBNTYuMjYsNTYuMjYsMCwwLDAsMzIsODBjMCw3OS40LDY0LjYsMTQ0LDE0NCwxNDRhNTYuMjYsNTYuMjYsMCwwLDAsNTUuODgtNDguOTJBMTYsMTYsMCwwLDAsMjIyLjM3LDE1OC40NlpNMTc2LDIwOEExMjguMTQsMTI4LjE0LDAsMCwxLDQ4LDgwLDQwLjIsNDAuMiwwLDAsMSw4Mi44Nyw0MGEuNjEuNjEsMCwwLDAsMCwuMTJsMjEsNDdMODMuMiwxMTEuODZhNi4xMyw2LjEzLDAsMCwwLS41Ny43NywxNiwxNiwwLDAsMC0xLDE1LjdjOS4wNiwxOC41MywyNy43MywzNy4wNiw0Ni40Niw0Ni4xMWExNiwxNiwwLDAsMCwxNS43NS0xLjE0LDguNDQsOC40NCwwLDAsMCwuNzQtLjU2TDE2OC44OSwxNTJsNDcsMjEuMDVoMHMuMDgsMCwuMTEsMEE0MC4yMSw0MC4yMSwwLDAsMSwxNzYsMjA4WiIvPjwvc3ZnPg==)}.icon-question{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNDAsMTgwYTEyLDEyLDAsMSwxLTEyLTEyQTEyLDEyLDAsMCwxLDE0MCwxODBaTTEyOCw3MmMtMjIuMDYsMC00MCwxNi4xNS00MCwzNnY0YTgsOCwwLDAsMCwxNiwwdi00YzAtMTEsMTAuNzctMjAsMjQtMjBzMjQsOSwyNCwyMC0xMC43NywyMC0yNCwyMGE4LDgsMCwwLDAtOCw4djhhOCw4LDAsMCwwLDE2LDB2LS43MmMxOC4yNC0zLjM1LDMyLTE3LjksMzItMzUuMjhDMTY4LDg4LjE1LDE1MC4wNiw3MiwxMjgsNzJabTEwNCw1NkExMDQsMTA0LDAsMSwxLDEyOCwyNCwxMDQuMTEsMTA0LjExLDAsMCwxLDIzMiwxMjhabS0xNiwwYTg4LDg4LDAsMSwwLTg4LDg4QTg4LjEsODguMSwwLDAsMCwyMTYsMTI4WiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNDAsMTgwYTEyLDEyLDAsMSwxLTEyLTEyQTEyLDEyLDAsMCwxLDE0MCwxODBaTTEyOCw3MmMtMjIuMDYsMC00MCwxNi4xNS00MCwzNnY0YTgsOCwwLDAsMCwxNiwwdi00YzAtMTEsMTAuNzctMjAsMjQtMjBzMjQsOSwyNCwyMC0xMC43NywyMC0yNCwyMGE4LDgsMCwwLDAtOCw4djhhOCw4LDAsMCwwLDE2LDB2LS43MmMxOC4yNC0zLjM1LDMyLTE3LjksMzItMzUuMjhDMTY4LDg4LjE1LDE1MC4wNiw3MiwxMjgsNzJabTEwNCw1NkExMDQsMTA0LDAsMSwxLDEyOCwyNCwxMDQuMTEsMTA0LjExLDAsMCwxLDIzMiwxMjhabS0xNiwwYTg4LDg4LDAsMSwwLTg4LDg4QTg4LjEsODguMSwwLDAsMCwyMTYsMTI4WiIvPjwvc3ZnPg==)}.icon-star{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzkuMTgsOTcuMjZBMTYuMzgsMTYuMzgsMCwwLDAsMjI0LjkyLDg2bC01OS00Ljc2TDE0My4xNCwyNi4xNWExNi4zNiwxNi4zNiwwLDAsMC0zMC4yNywwTDkwLjExLDgxLjIzLDMxLjA4LDg2YTE2LjQ2LDE2LjQ2LDAsMCwwLTkuMzcsMjguODZsNDUsMzguODNMNTMsMjExLjc1YTE2LjM4LDE2LjM4LDAsMCwwLDI0LjUsMTcuODJMMTI4LDE5OC40OWw1MC41MywzMS4wOEExNi40LDE2LjQsMCwwLDAsMjAzLDIxMS43NWwtMTMuNzYtNTguMDcsNDUtMzguODNBMTYuNDMsMTYuNDMsMCwwLDAsMjM5LjE4LDk3LjI2Wm0tMTUuMzQsNS40Ny00OC43LDQyYTgsOCwwLDAsMC0yLjU2LDcuOTFsMTQuODgsNjIuOGEuMzcuMzcsMCwwLDEtLjE3LjQ4Yy0uMTguMTQtLjIzLjExLS4zOCwwbC01NC43Mi0zMy42NWE4LDgsMCwwLDAtOC4zOCwwTDY5LjA5LDIxNS45NGMtLjE1LjA5LS4xOS4xMi0uMzgsMGEuMzcuMzcsMCwwLDEtLjE3LS40OGwxNC44OC02Mi44YTgsOCwwLDAsMC0yLjU2LTcuOTFsLTQ4LjctNDJjLS4xMi0uMS0uMjMtLjE5LS4xMy0uNXMuMTgtLjI3LjMzLS4yOWw2My45Mi01LjE2QTgsOCwwLDAsMCwxMDMsOTEuODZsMjQuNjItNTkuNjFjLjA4LS4xNy4xMS0uMjUuMzUtLjI1cy4yNy4wOC4zNS4yNUwxNTMsOTEuODZhOCw4LDAsMCwwLDYuNzUsNC45Mmw2My45Miw1LjE2Yy4xNSwwLC4yNCwwLC4zMy4yOVMyMjQsMTAyLjYzLDIyMy44NCwxMDIuNzNaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzkuMTgsOTcuMjZBMTYuMzgsMTYuMzgsMCwwLDAsMjI0LjkyLDg2bC01OS00Ljc2TDE0My4xNCwyNi4xNWExNi4zNiwxNi4zNiwwLDAsMC0zMC4yNywwTDkwLjExLDgxLjIzLDMxLjA4LDg2YTE2LjQ2LDE2LjQ2LDAsMCwwLTkuMzcsMjguODZsNDUsMzguODNMNTMsMjExLjc1YTE2LjM4LDE2LjM4LDAsMCwwLDI0LjUsMTcuODJMMTI4LDE5OC40OWw1MC41MywzMS4wOEExNi40LDE2LjQsMCwwLDAsMjAzLDIxMS43NWwtMTMuNzYtNTguMDcsNDUtMzguODNBMTYuNDMsMTYuNDMsMCwwLDAsMjM5LjE4LDk3LjI2Wm0tMTUuMzQsNS40Ny00OC43LDQyYTgsOCwwLDAsMC0yLjU2LDcuOTFsMTQuODgsNjIuOGEuMzcuMzcsMCwwLDEtLjE3LjQ4Yy0uMTguMTQtLjIzLjExLS4zOCwwbC01NC43Mi0zMy42NWE4LDgsMCwwLDAtOC4zOCwwTDY5LjA5LDIxNS45NGMtLjE1LjA5LS4xOS4xMi0uMzgsMGEuMzcuMzcsMCwwLDEtLjE3LS40OGwxNC44OC02Mi44YTgsOCwwLDAsMC0yLjU2LTcuOTFsLTQ4LjctNDJjLS4xMi0uMS0uMjMtLjE5LS4xMy0uNXMuMTgtLjI3LjMzLS4yOWw2My45Mi01LjE2QTgsOCwwLDAsMCwxMDMsOTEuODZsMjQuNjItNTkuNjFjLjA4LS4xNy4xMS0uMjUuMzUtLjI1cy4yNy4wOC4zNS4yNUwxNTMsOTEuODZhOCw4LDAsMCwwLDYuNzUsNC45Mmw2My45Miw1LjE2Yy4xNSwwLC4yNCwwLC4zMy4yOVMyMjQsMTAyLjYzLDIyMy44NCwxMDIuNzNaIi8+PC9zdmc+)}.icon-tiktok-logo{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsNzJhNDguMDUsNDguMDUsMCwwLDEtNDgtNDgsOCw4LDAsMCwwLTgtOEgxMjhhOCw4LDAsMCwwLTgsOFYxNTZhMjAsMjAsMCwxLDEtMjguNTctMTguMDhBOCw4LDAsMCwwLDk2LDEzMC42OVY4OGE4LDgsMCwwLDAtOS40LTcuODhDNTAuOTEsODYuNDgsMjQsMTE5LjEsMjQsMTU2YTc2LDc2LDAsMCwwLDE1MiwwVjExNi4yOUExMDMuMjUsMTAzLjI1LDAsMCwwLDIyNCwxMjhhOCw4LDAsMCwwLDgtOFY4MEE4LDgsMCwwLDAsMjI0LDcyWm0tOCwzOS42NGE4Ny4xOSw4Ny4xOSwwLDAsMS00My4zMy0xNi4xNUE4LDgsMCwwLDAsMTYwLDEwMnY1NGE2MCw2MCwwLDAsMS0xMjAsMGMwLTI1LjksMTYuNjQtNDkuMTMsNDAtNTcuNnYyNy42N0EzNiwzNiwwLDEsMCwxMzYsMTU2VjMyaDI0LjVBNjQuMTQsNjQuMTQsMCwwLDAsMjE2LDg3LjVaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsNzJhNDguMDUsNDguMDUsMCwwLDEtNDgtNDgsOCw4LDAsMCwwLTgtOEgxMjhhOCw4LDAsMCwwLTgsOFYxNTZhMjAsMjAsMCwxLDEtMjguNTctMTguMDhBOCw4LDAsMCwwLDk2LDEzMC42OVY4OGE4LDgsMCwwLDAtOS40LTcuODhDNTAuOTEsODYuNDgsMjQsMTE5LjEsMjQsMTU2YTc2LDc2LDAsMCwwLDE1MiwwVjExNi4yOUExMDMuMjUsMTAzLjI1LDAsMCwwLDIyNCwxMjhhOCw4LDAsMCwwLDgtOFY4MEE4LDgsMCwwLDAsMjI0LDcyWm0tOCwzOS42NGE4Ny4xOSw4Ny4xOSwwLDAsMS00My4zMy0xNi4xNUE4LDgsMCwwLDAsMTYwLDEwMnY1NGE2MCw2MCwwLDAsMS0xMjAsMGMwLTI1LjksMTYuNjQtNDkuMTMsNDAtNTcuNnYyNy42N0EzNiwzNiwwLDEsMCwxMzYsMTU2VjMyaDI0LjVBNjQuMTQsNjQuMTQsMCwwLDAsMjE2LDg3LjVaIi8+PC9zdmc+)}.icon-user{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzAuOTIsMjEyYy0xNS4yMy0yNi4zMy0zOC43LTQ1LjIxLTY2LjA5LTU0LjE2YTcyLDcyLDAsMSwwLTczLjY2LDBDNjMuNzgsMTY2Ljc4LDQwLjMxLDE4NS42NiwyNS4wOCwyMTJhOCw4LDAsMSwwLDEzLjg1LDhjMTguODQtMzIuNTYsNTIuMTQtNTIsODkuMDctNTJzNzAuMjMsMTkuNDQsODkuMDcsNTJhOCw4LDAsMSwwLDEzLjg1LThaTTcyLDk2YTU2LDU2LDAsMSwxLDU2LDU2QTU2LjA2LDU2LjA2LDAsMCwxLDcyLDk2WiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzAuOTIsMjEyYy0xNS4yMy0yNi4zMy0zOC43LTQ1LjIxLTY2LjA5LTU0LjE2YTcyLDcyLDAsMSwwLTczLjY2LDBDNjMuNzgsMTY2Ljc4LDQwLjMxLDE4NS42NiwyNS4wOCwyMTJhOCw4LDAsMSwwLDEzLjg1LDhjMTguODQtMzIuNTYsNTIuMTQtNTIsODkuMDctNTJzNzAuMjMsMTkuNDQsODkuMDcsNTJhOCw4LDAsMSwwLDEzLjg1LThaTTcyLDk2YTU2LDU2LDAsMSwxLDU2LDU2QTU2LjA2LDU2LjA2LDAsMCwxLDcyLDk2WiIvPjwvc3ZnPg==)}.icon-x{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDUuNjYsMTk0LjM0YTgsOCwwLDAsMS0xMS4zMiwxMS4zMkwxMjgsMTM5LjMxLDYxLjY2LDIwNS42NmE4LDgsMCwwLDEtMTEuMzItMTEuMzJMMTE2LjY5LDEyOCw1MC4zNCw2MS42NkE4LDgsMCwwLDEsNjEuNjYsNTAuMzRMMTI4LDExNi42OWw2Ni4zNC02Ni4zNWE4LDgsMCwwLDEsMTEuMzIsMTEuMzJMMTM5LjMxLDEyOFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDUuNjYsMTk0LjM0YTgsOCwwLDAsMS0xMS4zMiwxMS4zMkwxMjgsMTM5LjMxLDYxLjY2LDIwNS42NmE4LDgsMCwwLDEtMTEuMzItMTEuMzJMMTE2LjY5LDEyOCw1MC4zNCw2MS42NkE4LDgsMCwwLDEsNjEuNjYsNTAuMzRMMTI4LDExNi42OWw2Ni4zNC02Ni4zNWE4LDgsMCwwLDEsMTEuMzIsMTEuMzJMMTM5LjMxLDEyOFoiLz48L3N2Zz4=)}.icon-x-circle{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjUuNjYsMTAxLjY2LDEzOS4zMSwxMjhsMjYuMzUsMjYuMzRhOCw4LDAsMCwxLTExLjMyLDExLjMyTDEyOCwxMzkuMzFsLTI2LjM0LDI2LjM1YTgsOCwwLDAsMS0xMS4zMi0xMS4zMkwxMTYuNjksMTI4LDkwLjM0LDEwMS42NmE4LDgsMCwwLDEsMTEuMzItMTEuMzJMMTI4LDExNi42OWwyNi4zNC0yNi4zNWE4LDgsMCwwLDEsMTEuMzIsMTEuMzJaTTIzMiwxMjhBMTA0LDEwNCwwLDEsMSwxMjgsMjQsMTA0LjExLDEwNC4xMSwwLDAsMSwyMzIsMTI4Wm0tMTYsMGE4OCw4OCwwLDEsMC04OCw4OEE4OC4xLDg4LjEsMCwwLDAsMjE2LDEyOFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjUuNjYsMTAxLjY2LDEzOS4zMSwxMjhsMjYuMzUsMjYuMzRhOCw4LDAsMCwxLTExLjMyLDExLjMyTDEyOCwxMzkuMzFsLTI2LjM0LDI2LjM1YTgsOCwwLDAsMS0xMS4zMi0xMS4zMkwxMTYuNjksMTI4LDkwLjM0LDEwMS42NmE4LDgsMCwwLDEsMTEuMzItMTEuMzJMMTI4LDExNi42OWwyNi4zNC0yNi4zNWE4LDgsMCwwLDEsMTEuMzIsMTEuMzJaTTIzMiwxMjhBMTA0LDEwNCwwLDEsMSwxMjgsMjQsMTA0LjExLDEwNC4xMSwwLDAsMSwyMzIsMTI4Wm0tMTYsMGE4OCw4OCwwLDEsMC04OCw4OEE4OC4xLDg4LjEsMCwwLDAsMjE2LDEyOFoiLz48L3N2Zz4=)}
assets/css/nav.min.css
@@ -1 +1 @@
nav{--py:.25rem;--px:1rem;max-width:100%;font-family:var(--heading)}nav,nav a,nav li,nav ol,nav ul{height:var(--height);display:flex;justify-content:var(--justify);align-items:var(--align);gap:var(--gap);flex-wrap:var(--wrap);flex-direction:var(--dir)}ul.socials{--w:1.2em;--height:fit-content;gap:.5rem;display:flex;justify-content:center;align-items:center;flex-wrap:nowrap;flex-direction:row;overflow:auto hidden;touch-action:pan-x;list-style:none}nav:not(:has(>ul)),nav>ul{--justify:flex-start;--align:center;--wrap:nowrap;--w:1em;--dir:row;position:relative;overflow:auto hidden;touch-action:pan-x}nav a{padding:0 var(--px);white-space:nowrap;text-transform:uppercase}nav .current a,nav a.current,nav a:focus,nav a:focus:visited,nav a:hover,nav a:hover:visited{background-color:var(--action-0);color:var(--action-contrast);transition:background-color var(--transition-base),color var(--transition-base)}nav ol,nav ul{list-style:none;margin:0;padding:0}.has-submenu button:hover,nav a:hover{background-color:var(--action-0);color:var(--action-contrast)}.has-submenu button{height:var(--height);width:var(--height);padding:0;border:2px solid var(--base);color:var(--contrast);border-radius:0}.toggle svg{transform:rotate(0);transition:transform var(--timing) var(--function);transition-property:transform,background-color,color}.has-submenu.open>button:not(.notifications,.quick-help) svg,.has-submenu:hover>button:not(.notifications,.quick-help) svg{transform:rotate(900deg)}ul.submenu{--dir:column;--wrap:nowrap;--gap:0;position:absolute;top:100%;left:0;max-height:0;transform:scaleY(0);transform-origin:top;width:max-content;min-width:100%;background-color:var(--overlay-light);border:2px solid var(--overlay-light);transition:all var(--timing) var(--function);box-shadow:var(--shadow-none)}.always ul.submenu{position:relative;top:0;left:0}.submenu li{background-color:var(--overlay-heavy);border:1px solid var(--base-50)}.submenu li:hover{--c:var(--action-rgb);background-color:var(--overlay-heavy)}.submenu a:hover{background-color:transparent}.wp-site-blocks>header ul.submenu{right:0;left:auto}.has-submenu.open>ul.submenu,.has-submenu:hover>ul.submenu{transform:scaleY(1);max-height:1000%;box-shadow:var(--shadow)}nav.fixed.bottom,nav.on-this-page{--dir:row;--gap:0;width:calc(100% - var(--height));left:0;bottom:0;position:fixed;box-shadow:var(--shadow);z-index:var(--zz-top)}nav.fixed.bottom ul{width:100%;--justify:space-between;background-color:var(--base);padding:0 .25rem}nav.fixed a,nav.fixed li{--justify:center;width:100%}nav.fixed.bottom a,nav.fixed.bottom a:visited{color:var(--contrast);font-size:var(--small);padding:0}@media (min-width:768px){nav.fixed.bottom a,nav.fixed.bottom a:visited{font-size:var(--medium)}}nav.fixed.bottom a:focus,nav.fixed.bottom a:focus:visited,nav.fixed.bottom a:hover,nav.fixed.bottom a:hover:visited{color:var(--action-contrast)}.fixed.bottom li{flex:1}nav.always a{padding:0;--justify:center}nav.always .socials{width:100%}nav.always .socials li{width:100%}nav.always li{gap:0;--justify:flex-start}nav.always>ul>li>a{width:80%}nav.always .submenu{width:80%;min-width:80%;box-shadow:none!important;border:2px solid var(--action-0);background-color:rgba(var(--contrast-rgb),var(--rgb-subtle))}nav.always .submenu li{background-color:var(--overlay-light)}nav.always .has-submenu>a,nav.fixed .has-submenu>a{width:80%}.has-submenu>button{width:20%}nav#breadcrumbs{--height:1.5em;--w:20px;width:fit-content;max-width:var(--full);position:absolute;background-color:var(--overlay-medium);font-size:var(--small);padding:.125em;overflow:visible;--gap:0}nav#breadcrumbs li+li::before{content:'/';color:var(--contrast-200)}nav#breadcrumbs li:last-of-type{margin-right:.5em}nav#breadcrumbs a,nav#breadcrumbs span{padding:0 .125rem;white-space:nowrap;height:2em;color:var(--contrast);text-transform:none;width:max-content}nav#breadcrumbs span{display:flex;align-items:center;padding-left:.5em}nav#breadcrumbs a:focus,nav#breadcrumbs a:focus:visited,nav#breadcrumbs a:hover,nav#breadcrumbs a:hover:visited{background-color:transparent;color:var(--action-0)}nav#breadcrumbs a:has(.icon){width:2rem}nav.always{z-index:var(--z-above);position:fixed;width:var(--height);bottom:0;right:0}nav.always.open{width:100vw;height:100vh;padding-bottom:var(--offHeight);background-color:var(--overlay-heavy);backdrop-filter:blur(5px);justify-content:flex-end;flex-direction:column;z-index:var(--z-above)}nav.always>ul{--dir:column;--wrap:nowrap;--justify:flex-start;--align:center;--gap:0;position:relative;right:-300vw;padding:1rem 0 0;width:100vw;height:fit-content;max-height:100%;overflow:hidden auto;transition:right var(--transition-base)}nav.always.open>ul{right:0;transition:right var(--transition-base)}nav.always li{max-inline-size:none;width:100%;height:fit-content;--dir:row;--wrap:wrap}nav.always a{--py:1rem;width:100%;min-height:var(--height)}nav.always>button{position:fixed;bottom:0;right:0;width:var(--height);height:var(--height);border-radius:0;background-color:var(--base);color:var(--contrast);transition:width var(--timing) var(--function);transition-property:width,background-color;box-shadow:var(--shadow)}nav.always>button:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always.open>button{--c:var(--action-rgb);z-index:1000000;width:100%;background-color:var(--overlay-heavy);color:var(--contrast);backdrop-filter:blur(5px)}nav.always.open>button:focus,nav.always.open>button:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always.open>button .list,nav.always>button .x{transform:scale(0);height:0;width:0;position:absolute}nav.always.open>button .x,nav.always>button .list{transform:scale(1);height:32px;width:32px}nav.always .has-submenu.open>.submenu,nav.always .has-submenu:hover>.submenu{height:max-content}nav.always .has-submenu:hover>a,nav.always .submenu>li>a:focus,nav.always .submenu>li>a:hover{background-color:var(--action-0);color:var(--action-contrast)}@media (min-width:768px){nav.always>ul{padding:var(--height) 0 0}}nav.on-this-page{--justify:space-between;max-width:none;z-index:var(--z-6);margin:0;padding:0 .5rem;background-color:var(--overlay-medium);color:var(--base-200)}body:has(nav.fixed) nav.on-this-page{bottom:var(--offHeight)}.on-this-page ul{--justify:flex-start;gap:0;width:100%}.on-this-page li:not(.has){padding:0}nav.letters li{width:100%;max-width:calc(7.69% - 2px)}.on-this-page .active a{--c:var(--action-rgb);background-color:var(--overlay-heavy);color:var(--action-contrast)}@media (min-width:768px){nav.letters li{max-width:none;width:fit-content}nav.letters a,nav.letters li:not(.has){padding:.25rem .66rem}}nav.index{--justify:flex-start;--px:0;background-color:var(--overlay-heavy)}.index ul{--justify:flex-start;width:fit-content}.index li{flex-shrink:0;transform:scaleX(0);transform-origin:right;max-width:0;overflow:hidden;transition:transform var(--timing) var(--function)}.index li.active{transform:scaleX(1);transform-origin:left;width:100%;flex-shrink:1;max-width:fit-content}@media (min-width:768px){.index li.adj{transform:scaleX(1);transform-origin:left;width:100%;flex-shrink:1;max-width:fit-content}}.index a{border-bottom:4px solid transparent}.index .active a{border-color:var(--action-0);color:var(--contrast)}.index .active a:hover,.index a:hover{background-color:var(--action-0);color:var(--action-contrast)}.index label{display:flex;color:var(--contrast);align-items:center;margin:0}.index label button{margin-left:1em}.index.open{--dir:column-reverse;height:calc(100% - 8rem);z-index:var(--z-above);width:100%;background-color:var(--overlay-heavy);backdrop-filter:blur(5px);align-items:flex-end}.index.open label{max-width:90%;margin-top:1rem;margin-right:2rem}.index.open .toggle svg{transform:rotate(45deg)}.index.open ul{--dir:column;--justify:flex-end;height:100%;max-width:100%;width:100%}.index.open li{background-color:transparent;max-width:100%!important;width:100%;height:var(--height);transform:scaleX(1);flex-shrink:1;overflow:visible}.index.open a{--justify:flex-end;background-color:transparent;padding:0 2rem 0 0}.condensed{--dir:row;--wrap:wrap;--height:1.2em;--py:.25rem;--px:.25rem;height:fit-content}.condensed>ul{--wrap:wrap;height:fit-content}.condensed ul{--justify:center;--gap:0}.condensed li{width:fit-content}.condensed li+li::before{content:'·';display:block;padding:0 .25em}nav.condensed a{text-transform:none;white-space:nowrap;border-bottom:2px solid transparent}.condensed a:focus,.condensed a:focus:visited,.condensed a:hover,.condensed a:hover:visited{border-color:var(--action-0)}.dashboard-nav{width:100%;--dir:row;--justify:flex-start;--wrap:nowrap}.wp-site-blocks>header,body>header{--dir:row;position:sticky;top:0;left:0;right:0;height:var(--height);width:100vw;display:flex;justify-content:space-between;align-items:center;padding:0 .5rem;background-color:var(--base);z-index:var(--zz-top);box-shadow:var(--shadow);border-bottom:1px solid var(--action-0)}body>header{justify-content:space-between}header a[rel=home],header>img{position:absolute;width:var(--offHeight);left:calc(50% - (var(--offHeight)/ 2))}header .title{--w:5em;margin:0;position:absolute;width:100%;height:100%;display:flex;justify-content:center;align-items:flex-start;max-inline-size:none}.current-hours{position:sticky;top:var(--height);bottom:unset;width:unset;z-index:100;background-color:var(--action-0);color:var(--action-contrast);box-shadow:var(--shadow);padding:.25rem 1rem;display:flex;justify-content:space-between}.current-hours p{margin:0;display:flex;flex-wrap:wrap;flex:1}.current-hours p+p{justify-content:flex-end}.current-hours a{color:var(--action-contrast)}.current-hours a:hover{color:var(--action-200)}.current-hours b{margin-right:.25rem}.find-us{display:flex;align-items:center;gap:0 .5rem}.find-us a{display:flex;padding:.25rem 1rem;border:1px solid var(--action-contrast);border-radius:var(--innerRadius)}.find-us a:hover{background-color:var(--base);color:var(--contrast);border-color:var(--contrast)}nav.menu{--justify:flex-start}nav.menu a{padding:.5rem .66rem}nav.tabs{--gap:0;--wrap:nowrap;padding-bottom:2px;z-index:var(--z-6);position:fixed;bottom:var(--height);left:var(--doubleHeight);right:var(--doubleHeight)}nav.term-navigation:has([hidden]){display:none}ul.socials a{padding:.5rem}ul.socials a .icon{margin:0}
nav{--py:.25rem;--px:1rem;max-width:100%;font-family:var(--heading)}nav,nav a,nav li,nav ol,nav ul{height:var(--height);display:flex;justify-content:var(--justify);align-items:var(--align);gap:var(--gap);flex-wrap:var(--wrap);flex-direction:var(--dir)}ul.socials{--w:1.2em;--height:fit-content;gap:.5rem;display:flex;justify-content:center;align-items:center;flex-wrap:nowrap;flex-direction:row;overflow:auto hidden;touch-action:pan-x;list-style:none}nav:not(:has(>ul)),nav>ul{--justify:flex-start;--align:center;--wrap:nowrap;--w:1em;--dir:row;position:relative;overflow:auto hidden;touch-action:pan-x}nav a{padding:0 var(--px);white-space:nowrap;text-transform:uppercase}nav .current a,nav a.current,nav a:focus,nav a:focus:visited,nav a:hover,nav a:hover:visited{background-color:var(--action-0);color:var(--action-contrast);transition:background-color var(--transition-base),color var(--transition-base)}nav ol,nav ul{list-style:none;margin:0;padding:0}.has-submenu button:hover,nav a:hover{background-color:var(--action-0);color:var(--action-contrast)}.has-submenu button{height:var(--height);width:var(--height);padding:0;border:2px solid var(--base);color:var(--contrast);border-radius:0}.toggle .icon{transform:rotate(0);transition:transform var(--timing) var(--function);transition-property:transform,background-color,color}.has-submenu.open>button:not(.notifications,.quick-help) .icon,.has-submenu:hover>button:not(.notifications,.quick-help) .icon{transform:rotate(900deg)}ul.submenu{--dir:column;--wrap:nowrap;--gap:0;position:absolute;top:100%;left:0;max-height:0;transform:scaleY(0);transform-origin:top;width:max-content;min-width:100%;background-color:var(--overlay-light);border:2px solid var(--overlay-light);transition:all var(--timing) var(--function);box-shadow:var(--shadow-none)}.always ul.submenu{position:relative;top:0;left:0}.submenu li{background-color:var(--overlay-heavy);border:1px solid var(--base-50)}.submenu li:hover{--c:var(--action-rgb);background-color:var(--overlay-heavy)}.submenu a:hover{background-color:transparent}.wp-site-blocks>header ul.submenu{right:0;left:auto}.has-submenu.open>ul.submenu,.has-submenu:hover>ul.submenu{transform:scaleY(1);max-height:1000%;box-shadow:var(--shadow)}nav.fixed.bottom,nav.on-this-page{--dir:row;--gap:0;width:calc(100% - var(--height));left:0;bottom:0;position:fixed;box-shadow:var(--shadow);z-index:var(--zz-top)}nav.fixed.bottom ul{width:100%;--justify:space-between;background-color:var(--base);padding:0 .25rem}nav.fixed a,nav.fixed li{--justify:center;width:100%}nav.fixed.bottom a,nav.fixed.bottom a:visited{color:var(--contrast);font-size:var(--small);padding:0}@media (min-width:768px){nav.fixed.bottom a,nav.fixed.bottom a:visited{font-size:var(--medium)}}nav.fixed.bottom a:focus,nav.fixed.bottom a:focus:visited,nav.fixed.bottom a:hover,nav.fixed.bottom a:hover:visited{color:var(--action-contrast)}.fixed.bottom li{flex:1}nav.always a{padding:0;--justify:center}nav.always .socials{width:100%}nav.always .socials li{width:100%}nav.always li{gap:0;--justify:flex-start}nav.always>ul>li>a{width:80%}nav.always .submenu{width:80%;min-width:80%;box-shadow:none!important;border:2px solid var(--action-0);background-color:rgba(var(--contrast-rgb),var(--rgb-subtle))}nav.always .submenu li{background-color:var(--overlay-light)}nav.always .has-submenu>a,nav.fixed .has-submenu>a{width:80%}.has-submenu>button{width:20%}nav#breadcrumbs{--height:1.5em;--w:20px;width:fit-content;max-width:var(--full);position:absolute;background-color:var(--overlay-medium);font-size:var(--small);padding:.125em;overflow:visible;--gap:0}nav#breadcrumbs li+li::before{content:'/';color:var(--contrast-200)}nav#breadcrumbs li:last-of-type{margin-right:.5em}nav#breadcrumbs a,nav#breadcrumbs span{padding:0 .125rem;white-space:nowrap;height:2em;color:var(--contrast);text-transform:none;width:max-content}nav#breadcrumbs span{display:flex;align-items:center;padding-left:.5em}nav#breadcrumbs a:focus,nav#breadcrumbs a:focus:visited,nav#breadcrumbs a:hover,nav#breadcrumbs a:hover:visited{background-color:transparent;color:var(--action-0)}nav#breadcrumbs a:has(.icon){width:2rem}nav.always{z-index:var(--z-above);position:fixed;width:var(--height);bottom:0;right:0}nav.always.open{width:100vw;height:100vh;padding-bottom:var(--offHeight);background-color:var(--overlay-heavy);backdrop-filter:blur(5px);justify-content:flex-end;flex-direction:column;z-index:var(--z-above)}nav.always>ul{--dir:column;--wrap:nowrap;--justify:flex-start;--align:center;--gap:0;position:relative;right:-300vw;padding:1rem 0 0;width:100vw;height:fit-content;max-height:100%;overflow:hidden auto;transition:right var(--transition-base)}nav.always.open>ul{right:0;transition:right var(--transition-base)}nav.always li{max-inline-size:none;width:100%;height:fit-content;--dir:row;--wrap:wrap}nav.always a{--py:1rem;width:100%;min-height:var(--height)}nav.always>button{position:fixed;bottom:0;right:0;width:var(--height);height:var(--height);border-radius:0;background-color:var(--base);color:var(--contrast);transition:width var(--timing) var(--function);transition-property:width,background-color;box-shadow:var(--shadow)}nav.always>button:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always.open>button{--c:var(--action-rgb);z-index:1000000;width:100%;background-color:var(--overlay-heavy);color:var(--contrast);backdrop-filter:blur(5px)}nav.always.open>button:focus,nav.always.open>button:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always.open>button .icon-list,nav.always>button .icon-x{transform:scale(0);height:0;width:0;position:absolute}nav.always.open>button .icon-x,nav.always>button .icon-list{transform:scale(1);height:32px;width:32px}nav.always .has-submenu.open>.submenu,nav.always .has-submenu:hover>.submenu{height:max-content}nav.always .has-submenu:hover>a,nav.always .submenu>li>a:focus,nav.always .submenu>li>a:hover{background-color:var(--action-0);color:var(--action-contrast)}@media (min-width:768px){nav.always>ul{padding:var(--height) 0 0}}nav.on-this-page{--justify:space-between;max-width:none;z-index:var(--z-6);margin:0;padding:0 .5rem;background-color:var(--overlay-medium);color:var(--base-200)}body:has(nav.fixed) nav.on-this-page{bottom:var(--offHeight)}.on-this-page ul{--justify:flex-start;gap:0;width:100%}.on-this-page li:not(.has){padding:0}nav.letters li{width:100%;max-width:calc(7.69% - 2px)}.on-this-page .active a{--c:var(--action-rgb);background-color:var(--overlay-heavy);color:var(--action-contrast)}@media (min-width:768px){nav.letters li{max-width:none;width:fit-content}nav.letters a,nav.letters li:not(.has){padding:.25rem .66rem}}nav.index{--justify:flex-start;--px:0;background-color:var(--overlay-heavy)}.index ul{--justify:flex-start;width:fit-content}.index li{flex-shrink:0;transform:scaleX(0);transform-origin:right;max-width:0;overflow:hidden;transition:transform var(--timing) var(--function)}.index li.active{transform:scaleX(1);transform-origin:left;width:100%;flex-shrink:1;max-width:fit-content}@media (min-width:768px){.index li.adj{transform:scaleX(1);transform-origin:left;width:100%;flex-shrink:1;max-width:fit-content}}.index a{border-bottom:4px solid transparent}.index .active a{border-color:var(--action-0);color:var(--contrast)}.index .active a:hover,.index a:hover{background-color:var(--action-0);color:var(--action-contrast)}.index label{display:flex;color:var(--contrast);align-items:center;margin:0}.index label button{margin-left:1em}.index.open{--dir:column-reverse;height:calc(100% - 8rem);z-index:var(--z-above);width:100%;background-color:var(--overlay-heavy);backdrop-filter:blur(5px);align-items:flex-end}.index.open label{max-width:90%;margin-top:1rem;margin-right:2rem}.index.open .toggle .icon{transform:rotate(45deg)}.index.open ul{--dir:column;--justify:flex-end;height:100%;max-width:100%;width:100%}.index.open li{background-color:transparent;max-width:100%!important;width:100%;height:var(--height);transform:scaleX(1);flex-shrink:1;overflow:visible}.index.open a{--justify:flex-end;background-color:transparent;padding:0 2rem 0 0}.condensed{--dir:row;--wrap:wrap;--height:1.2em;--py:.25rem;--px:.25rem;height:fit-content}.condensed>ul{--wrap:wrap;height:fit-content}.condensed ul{--justify:center;--gap:0}.condensed li{width:fit-content}.condensed li+li::before{content:'·';display:block;padding:0 .25em}nav.condensed a{text-transform:none;white-space:nowrap;border-bottom:2px solid transparent}.condensed a:focus,.condensed a:focus:visited,.condensed a:hover,.condensed a:hover:visited{border-color:var(--action-0)}.dashboard-nav{width:100%;--dir:row;--justify:flex-start;--wrap:nowrap}.wp-site-blocks>header,body>header{--dir:row;position:sticky;top:0;left:0;right:0;height:var(--height);width:100vw;display:flex;justify-content:space-between;align-items:center;padding:0 .5rem;background-color:var(--base);z-index:var(--zz-top);box-shadow:var(--shadow);border-bottom:1px solid var(--action-0)}.wp-site-blocks>header img{width:var(--height)}body>header{justify-content:space-between}header .title{--w:5em;margin:0;position:absolute;width:100%;height:100%;display:flex;justify-content:center;align-items:flex-start;max-inline-size:none}.current-hours{position:sticky;top:var(--height);bottom:unset;width:unset;z-index:100;background-color:var(--action-0);color:var(--action-contrast);box-shadow:var(--shadow);padding:.25rem 1rem;display:flex;justify-content:space-between}.current-hours p{margin:0;display:flex;flex-wrap:wrap;flex:1}.current-hours p+p{justify-content:flex-end}.current-hours a{color:var(--action-contrast)}.current-hours a:hover{color:var(--action-200)}.current-hours b{margin-right:.25rem}.find-us{display:flex;align-items:center;gap:0 .5rem}.find-us a{display:flex;padding:.25rem 1rem;border:1px solid var(--action-contrast);border-radius:var(--innerRadius)}.find-us a:hover{background-color:var(--base);color:var(--contrast);border-color:var(--contrast)}nav.menu{--justify:flex-start}nav.menu a{padding:.5rem .66rem}nav.tabs{--gap:0;--wrap:nowrap;padding-bottom:2px;z-index:var(--z-6);position:fixed;bottom:var(--height);left:var(--doubleHeight);right:var(--doubleHeight)}nav.term-navigation:has([hidden]){display:none}ul.socials a{padding:.5rem}ul.socials a .icon{margin:0}
assets/js/Gallery.js
@@ -1,272 +1,226 @@
class Gallery {
    constructor(modal, config) {
        this.imageWrapper = config.imageWrapper??null;
        this.container = config.container ? document.querySelector(config.container) : document.querySelector('main');
        this.modal = new window.jvbModal(
            modal,
            {
                onOpen: false,
                onSave: false,
                onClose: () => this.closeGallery(false)
            }
        );
        this.items = this.getGalleryItems() || [];
        this.loadMore = config.loadMore??false;
    constructor(modal, config = {}) {
        this.container = config.container
            ? document.querySelector(config.container)
            : document.querySelector('main');
        this.modalElement = (typeof modal === 'string') ? document.querySelector(modal)??false : modal;
        if (!this.modal) {
            return;
        }
        this.gallerySelector = config.gallerySelector || 'img[data-small]';
        this.modal = new window.jvbModal(modal, {
            onOpen: false,
            onSave: false,
            onClose: () => this.closeGallery(false)
        });
        this.modalElement = (typeof modal === 'string')
            ? document.querySelector(modal)
            : modal;
        if (!this.modalElement) return;
        this.a11y = window.jvbA11y;
        this.openWhenReady = false;
        this.initElements();
        this.imageWrapper = (typeof this.imageWrapper === 'string') ? this.imageWrapper : '.'+this.imageWrapper.classList.join('.');
        this.index = 0;
        this.items = [];
        this.swipe = {
            touchStart: null,
            touchEnd: null,
            minSwipe: 50,
        };
        this.isLoading = false;
        this.initElements();
        this.initListeners();
    }
    initElements() {
        this.prevBtn = this.modalElement.querySelector('.prev');
        this.nextBtn = this.modalElement.querySelector('.next');
        this.favourite = this.modalElement.querySelector('.favourite');
        this.image = this.modalElement.querySelector('.image');
        this.counter = this.modalElement.querySelector('.counter');
        this.extra = this.modalElement.querySelector('.item-info');
        //If we don't have the wrapper set up, we can remove these elements
        if (!this.imageWrapper) {
            this.favourite.remove();
            this.extra.remove();
        }
    }
    getGalleryItems() {
        let search = (this.imageWrapper) ? this.imageWrapper : 'img';
        return Array.from(this.container.querySelectorAll(search))
            .map(item => {
                const img = (this.imageWrapper) ? item.querySelector('img') : item;
                if (!img) return null;
                return {
                    id: (this.imageWrapper) ? item.querySelector('button.favourite').dataset.id : '',
                    small: img.dataset.small || img.src,
                    large: img.dataset.large || img.src,
                    full: img.dataset.full || img.src,
                    alt: img.alt || '',
                    fav: (this.imageWrapper) ? item.querySelector('button.favourite')?.cloneNode(true) : '',
                    info: (this.imageWrapper) ? item.querySelector('.item-info')?.cloneNode(true) : ''
                };
            }).filter(Boolean);
    }
    initListeners() {
        // Delegate click handling to container
        this.container.addEventListener('click', this.handleClick.bind(this));
        document.addEventListener('keydown', this.handleKeys.bind(this));
        document.addEventListener('touchstart', this.handleTouchStart.bind(this));
        document.addEventListener('touchend', this.handleTouchEnd.bind(this));
        document.addEventListener('touchmove', this.handleTouchMove.bind(this));
        // Modal-specific listeners added only when open
        this.boundKeyHandler = this.handleKeys.bind(this);
        this.boundTouchStart = this.handleTouchStart.bind(this);
        this.boundTouchMove = this.handleTouchMove.bind(this);
        this.boundTouchEnd = this.handleTouchEnd.bind(this);
    }
    destroyListeners() {
        this.container.removeEventListener('click', this.handleClick.bind(this));
        document.removeEventListener('keydown', this.handleKeys.bind(this));
        document.removeEventListener('touchstart', this.handleTouchStart.bind(this));
        document.removeEventListener('touchend', this.handleTouchEnd.bind(this));
        document.removeEventListener('touchmove', this.handleTouchMove.bind(this));
    handleClick(e) {
        // Open gallery when clicking images
        const img = e.target.closest(this.gallerySelector);
        if (img && !this.modal.isOpen) {
            e.preventDefault();
            this.openGallery(img);
            return;
        }
        // Navigation within gallery
        if (this.modal.isOpen) {
            if (e.target.closest('.next')) {
                this.navigate(1);
            } else if (e.target.closest('.prev')) {
                this.navigate(-1);
            }
        }
    }
    handleKeys(e) {
        if (!this.modal.isOpen) return;
        switch (e.key) {
            case 'ArrowLeft':
                e.preventDefault();
                this.navigate(-1);
                break;
            case 'ArrowRight':
                e.preventDefault();
                this.navigate(1);
                break;
        }
    }
    handleTouchStart(e) {
        if (!this.modal.isOpen) return;
        this.swipe.touchStart = e.touches[0].clientX;
    }
    handleTouchMove(e) {
        if (!this.modal.isOpen) return;
        this.swipe.touchEnd = e.touches[0].clientX;
    }
    handleTouchEnd(e) {
        if (!this.swipe.touchStart || !this.swipe.touchEnd) return;
        const distance = this.swipe.touchStart - this.swipe.touchEnd;
        const isLeftSwipe = distance > this.swipe.minSwipe;
        const isRightSwipe = distance < -this.swipe.minSwipe;
        if (!this.modal.isOpen || !this.swipe.touchStart || !this.swipe.touchEnd) return;
        if (isLeftSwipe) {
            this.navigate(1);
        } else if (isRightSwipe) {
            this.navigate(-1);
        const distance = this.swipe.touchStart - this.swipe.touchEnd;
        if (Math.abs(distance) > this.swipe.minSwipe) {
            this.navigate(distance > 0 ? 1 : -1);
        }
        this.swipe.touchStart = null;
        this.swipe.touchEnd = null;
    }
    handleClick(e) {
        //test if it's a link click, and the click has an image
        if(window.targetCheck(e, '.feed-images')){
            this.handleGalleryOpen(e);
        }
    buildGalleryItems() {
        return Array.from(this.container.querySelectorAll(this.gallerySelector))
            .map((img, index) => ({
                id: img.dataset.id || index,
                small: img.dataset.small || img.src,
                medium: img.dataset.medium || img.src,
                full: img.dataset.full || img.src,
                alt: img.alt || '',
                element: img
            }));
    }
        if(window.targetCheck(e, '.nav')) {
            if (window.targetCheck(e, '.next')) {
                this.navigate(1);
            } else {
                this.navigate(-1);
            }
        }
        if (window.targetCheck(e, 'button.cancel')) {
            this.closeGallery();
    openGallery(clickedImg) {
        // Build fresh gallery items
        this.items = this.buildGalleryItems();
        // Find clicked image index
        this.index = this.items.findIndex(item =>
            item.element === clickedImg
        );
        if (this.index === -1) this.index = 0;
        // Attach modal-specific listeners
        document.addEventListener('keydown', this.boundKeyHandler);
        document.addEventListener('touchstart', this.boundTouchStart, { passive: true });
        document.addEventListener('touchmove', this.boundTouchMove, { passive: true });
        document.addEventListener('touchend', this.boundTouchEnd, { passive: true });
        this.modal.handleOpen();
        this.updateDisplay();
        this.preloadAdjacent();
        this.a11y.announce(
            `Gallery opened. Image ${this.index + 1} of ${this.items.length}. Use arrow keys to navigate.`
        );
    }
    closeGallery(useModal = true) {
        // Remove modal-specific listeners
        document.removeEventListener('keydown', this.boundKeyHandler);
        document.removeEventListener('touchstart', this.boundTouchStart);
        document.removeEventListener('touchmove', this.boundTouchMove);
        document.removeEventListener('touchend', this.boundTouchEnd);
        if (useModal) {
            this.modal.handleClose();
        }
    }
    async navigate(direction) {
    navigate(direction) {
        let newIndex = this.index + direction;
        //Check if out of bounds
        if (newIndex <0 || newIndex >= this.items.length) {
            this.a11y.announceNavigation(newIndex, this.items.length, direction < 0, direction > 0);
            if (newIndex <0) {
                newIndex = this.items.length - 1;
            } else {
                newIndex = 0;
            }
        // Wrap around
        if (newIndex < 0) {
            newIndex = this.items.length - 1;
        } else if (newIndex >= this.items.length) {
            newIndex = 0;
        }
        //update index
        this.index = newIndex;
        this.updateDisplay();
        this.preloadImages();
        this.preloadAdjacent();
        this.a11y.announceNavigation(this.index, this.items.length);
        if (typeof this.loadMore === 'function' &&
            direction > 0 &&
            newIndex >= (this.items.length - 3)) {
            if (window.feedBlock.hasMore) {
                await this.loadMore();
                this.updateGalleryItems(this.getGalleryItems());
            }
        }
    }
    updateGalleryItems(newItems) {
        const currentItem = this.items[this.index];
        this.items = newItems;
        if (currentItem) {
            const newIndex = this.items.findIndex(item =>
                item.full === currentItem.full ||
                item.large === currentItem.large
            );
            if (newIndex !== -1) {
                this.index = newIndex;
            }
        }
        this.updateNavigationButtons();
        if(this.items.length > 0 && this.openWhenReady) {
            let index = this.findIndex('id', this.openWhenReady);
            this.openGallery(index);
        }
    }
    preloadImages() {
        [-1,0,1].forEach(offset => {
            const index = this.index + offset;
            if (index >=0 && index < this.items.length) {
                const img = new Image();
                const item = this.items[index];
                if (window.innerWidth < 1000) {
                    img.src = item.large || item.src;
                } else {
                    img.src = item.full || item.src;
                }
            }
        });
        this.a11y.announce(`Image ${this.index + 1} of ${this.items.length}`);
    }
    updateDisplay() {
        const item = this.items[this.index];
        if (!item) return;
        [
            this.image.src,
            this.image.alt,
            this.counter.textContent
        ] = [
            (window.innerWidth < 1000) ?
                (item.large || item.src) :
                (item.full || item.src),
            item.alt || '',
            `${this.index + 1} / ${this.items.length}`
        ];
        if (this.imageWrapper) {
            if (item.fav) {
                window.removeChildren(this.favourite);
                this.favourite.appendChild(item.fav.cloneNode(true));
        // Use medium/full based on viewport
        const src = window.innerWidth < 1000
            ? (item.medium || item.src)
            : (item.full || item.src);
        this.image.src = src;
        this.image.alt = item.alt;
        this.counter.textContent = `${this.index + 1} / ${this.items.length}`;
        // Update button states
        this.prevBtn.classList.toggle('disabled', this.items.length <= 1);
        this.nextBtn.classList.toggle('disabled', this.items.length <= 1);
    }
    preloadAdjacent() {
        [-1, 1].forEach(offset => {
            const index = this.index + offset;
            if (index >= 0 && index < this.items.length) {
                const item = this.items[index];
                const img = new Image();
                img.src = window.innerWidth < 1000
                    ? (item.medium || item.src)
                    : (item.full || item.src);
            }
            if (item.info) {
                window.removeChildren(this.extra);
                this.extra.appendChild(item.info.cloneNode(true));
            }
        }
        this.updateNavigationButtons();
        });
    }
    updateNavigationButtons() {
        this.prevBtn.classList.toggle('end', this.index > 0 ? '' : 'none');
        this.nextBtn.classList.toggle('end', this.index < this.items.length -1 ? '' : 'none');
    }
    handleGalleryOpen(e) {
        let item = (this.imageWrapper) ? e.target.closest(this.imageWrapper) : e.target.closest('img');
        let key = (this.imageWrapper) ? 'id' : 'small';
        let value = item.dataset[key];
        let index = this.findIndex(key, value);
        this.openGallery(index);
    }
    findIndex(property, value) {
        return this.items.findIndex(obj => obj[property] === value);
    }
    openGallery(index) {
        this.initListeners();
        this.index = index;
        this.modal.handleOpen();
        this.updateDisplay();
        this.preloadImages();
        this.a11y.announce(`Image ${this.index + 1} of ${this.items.length}. Use arrow keys to navigate.`);
    }
    closeGallery(useModal = true) {
        this.destroyListeners();
        if (useModal) {
            this.modal.handleClose();
        }
    cleanup() {
        this.container.removeEventListener('click', this.handleClick);
        this.closeGallery(false);
    }
}
window.jvbGallery = Gallery;
document.addEventListener('DOMContentLoaded', function() {
    let galleries = document.querySelectorAll('dialog.gallery');
    if (galleries.length > 0) {
        window.galleries = new Map();
        galleries.forEach(gallery => {
            let id = gallery.id;
            window.galleries.set(id, new Gallery(gallery));
        });
    }
});
assets/js/concise/FormController.js
@@ -188,6 +188,7 @@
     * Register a standalone form (for front-end forms)
     */
    registerForm(formElement, options = {}) {
        if (!formElement) return;
        const formId = formElement.dataset.formId || `form_${Date.now()}`;
        formElement.dataset.formId = formId;
@@ -196,16 +197,17 @@
        const formConfig = {
            element: formElement,
            id: formId,
            status: '',
            options: {
                autoSave: 'autosave' in formElement.dataset,
                autosave: 'autosave' in formElement.dataset,
                saveDelay: this.autoSaveDefaults.delay,
                endpoint: formElement.dataset.save??'',
                formStatus: true,
                cache: true,
                ...options
            },
            dependencies: new Map(),
            data: this.collectFormData(formElement),
            isDirty: false
        };
        // Initialize special fields
@@ -444,8 +446,7 @@
        container.appendChild(row);
        // Schedule save if auto-save enabled
        if (formConfig && formConfig.options.autoSave) {
        if (formConfig) {
            this.scheduleSave(formConfig, {
                type: 'repeater',
                action: 'add',
@@ -472,7 +473,7 @@
        this.updateRepeaterOrder(repeater, formConfig);
        // Schedule save
        if (formConfig && formConfig.options.autoSave) {
        if (formConfig) {
            this.scheduleSave(formConfig, {
                type: 'repeater',
                action: 'remove',
@@ -515,7 +516,7 @@
        });
        // Schedule save
        if (formConfig && formConfig.options.autoSave) {
        if (formConfig) {
            this.scheduleSave(formConfig, {
                type: 'repeater',
                action: 'reorder',
@@ -650,16 +651,15 @@
    /* ========== Event Handlers ========== */
    handleSubmit(event) {
        //TODO: submit data, if successful, delete from store
        if (this.subscribers.size > 0 ){
            const form = event.target;
            if (!form.dataset.formId) return;
    async handleSubmit(event) {
        const form = event.target;
        if (!form.dataset.formId) return;
        const formConfig = this.forms.get(form.dataset.formId);
        // Handle subscriber-based forms
        if (this.subscribers.size > 0) {
            event.preventDefault();
            const formConfig = this.forms.get(form.dataset.formId);
            if (!formConfig) return;
            const formData = this.collectFormData(form);
            this.notify('form-submit', {
                formId: formConfig.id,
@@ -669,6 +669,133 @@
        }
    }
    handleFormSuccess(form, data) {
        // Clear previous errors
        form.querySelectorAll('.error-message').forEach(el => el.remove());
        form.querySelectorAll('.field-error').forEach(el =>
            el.classList.remove('field-error')
        );
        // Add success class to form
        form.classList.add('form-success');
        // Show success message if provided
        if (data.message) {
            const success = document.createElement('div');
            success.className = 'form-success-message success-message';
            success.textContent = data.message;
            form.insertBefore(success, form.firstChild);
            // Optionally add icon
            const icon = window.getIcon?.('check-circle');
            if (icon) {
                icon.classList.add('success-icon');
                success.prepend(icon);
            }
        }
        // If there's a title/description (for registration success)
        if (data.title || data.description) {
            const successBox = document.createElement('div');
            successBox.className = 'success-box';
            if (data.title) {
                const title = document.createElement('h3');
                title.textContent = data.title;
                successBox.appendChild(title);
            }
            if (data.description) {
                // Handle both string and array descriptions
                const descriptions = Array.isArray(data.description)
                    ? data.description
                    : [data.description];
                descriptions.forEach(desc => {
                    const p = document.createElement('p');
                    p.textContent = desc;
                    successBox.appendChild(p);
                });
            }
            form.insertBefore(successBox, form.firstChild);
        }
        // Announce success for accessibility
        if (window.jvbA11y) {
            window.jvbA11y.announce(data.message || 'Form submitted successfully');
        }
        // Trigger custom event
        form.dispatchEvent(new CustomEvent('jvb-form-success', {
            detail: data
        }));
    }
    handleFormError(form, data) {
        // Clear all previous errors
        form.querySelectorAll('.error-message').forEach(el => el.remove());
        form.querySelectorAll('.field-error, .has-error').forEach(el => {
            el.classList.remove('field-error', 'has-error');
        });
        // Clear validation states using existing method
        form.querySelectorAll('.field').forEach(fieldWrapper => {
            this.clearValidation(fieldWrapper);
        });
        // Handle field-specific errors
        if (data.field) {
            const fieldWrapper = form.querySelector(`[data-field="${data.field}"]`);
            if (fieldWrapper) {
                // Use existing showError method for consistency
                this.showError(fieldWrapper, data.message);
                // Mark as touched so validation persists
                this.touchedFields.add(data.field);
                // Scroll to error
                fieldWrapper.scrollIntoView({ behavior: 'smooth', block: 'center' });
                // Focus the input for better UX
                const input = fieldWrapper.querySelector('input, textarea, select');
                if (input) {
                    input.focus();
                }
            }
        } else {
            // General form error (not field-specific)
            const error = document.createElement('div');
            error.className = 'form-error error-message';
            error.textContent = data.message;
            // Add icon for consistency
            const icon = window.getIcon?.('close-circle');
            if (icon) {
                icon.classList.add('error-icon');
                error.prepend(icon);
            }
            form.insertBefore(error, form.firstChild);
            // Scroll to top to show the error
            form.scrollIntoView({ behavior: 'smooth', block: 'start' });
        }
        // Announce error for accessibility
        if (window.jvbA11y) {
            const announcement = data.field
                ? `Error in ${data.field}: ${data.message}`
                : `Form error: ${data.message}`;
            window.jvbA11y.announce(announcement);
        }
        // Trigger custom event
        form.dispatchEvent(new CustomEvent('jvb-form-error', {
            detail: data
        }));
    }
    handleClick(e) {
        if (window.targetCheck(e, 'div.quantity')) {
            let container = window.targetCheck(e, 'div.quantity');
@@ -749,15 +876,13 @@
        if (event.target.closest('[data-ignore]')) {
            return;
        }
        if (this.subscribers.size > 0) {
            const target = event.target;
            const form = target.form || target.closest('form');
        const target = event.target;
        const form = target.form || target.closest('form');if (!form) return;
            if (!form) return;
            const formConfig = this.forms?.get(form.dataset.formId);
            if (!formConfig) return;
        const formConfig = this.forms?.get(form.dataset.formId);
        if (!formConfig) return;
        console.log(formConfig.options);
        if (formConfig.options.autosave || this.subscribers.size > 0) {
            // Check conditional fields
            const dependencies = formConfig.dependencies.get(target.name);
            if (dependencies) {
@@ -767,10 +892,8 @@
            }
            // Schedule auto-save if enabled
            if (formConfig.options.autoSave && !form.dataset.noautosave) {
                const delay = this.getDelayForField(target);
                this.scheduleSave(formConfig, delay);
            }
            const delay = this.getDelayForField(target);
            this.scheduleSave(formConfig, delay);
        }
    }
@@ -807,7 +930,7 @@
                this.validateField(input, fieldWrapper);
            }
            const formConfig = this.forms?.get(form.dataset.formId);
            if (formConfig && formConfig.options.autoSave && !form.dataset.noautosave) {
            if (formConfig) {
                // Shorter delay on blur
                this.scheduleSave(formConfig, {
                    type: 'blur',
@@ -983,6 +1106,7 @@
        // All validations passed
        this.showSuccess(fieldWrapper);
        this.notify('field-validated', input);
        return true;
    }
@@ -1007,7 +1131,7 @@
    /**
     * Show success state (green checkmark)
     */
    showSuccess(fieldWrapper) {
    showSuccess(fieldWrapper, textMessage = '') {
        if (!fieldWrapper) return;
        // Find validation elements (they might be in field-input-wrapper or field-content)
@@ -1033,8 +1157,13 @@
        // Hide error message
        if (message) {
            message.hidden = true;
            message.textContent = '';
            if (textMessage === '') {
                message.hidden = true;
                message.textContent = '';
            } else {
                message.hidden = false;
                message.textContent = textMessage;
            }
        }
    }
@@ -1253,6 +1382,9 @@
        return this.autoSaveDefaults.delay;
    }
    scheduleSave(formConfig, delay = this.autoSaveDefaults.delay) {
        if (!formConfig.options.autosave) {
            return;
        }
        document.addEventListener('input', this.saveCheck, {passive: true});
        const saveKey = `autosave_${formConfig.id}`;
@@ -1288,7 +1420,9 @@
        // Get only changed fields
        const changes = this.getChangedFields(formConfig.data, formData);
        console.log('Changes:', changes);
        if (Object.keys(changes).length === 0) return;
        console.log('Continuing on:');
        // Update stored data
        formConfig.data = formData;
@@ -1322,14 +1456,23 @@
        // Check if current data differs from snapshot
        const currentData = this.collectFormData(formConfig.element);
        const changes = this.getChangedFields(formConfig.lastSnapshot, currentData);
        const changes = this.getChangedFields(formConfig.data, currentData);
        return Object.keys(changes).length > 0;
    }
    showFormStatus(formID, status) {
    showFormStatus(formID, status, message='') {
        // Remove existing status
        let form = this.forms.get(formID);
        if (!form.options.formStatus) {
            return;
        }
        if (form.status === status){
            return;
        }
        form.status = status;
        console.log('Setting status: ', status);
@@ -1350,9 +1493,9 @@
            'offline': 'Changes will be saved when online'
        };
        const icons = {
            'autosaved': 'check',
            'submitted': 'check',
            'error': 'close',
            'autosaved': 'check-circle',
            'submitted': 'check-circle',
            'error': 'close-circle',
            'offline': 'cloud-slash',
            'pending': 'exclamation-mark'
        }
@@ -1361,9 +1504,10 @@
        if (icon) {
            statusWrap.prepend(icon);
        }
        console.log(status, messages[status]);
        console.log(status, icons[status]);
        statusElement.textContent = messages[status] || status;
        if (message === '') {
            message = messages[status] || status;
        }
        statusElement.textContent = message;
        statusWrap.classList.toggle('loading', ['uploading', 'saving'].includes(status));
        // Auto-hide success messages
@@ -1391,6 +1535,9 @@
    /* ========== Form Data Methods ========== */
    collectFormData(form) {
        if (Object.hasOwn(form.dataset, 'timeline')) {
            return this.collectTimeline(form);
        }
        const formData = new FormData(form);
        let data = {};
        const repeaterData = {};
@@ -1409,6 +1556,46 @@
        return this.mergeRepeaterData(data, repeaterData);
    }
    collectTimeline(form) {
        console.log('Collecting Timeline data:');
        let data = {};
        let posts = {}; // Temporary object keyed by post ID
        let postOrder = []; // Track order as encountered (preserves DOM/drag order)
        let formData = new FormData(form);
        for (const [key, value] of formData.entries()) {
            if (this.ignore.includes(key) || key.endsWith('_temp')) {
                continue;
            }
            const match = key.match(/^\[(\d+)\](.+)$/);
            if (match) {
                // Timeline-specific field: [postId]fieldName
                const [, postId, fieldName] = match;
                if (!posts[postId]) {
                    posts[postId] = { id: parseInt(postId) };
                    postOrder.push(postId); // Track first occurrence
                }
                const processor = this.getFieldProcessor(fieldName);
                processor(fieldName, value, posts[postId], {}, {}, form);
            } else {
                // Shared field (post_title, taxonomies, etc.)
                const processor = this.getFieldProcessor(key);
                processor(key, value, data, {}, {}, form);
            }
        }
        // Convert to array in DOM order (matches menu_order)
        data.timeline = postOrder.map(id => posts[id]);
        delete data['form-id'];
        delete data['sendAll'];
        delete data['timeline_temp'];
        delete data['']; // Empty key
        console.log('Data: ', data);
        return data;
    }
    getFieldProcessor(key) {
        if (key.includes('|')) return this.processTableField;
        if (key.includes('::')) return this.processGroupField;
@@ -2118,5 +2305,4 @@
document.addEventListener('DOMContentLoaded', () => {
    window.jvbForm = FormController;
    console.log('FormController in window');
});
assets/js/concise/JVBase.js
@@ -181,6 +181,7 @@
     * ICONS
    ********************************************/
    getIcon(icon) {
        console.log('Getting Icon: '+icon);
        if (typeof icon === 'undefined') {
            return '';
        }
assets/js/concise/Media.js
@@ -1,207 +1,98 @@
/*
SITE GALLERY MANAGER
Handles two main functions:
1. Responsive Images
- Automatically loads appropriate image sizes based on screen width
- Listens for resize events and updates images accordingly
- Uses data-small, data-medium, and data-full attributes on images
2. Gallery Management
- HTML Structure:
  <a class="open-gallery" data-opens="gallery-id" data-focus="image-to-show">
    <figure><img...></figure>
  </a>
  <dialog class="gallery" id="gallery-id">
    - Main image display (.images with multiple <figure>s)
    - Navigation bar on bottom (nav.thumbnails) with:
      - Previous/Next buttons
      - Thumbnail list (toggles with .open class)
      - Expand button (toggles .expanded class)
    - Visibility toggle (toggles .hide-info class)
    - Close button
  </dialog>
Key Features:
- Opens in modal dialog with keyboard navigation
- Manages thumbnail navigation state
- Handles responsive image loading
- Uses class-based state management (open, expanded, hide-info)
- All event handling through delegation
- Built on UIHandler base class for state management
State Management:
- Dialog: open/closed
- Thumbnails: collapsed/open/expanded
- Info panel: visible/hidden
- Active image: tracked with .focused class
*/
class Media {
    constructor() {
        this.currentWidth = window.innerWidth;
        this.initElements();
        if (this.images.length === 0) {
            return;
        }
        this.store = new window.jvbStore({
            name: 'images',
            TTL: 604800
        });
        this.touch = {
            x: null,
            y: null
        }
        this.a11y = window.jvbA11y;
        this.debouncer = window.debouncer;
        this.isTouching = false;
        this.images = document.querySelectorAll('.wp-site-blocks img[data-small]');
        if (this.images.length === 0) return;
        // Immediately load visible images
        this.loadVisibleImages();
        this.initListeners();
        document.addEventListener('beforeunload', this.cleanup);
    }
    initElements(){
        this.images = document.querySelectorAll('.wp-site-blocks img');
        this.gallery = document.querySelector('dialog.gallery');
        this.modal = new window.jvbModal(this.gallery, {
            openMessage: 'Opened Gallery',
            closeMessage: 'Closed Gallery',
            open: '.open-gallery',
            close: '.close'
        });
    loadVisibleImages() {
        // Load first image immediately, plus any in viewport
        this.images.forEach((img, index) => {
            const rect = img.getBoundingClientRect();
            const isVisible = rect.top < window.innerHeight && rect.bottom > 0;
        this.modal.subscribe((event, data) => {
            if (event === 'modal-open') {
                this.openGallery();
            } else if (event === 'modal-close') {
                this.closeGallery();
            // Always load first image, or if currently visible
            if (index === 0 || isVisible) {
                this.loadAppropriateImage(img);
                img.dataset.loaded = 'true'; // Mark so we don't observe it
            }
        });
    }
    initListeners() {
        this.resizeHandler = this.handleResize.bind(this);
        this.clickHandler = this.handleClick.bind(this);
        this.keysHandler = this.handleKeys.bind(this);
        this.touchStartHandler = this.handleTouchStart.bind(this);
        this.touchEndHandler = this.handleTouchEnd.bind(this);
        document.addEventListener('click', this.clickHandler);
        window.addEventListener('resize', this.resizeHandler);
        console.log('window hash: ',window.location.hash);
        let target = document.querySelector(window.location.hash);
        if (target && target.tagName === 'LI') {
            let trigger = target.querySelector('.open-gallery');
            if (trigger) {
                this.openGallery(trigger);
            }
        }
        this.observer = new IntersectionObserver((entries) =>{
        // Only observe images that weren't immediately loaded
        this.observer = new IntersectionObserver((entries) => {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    let img = entry.target;
                    if (!img.closest('dialog')) {
                        this.loadAppropriateImage(img);
                        this.observer.unobserve(img);
                    }
                    this.loadAppropriateImage(entry.target);
                    this.observer.unobserve(entry.target);
                }
            })
            });
        }, {
            root: null,
            rootMargin: '50px',
            threshold: .1
            threshold: 0.1
        });
        this.images.forEach(img => {
            if (!img.closest('dialog')) {
            if (!img.dataset.loaded) {
                this.observer.observe(img);
            }
        })
        });
    }
    initTouchHandling() {
        this.isTouching = true;
        this.touch.x = 0;
        this.touch.y = 0;
        this.gallery.addEventListener('touchstart', this.touchStartHandler);
        this.gallery.addEventListener('touchend', this.touchEndHandler);
    }
    cancelTouchHandling() {
        this.isTouching = false;
        this.gallery.removeEventListener('touchstart', this.touchStartHandler);
        this.gallery.removeEventListener('touchend', this.touchEndHandler);
    }
    handleTouchStart(e) {
        this.touch.x = e.touches[0].clientX;
        this.touch.y = e.touches[0].clientY;
    }
    handleTouchEnd(e) {
        const diffX = e.changedTouches[0].clientX - this.touch.x;
        const diffY = e.changedTouches[0].clientY - this.touch.y;
        if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) {
            this.navigateImages({target: this.gallery}, diffX < 0 ? 'next' : 'previous');
        }
    }
    handleResize(e) {
        window.debouncer.schedule(
            'resize',
            ()=> {
                const currentWidth = window.innerWidth;
                if (Math.abs(currentWidth - this.currentWidth) > 100) {
                    this.currentWidth = currentWidth;
                    this.handleImageResize();
                }
            },
            150
        );
    }
    handleClick(e) {
    }
    handleKeys(e) {
        //Escape handled by Modal.js
        if (e.key === 'Tab') {
            if (e.shiftKey) {
            } else {
    handleResize() {
        window.debouncer.schedule('image-resize', () => {
            const newWidth = window.innerWidth;
            if (Math.abs(newWidth - this.currentWidth) > 100) {
                this.currentWidth = newWidth;
                this.updateVisibleImages();
            }
        }, 150);
    }
    updateVisibleImages() {
        this.images.forEach(img => {
            const rect = img.getBoundingClientRect();
            if (rect.top < window.innerHeight && rect.bottom > 0) {
                this.loadAppropriateImage(img, true);
            }
        });
    }
    loadAppropriateImage(img, forceUpdate = false) {
        const targetSize = this.getTargetSize();
        const newSrc = img.dataset[targetSize];
        if (newSrc && (forceUpdate || newSrc !== img.currentSrc)) {
            img.src = newSrc;
        }
    }
    openGallery(target = null) {
        document.addEventListener('keydown', this.clickHandler);
        const id = target.dataset.opens;
        this.initTouchHandling();
        let focus;
        if (!target) {
            focus = this.images[0];
        } else {
            focus = target.dataset.focus;
        }
        if (focus) {
            this.updateImage(this.gallery.querySelector(`#${focus}`));
        }
    }
    closeGallery() {
        this.cancelTouchHandling();
        document.removeEventListener('keydown', this.keysHandler);
        //Restore focus
        if (this.lastFocusedElement) {
            this.lastFocusedElement.focus();
        }
    getTargetSize() {
        if (this.currentWidth < 768) return 'small';
        if (this.currentWidth < 1200) return 'medium';
        return 'full';
    }
    cleanup() {
        this.observer.disconnect();
        window.removeEventListener('resize', this.resizeHandler)
        if (this.isTouching) {
            this.cancelTouchHandling();
            document.removeEventListener('keydown', this.keysHandler);
        }
        document.removeEventListener('click', this.clickHandler);
        this.observer?.disconnect();
        window.removeEventListener('resize', this.resizeHandler);
    }
}
window.isLoaded = false;
document.addEventListener('readystatechange', () => {
    if (!window.isLoaded && document.querySelector('.wp-site-blocks img')) {
        window.jvbMedia = new Media();
        window.isLoaded = true;
    }
});
assets/js/concise/PopulateForm.js
@@ -3,10 +3,6 @@
**********************************************/
class PopulateForm {
    constructor(form, fieldData = {}, imageData = {}, options = {}) {
        console.log('Populating field... ', form);
        console.log('fieldData: ', fieldData);
        console.log('imageData: ', imageData);
        console.log('options: ', options);
        for (let [fieldName, fieldValue] of Object.entries(fieldData)) {
            let wrapper = form.querySelector(`[data-field="${fieldName}"]`);
            if (wrapper) {
@@ -480,17 +476,78 @@
     * Populate gallery fields
     */
    populateGalleryField(fieldWrapper, fieldName, fieldValue, imagesData = {}) {
        this.populateImageField(fieldWrapper, fieldName, fieldValue, imagesData);
        if (fieldName === 'timeline') {
            this.populateTimelineGallery(fieldWrapper, fieldName, fieldValue, imagesData);
        } else {
            this.populateImageField(fieldWrapper, fieldName, fieldValue, imagesData);
        }
    }
    populateTimelineGallery(fieldWrapper, fieldName, fieldValue, imagesData)
    {
        if (!fieldValue) {
            return;
        }
        const imageIds = Object.keys(fieldValue);
        if (imageIds.length === 0) {
            return;
        }
        // Update image display
        const grid = fieldWrapper.querySelector('.item-grid');
        const uploadContainer = fieldWrapper.querySelector('.file-upload-container');
        fieldWrapper.querySelector('.progress')?.remove();
        if (grid) {
            window.removeChildren(grid);
            imageIds.forEach(imageId => {
                let image = window.getTemplate('timelineItem');
                let img = image.querySelector('img');
                image.querySelector('video')?.remove();
                image.querySelector('.select-item span')?.remove();
                let data = fieldValue[imageId];
                let imgData = imagesData[imageId];
                img.src = imgData.medium;
                data = { ... data, ... imgData};
                let fields = image.querySelectorAll('.field');
                fields.forEach(field => {
                    if (field.classList.contains('group')){
                        return;
                    }
                    let input = field.querySelector('input, textarea');
                    let label = field.querySelector('label');
                    let name = input.name.replace('upload_data::','');
                    let value = data[name];
                    this.populateField(field, name, value, imagesData);
                    let id = data.id;
                    let newName = `[${id}]${name}`;
                    [
                        input.name,
                        input.id,
                        label.htmlFor
                    ] = [
                        newName,
                        newName,
                        newName,
                    ]
                });
                grid.append(image);
            });
        }
    }
    /**
     * Populate repeater fields
     */
    populateRepeaterField(fieldWrapper, fieldName, fieldValue, options = {}) {
        console.log('fieldWrapper', fieldWrapper);
        console.log('fieldName', fieldName);
        console.log('fieldValue', fieldValue);
        console.log('options', options);
        if (!fieldValue || !Array.isArray(fieldValue)) {
            return;
        }
assets/js/concise/Queue.js
@@ -608,9 +608,9 @@
     *********************************************/
    initUI() {
        this.icons = {
            queued: 'refresh', localProcessing: 'refresh', uploading: 'syncing',
            pending: 'cloud', processing: 'syncing', completed: 'synced',
            failed: 'error', failed_permanent: 'error'
            queued: 'arrows-clockwise', localProcessing: 'arrows-clockwise', uploading: 'syncing',
            pending: 'cloud', processing: 'syncing', completed: 'cloud-check',
            failed: 'cloud-warning', failed_permanent: 'cloud-warning'
        };
        this.selectors = {
assets/js/concise/Referral.js
@@ -323,7 +323,7 @@
                data.context = {};
                data.context['redirect_to'] = window.location.href+'?seeReferral=1';
                console.log('Making Request with: ', data);
                result = await this.makeRequest('magic-link', data);
                result = await this.makeRequest('magic', data);
            }
@@ -344,7 +344,7 @@
    async makeRequest(endpoint, data) {
        if (![
            'magic-link',
            'magic',
            'referrals/register',
            'referrals/check-code'
        ].includes(endpoint)) {
assets/js/concise/TaxonomySelector.js
@@ -960,8 +960,9 @@
        if (!field) return;
        // Store current value immediately (fixes fast typing issue)
        const query = e.target.value.trim();
        field.currentAutocompleteQuery = query;
        if (query.length < 2) {
            if (field.autocompleteDropdown) {
@@ -972,12 +973,6 @@
        }
        this.activeField = fieldId;
        this.currentConfig = field;
        if (field.canCreate && ! this.creator) {
            this.creator = new window.jvbTaxCreator(this);
        }
        this.isAutocompleteActive = true;
        if (field.autocompleteDropdown) {
@@ -1041,15 +1036,26 @@
            });
        }
        // Offer to create new term if creator is available
        if (this.creator) {
            const createOption = this.creator.createAutocompleteOption(query, field);
        // Use stored current query instead of debounced one
        const currentQuery = field.currentAutocompleteQuery || query;
        if (field.canCreate && currentQuery && window.jvbTaxCreator) {
            const createOption = this.createNewTermOption(currentQuery);
            dropdown.appendChild(createOption);
        }
        dropdown.hidden = false;
    }
    createNewTermOption(query) {
        const button = document.createElement('button');
        button.type = 'button';
        button.className = 'autocomplete-item create-term';
        button.dataset.query = query;
        button.innerHTML = `<strong>Create:</strong> "${query}"`;
        return button;
    }
    createAutocompleteTermElement(field, term) {
        const item = document.createElement('button');
        item.type = 'button';
assets/js/concise/UploadManager.js
@@ -99,6 +99,20 @@
            'failed_permanent': 'Upload failed permanently'
        };
        // Sortable configuration
        this.sortableInstances = new Map();
        this.sortableConfig = {
            animation: 150,
            draggable: '.item',
            handle: '.select-item-label, img', // Can drag by image or checkbox label
            ghostClass: 'sortable-ghost',
            chosenClass: 'sortable-chosen',
            dragClass: 'sortable-drag',
            onEnd: (evt) => {
                this.handleReorder(evt);
            }
        };
        this.init();
    }
@@ -210,6 +224,9 @@
        if (config.destination === 'post_group' && !this.dragController) {
            this.initGroupFeatures();
        }
        if (config.type !== 'single') {
            this.initSortable(field);
        }
        return fieldId;
    }
@@ -356,6 +373,76 @@
        });
    }
    initSortable(field) {
        if (!window.Sortable) return;
        // Main grid
        const mainGrid = field.element.querySelector('.item-grid:not(.group)');
        if (mainGrid) {
            this.sortableInstances.set(`${field.id}-main`,
                new Sortable(mainGrid, {
                    ...this.sortableConfig,
                    group: {
                        name: field.id,
                        pull: true,
                        put: true
                    }
                })
            );
        }
        // Group grids (for selection mode with grouping)
        const groupGrids = field.element.querySelectorAll('.item-grid.group');
        groupGrids.forEach((grid, index) => {
            this.sortableInstances.set(`${field.id}-group-${index}`,
                new Sortable(grid, {
                    ...this.sortableConfig,
                    group: {
                        name: field.id,
                        pull: true,
                        put: true
                    }
                })
            );
        });
    }
// Add reorder handler
    handleReorder(evt) {
        const grid = evt.to;
        const fieldWrapper = grid.closest('.field, .upload');
        if (!fieldWrapper) return;
        const form = fieldWrapper.closest('form');
        if (!form) return;
        // Get form config if available
        const formId = form.dataset.formId;
        if (formId && window.jvbForms) {
            const formConfig = window.jvbForms.forms?.get(formId);
            if (formConfig?.options.autosave) {
                // Trigger autosave after reordering
                window.jvbForms.scheduleSave(formConfig, 1000);
            }
        }
        // Announce for accessibility
        if (window.jvbA11y) {
            window.jvbA11y.announce('Item reordered');
        }
        // Trigger custom event
        fieldWrapper.dispatchEvent(new CustomEvent('jvb-items-reordered', {
            detail: {
                from: evt.from,
                to: evt.to,
                oldIndex: evt.oldIndex,
                newIndex: evt.newIndex
            },
            bubbles: true
        }));
    }
    /*******************************************************************************
     * EXTERNAL FILE DROP HANDLERS (for new uploads from desktop)
     *******************************************************************************/
@@ -2936,6 +3023,12 @@
        this.selectionHandlers.clear();
        this.cleanupAllPreviewUrls();
        this.sortableInstances.forEach(instance => {
            if (instance?.destroy) {
                instance.destroy();
            }
        });
        this.sortableInstances.clear();
        // Clear data
        this.fields.clear();
@@ -2945,6 +3038,20 @@
        this.subscribers.clear();
    }
    destroySortable(fieldName) {
        // Destroy all sortable instances for this field
        const instances = Array.from(this.sortableInstances.keys())
            .filter(key => key.startsWith(fieldName));
        instances.forEach(key => {
            const instance = this.sortableInstances.get(key);
            if (instance?.destroy) {
                instance.destroy();
            }
            this.sortableInstances.delete(key);
        });
    }
    cleanupRestore() {
        this.restoreModal.handleClose();
        this.restoreSelection.destroy();
assets/js/concise/View.js
@@ -171,7 +171,7 @@
            console.error('No store connected to renderer');
            return;
        }
        console.log(items);
        // Handle empty state
        if (items.length === 0) {
@@ -268,15 +268,15 @@
            item.id,
            item.id
        ];
        if (this.store.config.storeName === 'progress') {
            [
                img.src,
                img.alt,
            ] = [
                item.images[item.fields['timeline'][0].post_thumbnail]?.medium??'',
                item.images[item.fields['timeline'][0].post_thumbnail]?.alt??'',
            ];
        } else {
        // if (this.store.config.storeName === 'progress') {
        //  [
        //      img.src,
        //      img.alt,
        //  ] = [
        //      item.images[item.fields['timeline'][0].post_thumbnail]?.medium??'',
        //      item.images[item.fields['timeline'][0].post_thumbnail]?.alt??'',
        //  ];
        // } else {
            [
                img.src,
                img.alt,
@@ -284,7 +284,7 @@
                item.images[item.fields.post_thumbnail]?.medium??'',
                item.images[item.fields.post_thumbnail]?.alt??'',
            ];
        }
        // }
        return card;
    }
assets/js/concise/quill.js
@@ -17,54 +17,54 @@
            toolbar.innerHTML = `
                <span class="ql-formats">
                    <button type="button" class="ql-p">
                        ${jvbSettings.icons.paragraph}
                        <i class="icon icon-paragraph"></i>
                    </button>
                    <button type="button" class="ql-h1">
                        ${jvbSettings.icons.h1}
                        <i class="icon icon-text-h-one"></i>
                    </button>
                    <button type="button" class="ql-h2">
                        ${jvbSettings.icons.h2}
                        <i class="icon icon-text-h-two"></i>
                    </button>
                    <button type="button" class="ql-h3">
                        ${jvbSettings.icons.h3}
                        <i class="icon icon-text-h-three"></i>
                    </button>
                </span>
                <span class="ql-formats">
                    <button type="button" class="ql-jvb_bold">
                        ${jvbSettings.icons['bold']}
                        <i class="icon icon-text-b-fi"></i>
                    </button>
                    <button type="button" class="ql-jvb_italic">
                        ${jvbSettings.icons['italic']}
                        <i class="icon icon-text-italic"></i>
                    </button>
                    <button type="button" class="ql-jvb_underline">
                        ${jvbSettings.icons['underline']}
                        <i class="icon icon-text-underline"></i>
                    </button>
                    <button type="button" class="ql-jvb_strike">
                        ${jvbSettings.icons['strike']}
                        <i class="icon icon-text-strikethrough"></i>
                    </button>
                </span>
                <span class="ql-formats">
                     <button type="button" class="ql-jvb_list" value="bullet">
                        ${jvbSettings.icons['list-bullets']}
                        <i class="icon icon-list-dashes"></i>
                    </button>
                    <button type="button" class="ql-jvb_list" value="ordered">
                        ${jvbSettings.icons['list-numbers']}
                        <i class="icon icon-list-numbers"></i>
                    </button>
                </span>
                <span class="ql-formats">
                     <button type="button" class="ql-jvb_align" value="left">
                        ${jvbSettings.icons['align-left']}
                        <i class="icon icon-text-align-left"></i>
                    </button>
                     <button type="button" class="ql-jvb_align" value="center">
                        ${jvbSettings.icons['align-center']}
                        <i class="icon icon-text-align-center"></i>
                    </button>
                     <button type="button" class="ql-jvb_align" value="right">
                        ${jvbSettings.icons['align-right']}
                        <i class="icon icon-text-align-right"></i>
                    </button>
                </span>
                <span class="ql-formats">
                     <button type="button" class="ql-jvb_link">
                        ${jvbSettings.icons.link}
                        <i class="icon icon-link"></i>
                    </button>
                    ${image}
                </span>
assets/js/dash/CRUD.js
@@ -12,6 +12,7 @@
        if (!this.content) {
            return;
        }
        this.isTimeline = false;
        this.initElements();
        this.updateBulkOptions();
@@ -69,6 +70,14 @@
        this.init();
    }
    handleFormChange(event, data) {
        if (this.isTimeline) {
            data.changes.content = this.content;
            let changes = {};
            let postID = data.config.id.replace('edit-', '');
            changes[postID] = data.changes;
            this.savePosts(changes, `Saving ${data.fullData.post_title}'s Changes`);
            return;
        }
        data.changes.content = this.content;
        let changes = {};
        let title = '';
@@ -185,6 +194,7 @@
            uploader: 'details.uploader'
        };
        this.ui = window.uiFromSelectors(this.elements);
        this.isTimeline = Object.hasOwn(this.ui.forms.edit.dataset, 'timeline');
    }
    init() {
        this.settings.addSetting(this.ui.uploader, 'open');
assets/js/dash/FavouritesManager.js
@@ -1244,15 +1244,15 @@
        actions.className = 'list-actions';
        actions.innerHTML = `
            <button type="button" class="share-list" title="Share List">
            <?= $icon->getIcon('share')?>
            <i class="icon icon-share-fat"></i>
            <span>Share</span>
        </button>
        <button type="button" class="duplicate-list" title="Duplicate List">
            <?= $icon->getIcon('copy')?>
            <i class="icon icon-copy"></i>
            <span>Duplicate</span>
        </button>
        <button type="button" class="delete-list" title="Delete List">
            <?= $icon->getIcon('delete')?>
            <i class="icon icon-trash"></i>
            <span>Delete</span>
        </button>
        `;
assets/js/dash/NewsManager.js
@@ -334,7 +334,7 @@
            [favourite.dataset.id, favourite.dataset.artist] = [item.id, item.artist.id];
            if(window.userFavourites.news?.includes(parseInt(item.id))){
                removeChildren(favourite);
                favourite.append(getIcon('star-filled'));
                favourite.append(getIcon('star-fi'));
            }else{
                removeChildren(favourite);
                favourite.append(getIcon('star'));
assets/js/dash/TaxonomyCreator.js
@@ -39,6 +39,11 @@
        if (window.targetCheck(e, '.submit-term')) {
            this.handleTermCreation(e);
        }
        // Handle autocomplete create button
        if (window.targetCheck(e, '.create-term')) {
            this.handleAutocompleteCreate(e);
        }
    }
    async handleTermCreation(e) {
@@ -94,6 +99,56 @@
        }
    }
    async handleAutocompleteCreate(e) {
        const button = e.target;
        const fieldId = this.selector.getFieldId(button);
        const field = this.selector.fields.get(fieldId);
        if (!field) return;
        // Get the current input value (not the debounced query)
        const input = field.container.querySelector('input[data-autocomplete]');
        const termName = input?.value.trim() || button.dataset.query;
        if (!termName) return;
        try {
            button.disabled = true;
            const originalText = button.innerHTML;
            button.textContent = 'Creating...';
            const response = await this.createTerm(termName, 0);
            if (response.success && response.term) {
                const term = response.term;
                // Add term to field
                field.selectedTerms.add(parseInt(term.id));
                this.selector.addTermToDisplay(field.id, term.id, term.name, term.path || term.name);
                // Update input
                field.input.value = Array.from(field.selectedTerms).join(',');
                field.input.dispatchEvent(new Event('change', { bubbles: true }));
                // Clear and hide dropdown
                field.autocompleteDropdown.hidden = true;
                if (input) input.value = '';
                // Refresh the store to include the new term
                await this.selector.store.setFilter('taxonomy', field.taxonomy);
            } else {
                button.innerHTML = originalText;
            }
        } catch (error) {
            console.error('Error creating term:', error);
            button.textContent = 'Failed - Try again';
            setTimeout(() => {
                button.innerHTML = `<strong>Create:</strong> "${termName}"`;
                button.disabled = false;
            }, 2000);
        }
    }
    initTermCreation() {
        if (!this.form) {
            return;
@@ -328,6 +383,7 @@
        return button;
    }
    /**
     * Handle term creation from autocomplete
     */
assets/js/dash/UtilityFunctions.js
@@ -189,10 +189,17 @@
 * @param icon
 * @returns {Node | ActiveX.IXMLDOMNode}
 */
window.icon = document.querySelector('.icon').cloneNode(true);
window.icon.className = 'icon';
window.getIcon = function getIcon(icon){
    if (typeof icon === 'undefined') {
        return '';
    }
    console.log('Getting icon: '+icon);
    let theIcon = window.icon.cloneNode(true);
    theIcon.classList.add('icon-'+icon);
    return theIcon;
    if(!window.jvbIcons){
assets/js/min/creator.min.js
@@ -1 +1 @@
window.jvbTaxCreator=class{constructor(e){this.selector=e,this.taxonomy=e.currentConfig?.taxonomy,this.taxonomy?(this.createNew=e.modal.querySelector(".create-new-term"),this.toggle=e.modal.querySelector(".new-term-toggle"),this.form=this.createNew.querySelector(".create-new-term-section"),this.initListeners(),this.initTermCreation()):console.error("TaxonomyCreator: No active field or taxonomy found")}initListeners(){this.clickHandler=this.handleClick.bind(this),document.addEventListener("click",this.clickHandler)}handleClick(e){window.targetCheck(e,".create-new-term summary")&&(this.createNew.open&&this.createNew.querySelector('input[name="term_name"]').focus(),this.resetParentOptions()),window.targetCheck(e,".submit-term")&&this.handleTermCreation(e)}async handleTermCreation(e){const t=this.form.querySelector('input[name="term_name"]').value.trim(),r=parseInt(this.form.querySelector("input#select_parent")?.value)||0;if(t)try{this.form.querySelector("button").disabled=!0;const e=await this.createTerm(t,r);if(e.success&&e.term){let t=e.term;this.createNew.open=!1,await this.selector.store.invalidate({taxonomy:this.taxonomy}),this.selector.addSelectedTermToModal(t.id,t.name,t.path||t.name),(this.selector.store.filters.parent||0)===r&&await this.selector.store.setFilters({taxonomy:this.taxonomy,parent:r,page:1,search:""}),this.form.querySelector('input[name="term_name"]').value="";const n=this.createNew.querySelector(".term-suggestions");n&&(n.hidden=!0)}}catch(e){console.error("Error creating term:",e),this.selector.error?.log(e,{component:"TaxonomyCreator",action:"handleTermCreation"})||console.error("Failed to create term")}finally{this.form.querySelector("button").disabled=!1}}initTermCreation(){this.form&&this.form.addEventListener("change",(e=>{e.preventDefault(),e.stopPropagation()}))}resetParentOptions(){let e=this.createNew.querySelector("#select_parent");if(!e)return;let t=e.querySelector("option");if(!t)return;window.removeChildren(e),e.append(t.cloneNode(!0));const r=this.selector.store.filters.parent||0;if(0!==r){const n=this.selector.store.data.get(r);if(n){let r=t.cloneNode(!0);r.value=n.id,r.textContent=n.name,e.append(r)}}const n=[];this.selector.store.data.forEach((e=>{e.taxonomy===this.taxonomy&&e.parent===r&&n.push(e)})),n.sort(((e,t)=>e.name.localeCompare(t.name))),n.forEach((r=>{let n=t.cloneNode(!0);n.id=`select-parent-${r.id}`,n.value=r.id,n.textContent="  â€” "+r.name,e.append(n)}))}async createTerm(e,t=0){let r=this.createNew.querySelector(".loading-message.create-term"),n=r?.querySelector("span");try{r&&(r.hidden=!1),n&&window.typeText?window.typeText(n,"Checking term..."):n&&(n.textContent="Checking term...");const o=await this.searchExistingTerms(e),a=o.filter((t=>t.name.toLowerCase()===e.toLowerCase()));if(a.length>0)return this.showTermSuggestions(a,!0),{success:!1,reason:"exists"};if(o.length>0)return this.showTermSuggestions(o,!1),{success:!1,reason:"similar"};n&&(n.textContent="Creating term...");const s=await fetch(`${jvbSettings.api}terms`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":jvbSettings.nonce},body:JSON.stringify({taxonomy:this.taxonomy,name:e,parent:t})});if(!s.ok)throw new Error(`Server error: ${s.status}`);return await s.json()}catch(e){throw console.error("Error creating term:",e),e}finally{this.form.querySelector("button").disabled=!1,r&&(r.hidden=!0)}}async searchExistingTerms(e){return new Promise((t=>{const r=(e,n)=>{"data-loaded"===e&&(this.selector.store.unsubscribe(r),t(n.data?.items||[]))};this.selector.store.subscribe(r),this.selector.store.setFilters({taxonomy:this.taxonomy,search:e,page:1,parent:0})}))}showTermSuggestions(e,t=!1){const r=this.createNew.querySelector(".term-suggestions")||this.createSuggestionContainer();window.removeChildren(r);const n=document.createElement("h4");n.textContent=t?"This term already exists:":"Similar terms already exist:",r.appendChild(n);const o=document.createElement("ul");o.className="term-suggestion-list",e.forEach((e=>{const t=document.createElement("li"),n=document.createElement("button");n.type="button",n.className="use-existing-term",n.setAttribute("data-id",e.id),n.textContent=e.path||e.name,n.addEventListener("click",(()=>{this.selector.addSelectedTermToModal(e.id,e.name,e.path||e.name),this.createNew.open=!1,r.hidden=!0,this.form.querySelector('input[name="term_name"]').value=""})),t.appendChild(n),o.appendChild(t)})),r.appendChild(o),r.hidden=!1}createSuggestionContainer(){const e=document.createElement("div");return e.className="term-suggestions",e.hidden=!0,this.createNew.querySelector("form").after(e),e}createAutocompleteOption(e,t){const r=document.createElement("button");return r.type="button",r.className="autocomplete-item create-term",r.innerHTML=`<span>Create "${e}"</span>`,r.dataset.query=e,r.dataset.fieldId=t.id,r.addEventListener("click",(async()=>{await this.handleAutocompleteCreate(r,e,t)})),r}async handleAutocompleteCreate(e,t,r){if(!r)return;const n=e.innerHTML;try{e.disabled=!0,e.innerHTML="<span>Creating...</span>";const n=0,o=await this.createTerm(t,n);if(o.success&&o.term){const e=o.term;r.selectedTerms.add(parseInt(e.id)),this.selector.addTermToDisplay(r.id,e.id,e.name,e.path||e.name),r.input.value=Array.from(r.selectedTerms).join(","),r.input.dispatchEvent(new Event("change",{bubbles:!0})),await this.selector.store.invalidate({taxonomy:r.taxonomy}),r.autocompleteDropdown.hidden=!0;const t=r.container.querySelector("input[data-autocomplete]");t&&(t.value="")}}catch(t){console.error("Error creating term:",t),e.innerHTML=n,e.disabled=!1,this.selector.error?.log(t,{component:"TaxonomyCreator",action:"handleAutocompleteCreate"})}}destroy(){this.clickHandler&&document.removeEventListener("click",this.clickHandler);const e=this.createNew?.querySelector(".loading-message.create-term");e&&(e.hidden=!0);const t=this.createNew?.querySelector(".term-suggestions");t&&(t.hidden=!0)}};
window.jvbTaxCreator=class{constructor(e){this.selector=e,this.taxonomy=e.currentConfig?.taxonomy,this.taxonomy?(this.createNew=e.modal.querySelector(".create-new-term"),this.toggle=e.modal.querySelector(".new-term-toggle"),this.form=this.createNew.querySelector(".create-new-term-section"),this.initListeners(),this.initTermCreation()):console.error("TaxonomyCreator: No active field or taxonomy found")}initListeners(){this.clickHandler=this.handleClick.bind(this),document.addEventListener("click",this.clickHandler)}handleClick(e){window.targetCheck(e,".create-new-term summary")&&(this.createNew.open&&this.createNew.querySelector('input[name="term_name"]').focus(),this.resetParentOptions()),window.targetCheck(e,".submit-term")&&this.handleTermCreation(e),window.targetCheck(e,".create-term")&&this.handleAutocompleteCreate(e)}async handleTermCreation(e){const t=this.form.querySelector('input[name="term_name"]').value.trim(),r=parseInt(this.form.querySelector("input#select_parent")?.value)||0;if(t)try{this.form.querySelector("button").disabled=!0;const e=await this.createTerm(t,r);if(e.success&&e.term){let t=e.term;this.createNew.open=!1,await this.selector.store.invalidate({taxonomy:this.taxonomy}),this.selector.addSelectedTermToModal(t.id,t.name,t.path||t.name),(this.selector.store.filters.parent||0)===r&&await this.selector.store.setFilters({taxonomy:this.taxonomy,parent:r,page:1,search:""}),this.form.querySelector('input[name="term_name"]').value="";const n=this.createNew.querySelector(".term-suggestions");n&&(n.hidden=!0)}}catch(e){console.error("Error creating term:",e),this.selector.error?.log(e,{component:"TaxonomyCreator",action:"handleTermCreation"})||console.error("Failed to create term")}finally{this.form.querySelector("button").disabled=!1}}async handleAutocompleteCreate(e){const t=e.target,r=this.selector.getFieldId(t),n=this.selector.fields.get(r);if(!n)return;const o=n.container.querySelector("input[data-autocomplete]"),a=o?.value.trim()||t.dataset.query;if(a)try{t.disabled=!0;const e=t.innerHTML;t.textContent="Creating...";const r=await this.createTerm(a,0);if(r.success&&r.term){const e=r.term;n.selectedTerms.add(parseInt(e.id)),this.selector.addTermToDisplay(n.id,e.id,e.name,e.path||e.name),n.input.value=Array.from(n.selectedTerms).join(","),n.input.dispatchEvent(new Event("change",{bubbles:!0})),n.autocompleteDropdown.hidden=!0,o&&(o.value=""),await this.selector.store.setFilter("taxonomy",n.taxonomy)}else t.innerHTML=e}catch(e){console.error("Error creating term:",e),t.textContent="Failed - Try again",setTimeout((()=>{t.innerHTML=`<strong>Create:</strong> "${a}"`,t.disabled=!1}),2e3)}}initTermCreation(){this.form&&this.form.addEventListener("change",(e=>{e.preventDefault(),e.stopPropagation()}))}resetParentOptions(){let e=this.createNew.querySelector("#select_parent");if(!e)return;let t=e.querySelector("option");if(!t)return;window.removeChildren(e),e.append(t.cloneNode(!0));const r=this.selector.store.filters.parent||0;if(0!==r){const n=this.selector.store.data.get(r);if(n){let r=t.cloneNode(!0);r.value=n.id,r.textContent=n.name,e.append(r)}}const n=[];this.selector.store.data.forEach((e=>{e.taxonomy===this.taxonomy&&e.parent===r&&n.push(e)})),n.sort(((e,t)=>e.name.localeCompare(t.name))),n.forEach((r=>{let n=t.cloneNode(!0);n.id=`select-parent-${r.id}`,n.value=r.id,n.textContent="  â€” "+r.name,e.append(n)}))}async createTerm(e,t=0){let r=this.createNew.querySelector(".loading-message.create-term"),n=r?.querySelector("span");try{r&&(r.hidden=!1),n&&window.typeText?window.typeText(n,"Checking term..."):n&&(n.textContent="Checking term...");const o=await this.searchExistingTerms(e),a=o.filter((t=>t.name.toLowerCase()===e.toLowerCase()));if(a.length>0)return this.showTermSuggestions(a,!0),{success:!1,reason:"exists"};if(o.length>0)return this.showTermSuggestions(o,!1),{success:!1,reason:"similar"};n&&(n.textContent="Creating term...");const s=await fetch(`${jvbSettings.api}terms`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":jvbSettings.nonce},body:JSON.stringify({taxonomy:this.taxonomy,name:e,parent:t})});if(!s.ok)throw new Error(`Server error: ${s.status}`);return await s.json()}catch(e){throw console.error("Error creating term:",e),e}finally{this.form.querySelector("button").disabled=!1,r&&(r.hidden=!0)}}async searchExistingTerms(e){return new Promise((t=>{const r=(e,n)=>{"data-loaded"===e&&(this.selector.store.unsubscribe(r),t(n.data?.items||[]))};this.selector.store.subscribe(r),this.selector.store.setFilters({taxonomy:this.taxonomy,search:e,page:1,parent:0})}))}showTermSuggestions(e,t=!1){const r=this.createNew.querySelector(".term-suggestions")||this.createSuggestionContainer();window.removeChildren(r);const n=document.createElement("h4");n.textContent=t?"This term already exists:":"Similar terms already exist:",r.appendChild(n);const o=document.createElement("ul");o.className="term-suggestion-list",e.forEach((e=>{const t=document.createElement("li"),n=document.createElement("button");n.type="button",n.className="use-existing-term",n.setAttribute("data-id",e.id),n.textContent=e.path||e.name,n.addEventListener("click",(()=>{this.selector.addSelectedTermToModal(e.id,e.name,e.path||e.name),this.createNew.open=!1,r.hidden=!0,this.form.querySelector('input[name="term_name"]').value=""})),t.appendChild(n),o.appendChild(t)})),r.appendChild(o),r.hidden=!1}createSuggestionContainer(){const e=document.createElement("div");return e.className="term-suggestions",e.hidden=!0,this.createNew.querySelector("form").after(e),e}createAutocompleteOption(e,t){const r=document.createElement("button");return r.type="button",r.className="autocomplete-item create-term",r.innerHTML=`<span>Create "${e}"</span>`,r.dataset.query=e,r.dataset.fieldId=t.id,r.addEventListener("click",(async()=>{await this.handleAutocompleteCreate(r,e,t)})),r}async handleAutocompleteCreate(e,t,r){if(!r)return;const n=e.innerHTML;try{e.disabled=!0,e.innerHTML="<span>Creating...</span>";const n=0,o=await this.createTerm(t,n);if(o.success&&o.term){const e=o.term;r.selectedTerms.add(parseInt(e.id)),this.selector.addTermToDisplay(r.id,e.id,e.name,e.path||e.name),r.input.value=Array.from(r.selectedTerms).join(","),r.input.dispatchEvent(new Event("change",{bubbles:!0})),await this.selector.store.invalidate({taxonomy:r.taxonomy}),r.autocompleteDropdown.hidden=!0;const t=r.container.querySelector("input[data-autocomplete]");t&&(t.value="")}}catch(t){console.error("Error creating term:",t),e.innerHTML=n,e.disabled=!1,this.selector.error?.log(t,{component:"TaxonomyCreator",action:"handleAutocompleteCreate"})}}destroy(){this.clickHandler&&document.removeEventListener("click",this.clickHandler);const e=this.createNew?.querySelector(".loading-message.create-term");e&&(e.hidden=!0);const t=this.createNew?.querySelector(".term-suggestions");t&&(t.hidden=!0)}};
assets/js/min/crud.min.js
@@ -1 +1 @@
(()=>{class e{constructor(e){this.queue=window.jvbQueue,console.log(this.queue),this.config=e,this.content=e.content||!1,this.settings=window.jvbUserSettings,this.content&&(this.initElements(),this.updateBulkOptions(),this.store=new window.jvbStore({name:this.content,storeName:this.content,endpoint:"content",headers:{action_nonce:jvbSettings.dash},indexes:[{name:"status",keyPath:"post_status"},{name:"modified",keyPath:"modified"}],filters:{content:this.content,user:jvbSettings.currentUser,page:1,status:"all"},TTL:36e5,cacheDOM:!0}),this.status="all",this.filterTimeout=null,this.viewController=new window.jvbViews(this.ui.container,this.store),this.formController=new window.jvbForm,this.formController.subscribe(((e,t)=>{switch(e){case"form-submit":case"form-autosave":this.handleFormChange(e,t)}})),window.jvbQueue&&window.jvbQueue.subscribe(((e,t)=>{"operation-completed"===e&&"form"===t.source?this.handleQueueSuccess(e,t):"operation-failed-permanent"===e&&"form"===t.source&&this.handleQueueFailure(e,t)})),this.initialized=!1,this.init())}handleFormChange(e,t){t.changes.content=this.content;let s={},i="",o=[];switch(!0){case t.config.element===this.ui.forms.edit:let l=t.config.id.replace("edit-","");console.log(l),s[l]=t.changes,i=`Saving ${t.fullData.post_title} Changes`,t.changes.post_status&&this.shouldRemoveItem(t.changes.post_status)&&o.push(l);break;case t.config.element===this.ui.forms.bulkEdit:let n=t.config.element.querySelectorAll(".selected input:checked");n.forEach((e=>{s[e.value]=t.changes,t.changes.post_status&&this.shouldRemoveItem(t.changes.post_status)&&o.push(e.value)})),i=`Updating ${n.length} ${this.config.plural??"posts"} Changes`;break;case t.config.element===this.ui.forms.create:"form-submit"===e&&(s[t.config.data["form-id"]]=t.fullData,i=`Saving ${t.fullData.post_title} Changes`)}if(o.length>0){let e=0;o.forEach((t=>{setTimeout((()=>{const e=document.querySelector(`.item[data-id="${t}"]`);e&&window.fade(e,!1)}),e),e+=50})),t.config.element===this.ui.forms.bulkEdit&&setTimeout((()=>{this.viewController.clearSelection()}),e+100)}window.isEmptyObject(s)||this.savePosts(s,i)}shouldRemoveItem(e){return"all"===this.status&&!["publish","draft"].includes(e)||e!==this.status}savePosts(e,t){if(window.isEmptyObject(e))return;let s={endpoint:"content",headers:{action_nonce:jvbSettings.dash},data:{posts:e},popup:"Saving changes",title:t};this.queue.addToQueue(s)}handleQueueSuccess(e,t){console.log("Handling queue success..."),console.log("Event",e),console.log("Data",t)}handleQueueFailure(e,t){console.log("Handling queue failure..."),console.log("Event",e),console.log("Data",t)}initElements(){this.elements={modals:{create:"dialog.create",edit:"dialog.edit",bulkEdit:"dialog.bulkEdit"},container:".crud[data-content]",grid:".item-grid",bulkSelectActions:".bulk-action-select",forms:{create:"dialog.create form",edit:"dialog.edit form",bulkEdit:"dialog.bulkEdit form"},uploader:"details.uploader"},this.ui=window.uiFromSelectors(this.elements)}init(){this.settings.addSetting(this.ui.uploader,"open"),this.ui.uploader.addEventListener("toggle",(e=>{console.log(e),console.log("Is Open: ",this.ui.uploader.open),console.log(this.ui.uploader.open?"on":"off"),this.settings.saveSetting("open",this.ui.uploader.open?"on":"off")})),this.filterHandler=this.handleFilterChange.bind(this),this.changeHandler=this.handleChange.bind(this),this.modals={};for(let[e,t]of Object.entries(this.ui.modals))this.modals[e]=new window.jvbModal(t),this.modals[e].subscribe(((t,s)=>{if("modal-close"===t)this.formController.cleanupForm(this.modals[e].modal.querySelector("form").dataset.formId),console.log("Data on modal close: ",s)}));this.setupEventDelegation(),this.setupFilters(),this.queue.subscribe(((e,t)=>{e})),this.initialized=!0}setupEventDelegation(){document.addEventListener("change",this.changeHandler),document.addEventListener("click",(e=>{const t=e.target.closest("[data-action]");if(t){e.preventDefault();const s=t.dataset.action,i=t.dataset.id;switch(s){case"edit":this.populateEditForm(i),this.modals.edit.handleOpen();break;case"delete":if(confirm("Delete this item?")){let e={};e[t.dataset.id]={post_status:"delete",content:this.content},window.fade(t.closest(".item"),!1),this.savePosts(e,`Sending ${this.singular} to trash...`),this.store.delete(i)}break;case"trash":let e={};e[t.dataset.id]={post_status:"trash",content:this.content},window.fade(t.closest(".item"),!1),this.savePosts(e,`Sending ${this.singular} to trash...`);break;case"create":this.modals.create.dataset.itemID="new",this.modals.create.dataset.content=this.content,this.modals.create.handleOpen();break;case"bulk-edit":Array.from(this.viewController.selectedItems).length>0&&this.modals.bulkEdit.handleOpen();break;case"bulk-delete":const s=Array.from(this.viewController.selectedItems);s.length>0&&confirm(`Delete ${s.length} items?`)&&(s.forEach((e=>this.store.delete(e))),this.viewController.clearSelection());break;case"sync":break;case"refresh":this.store.fetch()}}e.target.closest(".create-item")&&(this.formController.registerForm(this.ui.forms.create),this.modals.create.handleOpen()),e.target.closest(".cancel-bulk")&&this.viewController.selectAll(!1)})),document.addEventListener("keydown",(e=>{(e.ctrlKey||e.metaKey)&&"a"===e.key&&this.ui.container&&this.ui.container.contains(document.activeElement)&&(e.preventDefault(),this.viewController.selectAll()),"Escape"===e.key&&this.viewController?.selectedItems.size>0&&0===window.jvbModal.getAllModals().length&&this.viewController.clearSelection()}))}handleChange(e){if(e.target.classList.contains("bulk-action-select")){if(e.target.value.startsWith("tax-")){const t=e.target.value.replace("tax-","");return this.openTaxonomyModal(t),void(e.target.value="")}switch(e.target.value){case"edit":this.populateBulkEdit(),this.modals.bulkEdit.handleOpen();break;case"publish":this.setBulkStatus("publish");break;case"draft":case"restore":this.setBulkStatus("draft");break;case"trash":this.setBulkStatus("trash");break;case"delete":this.setBulkStatus("delete")}}}openTaxonomyModal(e){window.jvbSelector?window.jvbSelector.openForFilter(e,((e,t)=>this.handleBulkTaxonomy(e,t))):console.error("TaxonomySelector not initialized")}handleBulkTaxonomy(e,t){if(console.log(t,e),e.length>0){e=e.join(",");let s={},i=Array.from(this.viewController.selectedItems);console.log("selected",i),i.forEach((i=>{s[i]={content:this.content},s[i][t]=e})),console.log("Taxonomy changes: ",s);let o=`Adding ${i.length} ${this.config.plural??"posts"} to ${e.length} ${jvbSettings.labels[t].plural}`;this.viewController.clearSelection(),this.savePosts(s,o)}}setBulkStatus(e){if(!["publish","draft","trash","delete"].includes(e))return;console.log(`Setting status: ${e}`);let t,s={};for(let t of this.viewController.selectedItems)s[t]={post_status:e,content:this.content};if("delete"===e)t="Deleting";else t=window.uppercaseFirst(e)+"ing";if(console.log(this.status),"all"===this.status&&!["publish","draft"].includes(e)||e!==this.status){let e=0;for(let t of this.viewController.selectedItems)setTimeout((()=>{const e=document.querySelector(`.item[data-id="${t}"]`);e&&window.fade(e,!1)}),e),e+=50}this.viewController.clearSelection(),window.isEmptyObject(s)||this.savePosts(s,`${t} ${this.viewController.selectedItems.size} ${this.plural}...`)}handleFilterChange(e){let t=e.target,s=t.dataset.filter;if("taxonomies"===s){let e=t.dataset.taxonomy;this.store.setFilter(`tax_${e}`,s.value)}else this[t.dataset.filter]=t.value,this.store.setFilter(t.dataset.filter,t.value),"status"===t.dataset.filter&&this.updateBulkOptions(t.value)}updateBulkOptions(e="all"){if("trash"===e){if(this.ui.bulkSelectActions.querySelector('[value="edit"]')){window.removeChildren(this.ui.bulkSelectActions),window.getTemplate("trashOptions").querySelectorAll("option").forEach(((e,t)=>{0===t&&(e.checked=!0),this.ui.bulkSelectActions.append(e)}))}}else if(!this.ui.bulkSelectActions.querySelector('[value="edit"]')){window.removeChildren(this.ui.bulkSelectActions),window.getTemplate("notTrashOptions").querySelectorAll("option").forEach(((e,t)=>{this.ui.bulkSelectActions.append(e)}))}this.ui.bulkSelectActions.value=""}populateBulkEdit(){const e=this.modals.bulkEdit.modal.querySelector("form .selected");if(!e)return;window.removeChildren(e);for(let t of this.viewController.selectedItems){console.log(t);let s=this.store.get(t);console.log(s);const i=window.getTemplate("bulkItem");if(!i)return;const o=i.querySelector("input[type=checkbox]"),l=i.querySelector("img");o&&(o.id=`bulk_${s.id}`,o.value=s.id,o.checked=!0),l&&s.thumbnail&&(l.src=s.thumbnail,l.alt=s.alt||""),e.append(i)}let t=this.modals.bulkEdit.modal;[t.querySelector("h2 span").textContent]=[this.viewController.selectedItems.size],this.formController.registerForm(this.ui.forms.bulkEdit),console.log("Bulk Edit form registered")}populateEditForm(e){console.log(e);let t=this.store.get(parseInt(e));if(console.log(t),t){this.ui.modals.edit.dataset.itemID=e,this.ui.modals.edit.dataset.content=this.content;let s=this.ui.modals.edit.querySelector("form");[this.ui.modals.edit.querySelector("h2").textContent]=[`Editing ${t.fields.post_title}`],s.dataset.formId=`edit-${e}`,console.log(s.dataset.formId),new window.jvbPopulate(s,t.fields,t.images),this.formController.registerForm(this.ui.forms.edit),console.log("Edit form registered")}}setupFilters(){document.querySelectorAll("[data-filter]").forEach((e=>{this.settings.addSetting(e),e.addEventListener("change",(e=>{this.filterTimeout&&clearTimeout(this.filterTimeout),this.filterTimeout=setTimeout((()=>{this.filterHandler(e)}),300)}))}));const e=document.querySelector('input[type="search"]');if(e){let t;e.addEventListener("input",(()=>{e.value.length>3?(clearTimeout(t),t=setTimeout((()=>{this.store.setFilter("search",e.value)}),300)):0===e.value.length&&this.store.removeFilter("search")}))}}destroy(){document.querySelectorAll("[data-filter]").forEach((e=>{e.removeEventListener("change",this.filterHandler)})),this.store.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(()=>{let t=document.querySelector("[data-content]");t&&(window.crudManager=new e({content:t.dataset.content}))}))})();
(()=>{class e{constructor(e){this.queue=window.jvbQueue,console.log(this.queue),this.config=e,this.content=e.content||!1,this.settings=window.jvbUserSettings,this.content&&(this.isTimeline=!1,this.initElements(),this.updateBulkOptions(),this.store=new window.jvbStore({name:this.content,storeName:this.content,endpoint:"content",headers:{action_nonce:jvbSettings.dash},indexes:[{name:"status",keyPath:"post_status"},{name:"modified",keyPath:"modified"}],filters:{content:this.content,user:jvbSettings.currentUser,page:1,status:"all"},TTL:36e5,cacheDOM:!0}),this.status="all",this.filterTimeout=null,this.viewController=new window.jvbViews(this.ui.container,this.store),this.formController=new window.jvbForm,this.formController.subscribe(((e,t)=>{switch(e){case"form-submit":case"form-autosave":this.handleFormChange(e,t)}})),window.jvbQueue&&window.jvbQueue.subscribe(((e,t)=>{"operation-completed"===e&&"form"===t.source?this.handleQueueSuccess(e,t):"operation-failed-permanent"===e&&"form"===t.source&&this.handleQueueFailure(e,t)})),this.initialized=!1,this.init())}handleFormChange(e,t){if(this.isTimeline){t.changes.content=this.content;let e={};return e[t.config.id.replace("edit-","")]=t.changes,void this.savePosts(e,`Saving ${t.fullData.post_title}'s Changes`)}t.changes.content=this.content;let s={},i="",o=[];switch(!0){case t.config.element===this.ui.forms.edit:let l=t.config.id.replace("edit-","");console.log(l),s[l]=t.changes,i=`Saving ${t.fullData.post_title} Changes`,t.changes.post_status&&this.shouldRemoveItem(t.changes.post_status)&&o.push(l);break;case t.config.element===this.ui.forms.bulkEdit:let n=t.config.element.querySelectorAll(".selected input:checked");n.forEach((e=>{s[e.value]=t.changes,t.changes.post_status&&this.shouldRemoveItem(t.changes.post_status)&&o.push(e.value)})),i=`Updating ${n.length} ${this.config.plural??"posts"} Changes`;break;case t.config.element===this.ui.forms.create:"form-submit"===e&&(s[t.config.data["form-id"]]=t.fullData,i=`Saving ${t.fullData.post_title} Changes`)}if(o.length>0){let e=0;o.forEach((t=>{setTimeout((()=>{const e=document.querySelector(`.item[data-id="${t}"]`);e&&window.fade(e,!1)}),e),e+=50})),t.config.element===this.ui.forms.bulkEdit&&setTimeout((()=>{this.viewController.clearSelection()}),e+100)}window.isEmptyObject(s)||this.savePosts(s,i)}shouldRemoveItem(e){return"all"===this.status&&!["publish","draft"].includes(e)||e!==this.status}savePosts(e,t){if(window.isEmptyObject(e))return;let s={endpoint:"content",headers:{action_nonce:jvbSettings.dash},data:{posts:e},popup:"Saving changes",title:t};this.queue.addToQueue(s)}handleQueueSuccess(e,t){console.log("Handling queue success..."),console.log("Event",e),console.log("Data",t)}handleQueueFailure(e,t){console.log("Handling queue failure..."),console.log("Event",e),console.log("Data",t)}initElements(){this.elements={modals:{create:"dialog.create",edit:"dialog.edit",bulkEdit:"dialog.bulkEdit"},container:".crud[data-content]",grid:".item-grid",bulkSelectActions:".bulk-action-select",forms:{create:"dialog.create form",edit:"dialog.edit form",bulkEdit:"dialog.bulkEdit form"},uploader:"details.uploader"},this.ui=window.uiFromSelectors(this.elements),this.isTimeline=Object.hasOwn(this.ui.forms.edit.dataset,"timeline")}init(){this.settings.addSetting(this.ui.uploader,"open"),this.ui.uploader.addEventListener("toggle",(e=>{console.log(e),console.log("Is Open: ",this.ui.uploader.open),console.log(this.ui.uploader.open?"on":"off"),this.settings.saveSetting("open",this.ui.uploader.open?"on":"off")})),this.filterHandler=this.handleFilterChange.bind(this),this.changeHandler=this.handleChange.bind(this),this.modals={};for(let[e,t]of Object.entries(this.ui.modals))this.modals[e]=new window.jvbModal(t),this.modals[e].subscribe(((t,s)=>{if("modal-close"===t)this.formController.cleanupForm(this.modals[e].modal.querySelector("form").dataset.formId),console.log("Data on modal close: ",s)}));this.setupEventDelegation(),this.setupFilters(),this.queue.subscribe(((e,t)=>{e})),this.initialized=!0}setupEventDelegation(){document.addEventListener("change",this.changeHandler),document.addEventListener("click",(e=>{const t=e.target.closest("[data-action]");if(t){e.preventDefault();const s=t.dataset.action,i=t.dataset.id;switch(s){case"edit":this.populateEditForm(i),this.modals.edit.handleOpen();break;case"delete":if(confirm("Delete this item?")){let e={};e[t.dataset.id]={post_status:"delete",content:this.content},window.fade(t.closest(".item"),!1),this.savePosts(e,`Sending ${this.singular} to trash...`),this.store.delete(i)}break;case"trash":let e={};e[t.dataset.id]={post_status:"trash",content:this.content},window.fade(t.closest(".item"),!1),this.savePosts(e,`Sending ${this.singular} to trash...`);break;case"create":this.modals.create.dataset.itemID="new",this.modals.create.dataset.content=this.content,this.modals.create.handleOpen();break;case"bulk-edit":Array.from(this.viewController.selectedItems).length>0&&this.modals.bulkEdit.handleOpen();break;case"bulk-delete":const s=Array.from(this.viewController.selectedItems);s.length>0&&confirm(`Delete ${s.length} items?`)&&(s.forEach((e=>this.store.delete(e))),this.viewController.clearSelection());break;case"sync":break;case"refresh":this.store.fetch()}}e.target.closest(".create-item")&&(this.formController.registerForm(this.ui.forms.create),this.modals.create.handleOpen()),e.target.closest(".cancel-bulk")&&this.viewController.selectAll(!1)})),document.addEventListener("keydown",(e=>{(e.ctrlKey||e.metaKey)&&"a"===e.key&&this.ui.container&&this.ui.container.contains(document.activeElement)&&(e.preventDefault(),this.viewController.selectAll()),"Escape"===e.key&&this.viewController?.selectedItems.size>0&&0===window.jvbModal.getAllModals().length&&this.viewController.clearSelection()}))}handleChange(e){if(e.target.classList.contains("bulk-action-select")){if(e.target.value.startsWith("tax-")){const t=e.target.value.replace("tax-","");return this.openTaxonomyModal(t),void(e.target.value="")}switch(e.target.value){case"edit":this.populateBulkEdit(),this.modals.bulkEdit.handleOpen();break;case"publish":this.setBulkStatus("publish");break;case"draft":case"restore":this.setBulkStatus("draft");break;case"trash":this.setBulkStatus("trash");break;case"delete":this.setBulkStatus("delete")}}}openTaxonomyModal(e){window.jvbSelector?window.jvbSelector.openForFilter(e,((e,t)=>this.handleBulkTaxonomy(e,t))):console.error("TaxonomySelector not initialized")}handleBulkTaxonomy(e,t){if(console.log(t,e),e.length>0){e=e.join(",");let s={},i=Array.from(this.viewController.selectedItems);console.log("selected",i),i.forEach((i=>{s[i]={content:this.content},s[i][t]=e})),console.log("Taxonomy changes: ",s);let o=`Adding ${i.length} ${this.config.plural??"posts"} to ${e.length} ${jvbSettings.labels[t].plural}`;this.viewController.clearSelection(),this.savePosts(s,o)}}setBulkStatus(e){if(!["publish","draft","trash","delete"].includes(e))return;console.log(`Setting status: ${e}`);let t,s={};for(let t of this.viewController.selectedItems)s[t]={post_status:e,content:this.content};if("delete"===e)t="Deleting";else t=window.uppercaseFirst(e)+"ing";if(console.log(this.status),"all"===this.status&&!["publish","draft"].includes(e)||e!==this.status){let e=0;for(let t of this.viewController.selectedItems)setTimeout((()=>{const e=document.querySelector(`.item[data-id="${t}"]`);e&&window.fade(e,!1)}),e),e+=50}this.viewController.clearSelection(),window.isEmptyObject(s)||this.savePosts(s,`${t} ${this.viewController.selectedItems.size} ${this.plural}...`)}handleFilterChange(e){let t=e.target,s=t.dataset.filter;if("taxonomies"===s){let e=t.dataset.taxonomy;this.store.setFilter(`tax_${e}`,s.value)}else this[t.dataset.filter]=t.value,this.store.setFilter(t.dataset.filter,t.value),"status"===t.dataset.filter&&this.updateBulkOptions(t.value)}updateBulkOptions(e="all"){if("trash"===e){if(this.ui.bulkSelectActions.querySelector('[value="edit"]')){window.removeChildren(this.ui.bulkSelectActions),window.getTemplate("trashOptions").querySelectorAll("option").forEach(((e,t)=>{0===t&&(e.checked=!0),this.ui.bulkSelectActions.append(e)}))}}else if(!this.ui.bulkSelectActions.querySelector('[value="edit"]')){window.removeChildren(this.ui.bulkSelectActions),window.getTemplate("notTrashOptions").querySelectorAll("option").forEach(((e,t)=>{this.ui.bulkSelectActions.append(e)}))}this.ui.bulkSelectActions.value=""}populateBulkEdit(){const e=this.modals.bulkEdit.modal.querySelector("form .selected");if(!e)return;window.removeChildren(e);for(let t of this.viewController.selectedItems){console.log(t);let s=this.store.get(t);console.log(s);const i=window.getTemplate("bulkItem");if(!i)return;const o=i.querySelector("input[type=checkbox]"),l=i.querySelector("img");o&&(o.id=`bulk_${s.id}`,o.value=s.id,o.checked=!0),l&&s.thumbnail&&(l.src=s.thumbnail,l.alt=s.alt||""),e.append(i)}let t=this.modals.bulkEdit.modal;[t.querySelector("h2 span").textContent]=[this.viewController.selectedItems.size],this.formController.registerForm(this.ui.forms.bulkEdit),console.log("Bulk Edit form registered")}populateEditForm(e){console.log(e);let t=this.store.get(parseInt(e));if(console.log(t),t){this.ui.modals.edit.dataset.itemID=e,this.ui.modals.edit.dataset.content=this.content;let s=this.ui.modals.edit.querySelector("form");[this.ui.modals.edit.querySelector("h2").textContent]=[`Editing ${t.fields.post_title}`],s.dataset.formId=`edit-${e}`,console.log(s.dataset.formId),new window.jvbPopulate(s,t.fields,t.images),this.formController.registerForm(this.ui.forms.edit),console.log("Edit form registered")}}setupFilters(){document.querySelectorAll("[data-filter]").forEach((e=>{this.settings.addSetting(e),e.addEventListener("change",(e=>{this.filterTimeout&&clearTimeout(this.filterTimeout),this.filterTimeout=setTimeout((()=>{this.filterHandler(e)}),300)}))}));const e=document.querySelector('input[type="search"]');if(e){let t;e.addEventListener("input",(()=>{e.value.length>3?(clearTimeout(t),t=setTimeout((()=>{this.store.setFilter("search",e.value)}),300)):0===e.value.length&&this.store.removeFilter("search")}))}}destroy(){document.querySelectorAll("[data-filter]").forEach((e=>{e.removeEventListener("change",this.filterHandler)})),this.store.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(()=>{let t=document.querySelector("[data-content]");t&&(window.crudManager=new e({content:t.dataset.content}))}))})();
assets/js/min/favouritesManager.min.js
@@ -1 +1 @@
window.favouritesManager=class{constructor(){this.queue=window.jvbQueue,this.loadingManager=window.jvbLoading,this.cache=window.jvbCache,this.a11y=window.jvbA11y,this.error=window.jvbError,this.tabs=new window.jvbTabs(document.querySelector(".replace")),this.config={endpoints:{favourites:"favourites",lists:"favourites/lists",shares:"favourites/lists/shares"},selectors:{container:".favourites.container",itemsTab:'.tab-content[data-tab="items"]',listsTab:'.tab-content[data-tab="lists"]',grid:".item-grid",typeFilters:".type-filters",viewControls:".view-controls",bulkControls:".bulk-controls",selectAll:"#select-all",createListModal:".create-list-modal",addToListModal:".add-to-list-modal",shareListModal:".share-list-modal",noItems:".no-favourites",listContainer:".lists-container",listDetails:".list-details",loader:".favourites-loader"},defaultPage:1,defaultPerPage:24,defaultViewMode:"grid",refreshInterval:6e4,toastDuration:3e3},document.addEventListener("keydown",this.handleKeyDown.bind(this)),this.state={selectedItems:new Set,page:this.config.defaultPage,filter:{type:"all",order:"desc",orderBy:"date_added"},view:{mode:localStorage.getItem("favourites_view")||this.config.defaultViewMode,activeTab:"items"},pagination:{hasMore:!1,totalItems:0,totalPages:0},currentListId:null,loading:!1,initialized:!1},this.initDom(),this.initEvents(),this.loadInitialData(),this.state.initialized=!0}initDom(){this.container=document.querySelector(this.config.selectors.container),this.container?(this.grid=this.container.querySelector(this.config.selectors.grid),this.typeFilters=this.container.querySelector(this.config.selectors.typeFilters),this.viewControls=this.container.querySelector(this.config.selectors.viewControls),this.bulkControls=this.container.querySelector(this.config.selectors.bulkControls),this.listContainer=this.container.querySelector(this.config.selectors.listContainer),this.listDetails=this.container.querySelector(this.config.selectors.listDetails),this.loader=this.container.querySelector(this.config.selectors.loader),this.createListModal=document.querySelector(this.config.selectors.createListModal),this.addToListModal=document.querySelector(this.config.selectors.addToListModal),this.shareListModal=document.querySelector(this.config.selectors.shareListModal),this.grid&&this.state.view.mode&&this.grid.classList.add(`${this.state.view.mode}-view`)):console.warn("Favourites container not found")}initEvents(){if(this.typeFilters&&this.typeFilters.addEventListener("click",(t=>{const e=t.target.closest(".type-filter");e&&this.setFilterType(e.dataset.type)})),this.viewControls&&this.viewControls.addEventListener("click",(t=>{const e=t.target.closest(".view-toggle");e&&this.setView(e.dataset.view)})),this.container){const t=this.container.querySelector(this.config.selectors.selectAll);t&&t.addEventListener("change",(()=>{this.toggleSelectAll(t.checked)})),this.container.addEventListener("change",(t=>{t.target.matches(".item-select input[type=checkbox]")&&this.handleItemSelection(t.target)}));const e=this.container.querySelector(".bulk-action-select"),s=this.container.querySelector(".apply-bulk");e&&s&&s.addEventListener("click",(()=>{this.applyBulkAction(e.value)}));const i=this.container.querySelector(".cancel-bulk");i&&i.addEventListener("click",(()=>{this.clearSelection()}))}this.initModalEvents(),this.container.addEventListener("click",this.handleItemActions.bind(this)),this.grid&&this.setupInfiniteScroll()}initModalEvents(){if(this.createListModal){const t=this.createListModal.querySelector("form");t&&t.addEventListener("submit",(e=>{e.preventDefault(),this.handleCreateList(new FormData(t))}));const e=this.createListModal.querySelector(".cancel");e&&e.addEventListener("click",(()=>{this.createListModal.close()}))}if(this.addToListModal){const t=this.addToListModal.querySelector("form");t&&t.addEventListener("submit",(e=>{e.preventDefault(),this.handleAddToList(new FormData(t))}));const e=this.addToListModal.querySelector(".cancel");e&&e.addEventListener("click",(()=>{this.addToListModal.close()}))}if(this.shareListModal){const t=this.shareListModal.querySelector("form");t&&t.addEventListener("submit",(e=>{e.preventDefault(),this.handleShareList(new FormData(t))}));const e=this.shareListModal.querySelector(".cancel");e&&e.addEventListener("click",(()=>{this.shareListModal.close()}));const s=this.shareListModal.querySelector(".add-email");s&&s.addEventListener("click",(()=>{const e=this.shareListModal.querySelector("#share-email");e&&e.value&&this.handleShareList(new FormData(t))}))}}setupInfiniteScroll(){let t=this.container.querySelector(".scroll-sentinel");t||(t=document.createElement("div"),t.className="scroll-sentinel",t.setAttribute("aria-hidden","true"),this.grid.parentNode.appendChild(t)),new IntersectionObserver((t=>{t.forEach((t=>{t.isIntersecting&&this.state.pagination.hasMore&&!this.state.loading&&(this.state.page++,this.loadFavourites())}))}),{rootMargin:"200px"}).observe(t)}async loadInitialData(){this.loadingManager.show();try{await this.loadFavourites(),this.loadLists().catch((t=>{console.error("Error loading lists:",t)}))}catch(t){this.handleError(t,"loading initial data")}finally{this.loadingManager.hide()}}async loadFavourites(t=!0){if(!this.state.loading)try{this.state.loading=!0,this.loadingManager.show();const e=new URLSearchParams({page:this.state.page,per_page:this.config.defaultPerPage,type:"all"!==this.state.filter.type?this.state.filter.type:"",order:this.state.filter.order,orderby:this.state.filter.orderBy});t&&(this.state.page=1,removeChildren(this.grid),this.grid.classList.remove("empty"));const s=await this.cache.fetchWithCache(`${jvbSettings.api}${this.config.endpoints.favourites}?${e}`,{method:"GET",headers:{"X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbSettings.favourites}},{context:"favouritesManager",forceRefresh:!0});return this.renderFavourites(s.favourites||[],this.state.page>1),s.counts&&this.updateTypeFilters(s.counts),s.pagination&&(this.state.pagination={hasMore:s.pagination.has_more,totalItems:s.pagination.total_items,totalPages:s.pagination.total_pages}),s}catch(t){throw this.handleError(t,"loading favourites"),t}finally{this.state.loading=!1,this.loadingManager.hide()}}renderFavourites(t,e=!1){this.grid&&(0!==t.length||e?(this.hideEmptyState(),e||removeChildren(this.grid),t.forEach((t=>{const e=this.createItemElement(t);this.grid.appendChild(e),this.initItemFunctionality(e,t)})),window.jvbA11y&&window.jvbA11y.announce(`${e?"Added":"Loaded"} ${t.length} favourites`)):this.showEmptyState())}createItemElement(t){const e=document.createElement("div");e.className=`item ${t.type} favourited`,e.dataset.id=t.target_id,e.dataset.type=t.type;const s=sanitizeHtml(t.title||!1),i=sanitizeHtml(t.notes||"");let a="";return t.thumbnail&&(a=`\n                <div class="item-thumbnail">\n                    <a href="${t.url}">${t.thumbnail}</a>\n                </div>\n            `),e.innerHTML=`\n            <div class="item-select">\n                <input type="checkbox"\n                   class="favourite-checkbox"\n                    id="select-${t.target_id}"\n                    value="${t.target_id}">\n                <label for="select-${t.target_id}"><span class="screen-reader-text">Select this ${t.type}</span</label>\n            </div>\n\n            <button type="button" class="favourite-button favourited"\n                onclick="toggleFavourite(this)"\n                data-id="${t.target_id}"\n                data-type="${t.type}"\n                title="Remove from favourites">\n                ${jvbSettings.icons["heart-filled"]}\n            </button>\n\n            ${a}\n\n            <div class="item-info">\n                ${s?`<h3><a href="${t.url}">${s}</a></h3>`:`<a href="${t.url}">View Item</a>`}\n\n                ${t.author?`\n                <div class="item-artist">\n                    <span>By ${t.author.name}</span>\n                </div>`:""}\n\n                ${t.taxonomies?.length?`\n                <div class="taxonomy-lists">\n                    ${t.taxonomies.map((t=>`\n                        <div class="taxonomy-group">\n                            ${jvbSettings.icons[t.icon]}\n                            <ul>\n                                ${t.terms.slice(0,3).map((t=>`\n                                    <li>\n                                        <a href="${t.url}" ${t.umami_click}>\n                                            ${t.title}\n                                        </a>\n                                    </li>\n                                `)).join("")}\n                            </ul>\n                        </div>\n                    `)).join("")}\n                </div>\n            `:""}\n\n                <div class="notes-section">\n                    <button type="button" class="toggle-notes" aria-expanded="false">\n                        ${jvbSettings.icons.note||"Notes"}\n                        <span>Notes</span>\n                    </button>\n\n                    <div class="notes-content" hidden>\n                        <textarea class="notes-input"\n                            placeholder="Add notes about this item..."\n                            data-id="${t.target_id}"\n                            data-type="${t.type}">${i}</textarea>\n                        <button type="button" class="save-notes">Save Notes</button>\n                    </div>\n                </div>\n            </div>\n        `,e}initItemFunctionality(t,e){const s=t.querySelector(".toggle-notes"),i=t.querySelector(".notes-content");s&&i&&s.addEventListener("click",(()=>{const t="true"===s.ariaExpanded;s.ariaExpanded=!t.toString(),i.hidden=t,t||i.querySelector("textarea")?.focus()}));const a=t.querySelector(".save-notes"),n=t.querySelector(".notes-input");a&&n&&(a.addEventListener("click",(()=>{this.saveNotes(n)})),n.addEventListener("keydown",(t=>{"Enter"===t.key&&(t.ctrlKey||t.metaKey)&&(t.preventDefault(),this.saveNotes(n))})))}saveNotes(t){if(!t)return;const e=t.value.trim(),s=t.dataset.id,i=t.dataset.type;s&&i&&(this.queue.addToQueue({type:"favourite_notes",data:{type:i,target_id:parseInt(s),notes:e}}),showToast("Notes saved"),this.a11y.announce("Notes saved"))}showEmptyState(t=!1){const e=this.container.querySelector(this.config.selectors.noItems)??this.createEmptyElement;e&&(e.hidden=!1),this.grid&&this.grid.classList.add("empty"),this.a11y.announce("No favourites to show!")}hideEmptyState(){const t=this.container.querySelector(".no-favourites");t&&t.remove(),this.grid&&this.grid.classList.remove("empty")}createEmptyElement(t=!1){const e=document.createElement("div");e.className="no-favourites",e.innerHTML="\n            <h3>♡ BLANK CANVAS â™¡</h3>\n            <p>You haven't fallen in love with any pieces... yet!</p>\n            <p>Hit that heart icon when something stops your scroll.</p>\n            <p>Your dream collection is waiting to start.</p>\n        ",this.grid.after(e)}showEmptyListState(t=!1){const e=document.createElement("div");e.className="no-favourites",e.innerHTML="\n            <h3>♡ FULL OF POSSIBILITY â™¡</h3>\n            <p>There's nothing in this list yet.</p>\n            <p>Add some gap fillers from the main favourites tab.</p>\n        ",this.grid.after(e),this.grid.classList.add("empty"),this.a11y.announce("No favourites to show!")}async loadMoreItems(){!this.state.loading&&this.state.pagination.hasMore&&(this.state.page+=1,await this.loadFavourites())}updateTypeFilters(t){this.typeFilters&&this.typeFilters.querySelectorAll(".type-filter").forEach((e=>{const s=e.querySelector(".count");if(!s)return;const i=e.dataset.type;if("all"===i){const e=Object.values(t).reduce(((t,e)=>t+(parseInt(e)||0)),0);s.textContent=`(${e})`}else s.textContent=`(${t[i]||0})`}))}setFilterType(t){t!==this.state.filter.type&&(this.typeFilters&&this.typeFilters.querySelectorAll(".type-filter").forEach((e=>{e.classList.toggle("active",e.dataset.type===t),e.setAttribute("aria-selected",e.dataset.type===t)})),this.state.filter.type=t,this.state.page=1,this.loadFavourites(),window.jvbA11y&&window.jvbA11y.announce(`Filtered to show ${"all"===t?"all":t} items`))}setView(t){t!==this.state.view.mode&&(this.viewControls&&this.viewControls.querySelectorAll(".view-toggle").forEach((e=>{const s=e.dataset.view===t;e.setAttribute("aria-pressed",s)})),this.grid&&(this.grid.classList.remove("grid-view","list-view"),this.grid.classList.add(`${t}-view`)),this.state.view.mode=t,localStorage.setItem("favourites_view",t),window.jvbA11y&&window.jvbA11y.announce(`Changed to ${t} view`))}toggleSelectAll(t){const e=this.getVisibleItems();e.forEach((e=>{const s=e.querySelector('.item-select input[type="checkbox"]');s&&(s.checked=t,this.toggleItemSelection(s.value,t))})),this.updateBulkControls(),window.jvbA11y&&window.jvbA11y.announce(t?`Selected all ${e.length} items`:"Deselected all items")}getVisibleItems(){return this.grid?Array.from(this.grid.querySelectorAll(".item:not([hidden])")):[]}toggleItemSelection(t,e){e?this.state.selectedItems.add(t):this.state.selectedItems.delete(t);const s=this.grid.querySelector(`.item[data-id="${t}"]`);s&&s.classList.toggle("selected",e)}handleItemSelection(t){const e=t.checked,s=t.value;if(this.toggleItemSelection(s,e),this.updateBulkControls(),this.updateSelectAllState(),window.jvbA11y){const s=t.closest(".item"),i=s&&s.querySelector("h3")?.textContent||"item";window.jvbA11y.announce(e?`Selected ${i}`:`Deselected ${i}`)}}updateSelectAllState(){const t=this.container.querySelector(this.config.selectors.selectAll);if(!t)return;const e=this.getVisibleItems();if(0===e.length)return t.checked=!1,void(t.indeterminate=!1);const s=e.filter((t=>{const e=t.querySelector('.item-select input[type="checkbox"]');return e&&e.checked})).length;0===s?(t.checked=!1,t.indeterminate=!1):s===e.length?(t.checked=!0,t.indeterminate=!1):(t.checked=!1,t.indeterminate=!0)}updateBulkControls(){if(!this.bulkControls)return;const t=this.bulkControls.querySelector(".bulk-actions");if(!t)return;const e=this.state.selectedItems.size>0;t.hidden=!e;const s=this.bulkControls.querySelector(".selected-count");s&&(s.textContent=e?`${this.state.selectedItems.size} selected`:"")}handleKeyDown(t){"Escape"===t.key&&this.state.selectedItems.size>0&&(t.preventDefault(),this.clearSelection(),window.jvbA11y&&window.jvbA11y.announce("Selection cleared using Escape key"))}clearSelection(){this.state.selectedItems.clear(),this.getVisibleItems().forEach((t=>{const e=t.querySelector('.item-select input[type="checkbox"]');e&&(e.checked=!1),t.classList.remove("selected")}));const t=this.container.querySelector(this.config.selectors.selectAll);t&&(t.checked=!1,t.indeterminate=!1),this.updateBulkControls(),window.jvbA11y&&window.jvbA11y.announce("Selection cleared")}applyBulkAction(t){if(!t||0===this.state.selectedItems.size)return;switch(t){case"unfavourite":this.bulkUnfavourite();break;case"add-to-list":this.showAddToListModal();break;case"create-list":this.showCreateListModal();break;case"add-notes":this.showBulkNotesModal()}const e=this.container.querySelector(".bulk-action-select");e&&(e.value="")}handleItemActions(t){if(t.target.closest(".toggle-notes")){const e=t.target.closest(".toggle-notes"),s="true"===e.getAttribute("aria-expanded"),i=e.closest(".notes-section").querySelector(".notes-content");return e.setAttribute("aria-expanded",!s),i.hidden=s,!s&&i&&i.querySelector("textarea")?.focus(),void t.preventDefault()}if(t.target.closest(".save-notes")){const e=t.target.closest(".save-notes").closest(".notes-content").querySelector("textarea");return e&&this.saveNotes(e),void t.preventDefault()}if(t.target.closest(".view-list")){const e=t.target.closest(".view-list").closest(".list-card");return e&&e.dataset.id&&this.viewList(e.dataset.id),void t.preventDefault()}if(t.target.closest(".share-list")){const e=t.target.closest(".share-list").closest(".list-card");return e&&e.dataset.id&&this.showShareModal(e.dataset.id),void t.preventDefault()}if(t.target.closest(".delete-list")){const e=t.target.closest(".delete-list").closest(".list-card");return e&&e.dataset.id&&this.deleteList(e.dataset.id),void t.preventDefault()}if(t.target.closest(".back-to-lists"))return this.exitListView(),void t.preventDefault()}async loadLists(t=!0){try{this.state.loading=!0,this.loadingManager.show("Loading lists...");const t=await this.cache.fetchWithCache(`${jvbSettings.api}${this.config.endpoints.lists}`,{method:"GET",headers:{"X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbSettings.favourites}},{context:"favourite-lists",forceRefresh:!1});return t.lists&&this.renderLists(t.lists),t}catch(t){throw this.handleError(t,"loading lists"),t}finally{this.state.loading=!1,this.loadingManager.hide()}}renderLists(t){if(!this.listContainer)return;if(removeChildren(this.listContainer),!t||0===t.length)return void(this.listContainer.innerHTML='\n                <div class="no-lists">\n                    <h3>No Lists Yet</h3>\n                    <p>Select favourites from the main tab to organize into lists.</p>\n                </div>\n            ');const e=t.owned,s=t.shared;if(e.length>0){const t=document.createElement("details");t.className="lists-section owned-lists",t.open=!0,t.innerHTML="<summary>Your Lists:</summary>",e.forEach((e=>{const s=this.createListCard(e);t.appendChild(s)})),this.listContainer.appendChild(t)}if(s.length>0){const t=document.createElement("details");t.className="lists-section shared-lists",t.innerHTML="<summary>Lists Shared with You:</summary>",s.forEach((e=>{const s=this.createListCard(e);t.appendChild(s)})),this.listContainer.appendChild(t)}}createListCard(t){const e=document.createElement("div");e.className="list-card",e.dataset.id=t.id;const s="1"===t.is_shared;s&&e.classList.add("shared"),t.is_temp&&e.classList.add("temp"),t.is_owner;const i=sanitizeHtml(t.name||"Untitled List"),a=sanitizeHtml(t.description||"");return e.innerHTML=`\n            <div class="list-header">\n                <h3>${i}</h3>\n                <div class="list-actions">\n                    <button type="button" class="view-list" title="View List">\n                        ${jvbSettings.icons?.show||"View"}\n                    </button>\n                    ${s?"":`\n                        <button type="button" class="share-list" title="Share List">\n                            ${jvbSettings.icons?.share||"Share"}\n                        </button>\n                        <button type="button" class="delete-list" title="Delete List">\n                            ${jvbSettings.icons?.delete||"Delete"}\n                        </button>\n                    `}\n                </div>\n            </div>\n\n            ${a?`<p class="list-description">${a}</p>`:""}\n\n            <div class="list-meta">\n                <div class="meta-stats">\n                    <span class="item-count">${t.item_count||0} items</span>\n                    <span class="date">${formatDate(t.created_at)}</span>\n                </div>\n\n\n                ${s?`\n                    <div class="owner-info">\n                        Shared by ${t.owner_name||"another user"}\n                    </div>\n                `:t.share_count>0?`\n                    <div class="share-info">\n                        Shared with ${t.share_count} ${1===t.share_count?"person":"people"}\n                    </div>\n                `:""}\n            </div>\n        `,e}async viewList(t){try{this.state.loading=!0,this.loadingManager.show("Loading list..."),this.state.currentListId=t;const e=await this.cache.fetchWithCache(`${jvbSettings.api}${this.config.endpoints.lists}?id=${t}`,{method:"GET",headers:{"X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbSettings.favourites}},{context:"list-item",forceRefresh:!1});if(!e.list)throw new Error("List not found");this.showListDetails(e.list)}catch(t){this.handleError(t,"viewing list")}finally{this.state.loading=!1,this.loadingManager.hide()}}showListDetails(t){this.listDetails&&this.listContainer&&(console.log(t),this.listDetails.querySelector(".list-title").value=t.name||"Untitled List",this.listDetails.querySelector(".list-description").value=t.description||"",t.is_owner?this.listDetails.querySelector(".list-actions")||this.createListActions():this.listDetails.querySelector(".list-actions")?.remove(),removeChildren(this.grid),this.renderFavourites(t.items||[],!1),0===t.items.length&&this.showEmptyListState(),window.jvbA11y&&window.jvbA11y.announce(`Viewing list: ${t.name} with ${t.items?.length||0} items`))}createListActions(){const t=document.createElement("div");t.className="list-actions",t.innerHTML='\n            <button type="button" class="share-list" title="Share List">\n            <?= $icon->getIcon(\'share\')?>\n            <span>Share</span>\n        </button>\n        <button type="button" class="duplicate-list" title="Duplicate List">\n            <?= $icon->getIcon(\'copy\')?>\n            <span>Duplicate</span>\n        </button>\n        <button type="button" class="delete-list" title="Delete List">\n            <?= $icon->getIcon(\'delete\')?>\n            <span>Delete</span>\n        </button>\n        ',this.listDetails.insertBefore(t,this.listDetails.querySelector(".bulk-controls"))}exitListView(){this.listDetails&&this.listContainer&&(this.listDetails.hidden=!0,this.listContainer.hidden=!1,this.container.classList.remove("viewing-list"),this.state.currentListId=null,window.jvbA11y&&window.jvbA11y.announce("Returned to lists view"))}showCreateListModal(){this.createListModal&&(this.createListModal.querySelector("form")?.reset(),this.createListModal.showModal(),setTimeout((()=>{this.createListModal.querySelector("#list-name")?.focus()}),100),window.jvbA11y&&window.jvbA11y.announce("Create list dialog opened"))}showAddToListModal(){this.addToListModal&&(this.populateAddToListModal(),this.addToListModal.showModal(),window.jvbA11y&&window.jvbA11y.announce("Add to list dialog opened"))}async populateAddToListModal(){if(!this.addToListModal)return;const t=this.addToListModal.querySelector(".lists-options");if(t){removeChildren(t);try{const e=(await this.loadLists()).lists.owned;if(0===e.length)return t.innerHTML='\n                    <div class="no-lists">\n                        <p>You don\'t have any lists yet.</p>\n                        <button type="button" class="create-list-button">Create a list</button>\n                    </div>\n                ',void t.querySelector(".create-list-button")?.addEventListener("click",(()=>{this.addToListModal.close(),this.showCreateListModal()}));e.forEach((e=>{const s=document.createElement("div");s.className="list-option",s.innerHTML=`\n                    <input type="checkbox" id="${e.id}" name="list_ids[]" value="${e.id}">\n                    <label for="${e.id}">\n\n                        <span class="list-name">${sanitizeHtml(e.name)}</span>\n                        <span class="item-count">( ${e.item_count||0} items )</span>\n                    </label>\n                `,t.appendChild(s)}))}catch(e){t.innerHTML='\n                <div class="error-message">\n                    <p>Error loading lists. Please try again.</p>\n                </div>\n            ',console.error("Error loading lists for modal:",e)}}}async handleCreateList(t){const e=t.get("list_name"),s=t.get("list_description");if(e)try{this.showLoader("Creating list...");const t=[];this.state.selectedItems.forEach((e=>{const s=this.grid.querySelector(`.item[data-id="${e}"]`);s&&t.push({type:s.dataset.type,target_id:parseInt(e)})})),this.queue.addToQueue({type:"favourite_list_create",data:{name:e,description:s,items:t}}),showToast(`List "${e}" created`),this.a11y.announce(`List ${e} created with ${t.length} items`),this.createListModal.close(),this.clearSelection(),this.switchTab("lists")}catch(t){this.handleError(t,"creating list")}finally{this.hideLoader()}else showToast("Please enter a list name","error")}async handleAddToList(t){const e=t.getAll("list_ids[]");if(e.length)try{this.showLoader("Adding to list...");const t=[];this.state.selectedItems.forEach((e=>{const s=this.grid.querySelector(`.item[data-id="${e}"]`);s&&t.push({type:s.dataset.type,target_id:parseInt(e)})})),this.queue.addToQueue({type:"favourite_list_add",data:{list_id:e.join(","),items:t}}),showToast(`Added to ${e.length} ${1===e.length?"list":"lists"}`),this.a11y.announce(`Added ${t.length} items to ${e.length} ${1===e.length?"list":"lists"}`),this.addToListModal.close(),this.clearSelection()}catch(t){this.handleError(t,"adding to list")}finally{this.hideLoader()}else showToast("Please select at least one list","error")}async handleRemoveFromList(t){const e=t.getAll("list_ids[]");if(e.length)try{this.showLoader("Removing from list...");const t=[];this.state.selectedItems.forEach((e=>{const s=this.grid.querySelector(`.item[data-id="${e}"]`);s&&t.push({type:s.dataset.type,target_id:parseInt(e)})})),this.queue.addToQueue({type:"favourite_list_remove",data:{list_id:e.join(","),items:t}}),showToast(`Removed from ${e.length} ${1===e.length?"list":"lists"}`),this.a11y.announce(`Removed ${t.length} items to ${e.length} ${1===e.length?"list":"lists"}`),this.addToListModal.close(),this.clearSelection()}catch(t){this.handleError(t,"remove from list")}finally{this.hideLoader()}else showToast("Please select at least one list","error")}showShareModal(t){this.shareListModal&&(this.state.currentListId=t,this.shareListModal.querySelector("form")?.reset(),this.loadSharedUsers(t),this.shareListModal.showModal(),setTimeout((()=>{this.shareListModal.querySelector("#share-email")?.focus()}),100),window.jvbA11y&&window.jvbA11y.announce("Share list dialog opened"))}async loadSharedUsers(t){try{const e=this.shareListModal.querySelector(".shared-users");if(!e)return;e.innerHTML='<div class="loading">Loading shared users...</div>';const s=await this.cache.fetchWithCache(`${jvbSettings.api}${this.config.endpoints.lists}?id=${t}`,{method:"GET",headers:{"X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbSettings.favourites}},{context:"list-item",forceRefresh:!1});removeChildren(e),s.list&&s.list.shared_users&&s.list.shared_users.length>0?(s.list.shared_users.forEach((t=>{const s=document.createElement("div");s.className=`shared-user ${t.status}`,s.innerHTML=`\n                        <span class="user-email">${t.email}</span>\n                        ${"pending"===t.status?'<span class="pending-badge">Invitation sent</span>':`<span class="permission-badge">${t.permission_type||"view"}</span>`}\n                        <button type="button" class="remove-share" data-email="${t.email}">\n                            ${jvbSettings.icons?.delete||"Remove"}\n                        </button>\n                    `,e.appendChild(s)})),e.querySelectorAll(".remove-share").forEach((t=>{t.addEventListener("click",(()=>{this.unshareList(t.dataset.email)}))}))):e.innerHTML='<div class="no-shares">This list is not shared with anyone yet.</div>'}catch(t){console.error("Error loading shared users:",t)}}async unshareList(t){if(confirm(`Remove ${t}'s access to this list?`))if(this.state.currentListId)try{this.showLoader("Removing access..."),this.queue.addToQueue({type:"favourite_list_unshare",data:{list_id:parseInt(this.state.currentListId),email:t}});const e=Array.from(this.shareListModal.querySelectorAll(".shared-user")).find((e=>e.querySelector(".user-email")?.textContent===t));e&&(e.classList.add("removing"),setTimeout((()=>{if(e.remove(),0===this.shareListModal.querySelectorAll(".shared-user").length){const t=this.shareListModal.querySelector(".shared-users");t&&(t.innerHTML='<div class="no-shares">This list is not shared with anyone yet.</div>')}}),300)),showToast(`Removed ${t}'s access`),this.a11y.announce(`Removed ${t}'s access to list`)}catch(t){this.handleError(t,"removing share access")}finally{this.hideLoader()}else showToast("No list selected","error")}async deleteList(t){if(confirm("Are you sure you want to delete this list? This cannot be undone."))try{this.showLoader("Deleting list..."),this.queue.addToQueue({type:"favourite_list_delete",data:{list_id:parseInt(t)}});const e=this.container.querySelector(`.list-card[data-id="${t}"]`);e&&(e.classList.add("removing"),setTimeout((()=>{e.remove(),0===this.container.querySelectorAll(".list-card").length&&(this.listContainer.innerHTML='\n                                <div class="no-lists">\n                                    <h3>No Lists Yet</h3>\n                                    <p>Create your first list to organize your favourites!</p>\n                                </div>\n                            ')}),300)),showToast("List deleted"),this.a11y.announce("List deleted")}catch(t){this.handleError(t,"deleting list")}finally{this.hideLoader()}}showBulkNotesModal(){let t=document.querySelector(".bulk-notes-modal");t||(t=document.createElement("dialog"),t.className="bulk-notes-modal",t.innerHTML='\n                <form method="dialog" data-save="favourites">\n                    <h2>Add Notes to Selected Items</h2>\n\n                    <div class="field">\n                        <label for="bulk-notes">Notes (will be applied to all selected items)</label>\n                        <textarea id="bulk-notes" name="bulk_notes" rows="5"></textarea>\n                    </div>\n\n                    <div class="actions">\n                        <button type="button" class="cancel">Cancel</button>\n                        <button type="submit" class="save">Save Notes</button>\n                    </div>\n                </form>\n            ',document.body.appendChild(t),t.querySelector("form").addEventListener("submit",(e=>{e.preventDefault();const s=t.querySelector("#bulk-notes").value;this.saveBulkNotes(s),t.close()})),t.querySelector(".cancel").addEventListener("click",(()=>{t.close()}))),t.querySelector("form")?.reset(),t.showModal(),setTimeout((()=>{t.querySelector("#bulk-notes")?.focus()}),100),window.jvbA11y&&window.jvbA11y.announce("Add notes dialog opened")}saveBulkNotes(t){if(t)try{this.showLoader("Saving notes...");let e=Array.from(this.state.selectedItems.values());this.queue.addToQueue({type:"favourite_notes",data:{target_id:e.join(","),notes:t}}),showToast(`Notes saved for ${e.length} items`),this.a11y.announce(`Notes saved for ${e.length} items`),this.clearSelection()}catch(t){this.handleError(t,"saving bulk notes")}finally{this.hideLoader()}}async bulkUnfavourite(){if(confirm("Are you sure you want to remove these items from your favourites?"))try{this.showLoader("Removing from favourites...");const t=[];this.state.selectedItems.forEach((e=>{const s=this.grid.querySelector(`.item[data-id="${e}"]`);if(!s)return;const i=s.dataset.type;t.push({target_id:parseInt(e),type:i,action:"remove"})})),this.queue.addToQueue({type:"favourite_toggle",data:t});const e=[];this.state.selectedItems.forEach((t=>{const s=this.grid.querySelector(`.item[data-id="${t}"]`);if(!s)return;s.style.opacity="0",s.style.transform="scale(0.9)",s.style.transition="opacity 0.3s ease, transform 0.3s ease";const i=new Promise((t=>{setTimeout((()=>{s.remove(),t()}),300)}));e.push(i)})),await Promise.all(e),0===this.grid.children.length&&this.showEmptyState(),this.clearSelection(),showToast(`Removed ${t.length} items from favourites`),this.a11y.announce(`Removed ${t.length} items from favourites`)}catch(t){this.handleError(t,"removing favourites")}finally{this.hideLoader()}}async handleShareList(t){const e=t.get("share_email");if(e)if(this.validateEmail(e))try{this.showLoader("Sharing list..."),this.queue.addToQueue({type:"favourite_list_share",data:{list_id:parseInt(this.state.currentListId),email:e,permission_type:"view"}}),this.shareListModal.querySelector("#share-email").value="",this.loadSharedUsers(this.state.currentListId),showToast(`Invitation sent to ${e}`),this.a11y.announce(`Invitation sent to ${e}`)}catch(t){this.handleError(t,"sharing list")}finally{this.hideLoader()}else showToast("Please enter a valid email address","error");else showToast("Please enter an email address","error")}showLoader(t="Loading..."){if(!this.loader)return;const e=this.loader.querySelector(".loader-message");e&&(e.textContent=t),this.loader.hidden=!1}hideLoader(){this.loader&&(this.loader.hidden=!0)}showToast(t,e){window.jvbNotifications.showToast(t,e)}handleError(t,e){console.error(`Favourites error (${e}):`,t),showToast(`Error ${e}: ${t.message||"Something went wrong"}`,"error"),window.jvbError&&window.jvbError.log(t,{component:"FavouritesManager",action:e}),window.jvbA11y&&window.jvbA11y.announce(`Error ${e}. ${t.message||"Please try again."}`)}validateEmail(t){return/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(t)}};
window.favouritesManager=class{constructor(){this.queue=window.jvbQueue,this.loadingManager=window.jvbLoading,this.cache=window.jvbCache,this.a11y=window.jvbA11y,this.error=window.jvbError,this.tabs=new window.jvbTabs(document.querySelector(".replace")),this.config={endpoints:{favourites:"favourites",lists:"favourites/lists",shares:"favourites/lists/shares"},selectors:{container:".favourites.container",itemsTab:'.tab-content[data-tab="items"]',listsTab:'.tab-content[data-tab="lists"]',grid:".item-grid",typeFilters:".type-filters",viewControls:".view-controls",bulkControls:".bulk-controls",selectAll:"#select-all",createListModal:".create-list-modal",addToListModal:".add-to-list-modal",shareListModal:".share-list-modal",noItems:".no-favourites",listContainer:".lists-container",listDetails:".list-details",loader:".favourites-loader"},defaultPage:1,defaultPerPage:24,defaultViewMode:"grid",refreshInterval:6e4,toastDuration:3e3},document.addEventListener("keydown",this.handleKeyDown.bind(this)),this.state={selectedItems:new Set,page:this.config.defaultPage,filter:{type:"all",order:"desc",orderBy:"date_added"},view:{mode:localStorage.getItem("favourites_view")||this.config.defaultViewMode,activeTab:"items"},pagination:{hasMore:!1,totalItems:0,totalPages:0},currentListId:null,loading:!1,initialized:!1},this.initDom(),this.initEvents(),this.loadInitialData(),this.state.initialized=!0}initDom(){this.container=document.querySelector(this.config.selectors.container),this.container?(this.grid=this.container.querySelector(this.config.selectors.grid),this.typeFilters=this.container.querySelector(this.config.selectors.typeFilters),this.viewControls=this.container.querySelector(this.config.selectors.viewControls),this.bulkControls=this.container.querySelector(this.config.selectors.bulkControls),this.listContainer=this.container.querySelector(this.config.selectors.listContainer),this.listDetails=this.container.querySelector(this.config.selectors.listDetails),this.loader=this.container.querySelector(this.config.selectors.loader),this.createListModal=document.querySelector(this.config.selectors.createListModal),this.addToListModal=document.querySelector(this.config.selectors.addToListModal),this.shareListModal=document.querySelector(this.config.selectors.shareListModal),this.grid&&this.state.view.mode&&this.grid.classList.add(`${this.state.view.mode}-view`)):console.warn("Favourites container not found")}initEvents(){if(this.typeFilters&&this.typeFilters.addEventListener("click",(t=>{const e=t.target.closest(".type-filter");e&&this.setFilterType(e.dataset.type)})),this.viewControls&&this.viewControls.addEventListener("click",(t=>{const e=t.target.closest(".view-toggle");e&&this.setView(e.dataset.view)})),this.container){const t=this.container.querySelector(this.config.selectors.selectAll);t&&t.addEventListener("change",(()=>{this.toggleSelectAll(t.checked)})),this.container.addEventListener("change",(t=>{t.target.matches(".item-select input[type=checkbox]")&&this.handleItemSelection(t.target)}));const e=this.container.querySelector(".bulk-action-select"),s=this.container.querySelector(".apply-bulk");e&&s&&s.addEventListener("click",(()=>{this.applyBulkAction(e.value)}));const i=this.container.querySelector(".cancel-bulk");i&&i.addEventListener("click",(()=>{this.clearSelection()}))}this.initModalEvents(),this.container.addEventListener("click",this.handleItemActions.bind(this)),this.grid&&this.setupInfiniteScroll()}initModalEvents(){if(this.createListModal){const t=this.createListModal.querySelector("form");t&&t.addEventListener("submit",(e=>{e.preventDefault(),this.handleCreateList(new FormData(t))}));const e=this.createListModal.querySelector(".cancel");e&&e.addEventListener("click",(()=>{this.createListModal.close()}))}if(this.addToListModal){const t=this.addToListModal.querySelector("form");t&&t.addEventListener("submit",(e=>{e.preventDefault(),this.handleAddToList(new FormData(t))}));const e=this.addToListModal.querySelector(".cancel");e&&e.addEventListener("click",(()=>{this.addToListModal.close()}))}if(this.shareListModal){const t=this.shareListModal.querySelector("form");t&&t.addEventListener("submit",(e=>{e.preventDefault(),this.handleShareList(new FormData(t))}));const e=this.shareListModal.querySelector(".cancel");e&&e.addEventListener("click",(()=>{this.shareListModal.close()}));const s=this.shareListModal.querySelector(".add-email");s&&s.addEventListener("click",(()=>{const e=this.shareListModal.querySelector("#share-email");e&&e.value&&this.handleShareList(new FormData(t))}))}}setupInfiniteScroll(){let t=this.container.querySelector(".scroll-sentinel");t||(t=document.createElement("div"),t.className="scroll-sentinel",t.setAttribute("aria-hidden","true"),this.grid.parentNode.appendChild(t)),new IntersectionObserver((t=>{t.forEach((t=>{t.isIntersecting&&this.state.pagination.hasMore&&!this.state.loading&&(this.state.page++,this.loadFavourites())}))}),{rootMargin:"200px"}).observe(t)}async loadInitialData(){this.loadingManager.show();try{await this.loadFavourites(),this.loadLists().catch((t=>{console.error("Error loading lists:",t)}))}catch(t){this.handleError(t,"loading initial data")}finally{this.loadingManager.hide()}}async loadFavourites(t=!0){if(!this.state.loading)try{this.state.loading=!0,this.loadingManager.show();const e=new URLSearchParams({page:this.state.page,per_page:this.config.defaultPerPage,type:"all"!==this.state.filter.type?this.state.filter.type:"",order:this.state.filter.order,orderby:this.state.filter.orderBy});t&&(this.state.page=1,removeChildren(this.grid),this.grid.classList.remove("empty"));const s=await this.cache.fetchWithCache(`${jvbSettings.api}${this.config.endpoints.favourites}?${e}`,{method:"GET",headers:{"X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbSettings.favourites}},{context:"favouritesManager",forceRefresh:!0});return this.renderFavourites(s.favourites||[],this.state.page>1),s.counts&&this.updateTypeFilters(s.counts),s.pagination&&(this.state.pagination={hasMore:s.pagination.has_more,totalItems:s.pagination.total_items,totalPages:s.pagination.total_pages}),s}catch(t){throw this.handleError(t,"loading favourites"),t}finally{this.state.loading=!1,this.loadingManager.hide()}}renderFavourites(t,e=!1){this.grid&&(0!==t.length||e?(this.hideEmptyState(),e||removeChildren(this.grid),t.forEach((t=>{const e=this.createItemElement(t);this.grid.appendChild(e),this.initItemFunctionality(e,t)})),window.jvbA11y&&window.jvbA11y.announce(`${e?"Added":"Loaded"} ${t.length} favourites`)):this.showEmptyState())}createItemElement(t){const e=document.createElement("div");e.className=`item ${t.type} favourited`,e.dataset.id=t.target_id,e.dataset.type=t.type;const s=sanitizeHtml(t.title||!1),i=sanitizeHtml(t.notes||"");let a="";return t.thumbnail&&(a=`\n                <div class="item-thumbnail">\n                    <a href="${t.url}">${t.thumbnail}</a>\n                </div>\n            `),e.innerHTML=`\n            <div class="item-select">\n                <input type="checkbox"\n                   class="favourite-checkbox"\n                    id="select-${t.target_id}"\n                    value="${t.target_id}">\n                <label for="select-${t.target_id}"><span class="screen-reader-text">Select this ${t.type}</span</label>\n            </div>\n\n            <button type="button" class="favourite-button favourited"\n                onclick="toggleFavourite(this)"\n                data-id="${t.target_id}"\n                data-type="${t.type}"\n                title="Remove from favourites">\n                ${jvbSettings.icons["heart-filled"]}\n            </button>\n\n            ${a}\n\n            <div class="item-info">\n                ${s?`<h3><a href="${t.url}">${s}</a></h3>`:`<a href="${t.url}">View Item</a>`}\n\n                ${t.author?`\n                <div class="item-artist">\n                    <span>By ${t.author.name}</span>\n                </div>`:""}\n\n                ${t.taxonomies?.length?`\n                <div class="taxonomy-lists">\n                    ${t.taxonomies.map((t=>`\n                        <div class="taxonomy-group">\n                            ${jvbSettings.icons[t.icon]}\n                            <ul>\n                                ${t.terms.slice(0,3).map((t=>`\n                                    <li>\n                                        <a href="${t.url}" ${t.umami_click}>\n                                            ${t.title}\n                                        </a>\n                                    </li>\n                                `)).join("")}\n                            </ul>\n                        </div>\n                    `)).join("")}\n                </div>\n            `:""}\n\n                <div class="notes-section">\n                    <button type="button" class="toggle-notes" aria-expanded="false">\n                        ${jvbSettings.icons.note||"Notes"}\n                        <span>Notes</span>\n                    </button>\n\n                    <div class="notes-content" hidden>\n                        <textarea class="notes-input"\n                            placeholder="Add notes about this item..."\n                            data-id="${t.target_id}"\n                            data-type="${t.type}">${i}</textarea>\n                        <button type="button" class="save-notes">Save Notes</button>\n                    </div>\n                </div>\n            </div>\n        `,e}initItemFunctionality(t,e){const s=t.querySelector(".toggle-notes"),i=t.querySelector(".notes-content");s&&i&&s.addEventListener("click",(()=>{const t="true"===s.ariaExpanded;s.ariaExpanded=!t.toString(),i.hidden=t,t||i.querySelector("textarea")?.focus()}));const a=t.querySelector(".save-notes"),n=t.querySelector(".notes-input");a&&n&&(a.addEventListener("click",(()=>{this.saveNotes(n)})),n.addEventListener("keydown",(t=>{"Enter"===t.key&&(t.ctrlKey||t.metaKey)&&(t.preventDefault(),this.saveNotes(n))})))}saveNotes(t){if(!t)return;const e=t.value.trim(),s=t.dataset.id,i=t.dataset.type;s&&i&&(this.queue.addToQueue({type:"favourite_notes",data:{type:i,target_id:parseInt(s),notes:e}}),showToast("Notes saved"),this.a11y.announce("Notes saved"))}showEmptyState(t=!1){const e=this.container.querySelector(this.config.selectors.noItems)??this.createEmptyElement;e&&(e.hidden=!1),this.grid&&this.grid.classList.add("empty"),this.a11y.announce("No favourites to show!")}hideEmptyState(){const t=this.container.querySelector(".no-favourites");t&&t.remove(),this.grid&&this.grid.classList.remove("empty")}createEmptyElement(t=!1){const e=document.createElement("div");e.className="no-favourites",e.innerHTML="\n            <h3>♡ BLANK CANVAS â™¡</h3>\n            <p>You haven't fallen in love with any pieces... yet!</p>\n            <p>Hit that heart icon when something stops your scroll.</p>\n            <p>Your dream collection is waiting to start.</p>\n        ",this.grid.after(e)}showEmptyListState(t=!1){const e=document.createElement("div");e.className="no-favourites",e.innerHTML="\n            <h3>♡ FULL OF POSSIBILITY â™¡</h3>\n            <p>There's nothing in this list yet.</p>\n            <p>Add some gap fillers from the main favourites tab.</p>\n        ",this.grid.after(e),this.grid.classList.add("empty"),this.a11y.announce("No favourites to show!")}async loadMoreItems(){!this.state.loading&&this.state.pagination.hasMore&&(this.state.page+=1,await this.loadFavourites())}updateTypeFilters(t){this.typeFilters&&this.typeFilters.querySelectorAll(".type-filter").forEach((e=>{const s=e.querySelector(".count");if(!s)return;const i=e.dataset.type;if("all"===i){const e=Object.values(t).reduce(((t,e)=>t+(parseInt(e)||0)),0);s.textContent=`(${e})`}else s.textContent=`(${t[i]||0})`}))}setFilterType(t){t!==this.state.filter.type&&(this.typeFilters&&this.typeFilters.querySelectorAll(".type-filter").forEach((e=>{e.classList.toggle("active",e.dataset.type===t),e.setAttribute("aria-selected",e.dataset.type===t)})),this.state.filter.type=t,this.state.page=1,this.loadFavourites(),window.jvbA11y&&window.jvbA11y.announce(`Filtered to show ${"all"===t?"all":t} items`))}setView(t){t!==this.state.view.mode&&(this.viewControls&&this.viewControls.querySelectorAll(".view-toggle").forEach((e=>{const s=e.dataset.view===t;e.setAttribute("aria-pressed",s)})),this.grid&&(this.grid.classList.remove("grid-view","list-view"),this.grid.classList.add(`${t}-view`)),this.state.view.mode=t,localStorage.setItem("favourites_view",t),window.jvbA11y&&window.jvbA11y.announce(`Changed to ${t} view`))}toggleSelectAll(t){const e=this.getVisibleItems();e.forEach((e=>{const s=e.querySelector('.item-select input[type="checkbox"]');s&&(s.checked=t,this.toggleItemSelection(s.value,t))})),this.updateBulkControls(),window.jvbA11y&&window.jvbA11y.announce(t?`Selected all ${e.length} items`:"Deselected all items")}getVisibleItems(){return this.grid?Array.from(this.grid.querySelectorAll(".item:not([hidden])")):[]}toggleItemSelection(t,e){e?this.state.selectedItems.add(t):this.state.selectedItems.delete(t);const s=this.grid.querySelector(`.item[data-id="${t}"]`);s&&s.classList.toggle("selected",e)}handleItemSelection(t){const e=t.checked,s=t.value;if(this.toggleItemSelection(s,e),this.updateBulkControls(),this.updateSelectAllState(),window.jvbA11y){const s=t.closest(".item"),i=s&&s.querySelector("h3")?.textContent||"item";window.jvbA11y.announce(e?`Selected ${i}`:`Deselected ${i}`)}}updateSelectAllState(){const t=this.container.querySelector(this.config.selectors.selectAll);if(!t)return;const e=this.getVisibleItems();if(0===e.length)return t.checked=!1,void(t.indeterminate=!1);const s=e.filter((t=>{const e=t.querySelector('.item-select input[type="checkbox"]');return e&&e.checked})).length;0===s?(t.checked=!1,t.indeterminate=!1):s===e.length?(t.checked=!0,t.indeterminate=!1):(t.checked=!1,t.indeterminate=!0)}updateBulkControls(){if(!this.bulkControls)return;const t=this.bulkControls.querySelector(".bulk-actions");if(!t)return;const e=this.state.selectedItems.size>0;t.hidden=!e;const s=this.bulkControls.querySelector(".selected-count");s&&(s.textContent=e?`${this.state.selectedItems.size} selected`:"")}handleKeyDown(t){"Escape"===t.key&&this.state.selectedItems.size>0&&(t.preventDefault(),this.clearSelection(),window.jvbA11y&&window.jvbA11y.announce("Selection cleared using Escape key"))}clearSelection(){this.state.selectedItems.clear(),this.getVisibleItems().forEach((t=>{const e=t.querySelector('.item-select input[type="checkbox"]');e&&(e.checked=!1),t.classList.remove("selected")}));const t=this.container.querySelector(this.config.selectors.selectAll);t&&(t.checked=!1,t.indeterminate=!1),this.updateBulkControls(),window.jvbA11y&&window.jvbA11y.announce("Selection cleared")}applyBulkAction(t){if(!t||0===this.state.selectedItems.size)return;switch(t){case"unfavourite":this.bulkUnfavourite();break;case"add-to-list":this.showAddToListModal();break;case"create-list":this.showCreateListModal();break;case"add-notes":this.showBulkNotesModal()}const e=this.container.querySelector(".bulk-action-select");e&&(e.value="")}handleItemActions(t){if(t.target.closest(".toggle-notes")){const e=t.target.closest(".toggle-notes"),s="true"===e.getAttribute("aria-expanded"),i=e.closest(".notes-section").querySelector(".notes-content");return e.setAttribute("aria-expanded",!s),i.hidden=s,!s&&i&&i.querySelector("textarea")?.focus(),void t.preventDefault()}if(t.target.closest(".save-notes")){const e=t.target.closest(".save-notes").closest(".notes-content").querySelector("textarea");return e&&this.saveNotes(e),void t.preventDefault()}if(t.target.closest(".view-list")){const e=t.target.closest(".view-list").closest(".list-card");return e&&e.dataset.id&&this.viewList(e.dataset.id),void t.preventDefault()}if(t.target.closest(".share-list")){const e=t.target.closest(".share-list").closest(".list-card");return e&&e.dataset.id&&this.showShareModal(e.dataset.id),void t.preventDefault()}if(t.target.closest(".delete-list")){const e=t.target.closest(".delete-list").closest(".list-card");return e&&e.dataset.id&&this.deleteList(e.dataset.id),void t.preventDefault()}if(t.target.closest(".back-to-lists"))return this.exitListView(),void t.preventDefault()}async loadLists(t=!0){try{this.state.loading=!0,this.loadingManager.show("Loading lists...");const t=await this.cache.fetchWithCache(`${jvbSettings.api}${this.config.endpoints.lists}`,{method:"GET",headers:{"X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbSettings.favourites}},{context:"favourite-lists",forceRefresh:!1});return t.lists&&this.renderLists(t.lists),t}catch(t){throw this.handleError(t,"loading lists"),t}finally{this.state.loading=!1,this.loadingManager.hide()}}renderLists(t){if(!this.listContainer)return;if(removeChildren(this.listContainer),!t||0===t.length)return void(this.listContainer.innerHTML='\n                <div class="no-lists">\n                    <h3>No Lists Yet</h3>\n                    <p>Select favourites from the main tab to organize into lists.</p>\n                </div>\n            ');const e=t.owned,s=t.shared;if(e.length>0){const t=document.createElement("details");t.className="lists-section owned-lists",t.open=!0,t.innerHTML="<summary>Your Lists:</summary>",e.forEach((e=>{const s=this.createListCard(e);t.appendChild(s)})),this.listContainer.appendChild(t)}if(s.length>0){const t=document.createElement("details");t.className="lists-section shared-lists",t.innerHTML="<summary>Lists Shared with You:</summary>",s.forEach((e=>{const s=this.createListCard(e);t.appendChild(s)})),this.listContainer.appendChild(t)}}createListCard(t){const e=document.createElement("div");e.className="list-card",e.dataset.id=t.id;const s="1"===t.is_shared;s&&e.classList.add("shared"),t.is_temp&&e.classList.add("temp"),t.is_owner;const i=sanitizeHtml(t.name||"Untitled List"),a=sanitizeHtml(t.description||"");return e.innerHTML=`\n            <div class="list-header">\n                <h3>${i}</h3>\n                <div class="list-actions">\n                    <button type="button" class="view-list" title="View List">\n                        ${jvbSettings.icons?.show||"View"}\n                    </button>\n                    ${s?"":`\n                        <button type="button" class="share-list" title="Share List">\n                            ${jvbSettings.icons?.share||"Share"}\n                        </button>\n                        <button type="button" class="delete-list" title="Delete List">\n                            ${jvbSettings.icons?.delete||"Delete"}\n                        </button>\n                    `}\n                </div>\n            </div>\n\n            ${a?`<p class="list-description">${a}</p>`:""}\n\n            <div class="list-meta">\n                <div class="meta-stats">\n                    <span class="item-count">${t.item_count||0} items</span>\n                    <span class="date">${formatDate(t.created_at)}</span>\n                </div>\n\n\n                ${s?`\n                    <div class="owner-info">\n                        Shared by ${t.owner_name||"another user"}\n                    </div>\n                `:t.share_count>0?`\n                    <div class="share-info">\n                        Shared with ${t.share_count} ${1===t.share_count?"person":"people"}\n                    </div>\n                `:""}\n            </div>\n        `,e}async viewList(t){try{this.state.loading=!0,this.loadingManager.show("Loading list..."),this.state.currentListId=t;const e=await this.cache.fetchWithCache(`${jvbSettings.api}${this.config.endpoints.lists}?id=${t}`,{method:"GET",headers:{"X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbSettings.favourites}},{context:"list-item",forceRefresh:!1});if(!e.list)throw new Error("List not found");this.showListDetails(e.list)}catch(t){this.handleError(t,"viewing list")}finally{this.state.loading=!1,this.loadingManager.hide()}}showListDetails(t){this.listDetails&&this.listContainer&&(console.log(t),this.listDetails.querySelector(".list-title").value=t.name||"Untitled List",this.listDetails.querySelector(".list-description").value=t.description||"",t.is_owner?this.listDetails.querySelector(".list-actions")||this.createListActions():this.listDetails.querySelector(".list-actions")?.remove(),removeChildren(this.grid),this.renderFavourites(t.items||[],!1),0===t.items.length&&this.showEmptyListState(),window.jvbA11y&&window.jvbA11y.announce(`Viewing list: ${t.name} with ${t.items?.length||0} items`))}createListActions(){const t=document.createElement("div");t.className="list-actions",t.innerHTML='\n            <button type="button" class="share-list" title="Share List">\n            <i class="icon icon-share-fat"></i>\n            <span>Share</span>\n        </button>\n        <button type="button" class="duplicate-list" title="Duplicate List">\n            <i class="icon icon-copy"></i>\n            <span>Duplicate</span>\n        </button>\n        <button type="button" class="delete-list" title="Delete List">\n            <i class="icon icon-trash"></i>\n            <span>Delete</span>\n        </button>\n        ',this.listDetails.insertBefore(t,this.listDetails.querySelector(".bulk-controls"))}exitListView(){this.listDetails&&this.listContainer&&(this.listDetails.hidden=!0,this.listContainer.hidden=!1,this.container.classList.remove("viewing-list"),this.state.currentListId=null,window.jvbA11y&&window.jvbA11y.announce("Returned to lists view"))}showCreateListModal(){this.createListModal&&(this.createListModal.querySelector("form")?.reset(),this.createListModal.showModal(),setTimeout((()=>{this.createListModal.querySelector("#list-name")?.focus()}),100),window.jvbA11y&&window.jvbA11y.announce("Create list dialog opened"))}showAddToListModal(){this.addToListModal&&(this.populateAddToListModal(),this.addToListModal.showModal(),window.jvbA11y&&window.jvbA11y.announce("Add to list dialog opened"))}async populateAddToListModal(){if(!this.addToListModal)return;const t=this.addToListModal.querySelector(".lists-options");if(t){removeChildren(t);try{const e=(await this.loadLists()).lists.owned;if(0===e.length)return t.innerHTML='\n                    <div class="no-lists">\n                        <p>You don\'t have any lists yet.</p>\n                        <button type="button" class="create-list-button">Create a list</button>\n                    </div>\n                ',void t.querySelector(".create-list-button")?.addEventListener("click",(()=>{this.addToListModal.close(),this.showCreateListModal()}));e.forEach((e=>{const s=document.createElement("div");s.className="list-option",s.innerHTML=`\n                    <input type="checkbox" id="${e.id}" name="list_ids[]" value="${e.id}">\n                    <label for="${e.id}">\n\n                        <span class="list-name">${sanitizeHtml(e.name)}</span>\n                        <span class="item-count">( ${e.item_count||0} items )</span>\n                    </label>\n                `,t.appendChild(s)}))}catch(e){t.innerHTML='\n                <div class="error-message">\n                    <p>Error loading lists. Please try again.</p>\n                </div>\n            ',console.error("Error loading lists for modal:",e)}}}async handleCreateList(t){const e=t.get("list_name"),s=t.get("list_description");if(e)try{this.showLoader("Creating list...");const t=[];this.state.selectedItems.forEach((e=>{const s=this.grid.querySelector(`.item[data-id="${e}"]`);s&&t.push({type:s.dataset.type,target_id:parseInt(e)})})),this.queue.addToQueue({type:"favourite_list_create",data:{name:e,description:s,items:t}}),showToast(`List "${e}" created`),this.a11y.announce(`List ${e} created with ${t.length} items`),this.createListModal.close(),this.clearSelection(),this.switchTab("lists")}catch(t){this.handleError(t,"creating list")}finally{this.hideLoader()}else showToast("Please enter a list name","error")}async handleAddToList(t){const e=t.getAll("list_ids[]");if(e.length)try{this.showLoader("Adding to list...");const t=[];this.state.selectedItems.forEach((e=>{const s=this.grid.querySelector(`.item[data-id="${e}"]`);s&&t.push({type:s.dataset.type,target_id:parseInt(e)})})),this.queue.addToQueue({type:"favourite_list_add",data:{list_id:e.join(","),items:t}}),showToast(`Added to ${e.length} ${1===e.length?"list":"lists"}`),this.a11y.announce(`Added ${t.length} items to ${e.length} ${1===e.length?"list":"lists"}`),this.addToListModal.close(),this.clearSelection()}catch(t){this.handleError(t,"adding to list")}finally{this.hideLoader()}else showToast("Please select at least one list","error")}async handleRemoveFromList(t){const e=t.getAll("list_ids[]");if(e.length)try{this.showLoader("Removing from list...");const t=[];this.state.selectedItems.forEach((e=>{const s=this.grid.querySelector(`.item[data-id="${e}"]`);s&&t.push({type:s.dataset.type,target_id:parseInt(e)})})),this.queue.addToQueue({type:"favourite_list_remove",data:{list_id:e.join(","),items:t}}),showToast(`Removed from ${e.length} ${1===e.length?"list":"lists"}`),this.a11y.announce(`Removed ${t.length} items to ${e.length} ${1===e.length?"list":"lists"}`),this.addToListModal.close(),this.clearSelection()}catch(t){this.handleError(t,"remove from list")}finally{this.hideLoader()}else showToast("Please select at least one list","error")}showShareModal(t){this.shareListModal&&(this.state.currentListId=t,this.shareListModal.querySelector("form")?.reset(),this.loadSharedUsers(t),this.shareListModal.showModal(),setTimeout((()=>{this.shareListModal.querySelector("#share-email")?.focus()}),100),window.jvbA11y&&window.jvbA11y.announce("Share list dialog opened"))}async loadSharedUsers(t){try{const e=this.shareListModal.querySelector(".shared-users");if(!e)return;e.innerHTML='<div class="loading">Loading shared users...</div>';const s=await this.cache.fetchWithCache(`${jvbSettings.api}${this.config.endpoints.lists}?id=${t}`,{method:"GET",headers:{"X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbSettings.favourites}},{context:"list-item",forceRefresh:!1});removeChildren(e),s.list&&s.list.shared_users&&s.list.shared_users.length>0?(s.list.shared_users.forEach((t=>{const s=document.createElement("div");s.className=`shared-user ${t.status}`,s.innerHTML=`\n                        <span class="user-email">${t.email}</span>\n                        ${"pending"===t.status?'<span class="pending-badge">Invitation sent</span>':`<span class="permission-badge">${t.permission_type||"view"}</span>`}\n                        <button type="button" class="remove-share" data-email="${t.email}">\n                            ${jvbSettings.icons?.delete||"Remove"}\n                        </button>\n                    `,e.appendChild(s)})),e.querySelectorAll(".remove-share").forEach((t=>{t.addEventListener("click",(()=>{this.unshareList(t.dataset.email)}))}))):e.innerHTML='<div class="no-shares">This list is not shared with anyone yet.</div>'}catch(t){console.error("Error loading shared users:",t)}}async unshareList(t){if(confirm(`Remove ${t}'s access to this list?`))if(this.state.currentListId)try{this.showLoader("Removing access..."),this.queue.addToQueue({type:"favourite_list_unshare",data:{list_id:parseInt(this.state.currentListId),email:t}});const e=Array.from(this.shareListModal.querySelectorAll(".shared-user")).find((e=>e.querySelector(".user-email")?.textContent===t));e&&(e.classList.add("removing"),setTimeout((()=>{if(e.remove(),0===this.shareListModal.querySelectorAll(".shared-user").length){const t=this.shareListModal.querySelector(".shared-users");t&&(t.innerHTML='<div class="no-shares">This list is not shared with anyone yet.</div>')}}),300)),showToast(`Removed ${t}'s access`),this.a11y.announce(`Removed ${t}'s access to list`)}catch(t){this.handleError(t,"removing share access")}finally{this.hideLoader()}else showToast("No list selected","error")}async deleteList(t){if(confirm("Are you sure you want to delete this list? This cannot be undone."))try{this.showLoader("Deleting list..."),this.queue.addToQueue({type:"favourite_list_delete",data:{list_id:parseInt(t)}});const e=this.container.querySelector(`.list-card[data-id="${t}"]`);e&&(e.classList.add("removing"),setTimeout((()=>{e.remove(),0===this.container.querySelectorAll(".list-card").length&&(this.listContainer.innerHTML='\n                                <div class="no-lists">\n                                    <h3>No Lists Yet</h3>\n                                    <p>Create your first list to organize your favourites!</p>\n                                </div>\n                            ')}),300)),showToast("List deleted"),this.a11y.announce("List deleted")}catch(t){this.handleError(t,"deleting list")}finally{this.hideLoader()}}showBulkNotesModal(){let t=document.querySelector(".bulk-notes-modal");t||(t=document.createElement("dialog"),t.className="bulk-notes-modal",t.innerHTML='\n                <form method="dialog" data-save="favourites">\n                    <h2>Add Notes to Selected Items</h2>\n\n                    <div class="field">\n                        <label for="bulk-notes">Notes (will be applied to all selected items)</label>\n                        <textarea id="bulk-notes" name="bulk_notes" rows="5"></textarea>\n                    </div>\n\n                    <div class="actions">\n                        <button type="button" class="cancel">Cancel</button>\n                        <button type="submit" class="save">Save Notes</button>\n                    </div>\n                </form>\n            ',document.body.appendChild(t),t.querySelector("form").addEventListener("submit",(e=>{e.preventDefault();const s=t.querySelector("#bulk-notes").value;this.saveBulkNotes(s),t.close()})),t.querySelector(".cancel").addEventListener("click",(()=>{t.close()}))),t.querySelector("form")?.reset(),t.showModal(),setTimeout((()=>{t.querySelector("#bulk-notes")?.focus()}),100),window.jvbA11y&&window.jvbA11y.announce("Add notes dialog opened")}saveBulkNotes(t){if(t)try{this.showLoader("Saving notes...");let e=Array.from(this.state.selectedItems.values());this.queue.addToQueue({type:"favourite_notes",data:{target_id:e.join(","),notes:t}}),showToast(`Notes saved for ${e.length} items`),this.a11y.announce(`Notes saved for ${e.length} items`),this.clearSelection()}catch(t){this.handleError(t,"saving bulk notes")}finally{this.hideLoader()}}async bulkUnfavourite(){if(confirm("Are you sure you want to remove these items from your favourites?"))try{this.showLoader("Removing from favourites...");const t=[];this.state.selectedItems.forEach((e=>{const s=this.grid.querySelector(`.item[data-id="${e}"]`);if(!s)return;const i=s.dataset.type;t.push({target_id:parseInt(e),type:i,action:"remove"})})),this.queue.addToQueue({type:"favourite_toggle",data:t});const e=[];this.state.selectedItems.forEach((t=>{const s=this.grid.querySelector(`.item[data-id="${t}"]`);if(!s)return;s.style.opacity="0",s.style.transform="scale(0.9)",s.style.transition="opacity 0.3s ease, transform 0.3s ease";const i=new Promise((t=>{setTimeout((()=>{s.remove(),t()}),300)}));e.push(i)})),await Promise.all(e),0===this.grid.children.length&&this.showEmptyState(),this.clearSelection(),showToast(`Removed ${t.length} items from favourites`),this.a11y.announce(`Removed ${t.length} items from favourites`)}catch(t){this.handleError(t,"removing favourites")}finally{this.hideLoader()}}async handleShareList(t){const e=t.get("share_email");if(e)if(this.validateEmail(e))try{this.showLoader("Sharing list..."),this.queue.addToQueue({type:"favourite_list_share",data:{list_id:parseInt(this.state.currentListId),email:e,permission_type:"view"}}),this.shareListModal.querySelector("#share-email").value="",this.loadSharedUsers(this.state.currentListId),showToast(`Invitation sent to ${e}`),this.a11y.announce(`Invitation sent to ${e}`)}catch(t){this.handleError(t,"sharing list")}finally{this.hideLoader()}else showToast("Please enter a valid email address","error");else showToast("Please enter an email address","error")}showLoader(t="Loading..."){if(!this.loader)return;const e=this.loader.querySelector(".loader-message");e&&(e.textContent=t),this.loader.hidden=!1}hideLoader(){this.loader&&(this.loader.hidden=!0)}showToast(t,e){window.jvbNotifications.showToast(t,e)}handleError(t,e){console.error(`Favourites error (${e}):`,t),showToast(`Error ${e}: ${t.message||"Something went wrong"}`,"error"),window.jvbError&&window.jvbError.log(t,{component:"FavouritesManager",action:e}),window.jvbA11y&&window.jvbA11y.announce(`Error ${e}. ${t.message||"Please try again."}`)}validateEmail(t){return/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(t)}};
assets/js/min/form.min.js
@@ -1 +1 @@
(()=>{class e{constructor(){this.store=new window.jvbStore({name:"forms",storeName:"forms",keyPath:"formId",indexes:[{name:"status",keyPath:"status"},{name:"operationId",keyPath:"operationId"},{name:"timestamp",keyPath:"timestamp"},{name:"formType",keyPath:"type"}],TTL:6048e5}),this.debouncer=window.debouncer,this.ignore=[],this.populateForm=window.jvbPopulate,this.subscribers=new Set,this.forms=new Map,this.specialFields=new Map,this.dependencies=new Map,this.validators=this.initValidators(),this.touchedFields=new Set,this.autoSaveDefaults={delay:3e3,typingDelay:1500,enabled:!0},this.activeRepeaters=new Map,this.repeaterDelays={change:6e3,typing:3e3,blur:1500,add:500,remove:800,reorder:1e3},this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.submitHandler=this.handleSubmit.bind(this),this.inputHandler=this.handleInput.bind(this),this.focusHandler=this.handleFocus.bind(this),this.blurHandler=this.handleBlur.bind(this),this.scanForms(),this.init()}async init(){await this.checkPendingOperations(),this.store.subscribe(this.handleStoreEvent.bind(this)),this.initListeners()}handleStoreEvent(e,t){switch(e){case"item-saved":t.item.status;break;case"data-loaded":this.checkPendingForms()}}async checkPendingForms(){(await this.store.query("status","draft")).forEach((e=>{let t=this.forms.get(e.formId);t&&t.element&&(t.element.querySelector(".restore-form").hidden=!1,new this.populateForm(t.element,e.data))}))}async checkPendingOperations(){const e=await this.store.query("status","pending");if(0===e.length)return;const t=this.groupPendingForms(e);this.showPendingNotification(t)}showPendingNotification(e){const t=document.querySelector(`[data-form-id="${e.formId}"]`);if(!t)return;const r=document.createElement("div");r.className="pending-changes-notification",r.innerHTML=`\n\t\t\t<p>We noticed unsaved changes from last time. Would you like to restore them?</p>\n\t\t\t<button class="restore-changes" data-form-id="${e.formId}">Restore</button>\n\t\t\t<button class="discard-changes" data-form-id="${e.formId}">Discard</button>\n\t\t`,t.insertBefore(r,t.firstChild),r.querySelector(".restore-changes").addEventListener("click",(()=>{this.restorePendingForm(e),r.remove()})),r.querySelector(".discard-changes").addEventListener("click",(()=>{this.discardPendingForm(e.formId),r.remove()}))}restorePendingForm(e){const t=document.querySelector(`[data-form-id="${e.formId}"]`);t&&(new this.populateForm(t,e.formData),e.status="restored",this.pendingForms.set(e.formId,e),window.jvbA11y&&window.jvbA11y.announce("Previous changes restored"))}async discardPendingForm(e){this.store.delete(e),window.jvbA11y&&window.jvbA11y.announce("Previous changes discarded")}initListeners(){this.globalHandlersAdded||(document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),document.addEventListener("focus",this.focusHandler,!0),document.addEventListener("blur",this.blurHandler,!0),document.addEventListener("input",this.inputHandler),this.globalHandlersAdded=!0)}scanForms(){document.querySelectorAll("form").forEach((e=>{console.log("Registering form...",e),this.registerForm(e)}))}registerForm(e,t={}){const r=e.dataset.formId||`form_${Date.now()}`;e.dataset.formId=r,e.addEventListener("submit",this.submitHandler);const s={element:e,id:r,options:{autoSave:"autosave"in e.dataset,saveDelay:this.autoSaveDefaults.delay,endpoint:e.dataset.save??"",cache:!0,...t},dependencies:new Map,data:this.collectFormData(e),isDirty:!1};if(this.initializeFormFields(e,s),this.forms.set(r,s),this.store&&s.options.cache){const e=this.store.get(r);e&&e.formData&&this.showPendingNotification(e)}return s}initializeFormFields(e,t=null){this.initQuillEditors(e),this.initRepeaterFields(e,t),t&&this.initConditionalFields(e,t),this.initCharacterLimits(e),this.initImageUploadFields(e),window.jvbTabs&&e.querySelector("nav.tabs")&&(t.tabs=new window.jvbTabs(e),this.forms.set(t.formId,t),this.initSteppedForm(t.formId)),window.jvbSelector&&window.jvbSelector.scanExistingFields(e)}initSteppedForm(e){const t=this.forms.get(e),r=t.element,s=t.tabs,a=r.querySelectorAll(".tab-content").length,i=r.querySelector(".form-progress .fill"),n=r.querySelector(".step-text .current"),o=r.querySelectorAll("nav.tabs button"),l=e=>{const t=e/a*100;i&&(i.style.width=t+"%"),n&&(n.textContent=e),o.forEach(((t,r)=>{const s=r+1;t.classList.remove("current","completed","pending"),s<e?t.classList.add("completed"):s===e?t.classList.add("current"):t.classList.add("pending")}))};r.addEventListener("click",(e=>{const t=e.target.closest('[data-action="next-step"]'),a=e.target.closest('[data-action="prev-step"]');if(t){e.preventDefault();const a=t.closest(".tab-content"),i=parseInt(a.dataset.step),n=r.querySelector(`.tab-content[data-step="${i+1}"]`);if(n&&this.validateStep(a)){const e=n.dataset.tab;s.switchTab(e,!0),l(i+1),r.scrollIntoView({behavior:"smooth",block:"start"})}}if(a){e.preventDefault();const t=a.closest(".tab-content"),i=parseInt(t.dataset.step),n=r.querySelector(`.tab-content[data-step="${i-1}"]`);if(n){const e=n.dataset.tab;s.switchTab(e,!0),l(i-1),r.scrollIntoView({behavior:"smooth",block:"start"})}}}));const c=s.switchTab.bind(s);s.switchTab=(e,t)=>{c(e,t);const s=r.querySelector(`.tab-content[data-tab="${e}"]`);if(s){const e=parseInt(s.dataset.step);l(e)}},l(1)}validateStep(e){const t=e.querySelectorAll(".field");let r=!0;return t.forEach((e=>{const t=e.querySelector("input, textarea, select");if(t&&!t.closest("[hidden]")){this.validateField(t,e)||(r=!1)}})),r}initQuillEditors(e){window.jvbQuill(e)}initRepeaterFields(e,t){e.querySelectorAll(".repeater").forEach((e=>{const r=e.querySelector(".add-repeater-row"),s=e.querySelector(".repeater-items"),a=e.querySelector("template");r&&a&&s&&(window.Sortable&&new Sortable(s,{handle:".repeater-row-header",animation:150,onEnd:()=>{this.updateRepeaterOrder(e,t)}}),r.addEventListener("click",(()=>{this.addRepeaterRow(e,t)})),s.addEventListener("click",(e=>{e.target.closest(".remove-row")&&this.removeRepeaterRow(e.target.closest(".repeater-row"),t)})))}))}addRepeaterRow(e,t){const r=e.querySelector(".repeater-items"),s=e.querySelector("template"),a=r.children.length,i=e.dataset.field,n=s.content.cloneNode(!0).firstElementChild;n.dataset.index=a,n.querySelectorAll("input, select, textarea").forEach((e=>{const t=e.name;e.name=`${i}:${a}:${t}`,e.id=`${i}-${a}-${t}`;const r=e.nextElementSibling;r&&"LABEL"===r.tagName&&(r.htmlFor=e.id)})),r.appendChild(n),t&&t.options.autoSave&&this.scheduleSave(t,{type:"repeater",action:"add",fieldName:i,delay:this.repeaterDelays.add}),window.jvbA11y&&window.jvbA11y.announce("Row added")}removeRepeaterRow(e,t){const r=e.closest(".repeater"),s=r.dataset.field;e.remove(),this.updateRepeaterOrder(r,t),t&&t.options.autoSave&&this.scheduleSave(t,{type:"repeater",action:"remove",fieldName:s,delay:this.repeaterDelays.remove}),window.jvbA11y&&window.jvbA11y.announce("Row removed")}updateRepeaterOrder(e,t){const r=e.querySelector(".repeater-items"),s=e.dataset.field;Array.from(r.children).forEach(((e,t)=>{e.dataset.index=t,e.querySelectorAll("input, select, textarea").forEach((e=>{const r=e.name.split(":");if(3===r.length){const a=r[2];e.name=`${s}:${t}:${a}`,e.id=`${s}-${t}-${a}`;const i=e.nextElementSibling;i&&"LABEL"===i.tagName&&(i.htmlFor=e.id)}}))})),t&&t.options.autoSave&&this.scheduleSave(t,{type:"repeater",action:"reorder",fieldName:s,delay:this.repeaterDelays.reorder})}initConditionalFields(e,t){e.querySelectorAll("[data-depends-on]").forEach((r=>{const s=r.dataset.dependsOn,a=r.dataset.dependsValue,i=r.dataset.dependsOperator||"==";t.dependencies.has(s)||t.dependencies.set(s,[]),t.dependencies.get(s).push({field:r,requiredValue:a,operator:i}),this.checkFieldDependency(e,r,s,a,i)}))}checkFieldDependency(e,t,r,s,a){const i=e.querySelector(`[name="${r}"]`);if(!i)return;const n=this.getFieldValue(i),o=this.evaluateCondition(n,s,a);this.toggleFieldVisibility(t,o)}evaluateCondition(e,t,r){const s=String(e||""),a=String(t||"");switch(r){case"==":default:return s==a;case"!=":return s!=a;case">":return parseFloat(s)>parseFloat(a);case"<":return parseFloat(s)<parseFloat(a);case">=":return parseFloat(s)>=parseFloat(a);case"<=":return parseFloat(s)<=parseFloat(a);case"contains":return s.includes(a);case"empty":return""===s;case"not_empty":return""!==s}}toggleFieldVisibility(e,t){const r=e.closest(".field, fieldset");r&&(r.hidden=!t,r.querySelectorAll("input, select, textarea").forEach((e=>{e.disabled=!t,!t&&e.hasAttribute("required")?(e.dataset.wasRequired="true",e.removeAttribute("required")):t&&"true"===e.dataset.wasRequired&&(e.setAttribute("required",""),delete e.dataset.wasRequired)})))}initCharacterLimits(e){e.querySelectorAll("[data-limit]").forEach((e=>{const t=parseInt(e.dataset.limit,10),r=e.closest(".field");let s=r?.querySelector(".char-count");!s&&r&&(s=document.createElement("div"),s.className="char-count",s.innerHTML=`<span class="current">0</span> / <span class="limit">${t}</span>`,r.appendChild(s));const a=()=>{const r=e.value.length;s&&(s.querySelector(".current").textContent=r,s.classList.toggle("exceeded",r>t)),r>t&&(e.value=e.value.substring(0,t),s&&(s.querySelector(".current").textContent=t))};e.addEventListener("input",a),a()}))}initImageUploadFields(e){window.jvbUploads.scanFields(e)}handleSubmit(e){if(this.subscribers.size>0){const t=e.target;if(!t.dataset.formId)return;e.preventDefault();const r=this.forms.get(t.dataset.formId);if(!r)return;const s=this.collectFormData(t);this.notify("form-submit",{formId:r.id,data:s,config:r})}}handleClick(e){if(window.targetCheck(e,"div.quantity")){let t=window.targetCheck(e,"div.quantity");this.handleNumberClick(e,t.querySelector("input"))}else if(window.targetCheck(e,"[data-action]")){let t=window.targetCheck(e,"[data-action]");switch(t=t.dataset.action,t){case"clear-form":let t=e.target.closest("form");this.store.delete(t.dataset.formId),t?.reset(),e.target.closest(".restore-form").hidden=!0;break;case"dismiss-restore":e.target.closest(".restore-form").hidden=!0}}}handleNumberClick(e,t){let r=0;if(e.target.closest(".increase")?r+=1:e.target.closest(".decrease")&&(r-=1),0!==r){let s=parseFloat(t.step);s=Math.max(s,1),e.ctrlKey&&e.shiftKey?s*=50:e.ctrlKey?s*=5:e.shiftKey&&(s*=10);let a=""===t.value?0:parseFloat(t.value);t.value=a+s*r,this.handleNumberLimits(t)}}handleNumberLimits(e){let[t,r,s,a]=[e.min,e.max,e.closest(".quantity")?.querySelector(".increase"),e.closest(".quantity")?.querySelector(".decrease")],i=parseFloat(e.value);i<t?(e.value=t,a.disabled=!0):i>r?(e.value=r,s.disabled=!1):s.disabled?s.disabled=!1:a.disabled&&(a.disabled=!1)}handleChange(e){if(!e.target.closest("[data-ignore]")&&this.subscribers.size>0){const t=e.target,r=t.form||t.closest("form");if(!r)return;const s=this.forms?.get(r.dataset.formId);if(!s)return;const a=s.dependencies.get(t.name);if(a&&a.forEach((e=>{this.checkFieldDependency(r,e.field,t.name,e.requiredValue,e.operator)})),s.options.autoSave&&!r.dataset.noautosave){const e=this.getDelayForField(t);this.scheduleSave(s,e)}}}handleFocus(e){const t=e.target;t.matches("input, textarea, select")&&(this.currentFocus=t)}handleBlur(e){if(e.target.closest("[data-ignore]"))return;const t=e.target,r=t.form||t.closest("form");if(!r)return;const s=e.target.closest("input, textarea, select");if(s){const e=this.findFieldWrapper(s);if(e){const t=e.dataset.field;t&&(this.shouldDebounce(s)&&window.debouncer.cancel(`validate_${t}`),this.touchedFields.add(t)),this.validateField(s,e)}const a=this.forms?.get(r.dataset.formId);a&&a.options.autoSave&&!r.dataset.noautosave&&this.scheduleSave(a,{type:"blur",fieldName:t.name,delay:1500})}}handleInput(e){if(e.target.closest("[data-ignore]")||!e.target.closest("form"))return;const t=e.target.closest("input, textarea, select");if(!t)return;let r=t.closest("form");this.showFormStatus(r.dataset.formId,"pending");const s=this.findFieldWrapper(t);if(!s)return;const a=s.dataset.field;a&&this.touchedFields.add(a),this.shouldDebounce(t)&&window.debouncer.schedule(`validate_${a}`,((e,t)=>this.validateField.bind(this)),500)}initValidators(){return{email:{pattern:/^[^\s@]+@[^\s@]+\.[^\s@]+$/,message:"Please enter a valid email address"},url:{pattern:/^https?:\/\/.+\..+/,message:"Please enter a valid URL starting with http:// or https://"},phone:{pattern:/^[\d\s\-\+\(\)\.]+$/,message:"Please enter a valid phone number"},number:{test:(e,t)=>{const r=parseFloat(e);if(isNaN(r))return"Please enter a valid number";const s=t.dataset.min,a=t.dataset.max;return void 0!==s&&r<parseFloat(s)?`Value must be at least ${s}`:!(void 0!==a&&r>parseFloat(a))||`Value must be at most ${a}`}},text:{test:(e,t)=>{const r=t.dataset.minlength,s=t.dataset.maxlength;return r&&e.length<parseInt(r)?`Must be at least ${r} characters`:!(s&&e.length>parseInt(s))||`Must be no more than ${s} characters`}}}}findFieldWrapper(e){let t=e.closest(".field");return t||(t=e.closest("[data-field]")),t}shouldDebounce(e){return["text","email","url","tel","search"].includes(e.type)||"TEXTAREA"===e.tagName}validateField(e,t){const r=this.getFieldValue(e),s=t.dataset.field;if(!this.touchedFields.has(s)&&!e.required)return!0;if(!r&&!e.required)return this.clearValidation(t),!0;if(e.required&&!r)return this.showError(t,"This field is required"),!1;if(e.checkValidity&&!e.checkValidity())return this.showError(t,e.validationMessage),!1;const a=t.dataset.pattern;if(a&&r){if(!new RegExp(a).test(r)){const e=t.dataset.validationMessage||"Invalid format";return this.showError(t,e),!1}}const i=t.dataset.validate||e.type;if(i&&this.validators[i]){const e=this.validators[i];if(e.pattern&&!e.pattern.test(r))return this.showError(t,e.message),!1;if(e.test){const s=e.test(r,t);if(!0!==s)return this.showError(t,s),!1}}return this.showSuccess(t),!0}getFieldValue(e){if(!e)return"";if("checkbox"===e.type)return e.checked?e.value||"1":"";if("radio"===e.type){const t=e.form?.querySelector(`[name="${e.name}"]:checked`);return t?t.value:""}return"select-multiple"===e.type?Array.from(e.selectedOptions).map((e=>e.value)):e.value?.trim()||""}showSuccess(e){if(!e)return;const t=e.querySelector(".validation-icon.success"),r=e.querySelector(".validation-icon.error"),s=e.querySelector(".validation-message"),a=e.querySelector("input, textarea, select");e.classList.remove("has-error"),a?.classList.remove("error"),e.classList.add("has-success"),t&&(t.hidden=!1),r&&(r.hidden=!0),s&&(s.hidden=!0,s.textContent="")}showError(e,t){if(!e)return;const r=e.querySelector(".validation-icon.success"),s=e.querySelector(".validation-icon.error"),a=e.querySelector(".validation-message"),i=e.querySelector("input, textarea, select");e.classList.remove("has-success"),e.classList.add("has-error"),i?.classList.add("error"),r&&(r.hidden=!0),s&&(s.hidden=!1),a&&(a.hidden=!1,a.textContent=t)}clearValidation(e){if(!e)return;const t=e.querySelector(".validation-icon"),r=e.querySelector(".validation-message"),s=e.querySelector("input, textarea, select");e.classList.remove("has-error","has-success"),s?.classList.remove("error"),t&&(t.hidden=!0),r&&(r.hidden=!0,r.textContent="")}validateAllFields(e){if(!e)return!0;const t=e.querySelectorAll(".field:not([hidden])");let r=!0;return t.forEach((e=>{if(this.isComplexFieldWrapper(e))return;const t=e.querySelector('input:not([type="hidden"]), textarea, select');if(t&&!t.closest("[hidden]")){const s=e.dataset.field;s&&this.touchedFields.add(s);this.validateField(t,e)||(r=!1,!1===r&&(t.scrollIntoView({behavior:"smooth",block:"center"}),t.focus()))}})),r}isComplexFieldWrapper(e){return e.classList.contains("repeater")||e.classList.contains("group")||e.classList.contains("upload")}attachRepeaterValidation(e){e.addEventListener("click",(t=>{t.target.closest(".add-repeater-row")&&setTimeout((()=>{e.querySelectorAll(".repeater-row").forEach((e=>{e.querySelectorAll("input, textarea, select").forEach((e=>{const t=this.findFieldWrapper(e);t&&this.clearValidation(t)}))}))}),100)}))}attachGroupValidation(e){e.addEventListener("change",(t=>{const r=t.target.closest("input, select");if(!r)return;const s=r.name;if(!s)return;e.querySelectorAll(`[data-show-if*="${s}"]`).forEach((e=>{e.hidden&&this.clearValidation(e)}))}))}resetForm(e){if(!e)return;this.touchedFields.clear();e.querySelectorAll(".field").forEach((e=>{this.clearValidation(e)}))}getFormErrors(e){const t={};return e.querySelectorAll(".field.has-error").forEach((e=>{const r=e.dataset.field,s=e.querySelector(".validation-message");r&&s&&(t[r]=s.textContent)})),t}addValidator(e,t){this.validators[e]=t}getDelayForField(e){return"text"===e.type||"textarea"===e.type?this.autoSaveDefaults.typingDelay:["checkbox","radio","select-one","select-multiple"].includes(e.type)?1e3:this.autoSaveDefaults.delay}scheduleSave(e,t=this.autoSaveDefaults.delay){document.addEventListener("input",this.saveCheck,{passive:!0});const r=`autosave_${e.id}`;this.debouncer.schedule(r,(()=>this.autosave(e)),t)}saveCheck(e){let t=e.target.closest("form[data-id]");t&&this.scheduleSave(this.forms.get(t.dataset.id))}async autosave(e){const t=this.collectFormData(e.element);this.showFormStatus(e.id,"saving"),await this.store.save({formId:e.id,data:t,status:"draft",timestamp:Date.now()}).then((()=>{this.showFormStatus(e.id,"autosaved")}));const r=this.getChangedFields(e.data,t);if(0!==Object.keys(r).length){e.data=t,this.forms.set(e.id,e),document.removeEventListener("input",this.handleInput);for(let[e,s]of Object.entries(t))"object"==typeof s&&(r[e]=s);this.notify("form-autosave",{formId:e.id,changes:r,fullData:t,config:e})}}hasUnsavedChanges(e){const t=this.forms.get(e);if(!t)return!1;if(t.operations?.size>0)return!0;const r=this.collectFormData(t.element),s=this.getChangedFields(t.lastSnapshot,r);return Object.keys(s).length>0}showFormStatus(e,t){let r=this.forms.get(e);console.log("Setting status: ",t);const s=r.element.querySelector(".fstatus");s.hidden=!1;const a=s.querySelector(".message");a.textContent="",s.querySelector(".icon")?.remove();const i={saving:"Saving changes...",autosaved:"Changes saved locally. Submit form to send to server.",uploading:"Uploading your form to server",submitted:"Successfully sent to server",pending:"Unsaved changes",error:"Failed to save changes. Refresh and try again?",offline:"Changes will be saved when online"},n={autosaved:"check",submitted:"check",error:"close",offline:"cloud-slash",pending:"exclamation-mark"};let o=window.getIcon(n[t]);o&&s.prepend(o),console.log(t,i[t]),console.log(t,n[t]),a.textContent=i[t]||t,s.classList.toggle("loading",["uploading","saving"].includes(t)),"submitted"===t&&setTimeout((()=>s.hidden=!0),3e3)}cleanupSpecialFields(){this.specialFields.forEach((e=>{if("quill"===e.type&&e.instance){const t=e.instance.container.previousSibling;t?.classList.contains("ql-toolbar")&&t.remove()}})),this.uploader?.destroy(),this.specialFields.clear()}collectFormData(e){const t=new FormData(e);let r={};const s={},a={};for(let[i,n]of t.entries()){if(this.ignore.includes(i)||i.endsWith("_temp"))continue;this.getFieldProcessor(i)(i,n,r,s,a,e)}return window.isEmptyObject(a)?this.mergeRepeaterData(r,s):(r=this.mergeRepeaterData(r,s),this.mergePostData(r,a))}getFieldProcessor(e){return e.includes("|")?this.processTableField:e.includes("::")?this.processGroupField:e.includes(":")?this.processRepeaterField:/\[[^\]]+\]/.test(e)?this.processLocationField:this.processRegularField}mergeRepeaterData(e,t){return Object.keys(t).forEach((r=>{const s={};Object.keys(t[r]).forEach((e=>{const a=t[r][e];Object.keys(a).length>0&&(s[e]=a)})),e[r]=Object.values(s)})),e}mergePostData(e,t){for(let[t,r]in Object.entries(r))e[t]=r;return e}processTableField(e,t,r,s,a,i){let[n,o]=e.split("|");!n in a&&(a[n]={});this.getFieldProcessor(o)(o,t,a,s,a,i)}processRepeaterField(e,t,r,s,a,i){let[n,o,l]=e.split(":");const c=l.endsWith("[]");l=l.replace("[]",""),s[n]||(s[n]={}),s[n][o]||(s[n][o]={}),c||s[n][o][l]?(s[n][o][l]?Array.isArray(s[n][o][l])||(s[n][o][l]=[s[n][o][l]]):s[n][o][l]=[],s[n][o][l].push(t)):s[n][o][l]=t}processGroupField(e,t,r,s,a,i){const n=e.split("::"),o=n[0];r[o]||(r[o]={});let l=r[o];for(let e=1;e<n.length-1;e++){const t=n[e];l[t]||(l[t]={}),l=l[t]}const c=n[n.length-1];void 0!==l[c]?(Array.isArray(l[c])||(l[c]=[l[c]]),l[c].push(t)):l[c]=t}processLocationField(e,t,r,s,a,i){let[n,o]=e.split("[");o=o.replace("]",""),Object.hasOwn(r,n)||(r[n]={},Object.hasOwn(r,"sendAll")?r.sendAll.includes(n)||r.sendAll.push(n):r.sendAll=[n]),r[n][o]=t}processRegularField(e,t,r,s,a,i){r[e=e.replace("[]","")]?(Array.isArray(r[e])||(r[e]=[r[e]]),r[e].push(t)):r[e]=t}getFieldValue(e){if(!e)return"";if("checkbox"===e.type)return e.checked?e.value||"1":"";if("radio"===e.type){const t=e.form.querySelector(`[name="${e.name}"]:checked`);return t?t.value:""}return"select-multiple"===e.type?Array.from(e.selectedOptions).map((e=>e.value)):e.value}getChangedFields(e,t){return window.getDifferences?.map(e,t)||{}}showSummary(e,t="form"){const r=this.forms.get(e);if(!r)return;const s=r.element||document.querySelector(`[data-form-id="${e}"]`),a=window.getTemplate("formSummary"),[i,n,o]=[a.querySelector("h2"),a.querySelector(".summary"),a.querySelector(".result")],l=["sendAll",...this.ignore];for(const[e,t]of Object.entries(r.data)){if(l.includes(e)||this.isEmptyValue(t))continue;const r=this.getFieldInfo(s,e);if(!r.label)continue;const a=this.createResultElement(o,r,t,s);a&&n.appendChild(a)}o.remove(),(t="form"!==t?s.closest(t)??s:s).after(a),window.fade(t,!1)}isEmptyValue(e){return null==e||""===e||(!(!Array.isArray(e)||0!==e.length)||"object"==typeof e&&0===Object.keys(e).length)}getFieldInfo(e,t){let r=e.querySelector(`label[for="${t}"]`),s=null,a=null;if(s||(s=e.querySelector(`[name="${t}"]`)),s||(s=e.querySelector(`[name="${t}[]"]`)),!s){const a=e.querySelector(`fieldset[data-field="${t}"]`);a&&(r=a.querySelector("legend"),s=a.querySelector("input, select, textarea"))}if(!r&&s){const e=s.closest(".field, fieldset");e&&(r=e.querySelector("label, legend"))}a=e.querySelector(`.field[data-field="${t}"], fieldset[data-field="${t}"]`);let i="text";return a?.dataset.type?i=a.dataset.type:s&&(i="checkbox"===s.type&&s.name.endsWith("[]")?"checkbox":"checkbox"===s.type?"true_false":"SELECT"===s.tagName&&s.multiple?"select":s.type||"text"),{label:r?.textContent.replace("*","").trim()||null,type:i,wrapper:a,input:s}}createResultElement(e,t,r,s){const a=e.cloneNode(!0),i=a.querySelector("h4"),n=a.querySelector("p");i.textContent=t.label;const o=this.formatFieldValue(r,t.type,s);return this.isHtmlContent(o)?n.innerHTML=o:n.textContent=o,a}isHtmlContent(e){return"string"==typeof e&&(e.includes("<br>")||e.includes("<p>")||e.includes("<ul>")||e.includes("<ol>")||e.includes("<a ")||e.includes("<strong>")||e.includes("<em>")||e.includes("<div"))}formatFieldValue(e,t,r){switch(t){case"textarea":case"wysiwyg":return this.formatTextareaValue(e,t);case"true_false":return"1"===e||1===e||!0===e?"Yes":"No";case"checkbox":return Array.isArray(e)?this.formatArrayValue(e):"1"===e||1===e||!0===e?"Yes":"No";case"select":return Array.isArray(e)?this.formatArrayValue(e):this.getSelectLabel(e,r,t);case"date":case"datetime":case"time":return window.formatDate?window.formatDate(e):e;case"radio":return this.getSelectLabel(e,r,t);case"repeater":return this.formatRepeaterValue(e);case"group":return this.formatGroupValue(e);case"location":return this.formatLocationValue(e);case"file":case"image":return this.formatFileValue(e);case"number":return this.formatNumber(e);case"email":return`<a href="mailto:${e}">${e}</a>`;case"url":return`<a href="${e}" target="_blank" rel="noopener">${e}</a>`;case"phone":return`<a href="tel:${e.replace(/\D/g,"")}">${e}</a>`;default:return Array.isArray(e)?this.formatArrayValue(e):e}}formatRepeaterValue(e){if(!Array.isArray(e)||0===e.length)return"<em>No entries</em>";let t='<div class="repeater-summary">';return e.forEach(((e,r)=>{t+='<div class="repeater-row">',t+=`<strong>Entry ${r+1}:</strong><ul>`;for(const[r,s]of Object.entries(e))if(!this.isEmptyValue(s)){const e=r.replace(/_/g," ").replace(/\b\w/g,(e=>e.toUpperCase()));t+=`<li><strong>${e}:</strong> ${s}</li>`}t+="</ul></div>"})),t+="</div>",t}formatGroupValue(e){if("object"!=typeof e||0===Object.keys(e).length)return"<em>No data</em>";let t='<div class="group-summary"><ul>';for(const[r,s]of Object.entries(e))if(!this.isEmptyValue(s)){const e=r.replace(/_/g," ").replace(/\b\w/g,(e=>e.toUpperCase()));"object"!=typeof s||Array.isArray(s)?t+=`<li><strong>${e}:</strong> ${s}</li>`:t+=`<li><strong>${e}:</strong> ${this.formatGroupValue(s)}</li>`}return t+="</ul></div>",t}formatLocationValue(e){if("object"!=typeof e)return e;const t=[];return["address","city","state","zip","country"].forEach((r=>{e[r]&&t.push(e[r])})),t.join(", ")}formatFileValue(e){return"string"==typeof e?e.startsWith("http")?`<a href="${e}" target="_blank">View file</a>`:e:Array.isArray(e)?e.map((e=>"string"==typeof e?`<a href="${e}" target="_blank">View file</a>`:e.name||"File")).join(", "):"File uploaded"}formatNumber(e){const t=parseFloat(e);return isNaN(t)?e:e.toString().includes(".")&&2===e.toString().split(".")[1].length?new Intl.NumberFormat("en-CA",{style:"currency",currency:"USD"}).format(t):new Intl.NumberFormat("en-CA").format(t)}formatArrayValue(e,t=null,r=null){if(0===e.length)return"<em>None selected</em>";if(t&&r&&r.input){return"<ul><li>"+e.map((e=>this.getSelectLabel(e,t,r.type))).join("</li><li>")+"</li></ul>"}return"<ul><li>"+e.join("</li><li>")+"</li></ul>"}getSelectLabel(e,t,r){if("select"===r){const r=t.querySelector(`option[value="${e}"]`);return r?.textContent||e}if("radio"===r){const r=t.querySelector(`input[type="radio"][value="${e}"]`),s=r?.nextElementSibling;return s?.textContent||e}if("checkbox"===r){const r=t.querySelector(`input[type="checkbox"][value="${e}"]`);if(r){const e=t.querySelector(`label[for="${r.id}"]`);if(e)return e.textContent.trim();const s=r.nextElementSibling;if("LABEL"===s?.tagName)return s.textContent.trim()}}return e}formatTextareaValue(e,t){return e?"wysiwyg"===t||this.containsHtml(e)?e:this.formatPlainText(e):"<em>Empty</em>"}containsHtml(e){return/<(p|strong|em|u|s|ol|ul|li|blockquote|h[1-6]|a|br|span)\b[^>]*>/i.test(e)}formatPlainText(e){if(!e)return"";const t=(e=e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")).split(/\n\n+/);return t.length>1?t.map((e=>`<p>${e.replace(/\n/g,"<br>")}</p>`)).join(""):e.replace(/\n/g,"<br>")}nl2br(e){return this.formatPlainText(e)}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((r=>r(e,t)))}cleanupForm(e){const t=this.forms.get(e);t&&(this.hasUnsavedChanges(e)&&this.autosave(t),this.cleanupSpecialFields(),this.forms.delete(e))}destroy(){this.globalHandlersAdded&&(document.removeEventListener("change",this.changeHandler),document.removeEventListener("focus",this.focusHandler,!0),document.removeEventListener("blur",this.blurHandler,!0),document.removeEventListener("input",this.inputHandler,!0)),this.forms.forEach((e=>{let t=e.element;t&&t.removeEventListener("submit",this.submitHandler)})),this.specialFields.clear(),this.forms.clear(),this.activeRepeaters.clear(),this.forms&&this.forms.clear()}}document.addEventListener("DOMContentLoaded",(()=>{window.jvbForm=e,console.log("FormController in window")}))})();
(()=>{class e{constructor(){this.store=new window.jvbStore({name:"forms",storeName:"forms",keyPath:"formId",indexes:[{name:"status",keyPath:"status"},{name:"operationId",keyPath:"operationId"},{name:"timestamp",keyPath:"timestamp"},{name:"formType",keyPath:"type"}],TTL:6048e5}),this.debouncer=window.debouncer,this.ignore=[],this.populateForm=window.jvbPopulate,this.subscribers=new Set,this.forms=new Map,this.specialFields=new Map,this.dependencies=new Map,this.validators=this.initValidators(),this.touchedFields=new Set,this.autoSaveDefaults={delay:3e3,typingDelay:1500,enabled:!0},this.activeRepeaters=new Map,this.repeaterDelays={change:6e3,typing:3e3,blur:1500,add:500,remove:800,reorder:1e3},this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.submitHandler=this.handleSubmit.bind(this),this.inputHandler=this.handleInput.bind(this),this.focusHandler=this.handleFocus.bind(this),this.blurHandler=this.handleBlur.bind(this),this.init()}async init(){await this.checkPendingOperations(),this.store.subscribe(this.handleStoreEvent.bind(this)),this.initListeners()}handleStoreEvent(e,t){switch(e){case"item-saved":t.item.status;break;case"data-loaded":this.checkPendingForms()}}async checkPendingForms(){(await this.store.query("status","draft")).forEach((e=>{let t=this.forms.get(e.formId);t&&t.element&&(t.element.querySelector(".restore-form").hidden=!1,new this.populateForm(t.element,e.data))}))}async checkPendingOperations(){const e=await this.store.query("status","pending");if(0===e.length)return;const t=this.groupPendingForms(e);this.showPendingNotification(t)}showPendingNotification(e){const t=document.querySelector(`[data-form-id="${e.formId}"]`);if(!t)return;const r=document.createElement("div");r.className="pending-changes-notification",r.innerHTML=`\n\t\t\t<p>We noticed unsaved changes from last time. Would you like to restore them?</p>\n\t\t\t<button class="restore-changes" data-form-id="${e.formId}">Restore</button>\n\t\t\t<button class="discard-changes" data-form-id="${e.formId}">Discard</button>\n\t\t`,t.insertBefore(r,t.firstChild),r.querySelector(".restore-changes").addEventListener("click",(()=>{this.restorePendingForm(e),r.remove()})),r.querySelector(".discard-changes").addEventListener("click",(()=>{this.discardPendingForm(e.formId),r.remove()}))}restorePendingForm(e){const t=document.querySelector(`[data-form-id="${e.formId}"]`);t&&(new this.populateForm(t,e.formData),e.status="restored",this.pendingForms.set(e.formId,e),window.jvbA11y&&window.jvbA11y.announce("Previous changes restored"))}async discardPendingForm(e){this.store.delete(e),window.jvbA11y&&window.jvbA11y.announce("Previous changes discarded")}initListeners(){this.globalHandlersAdded||(document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),document.addEventListener("focus",this.focusHandler,!0),document.addEventListener("blur",this.blurHandler,!0),document.addEventListener("input",this.inputHandler),this.globalHandlersAdded=!0)}registerForm(e,t={}){if(!e)return;const r=e.dataset.formId||`form_${Date.now()}`;e.dataset.formId=r,e.addEventListener("submit",this.submitHandler);const s={element:e,id:r,status:"",options:{autosave:"autosave"in e.dataset,saveDelay:this.autoSaveDefaults.delay,endpoint:e.dataset.save??"",formStatus:!0,cache:!0,...t},dependencies:new Map,data:this.collectFormData(e)};if(this.initializeFormFields(e,s),this.forms.set(r,s),this.store&&s.options.cache){const e=this.store.get(r);e&&e.formData&&this.showPendingNotification(e)}return s}initializeFormFields(e,t=null){this.initQuillEditors(e),this.initRepeaterFields(e,t),t&&this.initConditionalFields(e,t),this.initCharacterLimits(e),this.initImageUploadFields(e),window.jvbTabs&&e.querySelector("nav.tabs")&&(t.tabs=new window.jvbTabs(e),this.forms.set(t.formId,t),this.initSteppedForm(t.formId)),window.jvbSelector&&window.jvbSelector.scanExistingFields(e)}initSteppedForm(e){const t=this.forms.get(e),r=t.element,s=t.tabs,a=r.querySelectorAll(".tab-content").length,i=r.querySelector(".form-progress .fill"),n=r.querySelector(".step-text .current"),o=r.querySelectorAll("nav.tabs button"),l=e=>{const t=e/a*100;i&&(i.style.width=t+"%"),n&&(n.textContent=e),o.forEach(((t,r)=>{const s=r+1;t.classList.remove("current","completed","pending"),s<e?t.classList.add("completed"):s===e?t.classList.add("current"):t.classList.add("pending")}))};r.addEventListener("click",(e=>{const t=e.target.closest('[data-action="next-step"]'),a=e.target.closest('[data-action="prev-step"]');if(t){e.preventDefault();const a=t.closest(".tab-content"),i=parseInt(a.dataset.step),n=r.querySelector(`.tab-content[data-step="${i+1}"]`);if(n&&this.validateStep(a)){const e=n.dataset.tab;s.switchTab(e,!0),l(i+1),r.scrollIntoView({behavior:"smooth",block:"start"})}}if(a){e.preventDefault();const t=a.closest(".tab-content"),i=parseInt(t.dataset.step),n=r.querySelector(`.tab-content[data-step="${i-1}"]`);if(n){const e=n.dataset.tab;s.switchTab(e,!0),l(i-1),r.scrollIntoView({behavior:"smooth",block:"start"})}}}));const c=s.switchTab.bind(s);s.switchTab=(e,t)=>{c(e,t);const s=r.querySelector(`.tab-content[data-tab="${e}"]`);if(s){const e=parseInt(s.dataset.step);l(e)}},l(1)}validateStep(e){const t=e.querySelectorAll(".field");let r=!0;return t.forEach((e=>{const t=e.querySelector("input, textarea, select");if(t&&!t.closest("[hidden]")){this.validateField(t,e)||(r=!1)}})),r}initQuillEditors(e){window.jvbQuill(e)}initRepeaterFields(e,t){e.querySelectorAll(".repeater").forEach((e=>{const r=e.querySelector(".add-repeater-row"),s=e.querySelector(".repeater-items"),a=e.querySelector("template");r&&a&&s&&(window.Sortable&&new Sortable(s,{handle:".repeater-row-header",animation:150,onEnd:()=>{this.updateRepeaterOrder(e,t)}}),r.addEventListener("click",(()=>{this.addRepeaterRow(e,t)})),s.addEventListener("click",(e=>{e.target.closest(".remove-row")&&this.removeRepeaterRow(e.target.closest(".repeater-row"),t)})))}))}addRepeaterRow(e,t){const r=e.querySelector(".repeater-items"),s=e.querySelector("template"),a=r.children.length,i=e.dataset.field,n=s.content.cloneNode(!0).firstElementChild;n.dataset.index=a,n.querySelectorAll("input, select, textarea").forEach((e=>{const t=e.name;e.name=`${i}:${a}:${t}`,e.id=`${i}-${a}-${t}`;const r=e.nextElementSibling;r&&"LABEL"===r.tagName&&(r.htmlFor=e.id)})),r.appendChild(n),t&&this.scheduleSave(t,{type:"repeater",action:"add",fieldName:i,delay:this.repeaterDelays.add}),window.jvbA11y&&window.jvbA11y.announce("Row added")}removeRepeaterRow(e,t){const r=e.closest(".repeater"),s=r.dataset.field;e.remove(),this.updateRepeaterOrder(r,t),t&&this.scheduleSave(t,{type:"repeater",action:"remove",fieldName:s,delay:this.repeaterDelays.remove}),window.jvbA11y&&window.jvbA11y.announce("Row removed")}updateRepeaterOrder(e,t){const r=e.querySelector(".repeater-items"),s=e.dataset.field;Array.from(r.children).forEach(((e,t)=>{e.dataset.index=t,e.querySelectorAll("input, select, textarea").forEach((e=>{const r=e.name.split(":");if(3===r.length){const a=r[2];e.name=`${s}:${t}:${a}`,e.id=`${s}-${t}-${a}`;const i=e.nextElementSibling;i&&"LABEL"===i.tagName&&(i.htmlFor=e.id)}}))})),t&&this.scheduleSave(t,{type:"repeater",action:"reorder",fieldName:s,delay:this.repeaterDelays.reorder})}initConditionalFields(e,t){e.querySelectorAll("[data-depends-on]").forEach((r=>{const s=r.dataset.dependsOn,a=r.dataset.dependsValue,i=r.dataset.dependsOperator||"==";t.dependencies.has(s)||t.dependencies.set(s,[]),t.dependencies.get(s).push({field:r,requiredValue:a,operator:i}),this.checkFieldDependency(e,r,s,a,i)}))}checkFieldDependency(e,t,r,s,a){const i=e.querySelector(`[name="${r}"]`);if(!i)return;const n=this.getFieldValue(i),o=this.evaluateCondition(n,s,a);this.toggleFieldVisibility(t,o)}evaluateCondition(e,t,r){const s=String(e||""),a=String(t||"");switch(r){case"==":default:return s==a;case"!=":return s!=a;case">":return parseFloat(s)>parseFloat(a);case"<":return parseFloat(s)<parseFloat(a);case">=":return parseFloat(s)>=parseFloat(a);case"<=":return parseFloat(s)<=parseFloat(a);case"contains":return s.includes(a);case"empty":return""===s;case"not_empty":return""!==s}}toggleFieldVisibility(e,t){const r=e.closest(".field, fieldset");r&&(r.hidden=!t,r.querySelectorAll("input, select, textarea").forEach((e=>{e.disabled=!t,!t&&e.hasAttribute("required")?(e.dataset.wasRequired="true",e.removeAttribute("required")):t&&"true"===e.dataset.wasRequired&&(e.setAttribute("required",""),delete e.dataset.wasRequired)})))}initCharacterLimits(e){e.querySelectorAll("[data-limit]").forEach((e=>{const t=parseInt(e.dataset.limit,10),r=e.closest(".field");let s=r?.querySelector(".char-count");!s&&r&&(s=document.createElement("div"),s.className="char-count",s.innerHTML=`<span class="current">0</span> / <span class="limit">${t}</span>`,r.appendChild(s));const a=()=>{const r=e.value.length;s&&(s.querySelector(".current").textContent=r,s.classList.toggle("exceeded",r>t)),r>t&&(e.value=e.value.substring(0,t),s&&(s.querySelector(".current").textContent=t))};e.addEventListener("input",a),a()}))}initImageUploadFields(e){window.jvbUploads.scanFields(e)}async handleSubmit(e){const t=e.target;if(!t.dataset.formId)return;const r=this.forms.get(t.dataset.formId);if(this.subscribers.size>0){e.preventDefault();const s=this.collectFormData(t);this.notify("form-submit",{formId:r.id,data:s,config:r})}}handleFormSuccess(e,t){if(e.querySelectorAll(".error-message").forEach((e=>e.remove())),e.querySelectorAll(".field-error").forEach((e=>e.classList.remove("field-error"))),e.classList.add("form-success"),t.message){const r=document.createElement("div");r.className="form-success-message success-message",r.textContent=t.message,e.insertBefore(r,e.firstChild);const s=window.getIcon?.("check-circle");s&&(s.classList.add("success-icon"),r.prepend(s))}if(t.title||t.description){const r=document.createElement("div");if(r.className="success-box",t.title){const e=document.createElement("h3");e.textContent=t.title,r.appendChild(e)}if(t.description){(Array.isArray(t.description)?t.description:[t.description]).forEach((e=>{const t=document.createElement("p");t.textContent=e,r.appendChild(t)}))}e.insertBefore(r,e.firstChild)}window.jvbA11y&&window.jvbA11y.announce(t.message||"Form submitted successfully"),e.dispatchEvent(new CustomEvent("jvb-form-success",{detail:t}))}handleFormError(e,t){if(e.querySelectorAll(".error-message").forEach((e=>e.remove())),e.querySelectorAll(".field-error, .has-error").forEach((e=>{e.classList.remove("field-error","has-error")})),e.querySelectorAll(".field").forEach((e=>{this.clearValidation(e)})),t.field){const r=e.querySelector(`[data-field="${t.field}"]`);if(r){this.showError(r,t.message),this.touchedFields.add(t.field),r.scrollIntoView({behavior:"smooth",block:"center"});const e=r.querySelector("input, textarea, select");e&&e.focus()}}else{const r=document.createElement("div");r.className="form-error error-message",r.textContent=t.message;const s=window.getIcon?.("close-circle");s&&(s.classList.add("error-icon"),r.prepend(s)),e.insertBefore(r,e.firstChild),e.scrollIntoView({behavior:"smooth",block:"start"})}if(window.jvbA11y){const e=t.field?`Error in ${t.field}: ${t.message}`:`Form error: ${t.message}`;window.jvbA11y.announce(e)}e.dispatchEvent(new CustomEvent("jvb-form-error",{detail:t}))}handleClick(e){if(window.targetCheck(e,"div.quantity")){let t=window.targetCheck(e,"div.quantity");this.handleNumberClick(e,t.querySelector("input"))}else if(window.targetCheck(e,"[data-action]")){let t=window.targetCheck(e,"[data-action]");switch(t=t.dataset.action,t){case"clear-form":let t=e.target.closest("form");this.store.delete(t.dataset.formId),t?.reset(),e.target.closest(".restore-form").hidden=!0;break;case"dismiss-restore":e.target.closest(".restore-form").hidden=!0}}}handleNumberClick(e,t){let r=0;if(e.target.closest(".increase")?r+=1:e.target.closest(".decrease")&&(r-=1),0!==r){let s=parseFloat(t.step);s=Math.max(s,1),e.ctrlKey&&e.shiftKey?s*=50:e.ctrlKey?s*=5:e.shiftKey&&(s*=10);let a=""===t.value?0:parseFloat(t.value);t.value=a+s*r,this.handleNumberLimits(t)}}handleNumberLimits(e){let[t,r,s,a]=[e.min,e.max,e.closest(".quantity")?.querySelector(".increase"),e.closest(".quantity")?.querySelector(".decrease")],i=parseFloat(e.value);i<t?(e.value=t,a.disabled=!0):i>r?(e.value=r,s.disabled=!1):s.disabled?s.disabled=!1:a.disabled&&(a.disabled=!1)}handleChange(e){if(e.target.closest("[data-ignore]"))return;const t=e.target,r=t.form||t.closest("form");if(!r)return;const s=this.forms?.get(r.dataset.formId);if(s&&(console.log(s.options),s.options.autosave||this.subscribers.size>0)){const e=s.dependencies.get(t.name);e&&e.forEach((e=>{this.checkFieldDependency(r,e.field,t.name,e.requiredValue,e.operator)}));const a=this.getDelayForField(t);this.scheduleSave(s,a)}}handleFocus(e){const t=e.target;t.matches("input, textarea, select")&&(this.currentFocus=t)}handleBlur(e){if(e.target.closest("[data-ignore]"))return;const t=e.target,r=t.form||t.closest("form");if(!r)return;const s=e.target.closest("input, textarea, select");if(s){const e=this.findFieldWrapper(s);if(e){const t=e.dataset.field;t&&(this.shouldDebounce(s)&&window.debouncer.cancel(`validate_${t}`),this.touchedFields.add(t)),this.validateField(s,e)}const a=this.forms?.get(r.dataset.formId);a&&this.scheduleSave(a,{type:"blur",fieldName:t.name,delay:1500})}}handleInput(e){if(e.target.closest("[data-ignore]")||!e.target.closest("form"))return;const t=e.target.closest("input, textarea, select");if(!t)return;let r=t.closest("form");this.showFormStatus(r.dataset.formId,"pending");const s=this.findFieldWrapper(t);if(!s)return;const a=s.dataset.field;a&&this.touchedFields.add(a),this.shouldDebounce(t)&&window.debouncer.schedule(`validate_${a}`,((e,t)=>this.validateField.bind(this)),500)}initValidators(){return{email:{pattern:/^[^\s@]+@[^\s@]+\.[^\s@]+$/,message:"Please enter a valid email address"},url:{pattern:/^https?:\/\/.+\..+/,message:"Please enter a valid URL starting with http:// or https://"},phone:{pattern:/^[\d\s\-\+\(\)\.]+$/,message:"Please enter a valid phone number"},number:{test:(e,t)=>{const r=parseFloat(e);if(isNaN(r))return"Please enter a valid number";const s=t.dataset.min,a=t.dataset.max;return void 0!==s&&r<parseFloat(s)?`Value must be at least ${s}`:!(void 0!==a&&r>parseFloat(a))||`Value must be at most ${a}`}},text:{test:(e,t)=>{const r=t.dataset.minlength,s=t.dataset.maxlength;return r&&e.length<parseInt(r)?`Must be at least ${r} characters`:!(s&&e.length>parseInt(s))||`Must be no more than ${s} characters`}}}}findFieldWrapper(e){let t=e.closest(".field");return t||(t=e.closest("[data-field]")),t}shouldDebounce(e){return["text","email","url","tel","search"].includes(e.type)||"TEXTAREA"===e.tagName}validateField(e,t){const r=this.getFieldValue(e),s=t.dataset.field;if(!this.touchedFields.has(s)&&!e.required)return!0;if(!r&&!e.required)return this.clearValidation(t),!0;if(e.required&&!r)return this.showError(t,"This field is required"),!1;if(e.checkValidity&&!e.checkValidity())return this.showError(t,e.validationMessage),!1;const a=t.dataset.pattern;if(a&&r){if(!new RegExp(a).test(r)){const e=t.dataset.validationMessage||"Invalid format";return this.showError(t,e),!1}}const i=t.dataset.validate||e.type;if(i&&this.validators[i]){const e=this.validators[i];if(e.pattern&&!e.pattern.test(r))return this.showError(t,e.message),!1;if(e.test){const s=e.test(r,t);if(!0!==s)return this.showError(t,s),!1}}return this.showSuccess(t),this.notify("field-validated",e),!0}getFieldValue(e){if(!e)return"";if("checkbox"===e.type)return e.checked?e.value||"1":"";if("radio"===e.type){const t=e.form?.querySelector(`[name="${e.name}"]:checked`);return t?t.value:""}return"select-multiple"===e.type?Array.from(e.selectedOptions).map((e=>e.value)):e.value?.trim()||""}showSuccess(e,t=""){if(!e)return;const r=e.querySelector(".validation-icon.success"),s=e.querySelector(".validation-icon.error"),a=e.querySelector(".validation-message"),i=e.querySelector("input, textarea, select");e.classList.remove("has-error"),i?.classList.remove("error"),e.classList.add("has-success"),r&&(r.hidden=!1),s&&(s.hidden=!0),a&&(""===t?(a.hidden=!0,a.textContent=""):(a.hidden=!1,a.textContent=t))}showError(e,t){if(!e)return;const r=e.querySelector(".validation-icon.success"),s=e.querySelector(".validation-icon.error"),a=e.querySelector(".validation-message"),i=e.querySelector("input, textarea, select");e.classList.remove("has-success"),e.classList.add("has-error"),i?.classList.add("error"),r&&(r.hidden=!0),s&&(s.hidden=!1),a&&(a.hidden=!1,a.textContent=t)}clearValidation(e){if(!e)return;const t=e.querySelector(".validation-icon"),r=e.querySelector(".validation-message"),s=e.querySelector("input, textarea, select");e.classList.remove("has-error","has-success"),s?.classList.remove("error"),t&&(t.hidden=!0),r&&(r.hidden=!0,r.textContent="")}validateAllFields(e){if(!e)return!0;const t=e.querySelectorAll(".field:not([hidden])");let r=!0;return t.forEach((e=>{if(this.isComplexFieldWrapper(e))return;const t=e.querySelector('input:not([type="hidden"]), textarea, select');if(t&&!t.closest("[hidden]")){const s=e.dataset.field;s&&this.touchedFields.add(s);this.validateField(t,e)||(r=!1,!1===r&&(t.scrollIntoView({behavior:"smooth",block:"center"}),t.focus()))}})),r}isComplexFieldWrapper(e){return e.classList.contains("repeater")||e.classList.contains("group")||e.classList.contains("upload")}attachRepeaterValidation(e){e.addEventListener("click",(t=>{t.target.closest(".add-repeater-row")&&setTimeout((()=>{e.querySelectorAll(".repeater-row").forEach((e=>{e.querySelectorAll("input, textarea, select").forEach((e=>{const t=this.findFieldWrapper(e);t&&this.clearValidation(t)}))}))}),100)}))}attachGroupValidation(e){e.addEventListener("change",(t=>{const r=t.target.closest("input, select");if(!r)return;const s=r.name;if(!s)return;e.querySelectorAll(`[data-show-if*="${s}"]`).forEach((e=>{e.hidden&&this.clearValidation(e)}))}))}resetForm(e){if(!e)return;this.touchedFields.clear();e.querySelectorAll(".field").forEach((e=>{this.clearValidation(e)}))}getFormErrors(e){const t={};return e.querySelectorAll(".field.has-error").forEach((e=>{const r=e.dataset.field,s=e.querySelector(".validation-message");r&&s&&(t[r]=s.textContent)})),t}addValidator(e,t){this.validators[e]=t}getDelayForField(e){return"text"===e.type||"textarea"===e.type?this.autoSaveDefaults.typingDelay:["checkbox","radio","select-one","select-multiple"].includes(e.type)?1e3:this.autoSaveDefaults.delay}scheduleSave(e,t=this.autoSaveDefaults.delay){if(!e.options.autosave)return;document.addEventListener("input",this.saveCheck,{passive:!0});const r=`autosave_${e.id}`;this.debouncer.schedule(r,(()=>this.autosave(e)),t)}saveCheck(e){let t=e.target.closest("form[data-id]");t&&this.scheduleSave(this.forms.get(t.dataset.id))}async autosave(e){const t=this.collectFormData(e.element);this.showFormStatus(e.id,"saving"),await this.store.save({formId:e.id,data:t,status:"draft",timestamp:Date.now()}).then((()=>{this.showFormStatus(e.id,"autosaved")}));const r=this.getChangedFields(e.data,t);if(console.log("Changes:",r),0!==Object.keys(r).length){console.log("Continuing on:"),e.data=t,this.forms.set(e.id,e),document.removeEventListener("input",this.handleInput);for(let[e,s]of Object.entries(t))"object"==typeof s&&(r[e]=s);this.notify("form-autosave",{formId:e.id,changes:r,fullData:t,config:e})}}hasUnsavedChanges(e){const t=this.forms.get(e);if(!t)return!1;if(t.operations?.size>0)return!0;const r=this.collectFormData(t.element),s=this.getChangedFields(t.data,r);return Object.keys(s).length>0}showFormStatus(e,t,r=""){let s=this.forms.get(e);if(!s.options.formStatus)return;if(s.status===t)return;s.status=t,console.log("Setting status: ",t);const a=s.element.querySelector(".fstatus");a.hidden=!1;const i=a.querySelector(".message");i.textContent="",a.querySelector(".icon")?.remove();const n={saving:"Saving changes...",autosaved:"Changes saved locally. Submit form to send to server.",uploading:"Uploading your form to server",submitted:"Successfully sent to server",pending:"Unsaved changes",error:"Failed to save changes. Refresh and try again?",offline:"Changes will be saved when online"};let o=window.getIcon({autosaved:"check-circle",submitted:"check-circle",error:"close-circle",offline:"cloud-slash",pending:"exclamation-mark"}[t]);o&&a.prepend(o),""===r&&(r=n[t]||t),i.textContent=r,a.classList.toggle("loading",["uploading","saving"].includes(t)),"submitted"===t&&setTimeout((()=>a.hidden=!0),3e3)}cleanupSpecialFields(){this.specialFields.forEach((e=>{if("quill"===e.type&&e.instance){const t=e.instance.container.previousSibling;t?.classList.contains("ql-toolbar")&&t.remove()}})),this.uploader?.destroy(),this.specialFields.clear()}collectFormData(e){if(Object.hasOwn(e.dataset,"timeline"))return this.collectTimeline(e);const t=new FormData(e);let r={};const s={},a={};for(let[i,n]of t.entries()){if(this.ignore.includes(i)||i.endsWith("_temp"))continue;this.getFieldProcessor(i)(i,n,r,s,a,e)}return window.isEmptyObject(a)?this.mergeRepeaterData(r,s):(r=this.mergeRepeaterData(r,s),this.mergePostData(r,a))}collectTimeline(e){console.log("Collecting Timeline data:");let t={},r={},s=[],a=new FormData(e);for(const[i,n]of a.entries()){if(this.ignore.includes(i)||i.endsWith("_temp"))continue;const a=i.match(/^\[(\d+)\](.+)$/);if(a){const[,t,i]=a;r[t]||(r[t]={id:parseInt(t)},s.push(t));this.getFieldProcessor(i)(i,n,r[t],{},{},e)}else{this.getFieldProcessor(i)(i,n,t,{},{},e)}}return t.timeline=s.map((e=>r[e])),delete t["form-id"],delete t.sendAll,delete t.timeline_temp,delete t[""],console.log("Data: ",t),t}getFieldProcessor(e){return e.includes("|")?this.processTableField:e.includes("::")?this.processGroupField:e.includes(":")?this.processRepeaterField:/\[[^\]]+\]/.test(e)?this.processLocationField:this.processRegularField}mergeRepeaterData(e,t){return Object.keys(t).forEach((r=>{const s={};Object.keys(t[r]).forEach((e=>{const a=t[r][e];Object.keys(a).length>0&&(s[e]=a)})),e[r]=Object.values(s)})),e}mergePostData(e,t){for(let[t,r]in Object.entries(r))e[t]=r;return e}processTableField(e,t,r,s,a,i){let[n,o]=e.split("|");!n in a&&(a[n]={});this.getFieldProcessor(o)(o,t,a,s,a,i)}processRepeaterField(e,t,r,s,a,i){let[n,o,l]=e.split(":");const c=l.endsWith("[]");l=l.replace("[]",""),s[n]||(s[n]={}),s[n][o]||(s[n][o]={}),c||s[n][o][l]?(s[n][o][l]?Array.isArray(s[n][o][l])||(s[n][o][l]=[s[n][o][l]]):s[n][o][l]=[],s[n][o][l].push(t)):s[n][o][l]=t}processGroupField(e,t,r,s,a,i){const n=e.split("::"),o=n[0];r[o]||(r[o]={});let l=r[o];for(let e=1;e<n.length-1;e++){const t=n[e];l[t]||(l[t]={}),l=l[t]}const c=n[n.length-1];void 0!==l[c]?(Array.isArray(l[c])||(l[c]=[l[c]]),l[c].push(t)):l[c]=t}processLocationField(e,t,r,s,a,i){let[n,o]=e.split("[");o=o.replace("]",""),Object.hasOwn(r,n)||(r[n]={},Object.hasOwn(r,"sendAll")?r.sendAll.includes(n)||r.sendAll.push(n):r.sendAll=[n]),r[n][o]=t}processRegularField(e,t,r,s,a,i){r[e=e.replace("[]","")]?(Array.isArray(r[e])||(r[e]=[r[e]]),r[e].push(t)):r[e]=t}getFieldValue(e){if(!e)return"";if("checkbox"===e.type)return e.checked?e.value||"1":"";if("radio"===e.type){const t=e.form.querySelector(`[name="${e.name}"]:checked`);return t?t.value:""}return"select-multiple"===e.type?Array.from(e.selectedOptions).map((e=>e.value)):e.value}getChangedFields(e,t){return window.getDifferences?.map(e,t)||{}}showSummary(e,t="form"){const r=this.forms.get(e);if(!r)return;const s=r.element||document.querySelector(`[data-form-id="${e}"]`),a=window.getTemplate("formSummary"),[i,n,o]=[a.querySelector("h2"),a.querySelector(".summary"),a.querySelector(".result")],l=["sendAll",...this.ignore];for(const[e,t]of Object.entries(r.data)){if(l.includes(e)||this.isEmptyValue(t))continue;const r=this.getFieldInfo(s,e);if(!r.label)continue;const a=this.createResultElement(o,r,t,s);a&&n.appendChild(a)}o.remove(),(t="form"!==t?s.closest(t)??s:s).after(a),window.fade(t,!1)}isEmptyValue(e){return null==e||""===e||(!(!Array.isArray(e)||0!==e.length)||"object"==typeof e&&0===Object.keys(e).length)}getFieldInfo(e,t){let r=e.querySelector(`label[for="${t}"]`),s=null,a=null;if(s||(s=e.querySelector(`[name="${t}"]`)),s||(s=e.querySelector(`[name="${t}[]"]`)),!s){const a=e.querySelector(`fieldset[data-field="${t}"]`);a&&(r=a.querySelector("legend"),s=a.querySelector("input, select, textarea"))}if(!r&&s){const e=s.closest(".field, fieldset");e&&(r=e.querySelector("label, legend"))}a=e.querySelector(`.field[data-field="${t}"], fieldset[data-field="${t}"]`);let i="text";return a?.dataset.type?i=a.dataset.type:s&&(i="checkbox"===s.type&&s.name.endsWith("[]")?"checkbox":"checkbox"===s.type?"true_false":"SELECT"===s.tagName&&s.multiple?"select":s.type||"text"),{label:r?.textContent.replace("*","").trim()||null,type:i,wrapper:a,input:s}}createResultElement(e,t,r,s){const a=e.cloneNode(!0),i=a.querySelector("h4"),n=a.querySelector("p");i.textContent=t.label;const o=this.formatFieldValue(r,t.type,s);return this.isHtmlContent(o)?n.innerHTML=o:n.textContent=o,a}isHtmlContent(e){return"string"==typeof e&&(e.includes("<br>")||e.includes("<p>")||e.includes("<ul>")||e.includes("<ol>")||e.includes("<a ")||e.includes("<strong>")||e.includes("<em>")||e.includes("<div"))}formatFieldValue(e,t,r){switch(t){case"textarea":case"wysiwyg":return this.formatTextareaValue(e,t);case"true_false":return"1"===e||1===e||!0===e?"Yes":"No";case"checkbox":return Array.isArray(e)?this.formatArrayValue(e):"1"===e||1===e||!0===e?"Yes":"No";case"select":return Array.isArray(e)?this.formatArrayValue(e):this.getSelectLabel(e,r,t);case"date":case"datetime":case"time":return window.formatDate?window.formatDate(e):e;case"radio":return this.getSelectLabel(e,r,t);case"repeater":return this.formatRepeaterValue(e);case"group":return this.formatGroupValue(e);case"location":return this.formatLocationValue(e);case"file":case"image":return this.formatFileValue(e);case"number":return this.formatNumber(e);case"email":return`<a href="mailto:${e}">${e}</a>`;case"url":return`<a href="${e}" target="_blank" rel="noopener">${e}</a>`;case"phone":return`<a href="tel:${e.replace(/\D/g,"")}">${e}</a>`;default:return Array.isArray(e)?this.formatArrayValue(e):e}}formatRepeaterValue(e){if(!Array.isArray(e)||0===e.length)return"<em>No entries</em>";let t='<div class="repeater-summary">';return e.forEach(((e,r)=>{t+='<div class="repeater-row">',t+=`<strong>Entry ${r+1}:</strong><ul>`;for(const[r,s]of Object.entries(e))if(!this.isEmptyValue(s)){const e=r.replace(/_/g," ").replace(/\b\w/g,(e=>e.toUpperCase()));t+=`<li><strong>${e}:</strong> ${s}</li>`}t+="</ul></div>"})),t+="</div>",t}formatGroupValue(e){if("object"!=typeof e||0===Object.keys(e).length)return"<em>No data</em>";let t='<div class="group-summary"><ul>';for(const[r,s]of Object.entries(e))if(!this.isEmptyValue(s)){const e=r.replace(/_/g," ").replace(/\b\w/g,(e=>e.toUpperCase()));"object"!=typeof s||Array.isArray(s)?t+=`<li><strong>${e}:</strong> ${s}</li>`:t+=`<li><strong>${e}:</strong> ${this.formatGroupValue(s)}</li>`}return t+="</ul></div>",t}formatLocationValue(e){if("object"!=typeof e)return e;const t=[];return["address","city","state","zip","country"].forEach((r=>{e[r]&&t.push(e[r])})),t.join(", ")}formatFileValue(e){return"string"==typeof e?e.startsWith("http")?`<a href="${e}" target="_blank">View file</a>`:e:Array.isArray(e)?e.map((e=>"string"==typeof e?`<a href="${e}" target="_blank">View file</a>`:e.name||"File")).join(", "):"File uploaded"}formatNumber(e){const t=parseFloat(e);return isNaN(t)?e:e.toString().includes(".")&&2===e.toString().split(".")[1].length?new Intl.NumberFormat("en-CA",{style:"currency",currency:"USD"}).format(t):new Intl.NumberFormat("en-CA").format(t)}formatArrayValue(e,t=null,r=null){if(0===e.length)return"<em>None selected</em>";if(t&&r&&r.input){return"<ul><li>"+e.map((e=>this.getSelectLabel(e,t,r.type))).join("</li><li>")+"</li></ul>"}return"<ul><li>"+e.join("</li><li>")+"</li></ul>"}getSelectLabel(e,t,r){if("select"===r){const r=t.querySelector(`option[value="${e}"]`);return r?.textContent||e}if("radio"===r){const r=t.querySelector(`input[type="radio"][value="${e}"]`),s=r?.nextElementSibling;return s?.textContent||e}if("checkbox"===r){const r=t.querySelector(`input[type="checkbox"][value="${e}"]`);if(r){const e=t.querySelector(`label[for="${r.id}"]`);if(e)return e.textContent.trim();const s=r.nextElementSibling;if("LABEL"===s?.tagName)return s.textContent.trim()}}return e}formatTextareaValue(e,t){return e?"wysiwyg"===t||this.containsHtml(e)?e:this.formatPlainText(e):"<em>Empty</em>"}containsHtml(e){return/<(p|strong|em|u|s|ol|ul|li|blockquote|h[1-6]|a|br|span)\b[^>]*>/i.test(e)}formatPlainText(e){if(!e)return"";const t=(e=e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")).split(/\n\n+/);return t.length>1?t.map((e=>`<p>${e.replace(/\n/g,"<br>")}</p>`)).join(""):e.replace(/\n/g,"<br>")}nl2br(e){return this.formatPlainText(e)}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((r=>r(e,t)))}cleanupForm(e){const t=this.forms.get(e);t&&(this.hasUnsavedChanges(e)&&this.autosave(t),this.cleanupSpecialFields(),this.forms.delete(e))}destroy(){this.globalHandlersAdded&&(document.removeEventListener("change",this.changeHandler),document.removeEventListener("focus",this.focusHandler,!0),document.removeEventListener("blur",this.blurHandler,!0),document.removeEventListener("input",this.inputHandler,!0)),this.forms.forEach((e=>{let t=e.element;t&&t.removeEventListener("submit",this.submitHandler)})),this.specialFields.clear(),this.forms.clear(),this.activeRepeaters.clear(),this.forms&&this.forms.clear()}}document.addEventListener("DOMContentLoaded",(()=>{window.jvbForm=e}))})();
assets/js/min/gallery.min.js
@@ -1 +1 @@
window.jvbGallery=class{constructor(e,t){this.imageWrapper=t.imageWrapper??null,this.container=t.container?document.querySelector(t.container):document.querySelector("main"),this.modal=new window.jvbModal(e,{onOpen:!1,onSave:!1,onClose:()=>this.closeGallery(!1)}),this.items=this.getGalleryItems()||[],this.loadMore=t.loadMore??!1,this.modalElement="string"==typeof e?document.querySelector(e)??!1:e,this.modal&&(this.a11y=window.jvbA11y,this.openWhenReady=!1,this.initElements(),this.imageWrapper="string"==typeof this.imageWrapper?this.imageWrapper:"."+this.imageWrapper.classList.join("."),this.index=0,this.swipe={touchStart:null,touchEnd:null,minSwipe:50},this.isLoading=!1)}initElements(){this.prevBtn=this.modalElement.querySelector(".prev"),this.nextBtn=this.modalElement.querySelector(".next"),this.favourite=this.modalElement.querySelector(".favourite"),this.image=this.modalElement.querySelector(".image"),this.counter=this.modalElement.querySelector(".counter"),this.extra=this.modalElement.querySelector(".item-info"),this.imageWrapper||(this.favourite.remove(),this.extra.remove())}getGalleryItems(){let e=this.imageWrapper?this.imageWrapper:"img";return Array.from(this.container.querySelectorAll(e)).map((e=>{const t=this.imageWrapper?e.querySelector("img"):e;return t?{id:this.imageWrapper?e.querySelector("button.favourite").dataset.id:"",small:t.dataset.small||t.src,large:t.dataset.large||t.src,full:t.dataset.full||t.src,alt:t.alt||"",fav:this.imageWrapper?e.querySelector("button.favourite")?.cloneNode(!0):"",info:this.imageWrapper?e.querySelector(".item-info")?.cloneNode(!0):""}:null})).filter(Boolean)}initListeners(){this.container.addEventListener("click",this.handleClick.bind(this)),document.addEventListener("keydown",this.handleKeys.bind(this)),document.addEventListener("touchstart",this.handleTouchStart.bind(this)),document.addEventListener("touchend",this.handleTouchEnd.bind(this)),document.addEventListener("touchmove",this.handleTouchMove.bind(this))}destroyListeners(){this.container.removeEventListener("click",this.handleClick.bind(this)),document.removeEventListener("keydown",this.handleKeys.bind(this)),document.removeEventListener("touchstart",this.handleTouchStart.bind(this)),document.removeEventListener("touchend",this.handleTouchEnd.bind(this)),document.removeEventListener("touchmove",this.handleTouchMove.bind(this))}handleKeys(e){switch(e.key){case"ArrowLeft":this.navigate(-1);break;case"ArrowRight":this.navigate(1)}}handleTouchStart(e){this.swipe.touchStart=e.touches[0].clientX}handleTouchMove(e){this.swipe.touchEnd=e.touches[0].clientX}handleTouchEnd(e){if(!this.swipe.touchStart||!this.swipe.touchEnd)return;const t=this.swipe.touchStart-this.swipe.touchEnd,i=t>this.swipe.minSwipe,s=t<-this.swipe.minSwipe;i?this.navigate(1):s&&this.navigate(-1),this.swipe.touchStart=null,this.swipe.touchEnd=null}handleClick(e){window.targetCheck(e,".feed-images")&&this.handleGalleryOpen(e),window.targetCheck(e,".nav")&&(window.targetCheck(e,".next")?this.navigate(1):this.navigate(-1)),window.targetCheck(e,"button.cancel")&&this.closeGallery()}async navigate(e){let t=this.index+e;(t<0||t>=this.items.length)&&(this.a11y.announceNavigation(t,this.items.length,e<0,e>0),t=t<0?this.items.length-1:0),this.index=t,this.updateDisplay(),this.preloadImages(),this.a11y.announceNavigation(this.index,this.items.length),"function"==typeof this.loadMore&&e>0&&t>=this.items.length-3&&window.feedBlock.hasMore&&(await this.loadMore(),this.updateGalleryItems(this.getGalleryItems()))}updateGalleryItems(e){const t=this.items[this.index];if(this.items=e,t){const e=this.items.findIndex((e=>e.full===t.full||e.large===t.large));-1!==e&&(this.index=e)}if(this.updateNavigationButtons(),this.items.length>0&&this.openWhenReady){let e=this.findIndex("id",this.openWhenReady);this.openGallery(e)}}preloadImages(){[-1,0,1].forEach((e=>{const t=this.index+e;if(t>=0&&t<this.items.length){const e=new Image,i=this.items[t];window.innerWidth<1e3?e.src=i.large||i.src:e.src=i.full||i.src}}))}updateDisplay(){const e=this.items[this.index];e&&([this.image.src,this.image.alt,this.counter.textContent]=[window.innerWidth<1e3?e.large||e.src:e.full||e.src,e.alt||"",`${this.index+1} / ${this.items.length}`],this.imageWrapper&&(e.fav&&(window.removeChildren(this.favourite),this.favourite.appendChild(e.fav.cloneNode(!0))),e.info&&(window.removeChildren(this.extra),this.extra.appendChild(e.info.cloneNode(!0)))),this.updateNavigationButtons())}updateNavigationButtons(){this.prevBtn.classList.toggle("end",this.index>0?"":"none"),this.nextBtn.classList.toggle("end",this.index<this.items.length-1?"":"none")}handleGalleryOpen(e){let t=this.imageWrapper?e.target.closest(this.imageWrapper):e.target.closest("img"),i=this.imageWrapper?"id":"small",s=t.dataset[i],n=this.findIndex(i,s);this.openGallery(n)}findIndex(e,t){return this.items.findIndex((i=>i[e]===t))}openGallery(e){this.initListeners(),this.index=e,this.modal.handleOpen(),this.updateDisplay(),this.preloadImages(),this.a11y.announce(`Image ${this.index+1} of ${this.items.length}. Use arrow keys to navigate.`)}closeGallery(e=!0){this.destroyListeners(),e&&this.modal.handleClose()}};
(()=>{class e{constructor(e,t={}){this.container=t.container?document.querySelector(t.container):document.querySelector("main"),this.gallerySelector=t.gallerySelector||"img[data-small]",this.modal=new window.jvbModal(e,{onOpen:!1,onSave:!1,onClose:()=>this.closeGallery(!1)}),this.modalElement="string"==typeof e?document.querySelector(e):e,this.modalElement&&(this.a11y=window.jvbA11y,this.index=0,this.items=[],this.swipe={touchStart:null,touchEnd:null,minSwipe:50},this.initElements(),this.initListeners())}initElements(){this.prevBtn=this.modalElement.querySelector(".prev"),this.nextBtn=this.modalElement.querySelector(".next"),this.image=this.modalElement.querySelector(".image"),this.counter=this.modalElement.querySelector(".counter")}initListeners(){this.container.addEventListener("click",this.handleClick.bind(this)),this.boundKeyHandler=this.handleKeys.bind(this),this.boundTouchStart=this.handleTouchStart.bind(this),this.boundTouchMove=this.handleTouchMove.bind(this),this.boundTouchEnd=this.handleTouchEnd.bind(this)}handleClick(e){const t=e.target.closest(this.gallerySelector);if(t&&!this.modal.isOpen)return e.preventDefault(),void this.openGallery(t);this.modal.isOpen&&(e.target.closest(".next")?this.navigate(1):e.target.closest(".prev")&&this.navigate(-1))}handleKeys(e){if(this.modal.isOpen)switch(e.key){case"ArrowLeft":e.preventDefault(),this.navigate(-1);break;case"ArrowRight":e.preventDefault(),this.navigate(1)}}handleTouchStart(e){this.modal.isOpen&&(this.swipe.touchStart=e.touches[0].clientX)}handleTouchMove(e){this.modal.isOpen&&(this.swipe.touchEnd=e.touches[0].clientX)}handleTouchEnd(e){if(!this.modal.isOpen||!this.swipe.touchStart||!this.swipe.touchEnd)return;const t=this.swipe.touchStart-this.swipe.touchEnd;Math.abs(t)>this.swipe.minSwipe&&this.navigate(t>0?1:-1),this.swipe.touchStart=null,this.swipe.touchEnd=null}buildGalleryItems(){return Array.from(this.container.querySelectorAll(this.gallerySelector)).map(((e,t)=>({id:e.dataset.id||t,small:e.dataset.small||e.src,medium:e.dataset.medium||e.src,full:e.dataset.full||e.src,alt:e.alt||"",element:e})))}openGallery(e){this.items=this.buildGalleryItems(),this.index=this.items.findIndex((t=>t.element===e)),-1===this.index&&(this.index=0),document.addEventListener("keydown",this.boundKeyHandler),document.addEventListener("touchstart",this.boundTouchStart,{passive:!0}),document.addEventListener("touchmove",this.boundTouchMove,{passive:!0}),document.addEventListener("touchend",this.boundTouchEnd,{passive:!0}),this.modal.handleOpen(),this.updateDisplay(),this.preloadAdjacent(),this.a11y.announce(`Gallery opened. Image ${this.index+1} of ${this.items.length}. Use arrow keys to navigate.`)}closeGallery(e=!0){document.removeEventListener("keydown",this.boundKeyHandler),document.removeEventListener("touchstart",this.boundTouchStart),document.removeEventListener("touchmove",this.boundTouchMove),document.removeEventListener("touchend",this.boundTouchEnd),e&&this.modal.handleClose()}navigate(e){let t=this.index+e;t<0?t=this.items.length-1:t>=this.items.length&&(t=0),this.index=t,this.updateDisplay(),this.preloadAdjacent(),this.a11y.announce(`Image ${this.index+1} of ${this.items.length}`)}updateDisplay(){const e=this.items[this.index];if(!e)return;const t=window.innerWidth<1e3?e.medium||e.src:e.full||e.src;this.image.src=t,this.image.alt=e.alt,this.counter.textContent=`${this.index+1} / ${this.items.length}`,this.prevBtn.classList.toggle("disabled",this.items.length<=1),this.nextBtn.classList.toggle("disabled",this.items.length<=1)}preloadAdjacent(){[-1,1].forEach((e=>{const t=this.index+e;if(t>=0&&t<this.items.length){const e=this.items[t];(new Image).src=window.innerWidth<1e3?e.medium||e.src:e.full||e.src}}))}cleanup(){this.container.removeEventListener("click",this.handleClick),this.closeGallery(!1)}}document.addEventListener("DOMContentLoaded",(function(){let t=document.querySelectorAll("dialog.gallery");t.length>0&&(window.galleries=new Map,t.forEach((t=>{let i=t.id;window.galleries.set(i,new e(t))})))}))})();
assets/js/min/media.min.js
@@ -0,0 +1 @@
(()=>{class e{constructor(){this.currentWidth=window.innerWidth,this.images=document.querySelectorAll(".wp-site-blocks img[data-small]"),0!==this.images.length&&(this.loadVisibleImages(),this.initListeners())}loadVisibleImages(){this.images.forEach(((e,t)=>{const i=e.getBoundingClientRect(),s=i.top<window.innerHeight&&i.bottom>0;(0===t||s)&&(this.loadAppropriateImage(e),e.dataset.loaded="true")}))}initListeners(){this.resizeHandler=this.handleResize.bind(this),window.addEventListener("resize",this.resizeHandler),this.observer=new IntersectionObserver((e=>{e.forEach((e=>{e.isIntersecting&&(this.loadAppropriateImage(e.target),this.observer.unobserve(e.target))}))}),{rootMargin:"50px",threshold:.1}),this.images.forEach((e=>{e.dataset.loaded||this.observer.observe(e)}))}handleResize(){window.debouncer.schedule("image-resize",(()=>{const e=window.innerWidth;Math.abs(e-this.currentWidth)>100&&(this.currentWidth=e,this.updateVisibleImages())}),150)}updateVisibleImages(){this.images.forEach((e=>{const t=e.getBoundingClientRect();t.top<window.innerHeight&&t.bottom>0&&this.loadAppropriateImage(e,!0)}))}loadAppropriateImage(e,t=!1){const i=this.getTargetSize(),s=e.dataset[i];s&&(t||s!==e.currentSrc)&&(e.src=s)}getTargetSize(){return this.currentWidth<768?"small":this.currentWidth<1200?"medium":"full"}cleanup(){this.observer?.disconnect(),window.removeEventListener("resize",this.resizeHandler)}}window.isLoaded=!1,document.addEventListener("readystatechange",(()=>{!window.isLoaded&&document.querySelector(".wp-site-blocks img")&&(window.jvbMedia=new e,window.isLoaded=!0)}))})();
assets/js/min/news.min.js
@@ -1 +1 @@
window.newsManager=class{constructor(){this.queue=window.jvbQueue,this.loading=window.jvbLoading,this.cache=window.jvbCache,this.a11y=window.jvbA11y,this.error=window.jvbError,this.activeTab="all",this.tabs=new window.jvbTabs(document.querySelector(".replace"),{news:()=>{this.activeTab="all",this.resetFilters(),this.loadItems(!0).then((()=>{}))},mine:()=>{console.log("switching to mine tab"),this.activeTab="own",this.resetFilters(),this.filters.artist=jvbSettings.currentUser,this.loadItems(!0).then((()=>{}))},watching:()=>{this.activeTab="watching",this.resetFilters(),this.filters.watched=!0,this.loadItems(!0).then((()=>{}))}}),this.isLoading=!1,this.alreadyHandling=!1,this.template=new Map,this.endpoints={news:"news",vote:"news/vote"},this.resetFilters(),this.state={hasMore:!0,pages:1,items:0},this.initElements(),this.initEvents(),this.loadItems()}resetFilters(){this.filters={page:1,order:"DESC",orderby:"date",shop:null,type:null,artist:null,watched:!1}}initElements(){this.container=document.querySelector(".replace"),this.grid=this.container.querySelector(".item-grid"),this.addButton=this.container.querySelector(".add-item-btn"),this.addModal=new window.jvbModal(this.container.querySelector(".create-modal"),{render:this.renderModal.bind(this),open:this.addButton,content:"news",openMessage:"Opened modal to create a news post.",onSave:this.saveModal.bind(this)}),this.filterForm=this.container.querySelector("form"),this.dateRangeFilter=new window.jvbModal(this.container.querySelector("dialog.date-range"),{open:!1}),this.clearFilters=this.container.querySelector(".clear-filters"),this.replyModal=new window.jvbModal(this.container.querySelector(".create-response"),{open:!1,content:"response",openMessage:"Opened Response modal",onSave:this.saveCreatedResponse.bind(this)})}initEvents(){this.filterForm.addEventListener("change",(e=>{let t=e.target.value;if(!e.target.closest(".date-range"))if("custom"===t)this.handleCustomDateRange();else{let s=e.target.name;s?this.filters[s]=t:this.resetFilters(),this.loadItems(!0)}})),document.addEventListener("click",(e=>{if(e.target===this.clearFilters&&(this.filterForm.reset(),this.resetFilters(),this.loadItems(!0)),e.target.closest("button.reply")){let t=e.target.closest("button"),s=t.closest(".item").dataset.id,n="";"news"===t.dataset.type?n=t.closest(".item").querySelector(".item-info").innerHTML:(n=t.closest(".response").querySelector(".content").innerHTML,this.replyModal.modal.dataset.parent_id=t.id.replace("reply-to","")),this.replyModal.modal.dataset.id=s,this.replyModal.modal.dataset.type=t.dataset.type,this.replyModal.modal.querySelector(".original").innerHTML="<h5>Replying to:</h5>"+n,this.replyModal.handleOpen()}}))}renderModal(){}handleCustomDateRange(){this.dateRangeFilter.handleOpen();let e=this.dateRangeFilter.modal.querySelector("input.date-start"),t=this.dateRangeFilter.modal.querySelector("input.date-end"),s=this.dateRangeFilter.modal.querySelector("select");this.dateRangeFilter.modal.querySelectorAll("input, select").forEach((n=>{n.addEventListener("change",(i=>{n===e&&""!==t.value||n===t&&""!==e.value?(this.filters.dateFrom=e.value,this.filters.dateTo=t.value,this.dateRangeFilter.handleClose(),this.loadItems(!0)):n===s&&(this.filters.customDate=s.value,this.dateRangeFilter.handleClose(),this.loadItems(!0))}))}))}async saveModal(e){const t=new FormData(this.addModal.modal.querySelector("form"));t.append("user",jvbSettings.currentUser),this.queue.addToQueue({type:"new_news",data:t})}async loadItems(e=!0){if(!this.isLoading)try{this.isLoading=!0,this.loading.show(),e&&(this.filters.page=1,removeChildren(this.grid),this.grid.classList.remove("empty"));const t=this.buildFilters(),s=await this.cache.fetchWithCache(`${jvbSettings.api}${this.endpoints.news}?${t.toString()}`,{method:"GET",headers:{"X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbSettings.dash}},{context:"news",forceRefresh:!0});return this.renderItems(s.items||[],this.filters.page>1),s.pagination&&(this.state={hasMore:s.has_more,items:s.items,pages:s.pages}),s}catch(e){throw this.handleError(e,"loading news"),e}finally{this.isLoading=!1,this.loading.hide()}}buildFilters(){const e=JSON.parse(JSON.stringify(this.filters));let t={};for(var[s,n]of Object.entries(e))!1!==n&&null!==n&&(t[s]=n);return new URLSearchParams(t)}renderItems(e,t=!1){if(t||removeChildren(this.grid),0===e.length)return this.a11y.announceItems(0,t),void this.showEmptyState();const s=document.createDocumentFragment(),n=i=>{const a=Math.min(i+10,e.length);for(let t=i;t<a;t++){const n=e[t],i=this.createItemElement(n);s.appendChild(i)}a<e.length?requestAnimationFrame((()=>{n(a)})):(this.grid.appendChild(s),this.a11y.makeNavigable(this.grid.querySelectorAll(".item:not([data-keyboard-nav])")),this.a11y.announceItems(e.length,t,this.state.hasMore))};e.length>0?n(0):this.a11y.announceItems(0,t)}createItemElement(e){const t=window.getTemplate(`template-${this.activeTab}`);t.id=`news-${e.id}`,t.dataset.id=e.id;const[s]=t.getElementsByTagName("h3"),[n]=t.getElementsByClassName("published"),[i]=t.getElementsByClassName("artist"),[a]=t.getElementsByClassName("shop"),[r]=t.getElementsByClassName("tldr"),[o]=t.getElementsByClassName("item-info"),[l]=t.getElementsByClassName("image");[s.textContent,n.textContent,i.href,i.textContent,r.textContent,o.innerHTML]=[e.title,formatTimeAgo(e.date),e.artist.url,e.artist.name,e.tldr,e.post_content],e.shop?[a.href,a.innerHTML]=[e.shop.url,jvbSettings.icons.shop+e.shop.name]:a.hidden=!0;const[d]=t.getElementsByClassName("favourite-button");if("own"!==this.activeTab)[d.dataset.id,d.dataset.artist]=[e.id,e.artist.id],window.userFavourites.news?.includes(parseInt(e.id))?(removeChildren(d),d.append(getIcon("star-filled"))):(removeChildren(d),d.append(getIcon("star")));else{d.hidden=!0;const[s]=t.getElementsByClassName("select-checkbox"),[n]=t.getElementsByTagName("label");[s.id,s.value,n.for]=[`item-${e.id}`,e.id,`item-${e.id}`]}let h="";window.userVotes?.news?.has(e.id)&&(h=window.userVotes.news.get(e.id)),console.log(e),t.querySelector(".summary").appendChild(formatVote(e,h));let c=window.getTemplate("commentsButton");c.href=`#responses-to-${e.id}`,c.querySelector(".count").textContent=e.comments.items.length;let m=window.getTemplate("responses");m.id=`responses-to-${e.id}`,m.querySelector("summary").textContent+=" { "+e.comments.items.length+" }";let p=window.getTemplate("replyButton");return p.id="reply-to-"+e.id,p.dataset.type="news",p.dataset.action="reply",t.appendChild(p),e.comments.items.length>0&&e.comments.items.forEach((e=>{m.appendChild(this.formatComment(e))})),t.appendChild(m),t.querySelector(".vote").prepend(c,t.querySelector(".vote button")),e.image&&e.image.replace(/src="([^"]+)"/,'data-src="$1"'),t}formatComment(e,t=null){let s=window.getTemplate("response");s.id="response-"+e.id;let n=s.querySelector("summary");n.querySelector(".content").innerHTML=e.response,n.querySelector(".created").textContent=formatTimeAgo(e.created_at);let i=checkVoteStatus("response",e.id);e.content="response",s.querySelector(".footer").appendChild(formatVote(e,i)),console.log(e);let a=window.getTemplate("replyButton");a.id="reply-to-"+e.id,t&&(a.dataset.parent_id=t),a.dataset.action="reply",a.dataset.type=e.content,n.querySelector(".vote").prepend(a,n.querySelector(".vote").firstElementChild);let r=n.querySelector(".artist"),o=n.querySelector(".shop");if(console.log(e),e.artist?(e.artist.shop||o.remove(),[r.href,r.textContent,o.href,o.textContent]=[e.artist.url,e.artist.name,e.artist.shop.url,e.artist.shop.name]):(r.remove(),o.remove()),e.children.items.length>0){let t=window.getTemplate("responses");t.id="replies-to-"+e.id,t.querySelector("summary").textContent="See Responses {"+e.children.items.length+"}",e.children.items.forEach((s=>{t.appendChild(this.formatComment(s,e.id))})),s.appendChild(t)}return s}renderResponseCreate(){}saveCreatedResponse(){console.log("Saving create response"),console.log(this.replyModal.modal.id);const e=this.replyModal.modal;let t={user:jvbSettings.currentUser,item_id:e.dataset.id,response:e.querySelector(".ql-editor").innerHTML,content:e.dataset.type,action:"create"};e.dataset.parent_id&&(t.parent_id=e.dataset.parent_id),console.log(t),this.queue.addToQueue({type:"new_response",data:t})}showEmptyState(){const e=document.createElement("div");e.className="no-news",e.innerHTML="\n            <h3>Nothing here</h3>\n            <p>No updates here.</p>\n            <p>Add some gap fillers from the main favourites tab.</p>\n        ",this.grid.appendChild(e),this.grid.classList.add("empty"),this.a11y.announce("No favourites to show!")}hideEmptyState(){let e=this.grid.querySelector(".no-news");e&&e.remove()}handleError(e,t){console.error(`News error (${t}):`,e),window.jvbError&&window.jvbError.log(e,{component:"NewsManager",action:t}),window.jvbA11y&&window.jvbA11y.announce(`Error ${t}. ${e.message||"Please try again."}`)}};
window.newsManager=class{constructor(){this.queue=window.jvbQueue,this.loading=window.jvbLoading,this.cache=window.jvbCache,this.a11y=window.jvbA11y,this.error=window.jvbError,this.activeTab="all",this.tabs=new window.jvbTabs(document.querySelector(".replace"),{news:()=>{this.activeTab="all",this.resetFilters(),this.loadItems(!0).then((()=>{}))},mine:()=>{console.log("switching to mine tab"),this.activeTab="own",this.resetFilters(),this.filters.artist=jvbSettings.currentUser,this.loadItems(!0).then((()=>{}))},watching:()=>{this.activeTab="watching",this.resetFilters(),this.filters.watched=!0,this.loadItems(!0).then((()=>{}))}}),this.isLoading=!1,this.alreadyHandling=!1,this.template=new Map,this.endpoints={news:"news",vote:"news/vote"},this.resetFilters(),this.state={hasMore:!0,pages:1,items:0},this.initElements(),this.initEvents(),this.loadItems()}resetFilters(){this.filters={page:1,order:"DESC",orderby:"date",shop:null,type:null,artist:null,watched:!1}}initElements(){this.container=document.querySelector(".replace"),this.grid=this.container.querySelector(".item-grid"),this.addButton=this.container.querySelector(".add-item-btn"),this.addModal=new window.jvbModal(this.container.querySelector(".create-modal"),{render:this.renderModal.bind(this),open:this.addButton,content:"news",openMessage:"Opened modal to create a news post.",onSave:this.saveModal.bind(this)}),this.filterForm=this.container.querySelector("form"),this.dateRangeFilter=new window.jvbModal(this.container.querySelector("dialog.date-range"),{open:!1}),this.clearFilters=this.container.querySelector(".clear-filters"),this.replyModal=new window.jvbModal(this.container.querySelector(".create-response"),{open:!1,content:"response",openMessage:"Opened Response modal",onSave:this.saveCreatedResponse.bind(this)})}initEvents(){this.filterForm.addEventListener("change",(e=>{let t=e.target.value;if(!e.target.closest(".date-range"))if("custom"===t)this.handleCustomDateRange();else{let s=e.target.name;s?this.filters[s]=t:this.resetFilters(),this.loadItems(!0)}})),document.addEventListener("click",(e=>{if(e.target===this.clearFilters&&(this.filterForm.reset(),this.resetFilters(),this.loadItems(!0)),e.target.closest("button.reply")){let t=e.target.closest("button"),s=t.closest(".item").dataset.id,n="";"news"===t.dataset.type?n=t.closest(".item").querySelector(".item-info").innerHTML:(n=t.closest(".response").querySelector(".content").innerHTML,this.replyModal.modal.dataset.parent_id=t.id.replace("reply-to","")),this.replyModal.modal.dataset.id=s,this.replyModal.modal.dataset.type=t.dataset.type,this.replyModal.modal.querySelector(".original").innerHTML="<h5>Replying to:</h5>"+n,this.replyModal.handleOpen()}}))}renderModal(){}handleCustomDateRange(){this.dateRangeFilter.handleOpen();let e=this.dateRangeFilter.modal.querySelector("input.date-start"),t=this.dateRangeFilter.modal.querySelector("input.date-end"),s=this.dateRangeFilter.modal.querySelector("select");this.dateRangeFilter.modal.querySelectorAll("input, select").forEach((n=>{n.addEventListener("change",(i=>{n===e&&""!==t.value||n===t&&""!==e.value?(this.filters.dateFrom=e.value,this.filters.dateTo=t.value,this.dateRangeFilter.handleClose(),this.loadItems(!0)):n===s&&(this.filters.customDate=s.value,this.dateRangeFilter.handleClose(),this.loadItems(!0))}))}))}async saveModal(e){const t=new FormData(this.addModal.modal.querySelector("form"));t.append("user",jvbSettings.currentUser),this.queue.addToQueue({type:"new_news",data:t})}async loadItems(e=!0){if(!this.isLoading)try{this.isLoading=!0,this.loading.show(),e&&(this.filters.page=1,removeChildren(this.grid),this.grid.classList.remove("empty"));const t=this.buildFilters(),s=await this.cache.fetchWithCache(`${jvbSettings.api}${this.endpoints.news}?${t.toString()}`,{method:"GET",headers:{"X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbSettings.dash}},{context:"news",forceRefresh:!0});return this.renderItems(s.items||[],this.filters.page>1),s.pagination&&(this.state={hasMore:s.has_more,items:s.items,pages:s.pages}),s}catch(e){throw this.handleError(e,"loading news"),e}finally{this.isLoading=!1,this.loading.hide()}}buildFilters(){const e=JSON.parse(JSON.stringify(this.filters));let t={};for(var[s,n]of Object.entries(e))!1!==n&&null!==n&&(t[s]=n);return new URLSearchParams(t)}renderItems(e,t=!1){if(t||removeChildren(this.grid),0===e.length)return this.a11y.announceItems(0,t),void this.showEmptyState();const s=document.createDocumentFragment(),n=i=>{const a=Math.min(i+10,e.length);for(let t=i;t<a;t++){const n=e[t],i=this.createItemElement(n);s.appendChild(i)}a<e.length?requestAnimationFrame((()=>{n(a)})):(this.grid.appendChild(s),this.a11y.makeNavigable(this.grid.querySelectorAll(".item:not([data-keyboard-nav])")),this.a11y.announceItems(e.length,t,this.state.hasMore))};e.length>0?n(0):this.a11y.announceItems(0,t)}createItemElement(e){const t=window.getTemplate(`template-${this.activeTab}`);t.id=`news-${e.id}`,t.dataset.id=e.id;const[s]=t.getElementsByTagName("h3"),[n]=t.getElementsByClassName("published"),[i]=t.getElementsByClassName("artist"),[a]=t.getElementsByClassName("shop"),[r]=t.getElementsByClassName("tldr"),[o]=t.getElementsByClassName("item-info"),[l]=t.getElementsByClassName("image");[s.textContent,n.textContent,i.href,i.textContent,r.textContent,o.innerHTML]=[e.title,formatTimeAgo(e.date),e.artist.url,e.artist.name,e.tldr,e.post_content],e.shop?[a.href,a.innerHTML]=[e.shop.url,jvbSettings.icons.shop+e.shop.name]:a.hidden=!0;const[d]=t.getElementsByClassName("favourite-button");if("own"!==this.activeTab)[d.dataset.id,d.dataset.artist]=[e.id,e.artist.id],window.userFavourites.news?.includes(parseInt(e.id))?(removeChildren(d),d.append(getIcon("star-fi"))):(removeChildren(d),d.append(getIcon("star")));else{d.hidden=!0;const[s]=t.getElementsByClassName("select-checkbox"),[n]=t.getElementsByTagName("label");[s.id,s.value,n.for]=[`item-${e.id}`,e.id,`item-${e.id}`]}let h="";window.userVotes?.news?.has(e.id)&&(h=window.userVotes.news.get(e.id)),console.log(e),t.querySelector(".summary").appendChild(formatVote(e,h));let c=window.getTemplate("commentsButton");c.href=`#responses-to-${e.id}`,c.querySelector(".count").textContent=e.comments.items.length;let m=window.getTemplate("responses");m.id=`responses-to-${e.id}`,m.querySelector("summary").textContent+=" { "+e.comments.items.length+" }";let p=window.getTemplate("replyButton");return p.id="reply-to-"+e.id,p.dataset.type="news",p.dataset.action="reply",t.appendChild(p),e.comments.items.length>0&&e.comments.items.forEach((e=>{m.appendChild(this.formatComment(e))})),t.appendChild(m),t.querySelector(".vote").prepend(c,t.querySelector(".vote button")),e.image&&e.image.replace(/src="([^"]+)"/,'data-src="$1"'),t}formatComment(e,t=null){let s=window.getTemplate("response");s.id="response-"+e.id;let n=s.querySelector("summary");n.querySelector(".content").innerHTML=e.response,n.querySelector(".created").textContent=formatTimeAgo(e.created_at);let i=checkVoteStatus("response",e.id);e.content="response",s.querySelector(".footer").appendChild(formatVote(e,i)),console.log(e);let a=window.getTemplate("replyButton");a.id="reply-to-"+e.id,t&&(a.dataset.parent_id=t),a.dataset.action="reply",a.dataset.type=e.content,n.querySelector(".vote").prepend(a,n.querySelector(".vote").firstElementChild);let r=n.querySelector(".artist"),o=n.querySelector(".shop");if(console.log(e),e.artist?(e.artist.shop||o.remove(),[r.href,r.textContent,o.href,o.textContent]=[e.artist.url,e.artist.name,e.artist.shop.url,e.artist.shop.name]):(r.remove(),o.remove()),e.children.items.length>0){let t=window.getTemplate("responses");t.id="replies-to-"+e.id,t.querySelector("summary").textContent="See Responses {"+e.children.items.length+"}",e.children.items.forEach((s=>{t.appendChild(this.formatComment(s,e.id))})),s.appendChild(t)}return s}renderResponseCreate(){}saveCreatedResponse(){console.log("Saving create response"),console.log(this.replyModal.modal.id);const e=this.replyModal.modal;let t={user:jvbSettings.currentUser,item_id:e.dataset.id,response:e.querySelector(".ql-editor").innerHTML,content:e.dataset.type,action:"create"};e.dataset.parent_id&&(t.parent_id=e.dataset.parent_id),console.log(t),this.queue.addToQueue({type:"new_response",data:t})}showEmptyState(){const e=document.createElement("div");e.className="no-news",e.innerHTML="\n            <h3>Nothing here</h3>\n            <p>No updates here.</p>\n            <p>Add some gap fillers from the main favourites tab.</p>\n        ",this.grid.appendChild(e),this.grid.classList.add("empty"),this.a11y.announce("No favourites to show!")}hideEmptyState(){let e=this.grid.querySelector(".no-news");e&&e.remove()}handleError(e,t){console.error(`News error (${t}):`,e),window.jvbError&&window.jvbError.log(e,{component:"NewsManager",action:t}),window.jvbA11y&&window.jvbA11y.announce(`Error ${t}. ${e.message||"Please try again."}`)}};
assets/js/min/populate.min.js
@@ -1 +1 @@
window.jvbPopulate=class{constructor(e,t={},a={},l={}){console.log("Populating field... ",e),console.log("fieldData: ",t),console.log("imageData: ",a),console.log("options: ",l);for(let[r,o]of Object.entries(t)){let t=e.querySelector(`[data-field="${r}"]`);t&&this.populateField(t,r,o,a,l)}}populateField(e,t,a,l={},r={}){if(e&&null!=a)switch(this.getFieldType(e)){case"image":this.populateImageField(e,t,a,l);break;case"gallery":this.populateGalleryField(e,t,a,l);break;case"repeater":this.populateRepeaterField(e,t,a,r);break;case"taxonomy":this.populateTaxonomyField(e,t,a);break;case"user":this.populateUserField(e,t,a);break;case"location":this.populateLocationField(e,t,a);break;case"set":case"checkbox":this.populateSetField(e,t,a);break;case"select":case"radio":this.populateSelectField(e,t,a);break;case"true_false":this.populateBooleanField(e,t,a);break;case"date":case"time":case"datetime":this.populateDateField(e,t,a);break;case"number":this.populateNumberField(e,t,a);break;case"textarea":e.querySelector(".editor-container")?this.populateEditorField(e,t,a):this.populateTextareaField(e,t,a);break;default:this.populateTextField(e,t,a)}}getFieldType(e){const t=["image","gallery","repeater","taxonomy","user","location","set","checkbox","select","radio","true_false","date","time","datetime","editor","number","text","textarea","email","url","tel","phone"];for(const a of t)if(e.classList.contains(a))return a;if(e.dataset.type)return e.dataset.type;const a=e.querySelector("input, select, textarea");if(a){if("TEXTAREA"===a.tagName)return"true"===a.dataset.editor?"editor":"textarea";if(a.type)return"checkbox"!==a.type||e.classList.contains("true_false")?a.type:"set"}return"text"}populateTextField(e,t,a){const l=e.querySelector(`[name="${t}"], input, textarea`);if(l&&(l.value=String(a||""),l.dataset.limit)){const t=e.querySelector(".char-count .current");t&&(t.textContent=l.value.length)}}populateTextareaField(e,t,a){const l=e.querySelector(`textarea[name="${t}"]`)||e.querySelector('textarea:not([data-editor="true"])');if(l){if(l.value,l.value=String(a||""),l.dispatchEvent(new Event("change",{bubbles:!0})),l.dataset.limit){const t=e.querySelector(".char-count .current");if(t){t.textContent=l.value.length;const a=parseInt(l.dataset.limit,10);l.value.length>=a?e.classList.add("reached"):e.classList.remove("reached")}}}else console.warn(`No textarea found for field ${t} in wrapper:`,e)}populateNumberField(e,t,a){const l=e.querySelector(`[name="${t}"], input[type="number"]`);l&&(l.value=Number(a)||0)}populateBooleanField(e,t,a){const l=e.querySelector(`[name="${t}"], input[type="checkbox"]`);l&&(l.checked=Boolean(a))}populateSelectField(e,t,a){const l=String(a||""),r=e.querySelector(`select[name="${t}"]`);if(r)return void(r.value=l);const o=e.querySelector(`input[type="radio"][name="${t}"][value="${l}"]`);o&&(o.checked=!0)}populateSetField(e,t,a){let l=a;if("string"==typeof a)try{l=JSON.parse(a)}catch(e){l=a.split(",").map((e=>e.trim()))}Array.isArray(l)||(l=[String(l)]),e.querySelectorAll(`input[type="checkbox"][name*="${t}"]`).forEach((e=>{e.checked=l.includes(e.value)}))}populateDateField(e,t,a){const l=e.querySelector(`[name="${t}"], input`);if(l&&a){let e=a;"object"==typeof a&&a.date&&(e=a.date);try{const t=new Date(e);if(!isNaN(t.getTime()))switch(l.type){case"date":l.value=t.toISOString().split("T")[0];break;case"time":l.value=t.toTimeString().slice(0,5);break;case"datetime-local":l.value=t.toISOString().slice(0,16);break;default:l.value=e}}catch(t){l.value=e}}}populateEditorField(e,t,a){const l=e.querySelector(`textarea[name="${t}"]`)||e.querySelector('textarea[data-editor="true"]')||e.querySelector("textarea");if(!l)return void console.warn(`Editor field ${t}: textarea not found`);const r=String(a||"");l.value=r;const o=e.querySelector(".editor");if(o){let e=null;if(o.__quill)e=o.__quill;else if(o.quill)e=o.quill;else if(window.Quill&&window.Quill.find)e=window.Quill.find(o);else if(window.Quill&&window.Quill.instances)for(let t of window.Quill.instances)if(t.container===o){e=t;break}e?(e.root.innerHTML=r,o.__quill=e):(console.warn(`Quill instance not found for ${t}, setting HTML directly`),o.innerHTML=r)}else console.warn(`Editor container not found for ${t}`);l.dispatchEvent(new Event("change",{bubbles:!0}))}populateLocationField(e,t,a){a&&"object"==typeof a&&["address","lat","lng","street","city","province","postal_code","country"].forEach((l=>{if(void 0!==a[l]){const r=e.querySelector(`[name="${t}_${l}"], [name="${l}"]`);r&&(r.value=String(a[l]||""))}}))}populateTaxonomyField(e,t,a){let l=[];if(Array.isArray(a))l=a.map((e=>String(e)));else if("string"==typeof a)try{const e=JSON.parse(a);l=Array.isArray(e)?e.map((e=>String(e))):[String(e)]}catch(e){l=a.split(",").map((e=>e.trim()))}else a&&(l=[String(a)]);if(0===l.length)return;const r=e.querySelector(`input[type="hidden"][name="${t}"]`);r&&(r.value=l.join(","))}populateUserField(e,t,a){this.populateTaxonomyField(e,t,a)}populateImageField(e,t,a,l={}){if(!a)return;const r=String(a).split(",").filter((e=>parseInt(e.trim())));if(0===r.length)return;const o=e.querySelector(`input[type="hidden"][name="${t}"]`);o&&(o.value=r.join(","));const i=e.querySelector(".item-grid"),n=e.querySelector(".file-upload-container");e.querySelector(".progress")?.remove(),i&&(window.removeChildren(i),r.forEach((e=>{let t=window.getTemplate("uploadItem"),a=t.querySelector("img"),r=t.querySelector("details"),o=window.getTemplate("uploadMeta");r.append(o),[a.src,a.alt,t.querySelector('[name="image-title"]').value,t.querySelector('[name="image-alt-text"]').value,t.querySelector('[name="image-caption"]').value]=[l[e].medium,l[e].alt,l[e].title,l[e].alt,l[e].caption],r.querySelector(".upload-meta > .hint")?.remove(),i.append(t)})),r.length>0&&n&&(n.hidden=!0))}populateGalleryField(e,t,a,l={}){this.populateImageField(e,t,a,l)}populateRepeaterField(e,t,a,l={}){if(console.log("fieldWrapper",e),console.log("fieldName",t),console.log("fieldValue",a),console.log("options",l),!a||!Array.isArray(a))return;const r=e.querySelector(".repeater-items"),o=e.querySelector("template");r&&o?(window.removeChildren(r),a.forEach(((a,l)=>{if(!a||"object"!=typeof a)return;const i=window.getTemplate(o.className);if(!i)return void console.warn(`Repeater field ${t}: template not found`);i.id=`${e.closest("form").id}-${t}-row-${l}`,i.dataset.index=l;const n=i.querySelector(".row-number");n&&(n.textContent=`#${l+1}`),i.querySelectorAll("input, select, textarea").forEach((e=>{const r=e.name,o=`${t}:${l}:${r}`,i=`${t}-${l}-${r}-${e.value}`;e.name=o,e.id=i;const n=e.nextElementSibling;n&&"LABEL"===n.tagName&&(n.htmlFor=i),void 0!==a[r]&&this.populateRepeaterFieldValue(e,r,a[r])})),r.appendChild(i)}))):console.warn(`Repeater field ${t}: missing container or template`)}populateRepeaterFieldValue(e,t,a){switch(e.type){case"checkbox":e.checked=Boolean(a);break;case"radio":e.checked=e.value===String(a);break;default:e.value=String(a||"")}}};
window.jvbPopulate=class{constructor(e,t={},a={},r={}){for(let[l,i]of Object.entries(t)){let t=e.querySelector(`[data-field="${l}"]`);t&&this.populateField(t,l,i,a,r)}}populateField(e,t,a,r={},l={}){if(e&&null!=a)switch(this.getFieldType(e)){case"image":this.populateImageField(e,t,a,r);break;case"gallery":this.populateGalleryField(e,t,a,r);break;case"repeater":this.populateRepeaterField(e,t,a,l);break;case"taxonomy":this.populateTaxonomyField(e,t,a);break;case"user":this.populateUserField(e,t,a);break;case"location":this.populateLocationField(e,t,a);break;case"set":case"checkbox":this.populateSetField(e,t,a);break;case"select":case"radio":this.populateSelectField(e,t,a);break;case"true_false":this.populateBooleanField(e,t,a);break;case"date":case"time":case"datetime":this.populateDateField(e,t,a);break;case"number":this.populateNumberField(e,t,a);break;case"textarea":e.querySelector(".editor-container")?this.populateEditorField(e,t,a):this.populateTextareaField(e,t,a);break;default:this.populateTextField(e,t,a)}}getFieldType(e){const t=["image","gallery","repeater","taxonomy","user","location","set","checkbox","select","radio","true_false","date","time","datetime","editor","number","text","textarea","email","url","tel","phone"];for(const a of t)if(e.classList.contains(a))return a;if(e.dataset.type)return e.dataset.type;const a=e.querySelector("input, select, textarea");if(a){if("TEXTAREA"===a.tagName)return"true"===a.dataset.editor?"editor":"textarea";if(a.type)return"checkbox"!==a.type||e.classList.contains("true_false")?a.type:"set"}return"text"}populateTextField(e,t,a){const r=e.querySelector(`[name="${t}"], input, textarea`);if(r&&(r.value=String(a||""),r.dataset.limit)){const t=e.querySelector(".char-count .current");t&&(t.textContent=r.value.length)}}populateTextareaField(e,t,a){const r=e.querySelector(`textarea[name="${t}"]`)||e.querySelector('textarea:not([data-editor="true"])');if(r){if(r.value,r.value=String(a||""),r.dispatchEvent(new Event("change",{bubbles:!0})),r.dataset.limit){const t=e.querySelector(".char-count .current");if(t){t.textContent=r.value.length;const a=parseInt(r.dataset.limit,10);r.value.length>=a?e.classList.add("reached"):e.classList.remove("reached")}}}else console.warn(`No textarea found for field ${t} in wrapper:`,e)}populateNumberField(e,t,a){const r=e.querySelector(`[name="${t}"], input[type="number"]`);r&&(r.value=Number(a)||0)}populateBooleanField(e,t,a){const r=e.querySelector(`[name="${t}"], input[type="checkbox"]`);r&&(r.checked=Boolean(a))}populateSelectField(e,t,a){const r=String(a||""),l=e.querySelector(`select[name="${t}"]`);if(l)return void(l.value=r);const i=e.querySelector(`input[type="radio"][name="${t}"][value="${r}"]`);i&&(i.checked=!0)}populateSetField(e,t,a){let r=a;if("string"==typeof a)try{r=JSON.parse(a)}catch(e){r=a.split(",").map((e=>e.trim()))}Array.isArray(r)||(r=[String(r)]),e.querySelectorAll(`input[type="checkbox"][name*="${t}"]`).forEach((e=>{e.checked=r.includes(e.value)}))}populateDateField(e,t,a){const r=e.querySelector(`[name="${t}"], input`);if(r&&a){let e=a;"object"==typeof a&&a.date&&(e=a.date);try{const t=new Date(e);if(!isNaN(t.getTime()))switch(r.type){case"date":r.value=t.toISOString().split("T")[0];break;case"time":r.value=t.toTimeString().slice(0,5);break;case"datetime-local":r.value=t.toISOString().slice(0,16);break;default:r.value=e}}catch(t){r.value=e}}}populateEditorField(e,t,a){const r=e.querySelector(`textarea[name="${t}"]`)||e.querySelector('textarea[data-editor="true"]')||e.querySelector("textarea");if(!r)return void console.warn(`Editor field ${t}: textarea not found`);const l=String(a||"");r.value=l;const i=e.querySelector(".editor");if(i){let e=null;if(i.__quill)e=i.__quill;else if(i.quill)e=i.quill;else if(window.Quill&&window.Quill.find)e=window.Quill.find(i);else if(window.Quill&&window.Quill.instances)for(let t of window.Quill.instances)if(t.container===i){e=t;break}e?(e.root.innerHTML=l,i.__quill=e):(console.warn(`Quill instance not found for ${t}, setting HTML directly`),i.innerHTML=l)}else console.warn(`Editor container not found for ${t}`);r.dispatchEvent(new Event("change",{bubbles:!0}))}populateLocationField(e,t,a){a&&"object"==typeof a&&["address","lat","lng","street","city","province","postal_code","country"].forEach((r=>{if(void 0!==a[r]){const l=e.querySelector(`[name="${t}_${r}"], [name="${r}"]`);l&&(l.value=String(a[r]||""))}}))}populateTaxonomyField(e,t,a){let r=[];if(Array.isArray(a))r=a.map((e=>String(e)));else if("string"==typeof a)try{const e=JSON.parse(a);r=Array.isArray(e)?e.map((e=>String(e))):[String(e)]}catch(e){r=a.split(",").map((e=>e.trim()))}else a&&(r=[String(a)]);if(0===r.length)return;const l=e.querySelector(`input[type="hidden"][name="${t}"]`);l&&(l.value=r.join(","))}populateUserField(e,t,a){this.populateTaxonomyField(e,t,a)}populateImageField(e,t,a,r={}){if(!a)return;const l=String(a).split(",").filter((e=>parseInt(e.trim())));if(0===l.length)return;const i=e.querySelector(`input[type="hidden"][name="${t}"]`);i&&(i.value=l.join(","));const o=e.querySelector(".item-grid"),n=e.querySelector(".file-upload-container");e.querySelector(".progress")?.remove(),o&&(window.removeChildren(o),l.forEach((e=>{let t=window.getTemplate("uploadItem"),a=t.querySelector("img"),l=t.querySelector("details"),i=window.getTemplate("uploadMeta");l.append(i),[a.src,a.alt,t.querySelector('[name="image-title"]').value,t.querySelector('[name="image-alt-text"]').value,t.querySelector('[name="image-caption"]').value]=[r[e].medium,r[e].alt,r[e].title,r[e].alt,r[e].caption],l.querySelector(".upload-meta > .hint")?.remove(),o.append(t)})),l.length>0&&n&&(n.hidden=!0))}populateGalleryField(e,t,a,r={}){"timeline"===t?this.populateTimelineGallery(e,t,a,r):this.populateImageField(e,t,a,r)}populateTimelineGallery(e,t,a,r){if(!a)return;const l=Object.keys(a);if(0===l.length)return;const i=e.querySelector(".item-grid");e.querySelector(".file-upload-container"),e.querySelector(".progress")?.remove(),i&&(window.removeChildren(i),l.forEach((e=>{let t=window.getTemplate("timelineItem"),l=t.querySelector("img");t.querySelector("video")?.remove(),t.querySelector(".select-item span")?.remove();let o=a[e],n=r[e];l.src=n.medium,o={...o,...n},t.querySelectorAll(".field").forEach((e=>{if(e.classList.contains("group"))return;let t=e.querySelector("input, textarea"),a=e.querySelector("label"),l=t.name.replace("upload_data::",""),i=o[l];this.populateField(e,l,i,r);let n=`[${o.id}]${l}`;[t.name,t.id,a.htmlFor]=[n,n,n]})),i.append(t)})))}populateRepeaterField(e,t,a,r={}){if(!a||!Array.isArray(a))return;const l=e.querySelector(".repeater-items"),i=e.querySelector("template");l&&i?(window.removeChildren(l),a.forEach(((a,r)=>{if(!a||"object"!=typeof a)return;const o=window.getTemplate(i.className);if(!o)return void console.warn(`Repeater field ${t}: template not found`);o.id=`${e.closest("form").id}-${t}-row-${r}`,o.dataset.index=r;const n=o.querySelector(".row-number");n&&(n.textContent=`#${r+1}`),o.querySelectorAll("input, select, textarea").forEach((e=>{const l=e.name,i=`${t}:${r}:${l}`,o=`${t}-${r}-${l}-${e.value}`;e.name=i,e.id=o;const n=e.nextElementSibling;n&&"LABEL"===n.tagName&&(n.htmlFor=o),void 0!==a[l]&&this.populateRepeaterFieldValue(e,l,a[l])})),l.appendChild(o)}))):console.warn(`Repeater field ${t}: missing container or template`)}populateRepeaterFieldValue(e,t,a){switch(e.type){case"checkbox":e.checked=Boolean(a);break;case"radio":e.checked=e.value===String(a);break;default:e.value=String(a||"")}}};
assets/js/min/queue.min.js
@@ -1 +1 @@
(()=>{class t{constructor(t={}){this.canUpdateUI=!0,console.log("jvbSettings",jvbSettings),this.config={apiBase:jvbSettings.api,maxRetries:3,pollInterval:5e3,activityDelay:2e3,autosync:!0,endpoint:"queue",...t},this.user=jvbSettings.currentUser,console.log(this.user),this.headers={"X-WP-Nonce":jvbSettings.nonce,...t.headers},this.a11y=window.jvbA11y,this.errors=window.jvbError,this.store=new window.jvbStore({name:"queue",storeName:"operations",keyPath:"id",endpoint:this.config.endpoint,TTL:1/0,indexes:[{name:"status",keyPath:"status"},{name:"type",keyPath:"type"}],showLoading:!1,getBlobs:async t=>{if(window.jvbUploadBlobs){Array.isArray(t)||"string"!=typeof t||(t=[t]);return(await Promise.all(t.map((t=>window.jvbUploadBlobs.getBlob(t))))).filter(Boolean)}return null}}),this.classes=["offline","synced","pending"],this.isProcessing=!1,this.isPolling=!1,this.subscribers=new Set,this.statuses=["queued","localProcessing","uploading","pending","processing","completed","failed","failed_permanent"],this.initUI(),this.initListeners(),console.log(this.ui),this.ui.panel&&(this.popup=new window.jvbPopup({popup:this.ui.panel,toggle:this.ui.toggle,name:"Queue Panel"})),this.initQueue(),this.user&&(this.ui.toggle.hidden=!1,this.ui.panel.hidden=!1)}async initQueue(){const t=this.getOperationsByStatus(["completed","failed_permanent"],!1);t.length>0?this.startPolling():this.updateStatusPanel("synced"),this.store.subscribe(((t,e)=>{switch(t){case"data-loaded":this.getOperationsByStatus(["completed","failed_permanent"],!1).length>0&&this.startPolling(),this.updateUI();break;case"item-saved":this.hasQueuedOperations()&&this.startPolling();default:this.updateUI()}})),this.notify("queue-initialized",{operations:t})}addToQueue(t){const e={id:`u${this.user}_${Date.now()}_${Math.random().toString(36).substring(2,9)}`,endpoint:null,method:"POST",headers:{},data:{},canMerge:!0,popup:"Saving changes...",title:"Operation",status:"queued",timestamp:Date.now(),retries:0,user:this.user,...t};if(e.headers={...this.headers,...e.headers},!e.endpoint||!e.data)return console.error("Invalid operation queued: missing endpoint or data"),null;const s=Array.from(this.store.data.values()).filter((t=>"queued"===t.status&&t.endpoint===e.endpoint&&t.canMerge));if(s.length>0){const t=s[0];return t.data=window.deepMerge(t.data,e.data),t.timestamp=Date.now(),this.updateOperationStatus(t.id,t.status),this.updateUI(),this.startActivityTracking(),t.id}return console.log("Added to Queue: ",e),this.setQueue(e),this.updateOperationStatus(e.id,e.status),this.updateUI(),this.startActivityTracking(),e.id}setQueue(t){this.store.save(t)}updateOperationStatus(t,e){let s=this.store.get(t);s&&(s.status=e,this.notify("operation-status",s),this.updateOperationUI(s))}getQueue(t){return this.store.get(t)}clearQueue(t){this.store.delete(t)}startActivityTracking(){if(!this.activityListeners){const t=["mousedown","mousemove","keypress","scroll","touchstart"];this.activityListeners=t.map((t=>{const e=()=>this.resetActivityTimer();return document.addEventListener(t,e,{passive:!0}),{event:t,handler:e}}))}this.resetActivityTimer()}resetActivityTimer(){this.lastActivity=Date.now(),this.activityTimer&&clearTimeout(this.activityTimer),this.activityTimer=setTimeout((()=>{this.processQueue()}),this.config.activityDelay)}stopActivityTracking(){this.activityTimer&&(clearTimeout(this.activityTimer),this.activityTimer=null),this.activityListeners&&(this.activityListeners.forEach((({event:t,handler:e})=>{document.removeEventListener(t,e)})),this.activityListeners=null)}setProcessing(t){this.isProcessing=t,this.ui.toggle.classList.toggle("saving",t)}async processQueue(){if(this.isProcessing)return;const t=this.getOperationsByStatus("queued");if(0===t.length)return void this.stopActivityTracking();this.setProcessing(!0);for(const e of t)await this.processOperation(e);this.setProcessing(!1),this.stopActivityTracking();this.getOperationsByStatus(["queued","completed","failed_permanent"],!1).length>0&&this.startPolling()}async processOperation(t){try{this.updateOperationStatus(t.id,"uploading"),t=this.getQueue(t.id);const i=`${this.config.apiBase}${t.endpoint}`;let a;if(console.log(t.data),t.data instanceof FormData)for(var[e,s]of(t.data.append("id",t.id),t.data.append("user",this.user),a=t.data,console.log("Sending to server:"),a.entries()))console.log(e,s);else a=JSON.stringify({...t.data,id:t.id,user:this.user}),t.headers["Content-Type"]="application/json";const n=await fetch(i,{method:t.method,headers:t.headers,body:a}),r=await n.json();if(!n.ok||!1===r.success)throw new Error(r.message||`HTTP ${n.status}`);if(r.id&&t.id!==r.id){const e=this.getQueue(r.id);e?(e.data=window.deepMerge(e.data,t.data),e.status="pending",e.serverData=r,this.updateOperationStatus(e.id,e.status),this.setQueue(e),this.removeOperationFromUI(t.id),t=e):(this.clearQueue(t.id),t.id=r.id,t.status="pending",t.serverData=r,this.updateOperationStatus(t.id,t.status),this.setQueue(t))}else t.status="pending",t.serverData=r,this.updateOperationStatus(t.id,"pending"),this.setQueue(t);this.a11y.announce(`${t.title} sent to server for processing.`)}catch(e){console.error("Operation failed:",e),t.retries++,t.lastError=e.message,t.retries>=this.config.maxRetries?t.status="failed_permanent":(t.status="failed",t.nextRetry=Date.now()+1e3*Math.pow(2,t.retries)),this.updateOperationStatus(t.id,t.status),this.setQueue(t)}}startPolling(){this.isPolling||(this.isPolling=!0,this.updateStatusPanel("pending"),this.pollTimer=setInterval((async()=>{try{await this.store.fetch();0===this.getOperationsByStatus(["completed","failed_permanent"],!1).length&&(this.stopPolling(),this.updateStatusPanel("synced"))}catch(t){console.error("Polling error:",t)}}),this.config.pollInterval))}stopPolling(){this.isPolling&&(this.isPolling=!1,this.pollTimer&&(clearInterval(this.pollTimer),this.pollTimer=null),this.countdownTimer&&(clearInterval(this.countdownTimer),this.countdownTimer=null))}async updateServerOperations(t,e){if(0!==(t=(t=Array.isArray(t)?t:t.includes(",")?t.split(","):[t]).filter((t=>{let s=this.getQueue(t);return this.getAllowedActions(s.status).includes(e)}))).length){["cancel","dismiss"].includes(e)&&t.forEach((t=>{this.removeOperationFromUI(t)}));try{const s=`${this.config.apiBase}${this.config.endpoint}`,i=await fetch(s,{method:"POST",headers:{"Content-Type":"application/json",...this.headers},body:JSON.stringify({ids:t,action:e})});if(!i.ok){const t=await i.json().catch((()=>{}));throw new Error(t.message||`${e} failed: ${i.status}`)}const a=await i.json();if(!a.success)throw new Error(a.message||`${e} operation failed`);return["cancel","dismiss"].includes(e)?t.forEach((t=>{let s=this.getQueue(t);this.notify(`${e}-operation`,s),this.clearQueue(t)})):(t.forEach((t=>{let s=this.getQueue(t);this.notify(`${e}-operation`,s),s.status="queued",s.retries=0,this.setQueue(s),this.updateOperationStatus(s.id,s.status)})),this.startActivityTracking()),this.updateUI(),a}catch(s){const i=await window.jvbError.log(s,{component:"QueueManager",operation:"performQueueAction",action:e,operationIds:t,itemCount:t.length},(()=>this.updateServerOperations(t,e)));if(i.retried)return i;throw s}}}getAllowedActions(t){return{queued:["cancel"],localProcessing:["cancel"],pending:["cancel"],processing:[],completed:["dismiss"],failed:["retry","dismiss"],failed_permanent:["dismiss"]}[t]||[]}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),document.addEventListener("click",this.clickHandler),this.ui.panel?.addEventListener("change",this.changeHandler),this.handleOnline=()=>{this.updateStatusPanel(),this.hasQueuedOperations()&&this.processQueue()},this.handleOffline=()=>this.updateStatusPanel("offline"),this.handleBeforeUnload=t=>{if(this.getOperationsByStatus(["queued","uploading"]).length>0)return t.preventDefault(),"You have unsaved changes in the queue."},window.addEventListener("online",this.handleOnline),window.addEventListener("offline",this.handleOffline),window.addEventListener("beforeunload",this.handleBeforeUnload)}handleClick(t){if(t.target.closest(this.selectors.panel,this.selectors.toggle))if(t.target.closest(this.selectors.refreshButton))this.store.fetch();else if(t.target.closest(this.selectors.clearButton)){const t=this.getOperationsByStatus("completed");if(t.length>0){const e=t.map((t=>t.id));this.updateServerOperations(e,"dismiss")}}else if(t.target.closest(this.selectors.retryButton)){const t=this.getOperationsByStatus("failed");if(t.length>0){const e=t.map((t=>t.id));this.updateServerOperations(e,"retry")}}else if(t.target.closest("[data-action]")){const e=t.target.closest("[data-action]"),s=e.closest("[data-id]")?.dataset.id;s&&this.updateServerOperations(s,e.dataset.action)}else if(t.target.closest(".filters [data-filter]")){const e=t.target.closest("[data-filter]").dataset.filter;this.setFilter(e)}}handleChange(t){}initUI(){if(this.icons={queued:"refresh",localProcessing:"refresh",uploading:"syncing",pending:"cloud",processing:"syncing",completed:"synced",failed:"error",failed_permanent:"error"},this.selectors={panel:"aside#queue",toggle:"button.qtoggle",refreshButton:"button.refreshNow",countdown:".countdown",indicator:".qtoggle .indicator",count:".qtoggle .count",popup:".popup",itemsContainer:".qitems",clearButton:".dismiss-all",retryButton:".retry-all",filters:{all:'.filters [data-filter="all"]',received:'.filters [data-filter="queued"]',localProcessing:'.filters [data-filter="localProcessing"]',uploading:'.filters [data-filter="uploading"]',pending:'.filters [data-filter="pending"]',processing:'.filters [data-filter="processing"]',completed:'.filters [data-filter="completed"]',failed:'.filters [data-filter="failed"]'}},this.ui={panel:document.querySelector(this.selectors.panel),toggle:document.querySelector(this.selectors.toggle),count:document.querySelector(this.selectors.count),indicator:document.querySelector(this.selectors.indicator)},this.ui.panel){for(let[t,e]of Object.entries(this.selectors))if(!["panel","toggle","count","indicator"].includes(t))if("object"==typeof e){this.ui[t]={};for(let[s,i]of Object.entries(e))this.ui[t][s]=this.ui.panel.querySelector(i)}else this.ui[t]=this.ui.panel.querySelector(e)}else this.canUpdateUI=!1}updateUI(){if(!this.canUpdateUI)return;const t=this.getQueueStats();if(this.ui.count){const e=t.total-t.completed;this.ui.count.textContent=e>0?e:"",this.ui.count.style.display=e>0?"":"none"}if(this.ui.indicator){const e=t.queued>0||t.uploading>0||t.pending>0||t.processing>0;this.ui.indicator.classList.toggle("active",e)}let e=this.getOperationsByStatus("failed"),s=this.getOperationsByStatus("completed");this.ui.clearButton.disabled=0===s.length,this.ui.retryButton.disabled=0===e.length,Object.entries(this.ui.filters).forEach((([e,s])=>{const i="all"===e?t.total:t[e]||0,a=s.querySelector(".count");a&&(a.textContent=i>0?i:""),s.setAttribute("data-count",i)})),this.renderOperations()}getStatusLabel(t){return{queued:"Queued",localProcessing:"Processing locally",uploading:"Uploading",pending:"Waiting on server",processing:"Processing",completed:"Completed",failed:"Failed (will retry)",failed_permanent:"Failed permanently"}[t]||t}getItemMessage(t){if(t.message)return t.message;if(t.error_message)return t.error_message;switch(t.status){case"queued":return"Waiting to send...";case"uploading":return"Sending to server...";case"pending":return t.position?`Position ${t.position} in queue`:"In server queue";case"processing":return t.progress?`${t.progress}% complete`:"Processing...";case"completed":return"Successfully completed";case"failed":return`Failed: ${t.lastError||"Unknown error"} (Retry ${t.retries}/${this.config.maxRetries})`;case"failed_permanent":return`Failed: ${t.lastError||"Unknown error"}`;default:return""}}calculateProgress(t){if(t.progress)return t.progress;return{queued:10,uploading:25,pending:40,processing:70,completed:100,failed:0,failed_permanent:0}[t.status]||0}getQueueStats(){const t={};return this.statuses.forEach((e=>{t[e]=0})),Array.from(this.store.data.values()).forEach((e=>{t.hasOwnProperty(e.status)&&t[e.status]++})),t.total=Array.from(this.store.data.values()).length,t}renderOperations(){if(!this.ui.itemsContainer)return;const t=this.getActiveFilter(),e=this.getFilteredOperations(t);if(window.removeChildren(this.ui.itemsContainer),0===e.length){let t=window.getTemplate("emptyQueue");this.ui.itemsContainer.append(t),this.a11y.announce("Nothing queued.")}else{let t=this.ui.itemsContainer.querySelector(".emptyQueue");t&&t.remove(),e.forEach((t=>{const e=this.createOperationUI(t);this.ui.itemsContainer.appendChild(e)}))}}createOperationUI(t){const e=window.getTemplate("queueItem");return e.dataset.id=t.id,this.updateOperationUI(t,e),e}updateOperationUI(t,e=null){e||(e=this.ui.itemsContainer?.querySelector(`[data-id="${t.id}"]`)),e||(e=this.createOperationUI(t)),this.statuses.forEach((t=>e.classList.remove(t))),e.classList.add(t.status);let s="";t.updated_at?s=window.formatTimeAgo(new Date(t.updated_at)):t.created_at&&(s=window.formatTimeAgo(new Date(t.created_at)));const i=this.calculateProgress(t),a=e.querySelector(".type"),n=e.querySelector(".status"),r=e.querySelector(".info .details"),o=e.querySelector(".info .time"),l=e.querySelector(".progress .fill");if(a&&(a.textContent=t.title),n){n.querySelector(".icon")?.remove();let e=this.getStatusLabel(t.status);n.title=e,n.prepend(window.getIcon(this.icons[t.status])),n.querySelector("span").textContent=e}r&&(r.textContent=this.getItemMessage(t)),o&&(o.textContent=s),l&&(l.style.width=`${i}%`);const u=e.querySelector(".actions");u&&this.updateActionButtons(t,u)}updateActionButtons(t,e){switch(window.removeChildren(e),t.status){case"queued":case"localProcessing":case"pending":const s=window.getTemplate("button");s.classList.add("cancel"),s.dataset.action="cancel",s.textContent="Cancel",e.appendChild(s);break;case"failed":case"failed_permanent":const i=window.getTemplate("button"),a=window.getTemplate("button");i.classList.add("retry"),i.textContent="Retry",i.disabled=t.retries>=this.maxRetries,i.dataset.action="retry",a.classList.add("dismiss"),a.textContent="Dismiss",a.dataset.action="dismiss",e.appendChild(i),e.appendChild(a);break;case"completed":const n=window.getTemplate("button");n.dataset.action="dismiss",n.classList.add("dismiss"),n.textContent="Dismiss",e.appendChild(n)}}removeOperationFromUI(t){const e=this.ui.itemsContainer?.querySelector(`[data-id="${t}"]`);e&&(e.style.opacity="0",e.style.transform="scale(0.9)",setTimeout((()=>e.remove()),300))}updateCountdown(){if(!this.ui.countdown||!this.isPolling)return;let t=this.config.pollInterval/1e3;this.countdownTimer=setInterval((()=>{t--,this.ui.countdown.textContent=t,t<=0&&(clearInterval(this.countdownTimer),this.isPolling&&setTimeout((()=>this.updateCountdown()),100))}),1e3)}updateStatusPanel(t){this.ui.panel?.classList.remove(...this.classes),this.classes.includes(t)&&this.ui.panel?.classList.add(t)}setFilter(t){Object.values(this.ui.filters).forEach((e=>{e&&e.classList.toggle("active",e.dataset.filter===t)})),this.activeFilter=t,this.renderOperations()}getActiveFilter(){const t=this.ui.panel?.querySelector(".filter.active");return t?.dataset.filter||"all"}getFilteredOperations(t){const e=Array.from(this.store.data.values());return"all"===t?e:e.filter((e=>e.status===t))}showPopup(t,e="success"){if(!this.ui.popup)return;const s=this.ui.popup.querySelector("span");s&&(s.textContent=t),this.ui.popup.className=`popup ${e} show`,setTimeout((()=>{this.ui.popup.classList.remove("show")}),3e3)}getOperationsByStatus(t,e=!0){return Array.isArray(t)||"string"!=typeof t||(t=[t]),e?Array.from(this.store.data.values()).filter((e=>t.includes(e.status))):Array.from(this.store.data.values()).filter((e=>!t.includes(e.status)))}async hasQueuedOperations(){return(await this.store.query("status","queued")).length>0}subscribe(t){return this.subscribers.add(t),()=>this.subscribers.delete(t)}notify(t,e){this.subscribers.forEach((s=>s(t,e)))}destroy(){this.stopPolling(),this.stopActivityTracking(),this.clickHandler&&document.removeEventListener("click",this.clickHandler),this.keyHandler&&document.removeEventListener("keydown",this.keyHandler),this.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(function(){window.jvbQueue=new t}))})();
(()=>{class t{constructor(t={}){this.canUpdateUI=!0,console.log("jvbSettings",jvbSettings),this.config={apiBase:jvbSettings.api,maxRetries:3,pollInterval:5e3,activityDelay:2e3,autosync:!0,endpoint:"queue",...t},this.user=jvbSettings.currentUser,console.log(this.user),this.headers={"X-WP-Nonce":jvbSettings.nonce,...t.headers},this.a11y=window.jvbA11y,this.errors=window.jvbError,this.store=new window.jvbStore({name:"queue",storeName:"operations",keyPath:"id",endpoint:this.config.endpoint,TTL:1/0,indexes:[{name:"status",keyPath:"status"},{name:"type",keyPath:"type"}],showLoading:!1,getBlobs:async t=>{if(window.jvbUploadBlobs){Array.isArray(t)||"string"!=typeof t||(t=[t]);return(await Promise.all(t.map((t=>window.jvbUploadBlobs.getBlob(t))))).filter(Boolean)}return null}}),this.classes=["offline","synced","pending"],this.isProcessing=!1,this.isPolling=!1,this.subscribers=new Set,this.statuses=["queued","localProcessing","uploading","pending","processing","completed","failed","failed_permanent"],this.initUI(),this.initListeners(),console.log(this.ui),this.ui.panel&&(this.popup=new window.jvbPopup({popup:this.ui.panel,toggle:this.ui.toggle,name:"Queue Panel"})),this.initQueue(),this.user&&(this.ui.toggle.hidden=!1,this.ui.panel.hidden=!1)}async initQueue(){const t=this.getOperationsByStatus(["completed","failed_permanent"],!1);t.length>0?this.startPolling():this.updateStatusPanel("synced"),this.store.subscribe(((t,e)=>{switch(t){case"data-loaded":this.getOperationsByStatus(["completed","failed_permanent"],!1).length>0&&this.startPolling(),this.updateUI();break;case"item-saved":this.hasQueuedOperations()&&this.startPolling();default:this.updateUI()}})),this.notify("queue-initialized",{operations:t})}addToQueue(t){const e={id:`u${this.user}_${Date.now()}_${Math.random().toString(36).substring(2,9)}`,endpoint:null,method:"POST",headers:{},data:{},canMerge:!0,popup:"Saving changes...",title:"Operation",status:"queued",timestamp:Date.now(),retries:0,user:this.user,...t};if(e.headers={...this.headers,...e.headers},!e.endpoint||!e.data)return console.error("Invalid operation queued: missing endpoint or data"),null;const s=Array.from(this.store.data.values()).filter((t=>"queued"===t.status&&t.endpoint===e.endpoint&&t.canMerge));if(s.length>0){const t=s[0];return t.data=window.deepMerge(t.data,e.data),t.timestamp=Date.now(),this.updateOperationStatus(t.id,t.status),this.updateUI(),this.startActivityTracking(),t.id}return console.log("Added to Queue: ",e),this.setQueue(e),this.updateOperationStatus(e.id,e.status),this.updateUI(),this.startActivityTracking(),e.id}setQueue(t){this.store.save(t)}updateOperationStatus(t,e){let s=this.store.get(t);s&&(s.status=e,this.notify("operation-status",s),this.updateOperationUI(s))}getQueue(t){return this.store.get(t)}clearQueue(t){this.store.delete(t)}startActivityTracking(){if(!this.activityListeners){const t=["mousedown","mousemove","keypress","scroll","touchstart"];this.activityListeners=t.map((t=>{const e=()=>this.resetActivityTimer();return document.addEventListener(t,e,{passive:!0}),{event:t,handler:e}}))}this.resetActivityTimer()}resetActivityTimer(){this.lastActivity=Date.now(),this.activityTimer&&clearTimeout(this.activityTimer),this.activityTimer=setTimeout((()=>{this.processQueue()}),this.config.activityDelay)}stopActivityTracking(){this.activityTimer&&(clearTimeout(this.activityTimer),this.activityTimer=null),this.activityListeners&&(this.activityListeners.forEach((({event:t,handler:e})=>{document.removeEventListener(t,e)})),this.activityListeners=null)}setProcessing(t){this.isProcessing=t,this.ui.toggle.classList.toggle("saving",t)}async processQueue(){if(this.isProcessing)return;const t=this.getOperationsByStatus("queued");if(0===t.length)return void this.stopActivityTracking();this.setProcessing(!0);for(const e of t)await this.processOperation(e);this.setProcessing(!1),this.stopActivityTracking();this.getOperationsByStatus(["queued","completed","failed_permanent"],!1).length>0&&this.startPolling()}async processOperation(t){try{this.updateOperationStatus(t.id,"uploading"),t=this.getQueue(t.id);const i=`${this.config.apiBase}${t.endpoint}`;let a;if(console.log(t.data),t.data instanceof FormData)for(var[e,s]of(t.data.append("id",t.id),t.data.append("user",this.user),a=t.data,console.log("Sending to server:"),a.entries()))console.log(e,s);else a=JSON.stringify({...t.data,id:t.id,user:this.user}),t.headers["Content-Type"]="application/json";const n=await fetch(i,{method:t.method,headers:t.headers,body:a}),r=await n.json();if(!n.ok||!1===r.success)throw new Error(r.message||`HTTP ${n.status}`);if(r.id&&t.id!==r.id){const e=this.getQueue(r.id);e?(e.data=window.deepMerge(e.data,t.data),e.status="pending",e.serverData=r,this.updateOperationStatus(e.id,e.status),this.setQueue(e),this.removeOperationFromUI(t.id),t=e):(this.clearQueue(t.id),t.id=r.id,t.status="pending",t.serverData=r,this.updateOperationStatus(t.id,t.status),this.setQueue(t))}else t.status="pending",t.serverData=r,this.updateOperationStatus(t.id,"pending"),this.setQueue(t);this.a11y.announce(`${t.title} sent to server for processing.`)}catch(e){console.error("Operation failed:",e),t.retries++,t.lastError=e.message,t.retries>=this.config.maxRetries?t.status="failed_permanent":(t.status="failed",t.nextRetry=Date.now()+1e3*Math.pow(2,t.retries)),this.updateOperationStatus(t.id,t.status),this.setQueue(t)}}startPolling(){this.isPolling||(this.isPolling=!0,this.updateStatusPanel("pending"),this.pollTimer=setInterval((async()=>{try{await this.store.fetch();0===this.getOperationsByStatus(["completed","failed_permanent"],!1).length&&(this.stopPolling(),this.updateStatusPanel("synced"))}catch(t){console.error("Polling error:",t)}}),this.config.pollInterval))}stopPolling(){this.isPolling&&(this.isPolling=!1,this.pollTimer&&(clearInterval(this.pollTimer),this.pollTimer=null),this.countdownTimer&&(clearInterval(this.countdownTimer),this.countdownTimer=null))}async updateServerOperations(t,e){if(0!==(t=(t=Array.isArray(t)?t:t.includes(",")?t.split(","):[t]).filter((t=>{let s=this.getQueue(t);return this.getAllowedActions(s.status).includes(e)}))).length){["cancel","dismiss"].includes(e)&&t.forEach((t=>{this.removeOperationFromUI(t)}));try{const s=`${this.config.apiBase}${this.config.endpoint}`,i=await fetch(s,{method:"POST",headers:{"Content-Type":"application/json",...this.headers},body:JSON.stringify({ids:t,action:e})});if(!i.ok){const t=await i.json().catch((()=>{}));throw new Error(t.message||`${e} failed: ${i.status}`)}const a=await i.json();if(!a.success)throw new Error(a.message||`${e} operation failed`);return["cancel","dismiss"].includes(e)?t.forEach((t=>{let s=this.getQueue(t);this.notify(`${e}-operation`,s),this.clearQueue(t)})):(t.forEach((t=>{let s=this.getQueue(t);this.notify(`${e}-operation`,s),s.status="queued",s.retries=0,this.setQueue(s),this.updateOperationStatus(s.id,s.status)})),this.startActivityTracking()),this.updateUI(),a}catch(s){const i=await window.jvbError.log(s,{component:"QueueManager",operation:"performQueueAction",action:e,operationIds:t,itemCount:t.length},(()=>this.updateServerOperations(t,e)));if(i.retried)return i;throw s}}}getAllowedActions(t){return{queued:["cancel"],localProcessing:["cancel"],pending:["cancel"],processing:[],completed:["dismiss"],failed:["retry","dismiss"],failed_permanent:["dismiss"]}[t]||[]}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),document.addEventListener("click",this.clickHandler),this.ui.panel?.addEventListener("change",this.changeHandler),this.handleOnline=()=>{this.updateStatusPanel(),this.hasQueuedOperations()&&this.processQueue()},this.handleOffline=()=>this.updateStatusPanel("offline"),this.handleBeforeUnload=t=>{if(this.getOperationsByStatus(["queued","uploading"]).length>0)return t.preventDefault(),"You have unsaved changes in the queue."},window.addEventListener("online",this.handleOnline),window.addEventListener("offline",this.handleOffline),window.addEventListener("beforeunload",this.handleBeforeUnload)}handleClick(t){if(t.target.closest(this.selectors.panel,this.selectors.toggle))if(t.target.closest(this.selectors.refreshButton))this.store.fetch();else if(t.target.closest(this.selectors.clearButton)){const t=this.getOperationsByStatus("completed");if(t.length>0){const e=t.map((t=>t.id));this.updateServerOperations(e,"dismiss")}}else if(t.target.closest(this.selectors.retryButton)){const t=this.getOperationsByStatus("failed");if(t.length>0){const e=t.map((t=>t.id));this.updateServerOperations(e,"retry")}}else if(t.target.closest("[data-action]")){const e=t.target.closest("[data-action]"),s=e.closest("[data-id]")?.dataset.id;s&&this.updateServerOperations(s,e.dataset.action)}else if(t.target.closest(".filters [data-filter]")){const e=t.target.closest("[data-filter]").dataset.filter;this.setFilter(e)}}handleChange(t){}initUI(){if(this.icons={queued:"arrows-clockwise",localProcessing:"arrows-clockwise",uploading:"syncing",pending:"cloud",processing:"syncing",completed:"cloud-check",failed:"cloud-warning",failed_permanent:"cloud-warning"},this.selectors={panel:"aside#queue",toggle:"button.qtoggle",refreshButton:"button.refreshNow",countdown:".countdown",indicator:".qtoggle .indicator",count:".qtoggle .count",popup:".popup",itemsContainer:".qitems",clearButton:".dismiss-all",retryButton:".retry-all",filters:{all:'.filters [data-filter="all"]',received:'.filters [data-filter="queued"]',localProcessing:'.filters [data-filter="localProcessing"]',uploading:'.filters [data-filter="uploading"]',pending:'.filters [data-filter="pending"]',processing:'.filters [data-filter="processing"]',completed:'.filters [data-filter="completed"]',failed:'.filters [data-filter="failed"]'}},this.ui={panel:document.querySelector(this.selectors.panel),toggle:document.querySelector(this.selectors.toggle),count:document.querySelector(this.selectors.count),indicator:document.querySelector(this.selectors.indicator)},this.ui.panel){for(let[t,e]of Object.entries(this.selectors))if(!["panel","toggle","count","indicator"].includes(t))if("object"==typeof e){this.ui[t]={};for(let[s,i]of Object.entries(e))this.ui[t][s]=this.ui.panel.querySelector(i)}else this.ui[t]=this.ui.panel.querySelector(e)}else this.canUpdateUI=!1}updateUI(){if(!this.canUpdateUI)return;const t=this.getQueueStats();if(this.ui.count){const e=t.total-t.completed;this.ui.count.textContent=e>0?e:"",this.ui.count.style.display=e>0?"":"none"}if(this.ui.indicator){const e=t.queued>0||t.uploading>0||t.pending>0||t.processing>0;this.ui.indicator.classList.toggle("active",e)}let e=this.getOperationsByStatus("failed"),s=this.getOperationsByStatus("completed");this.ui.clearButton.disabled=0===s.length,this.ui.retryButton.disabled=0===e.length,Object.entries(this.ui.filters).forEach((([e,s])=>{const i="all"===e?t.total:t[e]||0,a=s.querySelector(".count");a&&(a.textContent=i>0?i:""),s.setAttribute("data-count",i)})),this.renderOperations()}getStatusLabel(t){return{queued:"Queued",localProcessing:"Processing locally",uploading:"Uploading",pending:"Waiting on server",processing:"Processing",completed:"Completed",failed:"Failed (will retry)",failed_permanent:"Failed permanently"}[t]||t}getItemMessage(t){if(t.message)return t.message;if(t.error_message)return t.error_message;switch(t.status){case"queued":return"Waiting to send...";case"uploading":return"Sending to server...";case"pending":return t.position?`Position ${t.position} in queue`:"In server queue";case"processing":return t.progress?`${t.progress}% complete`:"Processing...";case"completed":return"Successfully completed";case"failed":return`Failed: ${t.lastError||"Unknown error"} (Retry ${t.retries}/${this.config.maxRetries})`;case"failed_permanent":return`Failed: ${t.lastError||"Unknown error"}`;default:return""}}calculateProgress(t){if(t.progress)return t.progress;return{queued:10,uploading:25,pending:40,processing:70,completed:100,failed:0,failed_permanent:0}[t.status]||0}getQueueStats(){const t={};return this.statuses.forEach((e=>{t[e]=0})),Array.from(this.store.data.values()).forEach((e=>{t.hasOwnProperty(e.status)&&t[e.status]++})),t.total=Array.from(this.store.data.values()).length,t}renderOperations(){if(!this.ui.itemsContainer)return;const t=this.getActiveFilter(),e=this.getFilteredOperations(t);if(window.removeChildren(this.ui.itemsContainer),0===e.length){let t=window.getTemplate("emptyQueue");this.ui.itemsContainer.append(t),this.a11y.announce("Nothing queued.")}else{let t=this.ui.itemsContainer.querySelector(".emptyQueue");t&&t.remove(),e.forEach((t=>{const e=this.createOperationUI(t);this.ui.itemsContainer.appendChild(e)}))}}createOperationUI(t){const e=window.getTemplate("queueItem");return e.dataset.id=t.id,this.updateOperationUI(t,e),e}updateOperationUI(t,e=null){e||(e=this.ui.itemsContainer?.querySelector(`[data-id="${t.id}"]`)),e||(e=this.createOperationUI(t)),this.statuses.forEach((t=>e.classList.remove(t))),e.classList.add(t.status);let s="";t.updated_at?s=window.formatTimeAgo(new Date(t.updated_at)):t.created_at&&(s=window.formatTimeAgo(new Date(t.created_at)));const i=this.calculateProgress(t),a=e.querySelector(".type"),n=e.querySelector(".status"),r=e.querySelector(".info .details"),o=e.querySelector(".info .time"),l=e.querySelector(".progress .fill");if(a&&(a.textContent=t.title),n){n.querySelector(".icon")?.remove();let e=this.getStatusLabel(t.status);n.title=e,n.prepend(window.getIcon(this.icons[t.status])),n.querySelector("span").textContent=e}r&&(r.textContent=this.getItemMessage(t)),o&&(o.textContent=s),l&&(l.style.width=`${i}%`);const u=e.querySelector(".actions");u&&this.updateActionButtons(t,u)}updateActionButtons(t,e){switch(window.removeChildren(e),t.status){case"queued":case"localProcessing":case"pending":const s=window.getTemplate("button");s.classList.add("cancel"),s.dataset.action="cancel",s.textContent="Cancel",e.appendChild(s);break;case"failed":case"failed_permanent":const i=window.getTemplate("button"),a=window.getTemplate("button");i.classList.add("retry"),i.textContent="Retry",i.disabled=t.retries>=this.maxRetries,i.dataset.action="retry",a.classList.add("dismiss"),a.textContent="Dismiss",a.dataset.action="dismiss",e.appendChild(i),e.appendChild(a);break;case"completed":const n=window.getTemplate("button");n.dataset.action="dismiss",n.classList.add("dismiss"),n.textContent="Dismiss",e.appendChild(n)}}removeOperationFromUI(t){const e=this.ui.itemsContainer?.querySelector(`[data-id="${t}"]`);e&&(e.style.opacity="0",e.style.transform="scale(0.9)",setTimeout((()=>e.remove()),300))}updateCountdown(){if(!this.ui.countdown||!this.isPolling)return;let t=this.config.pollInterval/1e3;this.countdownTimer=setInterval((()=>{t--,this.ui.countdown.textContent=t,t<=0&&(clearInterval(this.countdownTimer),this.isPolling&&setTimeout((()=>this.updateCountdown()),100))}),1e3)}updateStatusPanel(t){this.ui.panel?.classList.remove(...this.classes),this.classes.includes(t)&&this.ui.panel?.classList.add(t)}setFilter(t){Object.values(this.ui.filters).forEach((e=>{e&&e.classList.toggle("active",e.dataset.filter===t)})),this.activeFilter=t,this.renderOperations()}getActiveFilter(){const t=this.ui.panel?.querySelector(".filter.active");return t?.dataset.filter||"all"}getFilteredOperations(t){const e=Array.from(this.store.data.values());return"all"===t?e:e.filter((e=>e.status===t))}showPopup(t,e="success"){if(!this.ui.popup)return;const s=this.ui.popup.querySelector("span");s&&(s.textContent=t),this.ui.popup.className=`popup ${e} show`,setTimeout((()=>{this.ui.popup.classList.remove("show")}),3e3)}getOperationsByStatus(t,e=!0){return Array.isArray(t)||"string"!=typeof t||(t=[t]),e?Array.from(this.store.data.values()).filter((e=>t.includes(e.status))):Array.from(this.store.data.values()).filter((e=>!t.includes(e.status)))}async hasQueuedOperations(){return(await this.store.query("status","queued")).length>0}subscribe(t){return this.subscribers.add(t),()=>this.subscribers.delete(t)}notify(t,e){this.subscribers.forEach((s=>s(t,e)))}destroy(){this.stopPolling(),this.stopActivityTracking(),this.clickHandler&&document.removeEventListener("click",this.clickHandler),this.keyHandler&&document.removeEventListener("keydown",this.keyHandler),this.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(function(){window.jvbQueue=new t}))})();
assets/js/min/quill.min.js
@@ -1 +1 @@
window.jvbQuill=function(t){t.querySelectorAll("textarea[data-editor=true]").forEach((t=>{let n,e,i;if(t.parentNode.querySelector(".editor-container"))n=t.parentNode.querySelector(".editor-container"),e=n.querySelector(".editor"),i=n.querySelector(".toolbar");else{n=document.createElement("div"),n.className="editor-container",e=document.createElement("div"),e.className="editor",i=document.createElement("div"),i.className="toolbar";const l=!0===t.dataset.allowimage?`<button type="button" class="ql-jvb_image">\n                    ${dashboardSettings.icons.image}\n                </button>`:"";i.id=`toolbar-${t.id}`,i.innerHTML=`\n                <span class="ql-formats">\n                    <button type="button" class="ql-p">\n                        ${jvbSettings.icons.paragraph}\n                    </button>\n                    <button type="button" class="ql-h1">\n                        ${jvbSettings.icons.h1}\n                    </button>\n                    <button type="button" class="ql-h2">\n                        ${jvbSettings.icons.h2}\n                    </button>\n                    <button type="button" class="ql-h3">\n                        ${jvbSettings.icons.h3}\n                    </button>\n                </span>\n                <span class="ql-formats">\n                    <button type="button" class="ql-jvb_bold">\n                        ${jvbSettings.icons.bold}\n                    </button>\n                    <button type="button" class="ql-jvb_italic">\n                        ${jvbSettings.icons.italic}\n                    </button>\n                    <button type="button" class="ql-jvb_underline">\n                        ${jvbSettings.icons.underline}\n                    </button>\n                    <button type="button" class="ql-jvb_strike">\n                        ${jvbSettings.icons.strike}\n                    </button>\n                </span>\n                <span class="ql-formats">\n                     <button type="button" class="ql-jvb_list" value="bullet">\n                        ${jvbSettings.icons["list-bullets"]}\n                    </button>\n                    <button type="button" class="ql-jvb_list" value="ordered">\n                        ${jvbSettings.icons["list-numbers"]}\n                    </button>\n                </span>\n                <span class="ql-formats">\n                     <button type="button" class="ql-jvb_align" value="left">\n                        ${jvbSettings.icons["align-left"]}\n                    </button>\n                     <button type="button" class="ql-jvb_align" value="center">\n                        ${jvbSettings.icons["align-center"]}\n                    </button>\n                     <button type="button" class="ql-jvb_align" value="right">\n                        ${jvbSettings.icons["align-right"]}\n                    </button>\n                </span>\n                <span class="ql-formats">\n                     <button type="button" class="ql-jvb_link">\n                        ${jvbSettings.icons.link}\n                    </button>\n                    ${l}\n                </span>\n            `,n.appendChild(i),n.appendChild(e),t.parentNode.insertBefore(n,t),t.style.display="none",e.innerHTML=t.value}const l=new Quill(e,{theme:"snow",modules:{toolbar:{container:i,handlers:{p:function(){this.quill.format("header",!1)},h1:function(){this.quill.format("header",1)},h2:function(){this.quill.format("header",2)},h3:function(){this.quill.format("header",3)},jvb_bold:function(){this.quill.format("bold",!0)},jvb_italic:function(){this.quill.format("italic",!0)},jvb_strike:function(){this.quill.format("strike",!0)},jvb_underline:function(){this.quill.format("underline",!0)},jvb_align:function(t){this.quill.format("align",t!==this.quill.getFormat().list&&t)},jvb_list:function(t){this.quill.format("list",t!==this.quill.getFormat().list&&t)},jvb_link:function(t){if(t){const t=this.quill.getSelection();if(null==t||0===t.length)return;this.quill.getText(t.index,t.length);const n=this.quill.getFormat(t).link,e=document.createElement("dialog");e.className="quill-link-modal",e.innerHTML=`\n                                    <div class="quill-link-modal-content ">\n                                        <label for="link">Enter URL</label>\n                                        <input type="url" id="link" placeholder="Enter URL" value="${n||""}" />\n                                        <div class="buttons">\n                                            <button type="button" class="save">Save</button>\n                                            ${n?'<button type="button" class="remove">Remove</button>':""}\n                                            <button type="button" class="cancel">Cancel</button>\n                                        </div>\n                                    </div>\n                                `,document.body.appendChild(e),e.showModal();const i=e.querySelector("input");i.focus(),e.querySelector(".save").addEventListener("click",(()=>{const t=i.value;t&&this.quill.format("link",t),e.remove()}));const l=e.querySelector(".remove");l&&l.addEventListener("click",(()=>{this.quill.format("link",!1),e.remove()})),e.querySelector(".cancel").addEventListener("click",(()=>{e.remove()})),i.addEventListener("keyup",(t=>{if("Enter"===t.key){const t=i.value;t&&this.quill.format("link",t),e.remove()}}))}},jvb_image:function(){const t=document.createElement("input");t.setAttribute("type","file"),t.setAttribute("accept","image/jpeg,image/png,image/gif,image/webp"),t.style.display="none",document.body.appendChild(t),t.onchange=async n=>{const e=n.target.files?.[0];if(!e)return;if(e.size>5242880)return this.quill.insertText(i.index,"File too large. Maximum size is 5MB",{color:"#f00",italic:!0},!0),void t.remove();const i=this.quill.getSelection(!0),l=new FormData;l.append("image",e),objectID&&l.append("post_id",objectID),window.jvbLoading&&window.jvbLoading.showLoading("Uploading image...","Processing Upload");try{const t=await fetch(`${jvbSettings.api}uploads/`,{method:"POST",headers:{"X-WP-Nonce":jvbSettings.nonce},body:l});if(!t.ok)throw new Error("Upload failed");const n=await t.json();this.quill.insertEmbed(i.index,"image",n.url)}catch(t){this.handleError("Upload error:",t),this.quill.insertText(i.index,"Failed to upload image. Please try again.",{color:"#f00",italic:!0},!0)}finally{window.jvbLoading&&window.jvbLoading.hide(),t.remove()}},t.click()}}},history:{delay:2e3,maxStack:500},clipboard:{matchVisual:!1}}});l.on("selection-change",(function(t){const n=i.querySelector(".ql-align");if(n){if(t&&0===t.length){const[e]=this.quill.getLeaf(t.index);if(e&&e.domNode&&"IMG"===e.domNode.tagName)return void(n.style.display="inline-block")}n.style.display="none"}})),l.on("text-change",(()=>{t.value=l.root.innerHTML,t.dispatchEvent(new Event("change",{bubbles:!0}))}))}))};
window.jvbQuill=function(t){t.querySelectorAll("textarea[data-editor=true]").forEach((t=>{let n,e,i;if(t.parentNode.querySelector(".editor-container"))n=t.parentNode.querySelector(".editor-container"),e=n.querySelector(".editor"),i=n.querySelector(".toolbar");else{n=document.createElement("div"),n.className="editor-container",e=document.createElement("div"),e.className="editor",i=document.createElement("div"),i.className="toolbar";const o=!0===t.dataset.allowimage?`<button type="button" class="ql-jvb_image">\n                    ${dashboardSettings.icons.image}\n                </button>`:"";i.id=`toolbar-${t.id}`,i.innerHTML=`\n                <span class="ql-formats">\n                    <button type="button" class="ql-p">\n                        <i class="icon icon-paragraph"></i>\n                    </button>\n                    <button type="button" class="ql-h1">\n                        <i class="icon icon-text-h-one"></i>\n                    </button>\n                    <button type="button" class="ql-h2">\n                        <i class="icon icon-text-h-two"></i>\n                    </button>\n                    <button type="button" class="ql-h3">\n                        <i class="icon icon-text-h-three"></i>\n                    </button>\n                </span>\n                <span class="ql-formats">\n                    <button type="button" class="ql-jvb_bold">\n                        <i class="icon icon-text-b-fi"></i>\n                    </button>\n                    <button type="button" class="ql-jvb_italic">\n                        <i class="icon icon-text-italic"></i>\n                    </button>\n                    <button type="button" class="ql-jvb_underline">\n                        <i class="icon icon-text-underline"></i>\n                    </button>\n                    <button type="button" class="ql-jvb_strike">\n                        <i class="icon icon-text-strikethrough"></i>\n                    </button>\n                </span>\n                <span class="ql-formats">\n                     <button type="button" class="ql-jvb_list" value="bullet">\n                        <i class="icon icon-list-dashes"></i>\n                    </button>\n                    <button type="button" class="ql-jvb_list" value="ordered">\n                        <i class="icon icon-list-numbers"></i>\n                    </button>\n                </span>\n                <span class="ql-formats">\n                     <button type="button" class="ql-jvb_align" value="left">\n                        <i class="icon icon-text-align-left"></i>\n                    </button>\n                     <button type="button" class="ql-jvb_align" value="center">\n                        <i class="icon icon-text-align-center"></i>\n                    </button>\n                     <button type="button" class="ql-jvb_align" value="right">\n                        <i class="icon icon-text-align-right"></i>\n                    </button>\n                </span>\n                <span class="ql-formats">\n                     <button type="button" class="ql-jvb_link">\n                        <i class="icon icon-link"></i>\n                    </button>\n                    ${o}\n                </span>\n            `,n.appendChild(i),n.appendChild(e),t.parentNode.insertBefore(n,t),t.style.display="none",e.innerHTML=t.value}const o=new Quill(e,{theme:"snow",modules:{toolbar:{container:i,handlers:{p:function(){this.quill.format("header",!1)},h1:function(){this.quill.format("header",1)},h2:function(){this.quill.format("header",2)},h3:function(){this.quill.format("header",3)},jvb_bold:function(){this.quill.format("bold",!0)},jvb_italic:function(){this.quill.format("italic",!0)},jvb_strike:function(){this.quill.format("strike",!0)},jvb_underline:function(){this.quill.format("underline",!0)},jvb_align:function(t){this.quill.format("align",t!==this.quill.getFormat().list&&t)},jvb_list:function(t){this.quill.format("list",t!==this.quill.getFormat().list&&t)},jvb_link:function(t){if(t){const t=this.quill.getSelection();if(null==t||0===t.length)return;this.quill.getText(t.index,t.length);const n=this.quill.getFormat(t).link,e=document.createElement("dialog");e.className="quill-link-modal",e.innerHTML=`\n                                    <div class="quill-link-modal-content ">\n                                        <label for="link">Enter URL</label>\n                                        <input type="url" id="link" placeholder="Enter URL" value="${n||""}" />\n                                        <div class="buttons">\n                                            <button type="button" class="save">Save</button>\n                                            ${n?'<button type="button" class="remove">Remove</button>':""}\n                                            <button type="button" class="cancel">Cancel</button>\n                                        </div>\n                                    </div>\n                                `,document.body.appendChild(e),e.showModal();const i=e.querySelector("input");i.focus(),e.querySelector(".save").addEventListener("click",(()=>{const t=i.value;t&&this.quill.format("link",t),e.remove()}));const o=e.querySelector(".remove");o&&o.addEventListener("click",(()=>{this.quill.format("link",!1),e.remove()})),e.querySelector(".cancel").addEventListener("click",(()=>{e.remove()})),i.addEventListener("keyup",(t=>{if("Enter"===t.key){const t=i.value;t&&this.quill.format("link",t),e.remove()}}))}},jvb_image:function(){const t=document.createElement("input");t.setAttribute("type","file"),t.setAttribute("accept","image/jpeg,image/png,image/gif,image/webp"),t.style.display="none",document.body.appendChild(t),t.onchange=async n=>{const e=n.target.files?.[0];if(!e)return;if(e.size>5242880)return this.quill.insertText(i.index,"File too large. Maximum size is 5MB",{color:"#f00",italic:!0},!0),void t.remove();const i=this.quill.getSelection(!0),o=new FormData;o.append("image",e),objectID&&o.append("post_id",objectID),window.jvbLoading&&window.jvbLoading.showLoading("Uploading image...","Processing Upload");try{const t=await fetch(`${jvbSettings.api}uploads/`,{method:"POST",headers:{"X-WP-Nonce":jvbSettings.nonce},body:o});if(!t.ok)throw new Error("Upload failed");const n=await t.json();this.quill.insertEmbed(i.index,"image",n.url)}catch(t){this.handleError("Upload error:",t),this.quill.insertText(i.index,"Failed to upload image. Please try again.",{color:"#f00",italic:!0},!0)}finally{window.jvbLoading&&window.jvbLoading.hide(),t.remove()}},t.click()}}},history:{delay:2e3,maxStack:500},clipboard:{matchVisual:!1}}});o.on("selection-change",(function(t){const n=i.querySelector(".ql-align");if(n){if(t&&0===t.length){const[e]=this.quill.getLeaf(t.index);if(e&&e.domNode&&"IMG"===e.domNode.tagName)return void(n.style.display="inline-block")}n.style.display="none"}})),o.on("text-change",(()=>{t.value=o.root.innerHTML,t.dispatchEvent(new Event("change",{bubbles:!0}))}))}))};
assets/js/min/referral.min.js
@@ -1 +1 @@
(()=>{class e{constructor(){this.container=document.querySelector(".jvb-referral"),this.container&&(this.a11y=window.jvbA11y,this.toggle=document.querySelector('button[data-action="toggle-referral"]'),this.initElements(),this.initListeners(),this.checkForReferral())}initElements(){this.selectors={copy:"button.copy",login:".login",submit:"[type=submit]"},this.forms=this.container.querySelectorAll("form"),console.log(this.forms),this.popup=new window.jvbPopup({toggle:this.toggle,popup:this.container,name:"Referral Box",onOpen:()=>{this.forms.forEach((e=>{e.addEventListener("submit",this.submitHandler)})),this.container.addEventListener("click",this.clickHandler),this.container.addEventListener("input",this.inputHandler)},onClose:()=>{this.forms.forEach((e=>{e.removeEventListener("submit",this.submitHandler)})),this.container.removeEventListener("click",this.clickHandler),this.container.removeEventListener("input",this.inputHandler)}}),this.tabs=null,this.container.querySelector("nav.tabs")&&(this.tabs=new window.jvbTabs(this.container,{updateURL:!1})),this.ui=window.uiFromSelectors(this.selectors,this.container)}initListeners(){this.clickHandler=this.handleClick.bind(this),this.inputHandler=this.handleInput.bind(this),this.submitHandler=this.handleFormSubmit.bind(this),this.changeHandler=this.handleChange.bind(this)}handleClick(e){if(e.target.classList.contains(".copy")){let t=e.target.dataset.target,r=this.container.querySelector(`#${t}`);r=!!r&&r.textContent,r&&this.handleCopy(e.target,r)}}handleChange(e){"referral-code"===e.target.id&&window.debouncer.schedule("check-referral",(()=>this.makeRequest("referrals/check-code",{code:e.target.value})))}handleInput(e){"referral-code"===e.target.id&&(e.target.value=e.target.value.toUpperCase())}initShareWidget(){this.initCopyButton(),this.loadStats()}async checkForReferral(){const e=this.getUrlParameter("seeReferral"),t=this.getUrlParameter("ref");if(!e&&!t)return;if(!t)return void this.popup.openPopup();const r=this.container.querySelector("#referral-code-input");if(!r)return;const n=t.toUpperCase();r.value=n,r.readOnly=!0,this.popup.togglePopup();try{const e=await this.validateCodeOnly(n);if(e.success){this.showReferrerBanner(e.referrer_name,n);const t=this.container.querySelector("#referral-name");t&&t.focus()}else r.readOnly=!1,this.showMessage("This referral link is invalid. Please enter a valid code.","error")}catch(e){console.error("Error validating code:",e),r.readOnly=!1}this.removeUrlParameter("ref")}getUrlParameter(e){return new URLSearchParams(window.location.search).get(e)}removeUrlParameter(e){const t=new URL(window.location);t.searchParams.delete(e),window.history.replaceState({},document.title,t.toString())}async validateCodeOnly(e){const t=await fetch(`${jvbSettings.api}/referrals/check-code`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:e})});return await t.json()}showReferrerBanner(e,t){const r=this.container.querySelector(".referral-header");if(!r)return;const n=document.createElement("div");n.className="referrer-banner",n.innerHTML=`\n            <div class="banner-icon">🎉</div>\n            <div class="banner-content">\n                <strong>${window.escapeHtml(e)}</strong> referred you!\n                <div class="banner-code">Code: <code>${window.escapeHtml(t)}</code></div>\n            </div>\n        `,r.parentNode.insertBefore(n,r.nextSibling);const a=r.querySelector("h3");a&&(a.textContent="Complete Your Registration");const o=r.querySelector("p");o&&(o.textContent="Enter your details below to claim your welcome reward!")}handleCopy(e,t=""){if(""===t||"string"!=typeof t)return;let r=e.textContent;(navigator.clipboard||navigator.clipboard.writeText)&&navigator.clipboard.writeText(t).then((()=>{e.textContent="Copied!",e.style.background="#00a32a",setTimeout((()=>{e.textContent=r,e.style.background=""}),2e3)}))}async loadStats(){if(this.container.querySelector(".referral-stats"))try{const e=await fetch(`${jvbSettings.api}/referrals/stats`,{headers:{"X-WP-Nonce":jvbSettings.nonce}}),t=await e.json();t.success&&t.stats&&this.updateStats(t.stats)}catch(e){console.error("Error loading stats:",e)}}updateStats(e){const t={total:this.container.querySelector('[data-stat="total"]'),treated:this.container.querySelector('[data-stat="treated"]'),pending:this.container.querySelector('[data-stat="pending"]'),rewards:this.container.querySelector('[data-stat="rewards"]')};t.total&&(t.total.textContent=e.total_referrals||0),t.treated&&(t.treated.textContent=e.treated_count||0),t.pending&&(t.pending.textContent=e.pending_count||0),t.rewards&&(t.rewards.textContent="$"+parseFloat(e.available_rewards||0).toFixed(2))}async handleFormSubmit(e){console.log("Form Submission!"),window.debouncer.cancel("check-referral"),e.preventDefault(),console.log("Still working?");const t=e.target,r=new FormData(t);let n={};this.setFormLoading(!0,t);try{let e={success:!1,message:""};console.log(t),console.log(t.id),"referral-code-form"===t.id?(r.get("name")||(e.message+="We need your name to know who you are."),r.get("email")||(e.message+="We need your email to confirm you have access to it."),r.get("referral_code")||(e.message+="We need the referral code to know who sent you."),r.get("name")&&r.get("email")&&r.get("referral_code")&&(n.name=r.get("name"),n.email=r.get("email"),n.code=r.get("referral_code"),e=await this.makeRequest("referrals/register",n))):"login-form"===t.id&&r.get("login-email")&&(n.type="login",n.email=r.get("login-email"),n.context={},n.context.redirect_to=window.location.href+"?seeReferral=1",console.log("Making Request with: ",n),e=await this.makeRequest("magic-link",n)),e.success?this.handleSuccess(e):(this.showMessage(e.message||"Something went wrong. Please try again.","error"),this.setFormLoading(!1,t))}catch(e){console.error("Error registering:",e),this.showMessage("Something went wrong. Please try again.","error"),this.setFormLoading(!1,t)}finally{this.setFormLoading(!1,t)}}async makeRequest(e,t){if(!["magic-link","referrals/register","referrals/check-code"].includes(e))return{success:!1,message:"Something went wrong (Invalid endpoint)."};console.log("Endpoint: ",e),console.log("Data: ",t);const r=await fetch(`${jvbSettings.api}${e}`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":jvbSettings.nonce},body:JSON.stringify(t)});return await r.json()}handleSuccess(e){this.container.querySelectorAll("form").forEach((e=>{window.fade(e,!1)}));const t=this.container.querySelector(".success-message");t&&(t.hidden=!1,t.scrollIntoView({behavior:"smooth",block:"center"}),this.dispatchEvent("emailSent",{email:e.email}))}setFormLoading(e,t){t.querySelectorAll("input").forEach((t=>t.disabled=e));let r=t.querySelector(".status"),n=r.querySelector(".message");r.hidden=e,r.classList.toggle("loading",e),n.textContent=e?"Checking with server...":""}showMessage(e,t="success"){const r=this.container.querySelector("#referral-message");r&&(r.textContent=e,r.className="message "+t,r.style.display="block","error"===t&&setTimeout((()=>{r.style.display="none"}),5e3))}dispatchEvent(e,t){const r=new CustomEvent("referralWidget:"+e,{detail:t,bubbles:!0});this.container.dispatchEvent(r)}}document.addEventListener("DOMContentLoaded",(()=>{window.jvbReferral=new e}))})();
(()=>{class e{constructor(){this.container=document.querySelector(".jvb-referral"),this.container&&(this.a11y=window.jvbA11y,this.toggle=document.querySelector('button[data-action="toggle-referral"]'),this.initElements(),this.initListeners(),this.checkForReferral())}initElements(){this.selectors={copy:"button.copy",login:".login",submit:"[type=submit]"},this.forms=this.container.querySelectorAll("form"),console.log(this.forms),this.popup=new window.jvbPopup({toggle:this.toggle,popup:this.container,name:"Referral Box",onOpen:()=>{this.forms.forEach((e=>{e.addEventListener("submit",this.submitHandler)})),this.container.addEventListener("click",this.clickHandler),this.container.addEventListener("input",this.inputHandler)},onClose:()=>{this.forms.forEach((e=>{e.removeEventListener("submit",this.submitHandler)})),this.container.removeEventListener("click",this.clickHandler),this.container.removeEventListener("input",this.inputHandler)}}),this.tabs=null,this.container.querySelector("nav.tabs")&&(this.tabs=new window.jvbTabs(this.container,{updateURL:!1})),this.ui=window.uiFromSelectors(this.selectors,this.container)}initListeners(){this.clickHandler=this.handleClick.bind(this),this.inputHandler=this.handleInput.bind(this),this.submitHandler=this.handleFormSubmit.bind(this),this.changeHandler=this.handleChange.bind(this)}handleClick(e){if(e.target.classList.contains(".copy")){let t=e.target.dataset.target,r=this.container.querySelector(`#${t}`);r=!!r&&r.textContent,r&&this.handleCopy(e.target,r)}}handleChange(e){"referral-code"===e.target.id&&window.debouncer.schedule("check-referral",(()=>this.makeRequest("referrals/check-code",{code:e.target.value})))}handleInput(e){"referral-code"===e.target.id&&(e.target.value=e.target.value.toUpperCase())}initShareWidget(){this.initCopyButton(),this.loadStats()}async checkForReferral(){const e=this.getUrlParameter("seeReferral"),t=this.getUrlParameter("ref");if(!e&&!t)return;if(!t)return void this.popup.openPopup();const r=this.container.querySelector("#referral-code-input");if(!r)return;const n=t.toUpperCase();r.value=n,r.readOnly=!0,this.popup.togglePopup();try{const e=await this.validateCodeOnly(n);if(e.success){this.showReferrerBanner(e.referrer_name,n);const t=this.container.querySelector("#referral-name");t&&t.focus()}else r.readOnly=!1,this.showMessage("This referral link is invalid. Please enter a valid code.","error")}catch(e){console.error("Error validating code:",e),r.readOnly=!1}this.removeUrlParameter("ref")}getUrlParameter(e){return new URLSearchParams(window.location.search).get(e)}removeUrlParameter(e){const t=new URL(window.location);t.searchParams.delete(e),window.history.replaceState({},document.title,t.toString())}async validateCodeOnly(e){const t=await fetch(`${jvbSettings.api}/referrals/check-code`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:e})});return await t.json()}showReferrerBanner(e,t){const r=this.container.querySelector(".referral-header");if(!r)return;const n=document.createElement("div");n.className="referrer-banner",n.innerHTML=`\n            <div class="banner-icon">🎉</div>\n            <div class="banner-content">\n                <strong>${window.escapeHtml(e)}</strong> referred you!\n                <div class="banner-code">Code: <code>${window.escapeHtml(t)}</code></div>\n            </div>\n        `,r.parentNode.insertBefore(n,r.nextSibling);const a=r.querySelector("h3");a&&(a.textContent="Complete Your Registration");const o=r.querySelector("p");o&&(o.textContent="Enter your details below to claim your welcome reward!")}handleCopy(e,t=""){if(""===t||"string"!=typeof t)return;let r=e.textContent;(navigator.clipboard||navigator.clipboard.writeText)&&navigator.clipboard.writeText(t).then((()=>{e.textContent="Copied!",e.style.background="#00a32a",setTimeout((()=>{e.textContent=r,e.style.background=""}),2e3)}))}async loadStats(){if(this.container.querySelector(".referral-stats"))try{const e=await fetch(`${jvbSettings.api}/referrals/stats`,{headers:{"X-WP-Nonce":jvbSettings.nonce}}),t=await e.json();t.success&&t.stats&&this.updateStats(t.stats)}catch(e){console.error("Error loading stats:",e)}}updateStats(e){const t={total:this.container.querySelector('[data-stat="total"]'),treated:this.container.querySelector('[data-stat="treated"]'),pending:this.container.querySelector('[data-stat="pending"]'),rewards:this.container.querySelector('[data-stat="rewards"]')};t.total&&(t.total.textContent=e.total_referrals||0),t.treated&&(t.treated.textContent=e.treated_count||0),t.pending&&(t.pending.textContent=e.pending_count||0),t.rewards&&(t.rewards.textContent="$"+parseFloat(e.available_rewards||0).toFixed(2))}async handleFormSubmit(e){console.log("Form Submission!"),window.debouncer.cancel("check-referral"),e.preventDefault(),console.log("Still working?");const t=e.target,r=new FormData(t);let n={};this.setFormLoading(!0,t);try{let e={success:!1,message:""};console.log(t),console.log(t.id),"referral-code-form"===t.id?(r.get("name")||(e.message+="We need your name to know who you are."),r.get("email")||(e.message+="We need your email to confirm you have access to it."),r.get("referral_code")||(e.message+="We need the referral code to know who sent you."),r.get("name")&&r.get("email")&&r.get("referral_code")&&(n.name=r.get("name"),n.email=r.get("email"),n.code=r.get("referral_code"),e=await this.makeRequest("referrals/register",n))):"login-form"===t.id&&r.get("login-email")&&(n.type="login",n.email=r.get("login-email"),n.context={},n.context.redirect_to=window.location.href+"?seeReferral=1",console.log("Making Request with: ",n),e=await this.makeRequest("magic",n)),e.success?this.handleSuccess(e):(this.showMessage(e.message||"Something went wrong. Please try again.","error"),this.setFormLoading(!1,t))}catch(e){console.error("Error registering:",e),this.showMessage("Something went wrong. Please try again.","error"),this.setFormLoading(!1,t)}finally{this.setFormLoading(!1,t)}}async makeRequest(e,t){if(!["magic","referrals/register","referrals/check-code"].includes(e))return{success:!1,message:"Something went wrong (Invalid endpoint)."};console.log("Endpoint: ",e),console.log("Data: ",t);const r=await fetch(`${jvbSettings.api}${e}`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":jvbSettings.nonce},body:JSON.stringify(t)});return await r.json()}handleSuccess(e){this.container.querySelectorAll("form").forEach((e=>{window.fade(e,!1)}));const t=this.container.querySelector(".success-message");t&&(t.hidden=!1,t.scrollIntoView({behavior:"smooth",block:"center"}),this.dispatchEvent("emailSent",{email:e.email}))}setFormLoading(e,t){t.querySelectorAll("input").forEach((t=>t.disabled=e));let r=t.querySelector(".status"),n=r.querySelector(".message");r.hidden=e,r.classList.toggle("loading",e),n.textContent=e?"Checking with server...":""}showMessage(e,t="success"){const r=this.container.querySelector("#referral-message");r&&(r.textContent=e,r.className="message "+t,r.style.display="block","error"===t&&setTimeout((()=>{r.style.display="none"}),5e3))}dispatchEvent(e,t){const r=new CustomEvent("referralWidget:"+e,{detail:t,bubbles:!0});this.container.dispatchEvent(r)}}document.addEventListener("DOMContentLoaded",(()=>{window.jvbReferral=new e}))})();
assets/js/min/selector.min.js
@@ -1 +1 @@
(()=>{class e{constructor(){this.a11y=window.jvbA11y,this.error=window.jvbError,this.index=-1,this.hasAutocomplete=!1,this.isInitializing=!0,this.taxonomiesToFetch=new Set,this.store=new window.jvbStore({name:"taxonomies",storeName:"terms",keyPath:"id",showLoading:!1,indexes:[{name:"taxonomy",keyPath:"taxonomy"},{name:"parent",keyPath:"parent"},{name:"slug",keyPath:"slug",unique:!0},{name:"count",keyPath:"count"}],endpoint:"terms",TTL:72e5,filters:{taxonomy:"",page:1,search:"",parent:0},required:"taxonomy"}),this.fields=new Map,this.selectedTerms=new Map,this.activeField=null,this.currentConfig=null,this.currentSingular=null,this.currentPlural=null,this.activeStore=null,this.disabled=!1,this.searchHandler=null,this.autocompleteHandler=null,this.isAutocompleteActive=!1,this.init()}init(){this.initModal(),this.scanExistingFields(),this.initGlobalListeners(),this.store.subscribe(this.handleStoreEvent.bind(this)),this.isInitializing=!1,this.batchFetchTaxonomies()}handleStoreEvent(e,t){switch(e){case"data-loaded":if(this.modal?.open&&this.handleTermsLoaded(t),this.isAutocompleteActive&&this.activeField){const e=this.fields.get(this.activeField),i=t.data?.items||[],s=t.filters?.search||"";this.showAutocompleteResults(e,i,s),this.isAutocompleteActive=!1}break;case"filters-changed":this.modal?.open&&this.showLoading();break;case"fetch-error":this.isAutocompleteActive&&this.activeField&&(this.showAutocompleteError(this.activeField),this.isAutocompleteActive=!1),this.handleFetchError(t.error)}}handleTermsLoaded(e){this.hideLoading();const t=e.data?.items||[],i=e.data?.pagination||{},s=e.filters?.search&&e.filters.search.length>0,o=e.filters?.page>1;0===t.length?(o||this.showEmptyState(s?"No results found.":"No items available."),this.observer.unobserve(this.ui.sentinel)):(this.renderTerms(t,o,s),this.currentTerms=t,i.has_more?this.observer.observe(this.ui.sentinel):this.observer.unobserve(this.ui.sentinel)),this.a11y?.announce(t.length,o)}handleFetchError(e){console.error("Taxonomy fetch error:",e),this.hideLoading(),this.error?.log?this.error.log(e,{component:"TaxonomySelector",action:"fetchTerms"},(()=>this.fetchCurrentTerms())):this.showEmptyState("Error loading terms. Please try again.")}updateFieldsForTaxonomy(e,t){this.fields.forEach((i=>{i.taxonomy===e&&i.selectedTerms.size>0&&i.selectedTerms.forEach((e=>{const s=t.find((t=>t.id===e));if(s){const t=i.selectedContainer.querySelector(`[data-id="${e}"]`);t&&(t.dataset.path=s.path,t.querySelector("span").textContent=s.path)}}))}))}scanExistingFields(e=null){e||(e=document.body);e.querySelectorAll(".field.taxonomy, .field.post").forEach((e=>{try{this.registerField(e)}catch(t){this.error.log(t,{component:"TaxonomySelector",action:"scanExistingFields",container:e.dataset.name})}}))}registerField(e,t={}){let i=e.querySelector("input[type=hidden]");if(!i)return;"fieldId"in e.dataset||(e.dataset.fieldId=this.createFieldId(e));let s=e.dataset.fieldId,o=e.querySelector("button.taxonomy-toggle"),n={id:s,input:i,container:e,taxonomy:o.dataset.taxonomy,name:e.dataset.field,maxSelection:parseInt(o.dataset.max)||0,canSearch:"search"in o.dataset,hasAutocomplete:"autocomplete"in o.dataset,autocompleteDropdown:e.querySelector(".autocomplete-dropdown")??!1,canCreate:"creatable"in o.dataset,isRequired:"required"in o.dataset,selectedTerms:new Set,toggle:o,selectedContainer:e.querySelector(".selected-items"),...t};!this.hasAutocomplete&&n.hasAutocomplete&&(this.hasAutocomplete=!0,this.initAutocomplete());const a=i.value.trim();if(""!==a){a.split(",").map((e=>parseInt(e.trim()))).filter((e=>!isNaN(e))).forEach((e=>n.selectedTerms.add(e)))}return this.fields.set(s,n),this.isInitializing?this.taxonomiesToFetch.add(n.taxonomy):this.store.setFilter("taxonomy",n.taxonomy),n.selectedTerms.size>0&&this.initFieldDisplay(s),s}async batchFetchTaxonomies(){if(0===this.taxonomiesToFetch.size)return;const e=Array.from(this.taxonomiesToFetch);this.taxonomiesToFetch.clear(),console.log(`Batch fetching ${e.length} unique taxonomies:`,e);for(const t of e)await this.store.setFilters({taxonomy:t,page:1,search:"",parent:0});this.fields.forEach(((e,t)=>{e.selectedTerms.size>0&&this.initFieldDisplay(t)}))}createFieldId(e){return this.index++,"selector-"+this.index}async initFieldDisplay(e){const t=this.fields.get(e);if(!t||0===t.selectedTerms.size)return;const i=Array.from(t.selectedTerms),s=[];i.forEach((e=>{const t=this.store.data.get(e);t&&s.push(t)})),s.forEach((t=>{this.addTermToDisplay(e,t.id,t.name,t.path)}))}initModal(){this.modalID="dialog#jvb-selector",this.modal=document.querySelector(this.modalID),this.modal?(this.initModalElements(),this.modalInstance=new window.jvbModal(this.modal,{handleForm:!1,save:null,open:null}),this.modalInstance.subscribe(((e,t)=>{switch(e){case"modal-open":console.log(t),this.openModal(t);break;case"modal-close":this.closeModal(t)}}))):console.warn("Taxonomy selector modal not found")}initModalElements(){this.selectors={search:{input:"[type=search]",clear:".clear-search",container:".search-wrapper"},termsList:".items-container",termsWrap:".items-wrap",breadcrumbs:{nav:"nav.term-navigation",back:".back-to-parent"},loading:{loading:".loading",text:".loading span"},selectedTerms:".selected-items",sentinel:".scroll-sentinel",modal:{title:"#modal-title",content:".modal-content"},create:{details:".create-new-term",parent:"#select_parent",summary:".create-new-term summary",name:"#term_name",button:".submit-term",label:{name:"[for=term_name]",parent:"[for=select_parent]"}},favouriteTerms:".favourite-terms"},this.ui=window.uiFromSelectors(this.selectors),this.observer=new IntersectionObserver((e=>{e.forEach((e=>{e.isIntersecting&&this.activeStore&&this.loadMoreTerms()}))}),{root:this.ui.termsWrap,threshold:.5})}initGlobalListeners(){document.addEventListener("click",this.handleClick.bind(this)),document.addEventListener("change",this.handleChange.bind(this)),this.hasAutocomplete&&this.initAutocomplete()}initAutocomplete(){console.log("Autocomplete init"),this.autocompleteHandler=window.debounce((e=>this.handleAutocomplete(e)),300),document.addEventListener("input",this.autocompleteHandler),document.addEventListener("blur",this.cleanupAutocomplete.bind(this))}handleClick(e){const t=window.targetCheck(e,".taxonomy-toggle");if(t)return e.preventDefault(),void this.handleToggleClick(t);const i=window.targetCheck(e,"button.remove-item");if(i&&e.target.closest(".jvb-selector")){const e=this.getFieldId(i),t=i.closest(".selected-item").dataset.id;this.removeSelectedTerm(e,t)}else e.target.matches(".modal-close")?this.modalInstance&&this.modalInstance.handleClose():this.modal&&this.modal.contains(e.target)&&this.handleModalClick(e)}handleChange(e){if(window.targetCheck(e,".taxonomy.field, .post.field")&&"hidden"===e.target.type){const t=this.getFieldId(e.target);this.updateFieldFromInput(t)}else this.modal&&this.modal.contains(e.target)&&this.handleModalChange(e)}handleToggleClick(e){try{const t=this.getFieldId(e);if(!this.fields.get(t))return void console.error("Field not found for toggle:",t);this.setActiveField(t),this.modalInstance.handleOpen()}catch(e){console.error("Error handling toggle click:",e),this.error?.handleError(e,{component:"TaxonomySelector",action:"handleToggleClick"})}}setActiveField(e){if(this.activeField=e,this.currentConfig=this.fields.get(e),this.currentSingular=jvbSettings.labels[this.currentConfig.taxonomy].single,this.currentPlural=jvbSettings.labels[this.currentConfig.taxonomy].plural,this.store.setFilter("taxonomy",this.currentConfig.taxonomy),this.selectedTerms.clear(),this.currentConfig.selectedTerms){let e=[];if(this.currentConfig.selectedTerms.forEach((t=>{const i=this.store.getItem(t);i?this.selectedTerms.set(t,{id:t,name:i.name,path:i.path}):e.push(t)})),e.length>0){this.fetchSpecificTerms(e).forEach((e=>{this.selectedTerms.set(e.id,{id:e.id,name:e.name,path:e.path})}))}}}fetchSpecificTerms(e){return[]}handleModalClick(e){if(window.targetCheck(e,".remove-item")){let t=window.targetCheck(e,".selected-item");t&&this.removeSelectedTermFromModal(t.dataset.id)}else if(window.targetCheck(e,".back-to-parent"))this.navigateToParent();else if(window.targetCheck(e,".toggle-children")){let t=e.target.closest("li");this.navigateToChild(parseInt(t.dataset.id),t.querySelector(".term-name").textContent)}else if(window.targetCheck(e,".path-level")){let t=window.targetCheck(e,".path-level");this.navigateToPath(t)}}handleModalChange(e){if(window.targetCheck(e,this.modalID)&&"checkbox"===e.target.type){e.preventDefault(),e.stopPropagation();const t=parseInt(e.target.closest("li").dataset.id),i=e.target.closest("li").querySelector("label");e.target.checked?this.addSelectedTermToModal(t,i.title,i.dataset.path):this.removeSelectedTermFromModal(t)}}openForFilter(e,t,i=[]){const s=`filter-${e}-${Date.now()}`;this.fields.set(s,{id:s,input:null,container:null,taxonomy:e,name:`filter_${e}`,maxSelection:0,canSearch:!0,hasAutocomplete:!1,autocompleteDropdown:document.querySelector(".autocomplete-dropdown")??!1,canCreate:!1,isRequired:!1,selectedTerms:new Set(i),toggle:null,selectedContainer:null,isFilterMode:!0,filterCallback:t}),this.setActiveField(s),this.modalInstance.handleOpen()}openModal(e){this.activeField=e.fieldId,this.currentConfig=e,e.canCreate&&"jvbTaxCreator"in window?this.creator=new window.jvbTaxCreator(this):this.creator&&delete this.creator,this.selectedTerms=new Set(e.selectedTerms);this.store.filters.taxonomy!==e.taxonomy&&this.store.setFilters({taxonomy:e.taxonomy,page:1,search:"",parent:0}),window.removeChildren(this.ui.termsList),this.ui.search.value="",this.updateSelectionCount(),this.modalInstance.open()}closeModal(){if(this.observer.unobserve(this.ui.sentinel),window.removeChildren(this.ui.termsList),this.currentConfig?.isFilterMode){if(this.currentConfig.filterCallback){const e=Array.from(this.selectedTerms.keys());this.currentConfig.filterCallback(e,this.currentConfig.taxonomy)}this.fields.delete(this.activeField)}else this.activeField&&this.saveSelectionsToField(this.activeField);this.currentConfig?.canSearch&&this.searchHandler&&this.ui.search.input.removeEventListener("input",this.searchHandler),this.creator&&delete this.creator,this.activeField=null,this.currentConfig=null}resetModalState(){this.disabled=!1,window.removeChildren(this.ui.termsList),window.removeChildren(this.ui.selectedTerms),this.ui.search.input.value="",window.removeChildren(this.ui.breadcrumbs.nav),this.ui.breadcrumbs.nav.appendChild(this.ui.breadcrumbs.back),this.ui.breadcrumbs.back.hidden=!0}updateModalForTaxonomy(){if(!this.currentConfig)return;this.ui.modal.title.textContent=`Select ${this.currentPlural}`,this.ui.search.container&&(this.ui.search.container.style.display=this.currentConfig.canSearch?"block":"none"),this.ui.create.details&&(this.ui.create.details.style.display=this.currentConfig.canCreate?"block":"none",this.ui.create.details.hidden=!this.currentConfig.canCreate,this.ui.create.summary&&(this.ui.create.summary.textContent=`Add new ${this.currentSingular}`),this.ui.create.label.name&&(this.ui.create.label.name.textContent=`Name this ${this.currentSingular}`),this.ui.create.label.parent&&(this.ui.create.label.parent.textContent="Nest it under"),this.ui.create.parent);const e=`Opened ${this.currentSingular} selection. Choose from checkboxes or search to filter results.`;this.a11y?.announce(e)}updateModalSelections(){window.removeChildren(this.ui.selectedTerms),this.selectedTerms.forEach(((e,t)=>{this.addTermToModalDisplay(t,e.name,e.path)})),this.checkSelectionLimits()}addSelectedTermToModal(e,t,i){this.selectedTerms.set(e,{id:e,name:t,path:i}),this.addTermToModalDisplay(e,t,i),this.checkSelectionLimits();const s=this.ui.termsList.querySelector(`input[value="${e}"]`);s&&(s.checked=!0)}removeSelectedTermFromModal(e){this.selectedTerms.delete(parseInt(e));const t=this.ui.selectedTerms.querySelector(`[data-id="${e}"]`);t&&t.remove();const i=this.ui.termsList.querySelector(`input[value="${e}"]`);i&&(i.checked=!1),this.checkSelectionLimits()}addTermToModalDisplay(e,t,i){const s=window.getTemplate("selectedTerm").cloneNode(!0);s.dataset.id=e,s.dataset.path=i,s.dataset.name=t,s.dataset.taxonomy=this.currentConfig.taxonomy,s.querySelector("span").textContent=i,s.querySelector("button").title=`Remove ${t}`,this.ui.selectedTerms.appendChild(s)}checkSelectionLimits(){this.currentConfig&&0!==this.currentConfig.maxSelection&&(this.disabled=this.selectedTerms.size>=this.currentConfig.maxSelection,this.setCheckboxes(this.disabled))}setCheckboxes(e){this.ui.termsList.querySelectorAll('input[type="checkbox"]').forEach((t=>{t.checked||(t.disabled=e)}))}saveSelectionsToField(e){const t=this.fields.get(e);if(!t)return;t.selectedTerms.clear(),window.removeChildren(t.selectedContainer),this.selectedTerms.forEach(((i,s)=>{t.selectedTerms.add(s),this.addTermToDisplay(e,s,i.name,i.path)}));const i=Array.from(t.selectedTerms);t.input.value=i.join(","),t.input.dispatchEvent(new Event("change",{bubbles:!0}))}removeSelectedTerm(e,t){const i=this.fields.get(e);if(!i)return;const s=parseInt(t);i.selectedTerms.delete(s);const o=i.selectedContainer.querySelector(`[data-id="${s}"]`);o&&o.remove();const n=Array.from(i.selectedTerms);i.input.value=n.join(","),i.input.dispatchEvent(new Event("change",{bubbles:!0}))}addTermToDisplay(e,t,i,s){const o=this.fields.get(e);if(!o||o.selectedContainer.querySelector(`[data-id="${t}"]`))return;const n=window.getTemplate("selectedTerm").cloneNode(!0);n.dataset.id=t,n.dataset.path=s,n.dataset.name=i,n.dataset.taxonomy=o.taxonomy,n.querySelector("span").textContent=s,n.querySelector("button").title=`Remove ${i}`,o.selectedContainer.appendChild(n)}updateFieldFromInput(e){const t=this.fields.get(e);if(!t)return;const i=t.input.value.trim();if(t.selectedTerms.clear(),window.removeChildren(t.selectedContainer),""!==i){i.split(",").map((e=>parseInt(e.trim()))).filter((e=>!isNaN(e))).forEach((e=>t.selectedTerms.add(e))),this.initFieldDisplay(e)}}handleSearch(e){const t=e.target.value.trim();this.searchHandler&&clearTimeout(this.searchHandler),this.searchHandler=setTimeout((()=>{this.store.setFilters({search:t,page:1,parent:t?0:this.store.filters.parent||0}),window.removeChildren(this.ui.termsList)}),300)}async handleAutocomplete(e){if(!("autocomplete"in e.target.dataset))return;const t=this.getFieldId(e.target),i=this.fields.get(t);if(!i)return;const s=e.target.value.trim();if(s.length<2)return i.autocompleteDropdown&&(i.autocompleteDropdown.hidden=!0),void(this.isAutocompleteActive=!1);this.activeField=t,this.currentConfig=i,i.canCreate&&!this.creator&&(this.creator=new window.jvbTaxCreator(this)),this.isAutocompleteActive=!0,i.autocompleteDropdown&&(i.autocompleteDropdown.hidden=!1),this.store.setFilters({taxonomy:i.taxonomy,search:s,page:1})}cleanupAutocomplete(e){if(!("autocomplete"in e.target.dataset))return;const t=this.getFieldId(e.target);this.fields.get(t)&&this.creator&&delete this.creator}showAutocompleteError(e){const t=this.fields.get(e);if(!t)return;t.config.autocompleteDropdown||(t.config.autocompleteDropdown=t.element.querySelector(".autocomplete-dropdown"));const i=t.config.autocompleteDropdown;i&&(window.removeChildren(i),this.showEmptyState("Hmmm... something went wrong",i))}showAutocompleteResults(e,t,i){if(!e||!e.autocompleteDropdown)return;const s=e.autocompleteDropdown;if(window.removeChildren(s),0===t.length?this.showEmptyState("No items found.",s):t.forEach((t=>{const i=this.createAutocompleteTermElement(e,t);i&&s.appendChild(i)})),this.creator){const t=this.creator.createAutocompleteOption(i,e);s.appendChild(t)}s.hidden=!1}createAutocompleteTermElement(e,t){const i=document.createElement("button");return i.type="button",i.className="autocomplete-item",i.dataset.id=t.id,i.dataset.name=t.name,i.dataset.path=t.path||t.name,i.textContent=t.path||t.name,i.addEventListener("click",(()=>{e.selectedTerms.add(parseInt(t.id)),this.addTermToDisplay(e.id,t.id,t.name,t.path),e.input.value=Array.from(e.selectedTerms).join(","),e.input.dispatchEvent(new Event("change",{bubbles:!0})),e.autocompleteDropdown.hidden=!0;const i=e.container.querySelector("input[data-autocomplete]");i&&(i.value="")})),i}navigateToParent(){this.store.setFilters({parent:0,page:1}),window.removeChildren(this.ui.termsList),this.ui.breadcrumbs.back.hidden=!0}navigateToChild(e,t){this.store.setFilters({parent:e,page:1}),window.removeChildren(this.ui.termsList),this.updateBreadcrumbs(e,t),this.ui.breadcrumbs.back.hidden=!1}navigateToPath(e){const t=parseInt(e.dataset.id)||0;this.store.setFilters({parent:t,page:1}),window.removeChildren(this.ui.termsList),this.ui.breadcrumbs.back.hidden=0===t}loadMoreTerms(){if(!this.activeStore)return;const e=this.activeStore.filters.page||1;this.store.setFilter("page",e+1)}renderTerms(e,t=!1,i=!1){if(t||window.removeChildren(this.ui.termsList),0===e.length)return void(t||this.showEmptyState());const s=this.store.filters.parent||0;this.ui.breadcrumbs.back.hidden=0===s,e.forEach((e=>{const t=this.createTermElement({id:parseInt(e.id),name:e.name,hasChildren:e.hasChildren,path:e.path||null,show:i});t&&this.ui.termsList.appendChild(t)}))}createTermElement(e){if(!e||!e.name)return null;const t=window.getTemplate("termListItem").cloneNode(!0);t.dataset.id=e.id;const i=this.selectedTerms.has(e.id),s=t.querySelector("input"),o=t.querySelector("label"),n=t.querySelector("span, .term-name");if(s&&o&&n&&(s.id=`${this.currentConfig.container.id}${e.id}`,s.name=`${this.currentConfig.container.id}${this.currentConfig.taxonomy}-select`,s.value=e.id,s.disabled=!i&&this.disabled,s.checked=i,o.htmlFor=s.id,o.title=e.path||e.name,o.dataset.path=e.path,n.textContent=e.show?e.path:e.name),e.hasChildren){const i=window.getTemplate?window.getTemplate("termChildrenToggle"):this.createChildrenToggle();i&&(i.ariaLabel=`View sub-terms of ${e.name}`,t.appendChild(i))}return t}createChildrenToggle(){const e=document.createElement("button");return e.type="button",e.className="toggle-children",e.innerHTML="→",e}updateBreadcrumbs(e,t){const i=window.getTemplate("termBreadcrumb").cloneNode(!0);i.dataset.id=e,i.textContent=t,i.title=t;const s=this.ui.breadcrumbs.nav.querySelector(`[data-id="${e}"]`);if(s)for(;s.nextElementSibling;)s.nextElementSibling.remove();else this.ui.breadcrumbs.nav.appendChild(i)}showLoading(){this.ui.loading.loading.hidden=!1,this.modal.classList.add("loading");const e=this.store?.filters?.search||"",t=this.store?.filters?.parent||0;let i=""!==e?`searching for "${e}" items`:0===t?"loading items":"loading child items";window.typeLoop?this.stopTyping=window.typeLoop(this.ui.loading.text,i):this.ui.loading.text.textContent=i}hideLoading(){this.ui.loading.loading.hidden=!0,this.modal.classList.remove("loading"),this.stopTyping&&this.stopTyping()}showEmptyState(e="No items found.",t=null){t||(t=this.ui.termsList);const i=window.getTemplate("noResults").cloneNode(!0);e&&i.querySelector("span")&&(i.querySelector("span").textContent=e),t.appendChild(i)}getFieldId(e){if(e.dataset.fieldId)return e.dataset.fieldId;const t=e.closest("[data-field-id]");return t?t.dataset.fieldId:null}destroy(){document.removeEventListener("click",this.handleClick),document.removeEventListener("change",this.handleChange),this.observer?.disconnect(),this.store.destroy(),this.fields.clear(),this.selectedTerms.clear()}}document.addEventListener("DOMContentLoaded",(function(){window.jvbSelector=new e}))})();
(()=>{class e{constructor(){this.a11y=window.jvbA11y,this.error=window.jvbError,this.index=-1,this.hasAutocomplete=!1,this.isInitializing=!0,this.taxonomiesToFetch=new Set,this.store=new window.jvbStore({name:"taxonomies",storeName:"terms",keyPath:"id",showLoading:!1,indexes:[{name:"taxonomy",keyPath:"taxonomy"},{name:"parent",keyPath:"parent"},{name:"slug",keyPath:"slug",unique:!0},{name:"count",keyPath:"count"}],endpoint:"terms",TTL:72e5,filters:{taxonomy:"",page:1,search:"",parent:0},required:"taxonomy"}),this.fields=new Map,this.selectedTerms=new Map,this.activeField=null,this.currentConfig=null,this.currentSingular=null,this.currentPlural=null,this.activeStore=null,this.disabled=!1,this.searchHandler=null,this.autocompleteHandler=null,this.isAutocompleteActive=!1,this.init()}init(){this.initModal(),this.scanExistingFields(),this.initGlobalListeners(),this.store.subscribe(this.handleStoreEvent.bind(this)),this.isInitializing=!1,this.batchFetchTaxonomies()}handleStoreEvent(e,t){switch(e){case"data-loaded":if(this.modal?.open&&this.handleTermsLoaded(t),this.isAutocompleteActive&&this.activeField){const e=this.fields.get(this.activeField),i=t.data?.items||[],s=t.filters?.search||"";this.showAutocompleteResults(e,i,s),this.isAutocompleteActive=!1}break;case"filters-changed":this.modal?.open&&this.showLoading();break;case"fetch-error":this.isAutocompleteActive&&this.activeField&&(this.showAutocompleteError(this.activeField),this.isAutocompleteActive=!1),this.handleFetchError(t.error)}}handleTermsLoaded(e){this.hideLoading();const t=e.data?.items||[],i=e.data?.pagination||{},s=e.filters?.search&&e.filters.search.length>0,o=e.filters?.page>1;0===t.length?(o||this.showEmptyState(s?"No results found.":"No items available."),this.observer.unobserve(this.ui.sentinel)):(this.renderTerms(t,o,s),this.currentTerms=t,i.has_more?this.observer.observe(this.ui.sentinel):this.observer.unobserve(this.ui.sentinel)),this.a11y?.announce(t.length,o)}handleFetchError(e){console.error("Taxonomy fetch error:",e),this.hideLoading(),this.error?.log?this.error.log(e,{component:"TaxonomySelector",action:"fetchTerms"},(()=>this.fetchCurrentTerms())):this.showEmptyState("Error loading terms. Please try again.")}updateFieldsForTaxonomy(e,t){this.fields.forEach((i=>{i.taxonomy===e&&i.selectedTerms.size>0&&i.selectedTerms.forEach((e=>{const s=t.find((t=>t.id===e));if(s){const t=i.selectedContainer.querySelector(`[data-id="${e}"]`);t&&(t.dataset.path=s.path,t.querySelector("span").textContent=s.path)}}))}))}scanExistingFields(e=null){e||(e=document.body);e.querySelectorAll(".field.taxonomy, .field.post").forEach((e=>{try{this.registerField(e)}catch(t){this.error.log(t,{component:"TaxonomySelector",action:"scanExistingFields",container:e.dataset.name})}}))}registerField(e,t={}){let i=e.querySelector("input[type=hidden]");if(!i)return;"fieldId"in e.dataset||(e.dataset.fieldId=this.createFieldId(e));let s=e.dataset.fieldId,o=e.querySelector("button.taxonomy-toggle"),n={id:s,input:i,container:e,taxonomy:o.dataset.taxonomy,name:e.dataset.field,maxSelection:parseInt(o.dataset.max)||0,canSearch:"search"in o.dataset,hasAutocomplete:"autocomplete"in o.dataset,autocompleteDropdown:e.querySelector(".autocomplete-dropdown")??!1,canCreate:"creatable"in o.dataset,isRequired:"required"in o.dataset,selectedTerms:new Set,toggle:o,selectedContainer:e.querySelector(".selected-items"),...t};!this.hasAutocomplete&&n.hasAutocomplete&&(this.hasAutocomplete=!0,this.initAutocomplete());const a=i.value.trim();if(""!==a){a.split(",").map((e=>parseInt(e.trim()))).filter((e=>!isNaN(e))).forEach((e=>n.selectedTerms.add(e)))}return this.fields.set(s,n),this.isInitializing?this.taxonomiesToFetch.add(n.taxonomy):this.store.setFilter("taxonomy",n.taxonomy),n.selectedTerms.size>0&&this.initFieldDisplay(s),s}async batchFetchTaxonomies(){if(0===this.taxonomiesToFetch.size)return;const e=Array.from(this.taxonomiesToFetch);this.taxonomiesToFetch.clear(),console.log(`Batch fetching ${e.length} unique taxonomies:`,e);for(const t of e)await this.store.setFilters({taxonomy:t,page:1,search:"",parent:0});this.fields.forEach(((e,t)=>{e.selectedTerms.size>0&&this.initFieldDisplay(t)}))}createFieldId(e){return this.index++,"selector-"+this.index}async initFieldDisplay(e){const t=this.fields.get(e);if(!t||0===t.selectedTerms.size)return;const i=Array.from(t.selectedTerms),s=[];i.forEach((e=>{const t=this.store.data.get(e);t&&s.push(t)})),s.forEach((t=>{this.addTermToDisplay(e,t.id,t.name,t.path)}))}initModal(){this.modalID="dialog#jvb-selector",this.modal=document.querySelector(this.modalID),this.modal?(this.initModalElements(),this.modalInstance=new window.jvbModal(this.modal,{handleForm:!1,save:null,open:null}),this.modalInstance.subscribe(((e,t)=>{switch(e){case"modal-open":console.log(t),this.openModal(t);break;case"modal-close":this.closeModal(t)}}))):console.warn("Taxonomy selector modal not found")}initModalElements(){this.selectors={search:{input:"[type=search]",clear:".clear-search",container:".search-wrapper"},termsList:".items-container",termsWrap:".items-wrap",breadcrumbs:{nav:"nav.term-navigation",back:".back-to-parent"},loading:{loading:".loading",text:".loading span"},selectedTerms:".selected-items",sentinel:".scroll-sentinel",modal:{title:"#modal-title",content:".modal-content"},create:{details:".create-new-term",parent:"#select_parent",summary:".create-new-term summary",name:"#term_name",button:".submit-term",label:{name:"[for=term_name]",parent:"[for=select_parent]"}},favouriteTerms:".favourite-terms"},this.ui=window.uiFromSelectors(this.selectors),this.observer=new IntersectionObserver((e=>{e.forEach((e=>{e.isIntersecting&&this.activeStore&&this.loadMoreTerms()}))}),{root:this.ui.termsWrap,threshold:.5})}initGlobalListeners(){document.addEventListener("click",this.handleClick.bind(this)),document.addEventListener("change",this.handleChange.bind(this)),this.hasAutocomplete&&this.initAutocomplete()}initAutocomplete(){console.log("Autocomplete init"),this.autocompleteHandler=window.debounce((e=>this.handleAutocomplete(e)),300),document.addEventListener("input",this.autocompleteHandler),document.addEventListener("blur",this.cleanupAutocomplete.bind(this))}handleClick(e){const t=window.targetCheck(e,".taxonomy-toggle");if(t)return e.preventDefault(),void this.handleToggleClick(t);const i=window.targetCheck(e,"button.remove-item");if(i&&e.target.closest(".jvb-selector")){const e=this.getFieldId(i),t=i.closest(".selected-item").dataset.id;this.removeSelectedTerm(e,t)}else e.target.matches(".modal-close")?this.modalInstance&&this.modalInstance.handleClose():this.modal&&this.modal.contains(e.target)&&this.handleModalClick(e)}handleChange(e){if(window.targetCheck(e,".taxonomy.field, .post.field")&&"hidden"===e.target.type){const t=this.getFieldId(e.target);this.updateFieldFromInput(t)}else this.modal&&this.modal.contains(e.target)&&this.handleModalChange(e)}handleToggleClick(e){try{const t=this.getFieldId(e);if(!this.fields.get(t))return void console.error("Field not found for toggle:",t);this.setActiveField(t),this.modalInstance.handleOpen()}catch(e){console.error("Error handling toggle click:",e),this.error?.handleError(e,{component:"TaxonomySelector",action:"handleToggleClick"})}}setActiveField(e){if(this.activeField=e,this.currentConfig=this.fields.get(e),this.currentSingular=jvbSettings.labels[this.currentConfig.taxonomy].single,this.currentPlural=jvbSettings.labels[this.currentConfig.taxonomy].plural,this.store.setFilter("taxonomy",this.currentConfig.taxonomy),this.selectedTerms.clear(),this.currentConfig.selectedTerms){let e=[];if(this.currentConfig.selectedTerms.forEach((t=>{const i=this.store.getItem(t);i?this.selectedTerms.set(t,{id:t,name:i.name,path:i.path}):e.push(t)})),e.length>0){this.fetchSpecificTerms(e).forEach((e=>{this.selectedTerms.set(e.id,{id:e.id,name:e.name,path:e.path})}))}}}fetchSpecificTerms(e){return[]}handleModalClick(e){if(window.targetCheck(e,".remove-item")){let t=window.targetCheck(e,".selected-item");t&&this.removeSelectedTermFromModal(t.dataset.id)}else if(window.targetCheck(e,".back-to-parent"))this.navigateToParent();else if(window.targetCheck(e,".toggle-children")){let t=e.target.closest("li");this.navigateToChild(parseInt(t.dataset.id),t.querySelector(".term-name").textContent)}else if(window.targetCheck(e,".path-level")){let t=window.targetCheck(e,".path-level");this.navigateToPath(t)}}handleModalChange(e){if(window.targetCheck(e,this.modalID)&&"checkbox"===e.target.type){e.preventDefault(),e.stopPropagation();const t=parseInt(e.target.closest("li").dataset.id),i=e.target.closest("li").querySelector("label");e.target.checked?this.addSelectedTermToModal(t,i.title,i.dataset.path):this.removeSelectedTermFromModal(t)}}openForFilter(e,t,i=[]){const s=`filter-${e}-${Date.now()}`;this.fields.set(s,{id:s,input:null,container:null,taxonomy:e,name:`filter_${e}`,maxSelection:0,canSearch:!0,hasAutocomplete:!1,autocompleteDropdown:document.querySelector(".autocomplete-dropdown")??!1,canCreate:!1,isRequired:!1,selectedTerms:new Set(i),toggle:null,selectedContainer:null,isFilterMode:!0,filterCallback:t}),this.setActiveField(s),this.modalInstance.handleOpen()}openModal(e){this.activeField=e.fieldId,this.currentConfig=e,e.canCreate&&"jvbTaxCreator"in window?this.creator=new window.jvbTaxCreator(this):this.creator&&delete this.creator,this.selectedTerms=new Set(e.selectedTerms);this.store.filters.taxonomy!==e.taxonomy&&this.store.setFilters({taxonomy:e.taxonomy,page:1,search:"",parent:0}),window.removeChildren(this.ui.termsList),this.ui.search.value="",this.updateSelectionCount(),this.modalInstance.open()}closeModal(){if(this.observer.unobserve(this.ui.sentinel),window.removeChildren(this.ui.termsList),this.currentConfig?.isFilterMode){if(this.currentConfig.filterCallback){const e=Array.from(this.selectedTerms.keys());this.currentConfig.filterCallback(e,this.currentConfig.taxonomy)}this.fields.delete(this.activeField)}else this.activeField&&this.saveSelectionsToField(this.activeField);this.currentConfig?.canSearch&&this.searchHandler&&this.ui.search.input.removeEventListener("input",this.searchHandler),this.creator&&delete this.creator,this.activeField=null,this.currentConfig=null}resetModalState(){this.disabled=!1,window.removeChildren(this.ui.termsList),window.removeChildren(this.ui.selectedTerms),this.ui.search.input.value="",window.removeChildren(this.ui.breadcrumbs.nav),this.ui.breadcrumbs.nav.appendChild(this.ui.breadcrumbs.back),this.ui.breadcrumbs.back.hidden=!0}updateModalForTaxonomy(){if(!this.currentConfig)return;this.ui.modal.title.textContent=`Select ${this.currentPlural}`,this.ui.search.container&&(this.ui.search.container.style.display=this.currentConfig.canSearch?"block":"none"),this.ui.create.details&&(this.ui.create.details.style.display=this.currentConfig.canCreate?"block":"none",this.ui.create.details.hidden=!this.currentConfig.canCreate,this.ui.create.summary&&(this.ui.create.summary.textContent=`Add new ${this.currentSingular}`),this.ui.create.label.name&&(this.ui.create.label.name.textContent=`Name this ${this.currentSingular}`),this.ui.create.label.parent&&(this.ui.create.label.parent.textContent="Nest it under"),this.ui.create.parent);const e=`Opened ${this.currentSingular} selection. Choose from checkboxes or search to filter results.`;this.a11y?.announce(e)}updateModalSelections(){window.removeChildren(this.ui.selectedTerms),this.selectedTerms.forEach(((e,t)=>{this.addTermToModalDisplay(t,e.name,e.path)})),this.checkSelectionLimits()}addSelectedTermToModal(e,t,i){this.selectedTerms.set(e,{id:e,name:t,path:i}),this.addTermToModalDisplay(e,t,i),this.checkSelectionLimits();const s=this.ui.termsList.querySelector(`input[value="${e}"]`);s&&(s.checked=!0)}removeSelectedTermFromModal(e){this.selectedTerms.delete(parseInt(e));const t=this.ui.selectedTerms.querySelector(`[data-id="${e}"]`);t&&t.remove();const i=this.ui.termsList.querySelector(`input[value="${e}"]`);i&&(i.checked=!1),this.checkSelectionLimits()}addTermToModalDisplay(e,t,i){const s=window.getTemplate("selectedTerm").cloneNode(!0);s.dataset.id=e,s.dataset.path=i,s.dataset.name=t,s.dataset.taxonomy=this.currentConfig.taxonomy,s.querySelector("span").textContent=i,s.querySelector("button").title=`Remove ${t}`,this.ui.selectedTerms.appendChild(s)}checkSelectionLimits(){this.currentConfig&&0!==this.currentConfig.maxSelection&&(this.disabled=this.selectedTerms.size>=this.currentConfig.maxSelection,this.setCheckboxes(this.disabled))}setCheckboxes(e){this.ui.termsList.querySelectorAll('input[type="checkbox"]').forEach((t=>{t.checked||(t.disabled=e)}))}saveSelectionsToField(e){const t=this.fields.get(e);if(!t)return;t.selectedTerms.clear(),window.removeChildren(t.selectedContainer),this.selectedTerms.forEach(((i,s)=>{t.selectedTerms.add(s),this.addTermToDisplay(e,s,i.name,i.path)}));const i=Array.from(t.selectedTerms);t.input.value=i.join(","),t.input.dispatchEvent(new Event("change",{bubbles:!0}))}removeSelectedTerm(e,t){const i=this.fields.get(e);if(!i)return;const s=parseInt(t);i.selectedTerms.delete(s);const o=i.selectedContainer.querySelector(`[data-id="${s}"]`);o&&o.remove();const n=Array.from(i.selectedTerms);i.input.value=n.join(","),i.input.dispatchEvent(new Event("change",{bubbles:!0}))}addTermToDisplay(e,t,i,s){const o=this.fields.get(e);if(!o||o.selectedContainer.querySelector(`[data-id="${t}"]`))return;const n=window.getTemplate("selectedTerm").cloneNode(!0);n.dataset.id=t,n.dataset.path=s,n.dataset.name=i,n.dataset.taxonomy=o.taxonomy,n.querySelector("span").textContent=s,n.querySelector("button").title=`Remove ${i}`,o.selectedContainer.appendChild(n)}updateFieldFromInput(e){const t=this.fields.get(e);if(!t)return;const i=t.input.value.trim();if(t.selectedTerms.clear(),window.removeChildren(t.selectedContainer),""!==i){i.split(",").map((e=>parseInt(e.trim()))).filter((e=>!isNaN(e))).forEach((e=>t.selectedTerms.add(e))),this.initFieldDisplay(e)}}handleSearch(e){const t=e.target.value.trim();this.searchHandler&&clearTimeout(this.searchHandler),this.searchHandler=setTimeout((()=>{this.store.setFilters({search:t,page:1,parent:t?0:this.store.filters.parent||0}),window.removeChildren(this.ui.termsList)}),300)}async handleAutocomplete(e){if(!("autocomplete"in e.target.dataset))return;const t=this.getFieldId(e.target),i=this.fields.get(t);if(!i)return;const s=e.target.value.trim();if(i.currentAutocompleteQuery=s,s.length<2)return i.autocompleteDropdown&&(i.autocompleteDropdown.hidden=!0),void(this.isAutocompleteActive=!1);this.activeField=t,this.isAutocompleteActive=!0,i.autocompleteDropdown&&(i.autocompleteDropdown.hidden=!1),this.store.setFilters({taxonomy:i.taxonomy,search:s,page:1})}cleanupAutocomplete(e){if(!("autocomplete"in e.target.dataset))return;const t=this.getFieldId(e.target);this.fields.get(t)&&this.creator&&delete this.creator}showAutocompleteError(e){const t=this.fields.get(e);if(!t)return;t.config.autocompleteDropdown||(t.config.autocompleteDropdown=t.element.querySelector(".autocomplete-dropdown"));const i=t.config.autocompleteDropdown;i&&(window.removeChildren(i),this.showEmptyState("Hmmm... something went wrong",i))}showAutocompleteResults(e,t,i){if(!e||!e.autocompleteDropdown)return;const s=e.autocompleteDropdown;window.removeChildren(s),0===t.length?this.showEmptyState("No items found.",s):t.forEach((t=>{const i=this.createAutocompleteTermElement(e,t);i&&s.appendChild(i)}));const o=e.currentAutocompleteQuery||i;if(e.canCreate&&o&&window.jvbTaxCreator){const e=this.createNewTermOption(o);s.appendChild(e)}s.hidden=!1}createNewTermOption(e){const t=document.createElement("button");return t.type="button",t.className="autocomplete-item create-term",t.dataset.query=e,t.innerHTML=`<strong>Create:</strong> "${e}"`,t}createAutocompleteTermElement(e,t){const i=document.createElement("button");return i.type="button",i.className="autocomplete-item",i.dataset.id=t.id,i.dataset.name=t.name,i.dataset.path=t.path||t.name,i.textContent=t.path||t.name,i.addEventListener("click",(()=>{e.selectedTerms.add(parseInt(t.id)),this.addTermToDisplay(e.id,t.id,t.name,t.path),e.input.value=Array.from(e.selectedTerms).join(","),e.input.dispatchEvent(new Event("change",{bubbles:!0})),e.autocompleteDropdown.hidden=!0;const i=e.container.querySelector("input[data-autocomplete]");i&&(i.value="")})),i}navigateToParent(){this.store.setFilters({parent:0,page:1}),window.removeChildren(this.ui.termsList),this.ui.breadcrumbs.back.hidden=!0}navigateToChild(e,t){this.store.setFilters({parent:e,page:1}),window.removeChildren(this.ui.termsList),this.updateBreadcrumbs(e,t),this.ui.breadcrumbs.back.hidden=!1}navigateToPath(e){const t=parseInt(e.dataset.id)||0;this.store.setFilters({parent:t,page:1}),window.removeChildren(this.ui.termsList),this.ui.breadcrumbs.back.hidden=0===t}loadMoreTerms(){if(!this.activeStore)return;const e=this.activeStore.filters.page||1;this.store.setFilter("page",e+1)}renderTerms(e,t=!1,i=!1){if(t||window.removeChildren(this.ui.termsList),0===e.length)return void(t||this.showEmptyState());const s=this.store.filters.parent||0;this.ui.breadcrumbs.back.hidden=0===s,e.forEach((e=>{const t=this.createTermElement({id:parseInt(e.id),name:e.name,hasChildren:e.hasChildren,path:e.path||null,show:i});t&&this.ui.termsList.appendChild(t)}))}createTermElement(e){if(!e||!e.name)return null;const t=window.getTemplate("termListItem").cloneNode(!0);t.dataset.id=e.id;const i=this.selectedTerms.has(e.id),s=t.querySelector("input"),o=t.querySelector("label"),n=t.querySelector("span, .term-name");if(s&&o&&n&&(s.id=`${this.currentConfig.container.id}${e.id}`,s.name=`${this.currentConfig.container.id}${this.currentConfig.taxonomy}-select`,s.value=e.id,s.disabled=!i&&this.disabled,s.checked=i,o.htmlFor=s.id,o.title=e.path||e.name,o.dataset.path=e.path,n.textContent=e.show?e.path:e.name),e.hasChildren){const i=window.getTemplate?window.getTemplate("termChildrenToggle"):this.createChildrenToggle();i&&(i.ariaLabel=`View sub-terms of ${e.name}`,t.appendChild(i))}return t}createChildrenToggle(){const e=document.createElement("button");return e.type="button",e.className="toggle-children",e.innerHTML="→",e}updateBreadcrumbs(e,t){const i=window.getTemplate("termBreadcrumb").cloneNode(!0);i.dataset.id=e,i.textContent=t,i.title=t;const s=this.ui.breadcrumbs.nav.querySelector(`[data-id="${e}"]`);if(s)for(;s.nextElementSibling;)s.nextElementSibling.remove();else this.ui.breadcrumbs.nav.appendChild(i)}showLoading(){this.ui.loading.loading.hidden=!1,this.modal.classList.add("loading");const e=this.store?.filters?.search||"",t=this.store?.filters?.parent||0;let i=""!==e?`searching for "${e}" items`:0===t?"loading items":"loading child items";window.typeLoop?this.stopTyping=window.typeLoop(this.ui.loading.text,i):this.ui.loading.text.textContent=i}hideLoading(){this.ui.loading.loading.hidden=!0,this.modal.classList.remove("loading"),this.stopTyping&&this.stopTyping()}showEmptyState(e="No items found.",t=null){t||(t=this.ui.termsList);const i=window.getTemplate("noResults").cloneNode(!0);e&&i.querySelector("span")&&(i.querySelector("span").textContent=e),t.appendChild(i)}getFieldId(e){if(e.dataset.fieldId)return e.dataset.fieldId;const t=e.closest("[data-field-id]");return t?t.dataset.fieldId:null}destroy(){document.removeEventListener("click",this.handleClick),document.removeEventListener("change",this.handleChange),this.observer?.disconnect(),this.store.destroy(),this.fields.clear(),this.selectedTerms.clear()}}document.addEventListener("DOMContentLoaded",(function(){window.jvbSelector=new e}))})();
assets/js/min/uploader.min.js
@@ -1 +1 @@
(()=>{class e{constructor(){this.queue=window.jvbQueue,this.a11y=window.jvbA11y,this.error=window.jvbError,this.fieldStore=new window.jvbStore({name:"upload_fields",storeName:"fieldStates",keyPath:"id",version:2,indexes:[{name:"fieldId",keyPath:"fieldId"},{name:"timestamp",keyPath:"timestamp"},{name:"content",keyPath:"content"},{name:"itemId",keyPath:"itemId"},{name:"status",keyPath:"status"}],stripDOMReferences:!0,TTL:6048e5}),this.uploadStore=new window.jvbStore({name:"uploads",storeName:"uploads",keyPath:"id",storeBlobs:!0,indexes:[{name:"fieldId",keyPath:"fieldId"},{name:"status",keyPath:"status"},{name:"groupId",keyPath:"groupId"},{name:"attachmentId",keyPath:"attachmentId"}]}),window.jvbUploadBlobs=this.uploadStore,this.fieldStore.subscribe(this.handleFieldStoreEvent.bind(this)),this.uploadStore.subscribe(this.handleUploadStoreEvent.bind(this)),this.initWorker(),this.fields=new Map,this.uploads=new Map,this.groups=new Map,this.selected=new Map,this.selectionHandlers=new Map,this.previewUrls=new Set,this.subscribers=new Set,this.dragController=null,this.selectors={field:{field:"[data-upload-field]",input:'input[type="file"]',hiddenValue:'input[type="hidden"]',dropZone:".file-upload-container",preview:".item-grid.preview",progress:".image-progress"},groups:{container:".upload-group",grid:".item-grid.group",header:".group-header",selectAll:'[name="select-all-group"]',actions:".group-actions",count:".selection-controls .info"},items:{item:"[data-upload-id]",checkbox:'[name*="select-item"]',featured:'[name="featured"]',details:"details"}},this.statusMapping={received:"Image Received",local_processing:"Processing Image...",queued:"Waiting to upload...",uploading:"Uploading to Server",pending:"Successfully sent to server. In line for further processing.",processing:"Processing on server...",completed:"Upload complete!",failed:"Upload failed (will retry)",failed_permanent:"Upload failed permanently"},this.init()}async init(){await this.loadFields(),await this.loadUploads(),this.initializeFields(),this.initListeners(),this.queue.subscribe(((e,t)=>{if("uploads"!==t.endpoint&&"uploads/meta"!==t.endpoint)return;const s=t.data instanceof FormData?t.data.get("fieldId"):t.data.fieldId;switch(e){case"cancel-operation":s&&this.clearField(s);break;case"operation-status":s&&this.updateFieldStatus(s,t.status);break;case"operation-complete":(t.result?.data||[]).forEach((e=>{const t=this.uploads.get(e.upload_id);t&&(t.attachmentId=e.attachment_id,t.status="completed",this.uploads.set(t.id,t))})),s&&this.cleanField(s)}})),window.addEventListener("beforeunload",(()=>{this.cleanupAllPreviewUrls()}))}initWorker(){this.worker={worker:null,timeout:null,tasks:new Map,restart:{count:0,max:3},settings:{timeout:1e4,batchSize:1,maxConcurrent:3,restartAfterTimeout:!0}}}initializeFields(){document.querySelectorAll(this.selectors.field.field).forEach((e=>{this.registerUploader(e)}))}scanFields(e){e.querySelectorAll(this.selectors.field.field).forEach((e=>{this.registerUploader(e)}))}registerUploader(e){const t=this.determineFieldId(e),s=this.extractFieldConfig(e),r={id:t,config:s,element:e,ui:this.buildFieldUI(e),uploads:new Set,groups:new Set,state:"ready"};return this.fields.set(t,r),e.dataset.uploader=t,this.addFieldSelectionHandler(t),"post_group"!==s.destination||this.dragController||this.initGroupFeatures(),t}extractFieldConfig(e){return{destination:e.dataset.destination||"meta",content:e.dataset.content||null,mode:e.dataset.mode||"direct",type:e.dataset.type||"single",name:e.dataset.field,itemID:e.dataset.itemId||0,maxFiles:parseInt(e.dataset.maxFiles)||999,subtype:e.dataset.subtype||"image"}}buildFieldUI(e){let t={field:e,input:e.querySelector(this.selectors.field.input),dropZone:e.querySelector(this.selectors.field.dropZone),preview:e.querySelector(this.selectors.field.preview),progress:{progress:e.querySelector(this.selectors.field.progress),bar:e.querySelector(".bar"),fill:e.querySelector(".fill"),details:e.querySelector(".details"),text:e.querySelector(".details .text"),count:e.querySelector(".details .count")}},s=e.querySelector(".group-display");return s&&(t.groups={display:s,container:e.querySelector(".item-grid.groups"),empty:e.querySelector(".empty-group"),groups:new Map}),t}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),this.dragEnterHandler=this.handleExternalDragEnter.bind(this),this.dragLeaveHandler=this.handleExternalDragLeave.bind(this),this.dragOverHandler=this.handleExternalDragOver.bind(this),this.dropHandler=this.handleExternalDrop.bind(this),document.addEventListener("dragenter",this.dragEnterHandler),document.addEventListener("dragleave",this.dragLeaveHandler),document.addEventListener("dragover",this.dragOverHandler),document.addEventListener("drop",this.dropHandler)}initGroupFeatures(){this.dragController=new window.jvbDragHandler({draggableSelector:this.selectors.items.item,dropTargetSelector:`${this.selectors.field.preview}, ${this.selectors.groups.grid}, .empty-group`,ignoreSelector:"input:not(.upload-select), button, select, textarea, details, summary, a",previewElement:"img, video, .icon",getItemId:e=>e.dataset.uploadId,getSelectedItems:e=>{const t=this.getFieldIdFromElement(e),s=e.dataset.uploadId,r=this.getCurrentSelection(t);return r&&r.includes(s)?r:[s]},validateDrop:(e,t)=>{const s=this.getFieldIdFromElement(t),r=document.querySelector(`[data-upload-id="${e[0]}"]`);return s===this.getFieldIdFromElement(r)},onDrop:(e,t)=>{this.handleItemDrop(e,t),t.scrollIntoView({behavior:"smooth",block:"center"})},onDragStart:e=>{},onDragEnd:(e,t)=>{if(t){const t=document.querySelector(`[data-upload-id="${e[0]}"]`),s=this.getFieldIdFromElement(t),r=this.selectionHandlers.get(s);r?.clearSelection()}},previewOptions:{multiOffset:{x:-60,y:-80},singleOffset:{x:-50,y:-60},showCount:!0}})}handleExternalDragLeave(e){const t=e.target.closest(this.selectors.field.dropZone);t&&!t.contains(e.relatedTarget)&&t.classList.remove("dragover")}handleExternalDragEnter(e){if(!e.dataTransfer.types.includes("Files"))return;const t=e.target.closest(this.selectors.field.dropZone);t&&(e.preventDefault(),t.classList.add("dragover"))}handleExternalDragOver(e){if(!e.dataTransfer.types.includes("Files"))return;e.target.closest(this.selectors.field.dropZone)&&(e.preventDefault(),e.dataTransfer.dropEffect="copy")}handleExternalDrop(e){const t=e.target.closest(this.selectors.field.dropZone);if(!t)return;e.preventDefault(),t.classList.remove("dragover");const s=Array.from(e.dataTransfer.files);if(0===s.length)return;const r=this.getFieldIdFromElement(t);r?(this.processFiles(r,s),this.a11y.announce(`${s.length} file(s) dropped for upload`)):console.error("No field ID found for drop zone")}handleItemDrop(e,t){const s=t.classList.contains("preview");let r=t;if(t.classList.contains("empty-group")){const e=this.getFieldIdFromElement(t),s=this.createGroup(e);if(!s)return void console.error("Failed to create group");r=s.grid}e.forEach((e=>{s?this.removeFromGroup(e):this.addToGroup(e,r)}));const o=this.getFieldIdFromElement(t);this.schedulePersistance(o);const i=e.length>1?`Moved ${e.length} items`:"Moved item";this.a11y.announce(i)}handleClick(e){if(e.target.matches(this.selectors.field.dropZone)||e.target.closest(this.selectors.field.dropZone)){const t=e.target.closest(this.selectors.field.dropZone);if(t&&!e.target.matches("input, button, a")){const e=t.querySelector(this.selectors.field.input);e?.click()}}const t=e.target.closest("[data-action]");t&&this.handleAction(t)}handleChange(e){const t=this.getFieldIdFromElement(e.target);if(e.target.matches(this.selectors.field.input)){const t=this.getFieldIdFromElement(e.target),s=Array.from(e.target.files);s.length>0&&t&&this.processFiles(t,s)}t&&("post_group"===this.fields.get(t).config.destination?this.handleGroupMetaChange(e.target):this.queueUploadMeta(e))}getCurrentSelection(e){let t=[];for(let[s,r]of this.selectionHandlers)(e===s||s.includes(e))&&r.selectedItems.size>0&&(t=t.concat([...r.selectedItems]));return t}getSubtypeFromMime(e){return e.startsWith("image/")?"image":e.startsWith("video/")?"video":"document"}getStatusText(e){return this.statusMapping[e]||e}getStatusIcon(e){return window.getIcon(this.queue.icons[e])}getStatusProgress(e){switch(e){case"local_processing":return 28;case"queued":return 50;case"uploading":return 66;case"pending":return 75;case"processing":return 89;case"completed":return 100;default:return 0}}getModalType(e){if(void 0!==e._cachedModalType)return e._cachedModalType;if(!e||!e.element)return e._cachedModalType=null,null;const t=e.element.closest("dialog");if(!t)return e._cachedModalType=null,null;let s=null;return s=t.classList.contains("edit")?"edit":t.classList.contains("create")?"create":t.classList.contains("bulkEdit")?"bulkEdit":t.className,e._cachedModalType=s,s}handleAction(e){const t=e.dataset.action,s=this.getFieldIdFromElement(e);switch(t){case"add-to-group":this.handleAddToGroup(e);break;case"delete-group":this.handleDeleteGroup(e);break;case"delete-upload":case"remove-from-group":this.handleRemoveItem(e);break;case"upload":this.fields.get(s).element.closest("details").open=!1,document.body.classList.add("uploading"),this.submitUploads(s);break;case"restore":this.handleRestoreUploads().then((()=>{}));break;case"clear-cache":confirm("Save these uploads for later?")||this.cleanupStoredUploads(),this.cleanupRestore()}}handleAddToGroup(e){const t=e.closest(this.selectors.field.field),s=t?.dataset.uploader;if(!s)return;const r=this.selected.get(s);if(r&&0!==r.size){const e=this.createGroup(s);if(!e)return;r.forEach((t=>{this.addToGroup(t,e.grid)}));const t=this.selectionHandlers.get(s);t?.clearSelection(),this.a11y.announce(`Created group with ${r.size} items`)}else this.createGroup(s);this.schedulePersistance(s)}handleDeleteGroup(e){const t=e.closest(this.selectors.groups.container);if(!t)return;const s=t.dataset.groupId,r=this.getFieldIdFromElement(t);if(!confirm("Delete this group? Items will be moved back to the upload area."))return;t.querySelectorAll(this.selectors.items.item).forEach((e=>{const t=e.dataset.uploadId;this.removeFromGroup(t)})),this.deleteGroup(s),this.a11y.announce("Group deleted, items returned to upload area"),this.schedulePersistance(r)}handleRemoveItem(e){const t=e.closest(this.selectors.items.item);if(!t)return;const s=t.dataset.uploadId,r=this.getFieldIdFromElement(t);confirm("Remove this item?")&&(this.removeUpload(r,s),this.a11y.announce("Item removed"),this.schedulePersistance(r))}addFieldSelectionHandler(e){if(this.selectionHandlers.has(e))return this.selectionHandlers.get(e);const t=this.fields.get(e);if(!t)return;const s=t.ui.field;if(!s)return;const r=new window.jvbHandleSelection({container:s,ui:{selectAll:s.querySelector('[name="select-all-uploads"]'),bulkControls:s.querySelector(".selection-actions"),count:s.querySelector(".selection-count")},itemSelector:"[data-upload-id]",checkboxSelector:'[name*="select-item"]'});return r.subscribe(((t,s)=>{switch(t){case"item-selected":case"item-deselected":case"range-selected":this.selected.set(e,s.selectedItems);break;case"select-all":this.handleSelectAll(s.container,s.selected)}})),this.selectionHandlers.set(e,r),r}addGroupSelectionHandler(e,t){const s=`${e}_${t}`;if(this.selectionHandlers.has(s))return this.selectionHandlers.get(s);const r=this.groups.get(t);if(!r)return;const o=new window.jvbHandleSelection({container:r.element,ui:{selectAll:r.element.querySelector(this.selectors.groups.selectAll),bulkControls:r.element.querySelector(this.selectors.groups.actions),count:r.element.querySelector(this.selectors.groups.count)},itemSelector:"[data-upload-id]",checkboxSelector:'[name*="select-item"]'});return o.subscribe(((t,s)=>{switch(t){case"item-selected":case"item-deselected":case"range-selected":this.selected.set(e,s.selectedItems);break;case"select-all":this.handleSelectAll(s.container,s.selected)}})),this.selectionHandlers.set(s,o),o}handleSelectAll(e,t){}determineFieldId(e){return`${e.dataset.content||e.closest("dialog")?.dataset.content||e.closest("form")?.dataset.save||""}_${e.dataset.itemId||e.closest("dialog")?.dataset.itemId||""}_${e.dataset.field||""}`}getFromElement(e,t){const s={field:{selector:this.selectors.field.field,key:"uploader",store:this.fields},upload:{selector:this.selectors.items.item,key:"uploadId",store:this.uploads},group:{selector:this.selectors.groups.container,key:"groupId",store:this.groups}}[t];if(!s)return null;const r=e.closest(s.selector);if(!r)return null;const o=r.dataset[s.key];return s.store.get(o)}getFieldFromElement(e){return this.getFromElement(e,"field")}getUploadFromElement(e){return this.getFromElement(e,"upload")}getGroupFromElement(e){return this.getFromElement(e,"group")}getFieldIdFromElement(e){return this.getFromElement(e,"field")?.id??null}getUploadIdFromElement(e){return this.getFromElement(e,"upload")?.id??null}getGroupIdFromElement(e){return this.getFromElement(e,"group")?.id??null}async processFiles(e,t){const s=this.fields.get(e);if(!s)return;s.ui.dropZone&&(s.ui.dropZone.hidden=!0),s.ui.groups.display&&(s.ui.groups.display.hidden=!1);const r=t.length;let o=0;this.updateUploadProgress(e,0,r,"Processing files..."),s.uploads||(s.uploads=new Set);const i=Array.from(t).map((async(t,i)=>{try{const i=`upload_${Date.now()}_${Math.random().toString(36).substr(2,9)}`,a={id:i,attachment_id:null,fieldId:e,originalFile:t,processedFile:null,preview:null,status:"local_processing",element:null,location:null,meta:{originalName:t.name,size:t.size,type:t.type}};a.preview=this.createPreviewUrl(t),t.type.startsWith("image/")?a.processedFile=await this.processImage(t,s.subtype):a.processedFile=t,await this.uploadStore.saveBlob(i,a.processedFile||t);const l=this.getSubtypeFromMime(t.type);return a.element=this.createUploadElement({...a,subtype:l},"post_group"===s.config.destination),this.showUploadProgress(i,!0),this.updateUploadItemProgress(i,50,"local_processing"),s.ui.preview&&(s.ui.preview.appendChild(a.element),a.location=s.ui.preview),this.uploads.set(i,a),s.uploads.add(i),o++,this.updateUploadProgress(e,o,r,"Processing files..."),this.updateUploadItemProgress(i,100,"processed"),a.status="processed",setTimeout((()=>{this.showUploadProgress(i,!1)}),1e3),i}catch(s){return console.error("Error processing file:",t.name,s),o++,this.updateUploadProgress(e,o,r,"Processing files..."),null}}));await Promise.all(i),this.updateFieldState(e),await this.schedulePersistance(e),"post_group"!==s.config.destination&&(await this.queueUpload(e),this.maybeLockUploads(e))}updateFieldState(e){const t=this.fields.get(e);if(!t||!t.ui.field)return;const s=t.ui.field,r=t.uploads?.size||0,o=t.ui.groups?.container?.querySelectorAll(".upload-group").length>0;s.dataset.hasUploads=r>0?"true":"false",s.dataset.uploadCount=r.toString(),s.dataset.hasGroups=o?"true":"false",t.ui.preview&&t.ui.preview.setAttribute("aria-label",`Upload preview area with ${r} item${1!==r?"s":""}`)}updateUploadProgress(e,t,s,r){const o=this.fields.get(e);if(!o?.ui?.progress?.progress)return;const i=o.ui.progress,a=s>0?t/s*100:0;i.fill&&(i.fill.style.width=`${a}%`),i.text&&(i.text.textContent=r),i.count&&(i.count.textContent=`${t}/${s}`),i.progress.hidden=t===s}updateFieldStatus(e,t){const s=this.fields.get(e);s&&(s.state=t)}updateUploadStatus(e,t){const s=this.uploads.get(e);s&&(s.status=t,this.updateUploadUI(e))}updateUploadUI(e){const t=this.uploads.get(e);if(!t?.element)return;t.element.className=t.element.className.replace(/status-[\w-]+/g,""),t.element.classList.add(`status-${t.status}`);t.element.querySelector(".progress")&&this.updateUploadItemProgress(e,this.getStatusProgress(t.status),t.status)}showUploadProgress(e,t=!0){const s=this.uploads.get(e);if(!s||!s.element)return;const r=s.element.querySelector(".progress");r&&(t?(r.style.removeProperty("animation"),r.hidden=!1):(r.style.animation="fadeOut var(--transition-base)",setTimeout((()=>{r.hidden=!0}),300)))}updateUploadItemProgress(e,t,s=null){const r=this.uploads.get(e);if(!r||!r.element)return;const o=r.element.querySelector(".progress");if(!o)return;const i=o.querySelector(".fill"),a=o.querySelector(".details"),l=o.querySelector(".icon");i&&(i.style.width=`${t}%`),s&&a&&(a.textContent=this.getStatusText(s)),s&&l&&(l.innerHTML=this.getStatusIcon(s).outerHTML)}checkFieldLimits(e,t){const s=this.fields.get(e);if(!s)return!1;return(s.uploads?.size||0)+t<=s.maxFiles}validateFile(e,t){return this.settings.allowedTypes.includes(e.type)?!(e.size>this.settings.maxFileSize)||(this.notify(`File too large: ${this.formatBytes(e.size)}`,"error"),!1):(this.notify(`Invalid file type: ${e.type}`,"error"),!1)}formatBytes(e,t=2){if(0===e)return"0 Bytes";const s=t<0?0:t,r=Math.floor(Math.log(e)/Math.log(1024));return parseFloat((e/Math.pow(1024,r)).toFixed(s))+" "+["Bytes","KB","MB","GB"][r]}shouldProcessClientSide(e,t){return!("image"!==t||!e.type.startsWith("image/"))}async processImage(e,t){const s=this.worker.settings.timeout;return new Promise(((r,o)=>{let i,a=!1;i=setTimeout((()=>{a||(a=!0,this.worker.tasks.delete(t),this.worker.settings.restartAfterTimeout&&this.restartCompressionWorker(),o(new Error(`Processing timeout for ${e.name}`)))}),s),this.worker.tasks.set(t,{file:e,timeoutId:i}),this.handleProcess(e,t).then((e=>{a||(a=!0,clearTimeout(i),this.worker.tasks.delete(t),r(e))})).catch((e=>{a||(a=!0,clearTimeout(i),this.worker.tasks.delete(t),o(e))}))}))}async handleProcess(e,t){if(!e.type.startsWith("image/"))return e;const s=this.getMaxDimension();if(this.shouldUseWorker(e))try{if(this.worker.worker||this.initCompressionWorker(),this.worker.worker)return await this.processWithWorker(e,t,s,.85)}catch(e){console.warn("Worker processing failed, falling back to main thread:",e)}return await this.processOnMainThread(e,s,.85)}async processOnMainThread(e,t,s){return new Promise(((r,o)=>{const i=new Image,a=document.createElement("canvas"),l=a.getContext("2d");let n=null;const d=()=>{i.onload=null,i.onerror=null,n&&(URL.revokeObjectURL(n),n=null),a.width=1,a.height=1,l.clearRect(0,0,1,1)};i.onload=()=>{try{const{width:n,height:c}=this.calculateOptimalDimensions(i,t);a.width=n,a.height=c,l.imageSmoothingEnabled=!0,l.imageSmoothingQuality="high",l.drawImage(i,0,0,n,c);const u=this.getOptimalFormat(e),p=this.getOptimalQuality(e,s);a.toBlob((t=>{if(d(),t){const s=new File([t],this.getProcessedFileName(e,u),{type:u,lastModified:Date.now()});r(s)}else o(new Error("Canvas toBlob failed"))}),u,p)}catch(e){d(),o(new Error(`Canvas processing failed: ${e.message}`))}},i.onerror=()=>{d(),o(new Error(`Failed to load image: ${e.name}`))};try{n=this.createPreviewUrl(e),i.src=n}catch(e){d(),o(new Error(`Failed to create object URL: ${e.message}`))}}))}getOptimalFormat(e){return"image/gif"===e.type||"image/svg+xml"===e.type?e.type:this.supportsWebP()?"image/webp":"image/jpeg"}getOptimalQuality(e,t){return e.size<512e3?Math.max(t,.9):e.size<2097152?t:Math.min(t,.8)}getProcessedFileName(e,t){return e.name.replace(/\.[^/.]+$/,"")+({"image/webp":".webp","image/jpeg":".jpg","image/png":".png","image/gif":".gif"}[t]||".jpg")}getMaxDimension(){const e=window.screen.width,t=window.devicePixelRatio||1;return e*t>2560?2400:e*t>1920?1920:1200}shouldUseWorker(e){return this.worker.worker&&e.size>1048576&&"undefined"!=typeof OffscreenCanvas}async processWithWorker(e,t,s,r){return new Promise(((o,i)=>{if(!this.worker.worker)return void i(new Error("Worker not available"));const a=`${t}_${Date.now()}`,l=t=>{if(t.data.messageId===a)if(this.worker.worker.removeEventListener("message",l),this.worker.worker.removeEventListener("error",n),t.data.success){const s=new File([t.data.blob],this.getProcessedFileName(e,t.data.format||"image/webp"),{type:t.data.format||"image/webp",lastModified:Date.now()});o(s)}else i(new Error(t.data.error||"Worker processing failed"))},n=e=>{this.worker.worker.removeEventListener("message",l),this.worker.worker.removeEventListener("error",n),i(new Error(`Worker error: ${e.message}`))};this.worker.worker.addEventListener("message",l),this.worker.worker.addEventListener("error",n),this.worker.worker.postMessage({messageId:a,file:e,maxDimension:s,quality:r,outputFormat:this.getOptimalFormat(e)})}))}restartCompressionWorker(){this.worker.worker&&(this.worker.worker.terminate(),this.worker.worker=null),this.worker.tasks.clear(),this.worker.restart.count>=this.worker.restart.max?console.error("Max worker restarts reached, disabling worker"):(this.worker.restart.count++,this.initCompressionWorker())}initCompressionWorker(){if(!this.worker.worker&&"undefined"!=typeof Worker)try{const e=new Blob(["\n            self.onmessage = async function(e) {\n                const { messageId, file, maxDimension, quality, outputFormat } = e.data;\n\n                try {\n                    // Create ImageBitmap from file\n                    const bitmap = await createImageBitmap(file);\n\n                    // Calculate dimensions\n                    const scale = Math.min(maxDimension / bitmap.width, maxDimension / bitmap.height, 1);\n                    const width = Math.round(bitmap.width * scale);\n                    const height = Math.round(bitmap.height * scale);\n\n                    // Create OffscreenCanvas\n                    const canvas = new OffscreenCanvas(width, height);\n                    const ctx = canvas.getContext('2d');\n\n                    // Draw and resize\n                    ctx.imageSmoothingEnabled = true;\n                    ctx.imageSmoothingQuality = 'high';\n                    ctx.drawImage(bitmap, 0, 0, width, height);\n\n                    // Clean up bitmap\n                    bitmap.close();\n\n                    // Convert to blob\n                    const blob = await canvas.convertToBlob({\n                        type: outputFormat,\n                        quality: quality\n                    });\n\n                    self.postMessage({\n                        messageId,\n                        success: true,\n                        blob: blob,\n                        format: outputFormat\n                    });\n\n                } catch (error) {\n                    self.postMessage({\n                        messageId,\n                        success: false,\n                        error: error.message\n                    });\n                }\n            };\n        "],{type:"application/javascript"});this.worker.worker=new Worker(this.createPreviewUrl(e))}catch(e){console.warn("Failed to initialize compression worker:",e),this.worker.worker=null}}calculateOptimalDimensions(e,t){let{width:s,height:r}=e;if(s<=t&&r<=t)return{width:s,height:r};const o=Math.min(t/s,t/r);return{width:Math.round(s*o),height:Math.round(r*o)}}supportsWebP(){return 0===document.createElement("canvas").toDataURL("image/webp").indexOf("data:image/webp")}createPreviewUrl(e){const t=URL.createObjectURL(e);return this.previewUrls||(this.previewUrls=new Set),this.previewUrls.add(t),t}revokePreviewUrl(e){e?.startsWith("blob:")&&(URL.revokeObjectURL(e),this.previewUrls?.delete(e))}maybeLockUploads(e){const t=this.fields.get(e);if(!t?.ui?.dropZone)return;if("post_group"===t.config.destination)return;const s=t.uploads?.size||0,r=t.config?.maxFiles||999;t.ui.dropZone.hidden=s>=r,t.element.classList.toggle("at-max-uploads",s>=r)}createUploadElement(e,t=!1){let s=window.getTemplate("uploadItem");if(!s)return void console.error("Image template not found");s.dataset.uploadId=e.id,e.originalFile&&(s.dataset.subtype=this.getSubtypeFromMime(e.originalFile.type)),s.querySelector('[name="featured"]').value=e.id;let[r,o,i,a,l]=[s.querySelector('[name="featured"]'),s.querySelector("img"),s.querySelector("video"),s.querySelector("label > span"),s.querySelector("details")];switch([r.value,o.src,o.alt]=[e.id,e.preview,e.originalFile?.name??e.meta?.originalName??""],s.dataset.subtype){case"image":[o.src,o.alt]=[e.preview,e.originalFile?.name??e.meta?.originalName??""],i.remove(),a.remove();break;case"video":i.src=e.preview,o.remove(),a.remove();break;case"document":const t=e.originalFile?.name??e.meta?.originalName??"",s=t.split(".").pop()?.toLowerCase()??"",r={pdf:"file-pdf",csv:"file-csv",doc:"file-doc",docx:"file-doc",txt:"file-txt",xls:"file-xls",xlsx:"file-xls"},l=window.getIcon(r[s]||"file");a.innerText=e.originalFile.name,a.prepend(l),o.remove(),i.remove()}if(l){let e=window.getTemplate("uploadMeta");e&&l.append(e)}return s.draggable=t,s.querySelectorAll("input").forEach((t=>{let s=t.id;if(s){let r=s+e.id,o=t.parentNode.querySelector(`label[for="${s}"]`);t.id=r,o&&(o.htmlFor=r)}})),s}async submitUploads(e){const t=this.fields.get(e);if(!t?.uploads||0===t.uploads.size)return;let s=Array.from(t.uploads);if(0===s.length)return void this.error.log("No uploads to upload",{component:"UploadManager",action:"submitGroupedUploads",fieldId:e});const r=this.getFieldGroups(e);if(0===r.length)return void this.error.log("No groups created for post_group upload",{component:"UploadManager",action:"submitGroupedUploads",fieldId:e});const o=[],i=new FormData;let a=[];s=s.map((e=>this.uploads.get(e))),r.forEach(((e,t)=>{const r={images:[],fields:{}};for(let[t,s]of Object.entries(e.changes))r.fields[t]=s;s.filter((t=>t.groupId===e.id)).forEach((e=>{if(e){const t=e.processedFile||e.originalFile;if(t){i.append("files[]",t);const s={upload_id:e.id,index:a.length};r.images.push(s),a.push(e.id)}}})),o.push(r)})),s.filter((e=>!Object.hasOwn(e,"groupId"))).forEach((e=>{if(e){const t={images:[],fields:{}},s=e.processedFile||e.originalFile;if(s){i.append("files[]",s);const r={upload_id:e.id,index:a.length};t.images.push(r),a.push(e.id)}o.push(t)}})),i.append("content",t.config.content),i.append("user",t.config.itemID),i.append("posts",JSON.stringify(o)),i.append("upload_ids",JSON.stringify(a));for(const[e,t]of i.entries())console.log(e,t);const l={endpoint:"uploads/groups",method:"POST",data:i,title:`Creating ${o.length} ${t.config.content}${o.length>1?"s":""} from uploads...`,popup:`Creating ${o.length} post${o.length>1?"s":""}...`,canMerge:!1,headers:{action_nonce:jvbSettings.dash},append:"_upload"};try{const e=await this.queue.addToQueue(l);return s.forEach((t=>{let s=this.uploads.get(t);s&&(s.operationId=e,this.updateUploadStatus(t,"queued"))})),t.operationId=e,this.a11y.announce(`Creating ${o.length} post${o.length>1?"s":""} from your uploads`),e}catch(t){throw this.error.log(t,{component:"UploadManager",action:"submitGroupedUploads",fieldId:e}),t}finally{this.schedulePersistance(t.id)}}async queueUpload(e){const t=this.fields.get(e);if(!t?.uploads)return;const s=Array.from(t.uploads);if(0===s.length)return;const r=this.prepareUploadData(t,s);this.a11y.announce("Queuing for upload");let o=1===s.length?"file":"files";const i={endpoint:"uploads",method:"POST",data:r,title:`Uploading ${s.length} ${o} to server...`,popup:`Uploading ${s.length} ${o}...`,canMerge:!1,headers:{action_nonce:jvbSettings.dash},append:"_upload"};try{const e=await this.queue.addToQueue(i);return s.forEach((t=>{let s=this.uploads.get(t);s&&(s.operationId=e,this.updateUploadStatus(t,"queued"))})),t.operationId=e,e}catch(e){throw e}finally{this.schedulePersistance(t.id)}}prepareUploadData(e,t){const s=new FormData;s.append("content",e.config.content),s.append("mode",e.config.mode),s.append("field_name",e.config.name),s.append("fieldId",e.id),s.append("field_type",e.config.type),s.append("subtype",e.config.subtype),s.append("item_id",e.config.itemID),s.append("destination",e.config.destination||"meta");let r=[];const o=this.getFieldGroups(e.id);if("post_group"===e.config.destination&&o.length>0){let e=[],t=[],i=[];o.forEach((o=>{let a=[],l=null;o.uploads.forEach((e=>{let t=this.uploads.get(e);if(t){const e=t.processedFile||t.originalFile;if(e){s.append("files[]",e);r.length;r.push(t.id),a.push(t.id);const o=t.element?.querySelector('[name="featured"]');o?.checked&&(l=t.id)}}})),e.push(a),t.push(o.title||""),i.push(l)})),s.append("groups",JSON.stringify(e)),s.append("group_titles",JSON.stringify(t)),s.append("featured_images",JSON.stringify(i))}else t.forEach((e=>{let t=this.uploads.get(e);if(t){const e=t.processedFile||t.originalFile;e&&(s.append("files[]",e),r.push(t.id))}}));return s.append("upload_ids",JSON.stringify(r)),s}getFieldGroups(e){const t=[];return this.groups.forEach(((s,r)=>{if(s.fieldId===e){const o=this.fields.get(e),i=o?.ui?.groups?.groups?.get(r);t.push({id:r,uploads:Array.from(s.uploads||new Set),changes:s.changes||{},element:i||null})}})),t}async queueUploadMeta(e){const t=this.getUploadFromElement(e.target);if(!t)return;if(!this.fields.get(t.fieldId))return;if(!e.target.closest(".upload-meta"))return;let s={};s[e.target.name]=e.target.value,t.meta={...t.meta,...s};let r={};r[t.attachmentId??t.id]=t.meta;const o={endpoint:"uploads/meta",method:"POST",data:r,title:"Updating meta",canMerge:!0,headers:{action_nonce:jvbSettings.dash}};try{await this.queue.addToQueue(o)}catch(e){this.error.log(e,{component:"UploadManager",action:"sendMetaUpdate",uploadId:t.id})}}createGroup(e,t=null){const s=this.fields.get(e);if(!s)return console.error("Field not found:",e),null;t||(t=`group_${Date.now()}_${Math.random().toString(36).substr(2,9)}`);const r=this.createGroupElement(t,e);if(!r)return console.error("Failed to create group element"),null;s.ui.groups||(s.ui.groups={groups:new Map,container:null,empty:null,display:null}),s.ui.groups.groups.set(t,r),s.ui.groups.container&&s.ui.groups.empty?s.ui.groups.container.insertBefore(r,s.ui.groups.empty):s.ui.groups.container&&s.ui.groups.container.appendChild(r);const o={id:t,fieldId:e,element:r,grid:r.querySelector(".item-grid.group"),uploads:new Set,changes:{}};return this.groups.set(t,o),this.addGroupSelectionHandler(e,t),this.schedulePersistance(e),o}createGroupElement(e,t){let s=window.getTemplate("imageGroup");if(!s)return;s.dataset.groupId=e,s.dataset.fieldId=t;let r=window.getTemplate("groupMetadata");const o=s.querySelector(".fields");if(o&&r){o.append(r);const i=o.querySelector('[name="post_title"]'),a=o.querySelector('[name="post_excerpt"]');i&&(i.id=`${e}_title`,i.name=`${e}[post_title]`),a&&(a.id=`${e}_excerpt`,a.name=`${e}[post_excerpt]`);let l=this.fields.get(t);if(""!==l.config.content){s.querySelector("summary").textContent=l.config.content+" Fields"}}else s.querySelector("details").remove();const i=s.querySelector(".item-grid.group");return i&&(i.dataset.groupId=e),s}deleteGroup(e,t=!0){let s=this.groups.get(e);if(!s)return;let r=!0;t&&s.uploads&&s.uploads.size>0&&(r=!window.confirm("Delete uploads in group?")),t&&r&&s.uploads&&s.uploads.size>0&&Array.from(s.uploads).forEach((e=>{this.addImageToGroup(e,null,!1)})),this.groups.delete(e);let o=s.element;o&&(o.remove(),this.a11y.announce("Group removed")),this.schedulePersistance(s.fieldId)}addToGroup(e,t=null,s=!0){let r=this.uploads.get(e);if(!r)return;let o=this.fields.get(r.fieldId);if(!o)return;if(!t&&r.location===o.ui.preview||t===r.location)return;if(r.location){let t=r.location.dataset.groupId;if(t){let s=this.groups.get(t);s&&s.uploads&&(s.uploads.delete(e),0===s.uploads.size&&this.deleteGroup(t))}}const i=r.element.querySelector('[name*="select-item"]');i&&(i.checked=!1);let a=r.element.querySelector('[name="featured"]');if(a.hidden=!t,t){if(!t.classList.contains("item-grid")||!t.classList.contains("preview")){let s=t.dataset.groupId;a.name=s+"_"+a.name;let o=this.groups.get(s);o||(o=this.createGroup(r.fieldId),t=o.grid,s=o.id),o&&(o.uploads.add(e),r.groupId=s)}}else t=o.ui.preview,r.groupId=null;r.location=t,t.append(r.element),s&&this.schedulePersistance(o.id)}removeFromGroup(e){const t=this.uploads.get(e);if(!t)return;const s=this.fields.get(t.fieldId);if(!s)return;if(t.groupId){const s=this.groups.get(t.groupId);s?.uploads&&(s.uploads.delete(e),0===s.uploads.size&&this.deleteGroup(t.groupId,!1)),t.groupId=null}s.ui?.preview&&(s.ui.preview.appendChild(t.element),t.location=s.ui.preview);const r=t.element.querySelector('[name="featured"]');r&&(r.hidden=!0,r.checked=!1)}removeUpload(e,t){const s=this.fields.get(e),r=this.uploads.get(t);if(!s||!r)return;if(s.uploads?.delete(t),r.groupId){const e=this.groups.get(r.groupId);e&&e.uploads&&(e.uploads.delete(t),0===e.uploads.size&&this.removeGroup(r.groupId))}r.element?.remove(),this.clearUpload(t),this.updateFieldState(e),this.maybeLockUploads(e);const o=this.selectionHandlers.get(s.id);o&&o.deselect(t),this.a11y.announce("Upload removed")}schedulePersistance(e){const t=`persist_${e}`;window.debouncer.schedule(t,(()=>this.persistFieldState(e)),1e3)}async persistFieldState(e){const t=this.fields.get(e);if(!t)return;const s={...t,id:e,fieldId:e,uploads:Array.from(t.uploads||[]).map((e=>this.uploads.get(e))),groups:Array.from(this.groups.entries()).filter((([t,s])=>s.fieldId===e&&s.uploads&&s.uploads.size>0)).map((([e,t])=>({id:t.id,uploads:Array.from(t.uploads),changes:t.changes||{}}))),context:{url:this.normalizeUrl(window.location.href),fullUrl:window.location.href,modalType:this.getModalType(t),formId:t.formId,fieldSelector:`.field.upload[data-field="${t.config.name}"]`},timestamp:Date.now()};await this.fieldStore.save(s)}normalizeUrl(e){try{const t=new URL(e);return t.origin+t.pathname}catch(t){return e}}getFieldUploads(e,t=!1){const s=this.fields.get(e);return s&&s.uploads?Array.from(s.uploads).map((e=>{const s=this.uploads.get(e);return s?t?{id:s.id,fieldId:s.fieldId,status:s.status,attachmentId:s.attachmentId,operationId:s.operationId,groupId:s.groupId||null,changes:s.changes||{},meta:{originalName:s.meta?.originalName||s.originalFile?.name,size:s.meta?.size||s.originalFile?.size,type:s.meta?.type||s.originalFile?.type,title:s.meta?.title,alt:s.meta?.alt,caption:s.meta?.caption}}:s:null})).filter(Boolean):[]}async checkForStoredUploads(){if(!this.db)return;const e=this.db.transaction(["fieldStates"],"readonly").objectStore("fieldStates"),t=(await new Promise((t=>{const s=e.getAll();s.onsuccess=()=>t(s.result)}))).filter((e=>e.uploads.some((e=>!e.operationId&&("completed"===e.status||"processed"===e.status||"local_processing"===e.status||"processed-original"===e.status)))));0!==t.length&&this.showRecoveryNotification(t)}async handleRestoreUploads(){let e=document.querySelector("dialog.restore-uploads");if(!e)return;const t=this.getSelectedRestorationUploads(e);0!==t.length&&(await this.restoreSelectedUploads(t),this.cleanupRestore())}getSelectedRestorationUploads(e){let t=[];return e.querySelectorAll("[type=checkbox]:checked").forEach((e=>{const s=e.closest(".item");s&&t.push({uploadId:s.dataset.uploadId,fieldId:s.dataset.fieldId})})),t}handleGroupMetaChange(e){let t=this.getGroupFromElement(e);if(!t)return;Object.hasOwn(t,"changes")||(t.changes={});let s=e.name;if(s.includes("group")){let e=t.id+"_",r=t.id+"[";s=s.replace(e,"").replace(r,"").replace("]","")}t.changes[`${s}`]=e.value,this.groups.set(t.id,t),this.schedulePersistance(t.fieldId)}async showRecoveryNotification(e){const t=e.reduce(((e,t)=>e+t.uploads.length),0),s=e.reduce(((e,t)=>e+(t.groups?.length||0)),0);let r,o=window.getTemplate("restoreNotification");if(!o)return void console.error("Restore notification template not found");if(s>0){r=`${s} ${s>1?"groups":"group"} with ${t} ${t>1?"uploads":"upload"} can be restored.`}else r=`${t} upload(s) from ${e.length} field(s) can be recovered.`;const i=o.querySelector(".restore-details");i&&(i.textContent=r);for(const t of e){let e=window.getTemplate("restoreField");if(!e)continue;const s=e.querySelector("h3");s&&(s.textContent=t.config.name||"Unnamed Field");const r=e.querySelector(".item-grid.restore");for(const e of t.uploads){let s=window.getTemplate("uploadItem");if(!s)continue;const o=await this.uploadStore.getBlob(e.id);if(o)try{const r=new Blob([o.data],{type:o.type}),i=this.createPreviewUrl(r);let[a,l,n,d,c]=[s.querySelector('[name="featured"]'),s.querySelector("img"),s.querySelector("video"),s.querySelector("label > span"),s.querySelector("details")];s.dataset.uploadId=e.id,s.dataset.fieldId=t.id;let u=this.getSubtypeFromMime(o.type);switch(s.dataset.subtype=u,u){case"image":[l.src,l.alt]=[i,e.originalFile?.name??e.meta?.originalName??""],n.remove(),d.remove();break;case"video":n.src=i,l.remove(),d.remove();break;case"document":let t;switch(""){case"pdf":t=window.getIcon("file-pdf");break;case"csv":t=window.getIcon("file-csv");break;case"doc":t=window.getIcon("file-doc");break;case"txt":t=window.getIcon("file-txt");break;case"xls":t=window.getIcon("file-xls");break;default:t=window.getIcon("file")}d.innerText=e.originalFile.name,d.prepend(t),l.remove(),n.remove()}s.dataset.previewUrl=i}catch(t){console.warn("Failed to create preview for upload:",e.id,t)}const i=s.querySelector("summary span");i&&(i.textContent=e.meta?.originalName||"Unknown file");const a=s.querySelector("details");a&&e.meta&&(a.textContent=`${this.formatBytes(e.meta.size)} â€¢ ${e.meta.type}`),s.querySelectorAll("input").forEach((t=>{let s=t.id;if(s){let r=s+e.id,o=t.parentNode.querySelector(`label[for="${s}"]`);t.id=r,o&&(o.htmlFor=r)}})),r&&r.appendChild(s)}o.querySelector(".wrap").appendChild(r)}document.querySelector(".field.upload").appendChild(o),o=document.querySelector("dialog.restore-uploads"),this.restoreModal=new window.jvbModal(o),this.restoreSelection=new window.jvbHandleSelection({container:o,ui:{selectAll:o.querySelector("#select-all-restore"),count:o.querySelector(".selection-count")}}),this.restoreModal.handleOpen()}async restoreSelectedUploads(e){const t=new Map;if(e.forEach((e=>{t.has(e.fieldId)||t.set(e.fieldId,[]),t.get(e.fieldId).push(e.uploadId)})),!this.db)return;const s=this.db.transaction(["fieldStates"],"readonly").objectStore("fieldStates");for(const[e,r]of t.entries()){const t=s.get(e),o=await new Promise((e=>{t.onsuccess=()=>e(t.result),t.onerror=()=>e(null)}));o&&(o.uploads=o.uploads.filter((e=>r.includes(e.id))),await this.restoreField(o))}}async restoreField(e){const{config:t,context:s,uploads:r,groups:o,id:i}=e;s.modalType&&await this.openModalForRestore(s);let a=document.querySelector(`.field.upload[data-field="${t.name}"]`);if(!a){const e=`${t.content}_${t.itemID}_${t.name}`;a=document.querySelector(`.field.upload[data-uploader="${e}"]`)}if(!a)return void console.warn(`Field ${t.name} not found for restoration`,t);let l=a.dataset.uploader;l&&this.fields.has(l)||(l=this.registerUploader(a,t));const n=this.fields.get(l);if(n){n.state=e.state||"ready",n.ui=this.buildFieldUI(a),n.ui.groups?.display&&(n.ui.groups.display.hidden=!1),o&&o.length>0&&await this.restoreGroups(l,o);for(const e of r)await this.restoreUpload(n,e);this.updateFieldState(l),this.maybeLockUploads(l),"direct"===t.mode&&"post_group"!==t.destination&&await this.queueUpload(l)}else console.error("Failed to register field for restoration")}async restoreUpload(e,t){const s=await this.uploadStore.getBlob(t.id);if(!s)return void console.warn("Blob data not found for upload:",t.id);{const e=s.data instanceof File?s.data:new File([s.data],s.name,{type:s.type,lastModified:s.lastModified});t.originalFile=e,t.processedFile=e,t.preview=this.createPreviewUrl(e)}e.uploads||(e.uploads=new Set),e.uploads.add(t.id);const r=this.getSubtypeFromMime(t.originalFile.type);let o;if(t.element=this.createUploadElement({...t,subtype:r},"post_group"===e.config.destination),o=t.groupId&&e.ui.groups.groups.has(t.groupId)?e.ui.groups.groups.get(t.groupId).querySelector(".item-grid"):e.ui.preview,o&&(o.appendChild(t.element),t.location=o),this.uploads.set(t.id,t),t.groupId){const e=this.groups.get(t.groupId);e&&e.uploads&&e.uploads.add(t.id)}}async restoreGroups(e,t){for(const s of t){const t=this.createGroup(e,s.id);if(t&&(s.meta&&(t.meta={...s.meta}),s.changes&&(t.changes={...s.changes}),s.title)){const e=t.element.querySelector('[name*="post_title"]');e&&(e.value=s.title)}}}async openModalForRestore(e){const{modalType:t,formId:s}=e;let r=null;switch(t){case"create":r=document.querySelector('[data-action="create"]');break;case"edit":r=document.querySelector(`[data-action="edit"][data-id="${e.itemId}"]`);break;case"bulkEdit":r=document.querySelector('[data-action="bulk-edit"]')}r&&(r.click(),await new Promise((e=>setTimeout(e,300))))}handleFieldStoreEvent(e,t){switch(e){case"data-loaded":break;case"item-saved":console.log(`Field state saved: ${t.key}`)}}handleUploadStoreEvent(e,t){switch(e){case"data-loaded":this.checkForStoredUploads();break;case"item-saved":this.showSaveIndicator(t.key)}}async saveUpload(e){const t=e.processedFile||e.originalFile||e.file;if(t instanceof File||t instanceof Blob){await this.uploadStore.saveBlob(e.id,t);const{file:s,originalFile:r,processedFile:o,...i}=e;await this.uploadStore.save(i)}else await this.uploadStore.save(e)}async loadFields(){(await this.fieldStore.getAll()).forEach((e=>{e.uploads&&Array.isArray(e.uploads)&&(e.uploads=new Set(e.uploads.map((e=>e.id)))),this.fields.set(e.fieldId,e)}))}async loadUploads(){(await this.uploadStore.getAll()).forEach((e=>{this.uploads.set(e.id,e)}))}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}destroy(){document.removeEventListener("click",this.clickHandler),document.removeEventListener("change",this.changeHandler),document.removeEventListener("dragenter",this.dragEnterHandler),document.removeEventListener("dragleave",this.dragLeaveHandler),document.removeEventListener("dragover",this.dragOverHandler),document.removeEventListener("drop",this.dropHandler),this.dragController&&this.dragController.destroy(),this.selectionHandlers.forEach((e=>e.destroy())),this.selectionHandlers.clear(),this.cleanupAllPreviewUrls(),this.fields.clear(),this.uploads.clear(),this.groups.clear(),this.selected.clear(),this.subscribers.clear()}cleanupRestore(){this.restoreModal.handleClose(),this.restoreSelection.destroy(),this.restoreSelection=null,this.restoreModal.destroy(),this.restoreModal.modal.remove(),this.restoreModal=null}async cleanupStoredUploads(){this.fieldStore.clear(),this.uploadStore.clear()}async clearField(e){await this.fieldStore.delete(e);const t=this.fields.get(e);if(t?.uploads)for(const e of t.uploads)await this.uploadStore.delete(e);this.fields.delete(e)}async clearUpload(e,t=!0){const s=this.uploads.get(e);if(s){if(this.revokePreviewUrl(s.preview),s.element){const e=s.element.dataset.previewUrl;this.revokePreviewUrl(e),delete s.element.dataset.previewUrl}t&&await this.schedulePersistance(s.fieldId),this.uploads.delete(e),this.uploadStore.delete(e),this.uploadStore.delete(e,"blobs")}}cleanupAllPreviewUrls(){this.previewUrls&&(this.previewUrls.forEach((e=>{try{URL.revokeObjectURL(e)}catch(e){}})),this.previewUrls.clear())}}document.addEventListener("DOMContentLoaded",(()=>{window.jvbUploads=new e}))})();
(()=>{class e{constructor(){this.queue=window.jvbQueue,this.a11y=window.jvbA11y,this.error=window.jvbError,this.fieldStore=new window.jvbStore({name:"upload_fields",storeName:"fieldStates",keyPath:"id",version:2,indexes:[{name:"fieldId",keyPath:"fieldId"},{name:"timestamp",keyPath:"timestamp"},{name:"content",keyPath:"content"},{name:"itemId",keyPath:"itemId"},{name:"status",keyPath:"status"}],stripDOMReferences:!0,TTL:6048e5}),this.uploadStore=new window.jvbStore({name:"uploads",storeName:"uploads",keyPath:"id",storeBlobs:!0,indexes:[{name:"fieldId",keyPath:"fieldId"},{name:"status",keyPath:"status"},{name:"groupId",keyPath:"groupId"},{name:"attachmentId",keyPath:"attachmentId"}]}),window.jvbUploadBlobs=this.uploadStore,this.fieldStore.subscribe(this.handleFieldStoreEvent.bind(this)),this.uploadStore.subscribe(this.handleUploadStoreEvent.bind(this)),this.initWorker(),this.fields=new Map,this.uploads=new Map,this.groups=new Map,this.selected=new Map,this.selectionHandlers=new Map,this.previewUrls=new Set,this.subscribers=new Set,this.dragController=null,this.selectors={field:{field:"[data-upload-field]",input:'input[type="file"]',hiddenValue:'input[type="hidden"]',dropZone:".file-upload-container",preview:".item-grid.preview",progress:".image-progress"},groups:{container:".upload-group",grid:".item-grid.group",header:".group-header",selectAll:'[name="select-all-group"]',actions:".group-actions",count:".selection-controls .info"},items:{item:"[data-upload-id]",checkbox:'[name*="select-item"]',featured:'[name="featured"]',details:"details"}},this.statusMapping={received:"Image Received",local_processing:"Processing Image...",queued:"Waiting to upload...",uploading:"Uploading to Server",pending:"Successfully sent to server. In line for further processing.",processing:"Processing on server...",completed:"Upload complete!",failed:"Upload failed (will retry)",failed_permanent:"Upload failed permanently"},this.sortableInstances=new Map,this.sortableConfig={animation:150,draggable:".item",handle:".select-item-label, img",ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",onEnd:e=>{this.handleReorder(e)}},this.init()}async init(){await this.loadFields(),await this.loadUploads(),this.initializeFields(),this.initListeners(),this.queue.subscribe(((e,t)=>{if("uploads"!==t.endpoint&&"uploads/meta"!==t.endpoint)return;const s=t.data instanceof FormData?t.data.get("fieldId"):t.data.fieldId;switch(e){case"cancel-operation":s&&this.clearField(s);break;case"operation-status":s&&this.updateFieldStatus(s,t.status);break;case"operation-complete":(t.result?.data||[]).forEach((e=>{const t=this.uploads.get(e.upload_id);t&&(t.attachmentId=e.attachment_id,t.status="completed",this.uploads.set(t.id,t))})),s&&this.cleanField(s)}})),window.addEventListener("beforeunload",(()=>{this.cleanupAllPreviewUrls()}))}initWorker(){this.worker={worker:null,timeout:null,tasks:new Map,restart:{count:0,max:3},settings:{timeout:1e4,batchSize:1,maxConcurrent:3,restartAfterTimeout:!0}}}initializeFields(){document.querySelectorAll(this.selectors.field.field).forEach((e=>{this.registerUploader(e)}))}scanFields(e){e.querySelectorAll(this.selectors.field.field).forEach((e=>{this.registerUploader(e)}))}registerUploader(e){const t=this.determineFieldId(e),s=this.extractFieldConfig(e),o={id:t,config:s,element:e,ui:this.buildFieldUI(e),uploads:new Set,groups:new Set,state:"ready"};return this.fields.set(t,o),e.dataset.uploader=t,this.addFieldSelectionHandler(t),"post_group"!==s.destination||this.dragController||this.initGroupFeatures(),"single"!==s.type&&this.initSortable(o),t}extractFieldConfig(e){return{destination:e.dataset.destination||"meta",content:e.dataset.content||null,mode:e.dataset.mode||"direct",type:e.dataset.type||"single",name:e.dataset.field,itemID:e.dataset.itemId||0,maxFiles:parseInt(e.dataset.maxFiles)||999,subtype:e.dataset.subtype||"image"}}buildFieldUI(e){let t={field:e,input:e.querySelector(this.selectors.field.input),dropZone:e.querySelector(this.selectors.field.dropZone),preview:e.querySelector(this.selectors.field.preview),progress:{progress:e.querySelector(this.selectors.field.progress),bar:e.querySelector(".bar"),fill:e.querySelector(".fill"),details:e.querySelector(".details"),text:e.querySelector(".details .text"),count:e.querySelector(".details .count")}},s=e.querySelector(".group-display");return s&&(t.groups={display:s,container:e.querySelector(".item-grid.groups"),empty:e.querySelector(".empty-group"),groups:new Map}),t}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),this.dragEnterHandler=this.handleExternalDragEnter.bind(this),this.dragLeaveHandler=this.handleExternalDragLeave.bind(this),this.dragOverHandler=this.handleExternalDragOver.bind(this),this.dropHandler=this.handleExternalDrop.bind(this),document.addEventListener("dragenter",this.dragEnterHandler),document.addEventListener("dragleave",this.dragLeaveHandler),document.addEventListener("dragover",this.dragOverHandler),document.addEventListener("drop",this.dropHandler)}initGroupFeatures(){this.dragController=new window.jvbDragHandler({draggableSelector:this.selectors.items.item,dropTargetSelector:`${this.selectors.field.preview}, ${this.selectors.groups.grid}, .empty-group`,ignoreSelector:"input:not(.upload-select), button, select, textarea, details, summary, a",previewElement:"img, video, .icon",getItemId:e=>e.dataset.uploadId,getSelectedItems:e=>{const t=this.getFieldIdFromElement(e),s=e.dataset.uploadId,o=this.getCurrentSelection(t);return o&&o.includes(s)?o:[s]},validateDrop:(e,t)=>{const s=this.getFieldIdFromElement(t),o=document.querySelector(`[data-upload-id="${e[0]}"]`);return s===this.getFieldIdFromElement(o)},onDrop:(e,t)=>{this.handleItemDrop(e,t),t.scrollIntoView({behavior:"smooth",block:"center"})},onDragStart:e=>{},onDragEnd:(e,t)=>{if(t){const t=document.querySelector(`[data-upload-id="${e[0]}"]`),s=this.getFieldIdFromElement(t),o=this.selectionHandlers.get(s);o?.clearSelection()}},previewOptions:{multiOffset:{x:-60,y:-80},singleOffset:{x:-50,y:-60},showCount:!0}})}initSortable(e){if(!window.Sortable)return;const t=e.element.querySelector(".item-grid:not(.group)");t&&this.sortableInstances.set(`${e.id}-main`,new Sortable(t,{...this.sortableConfig,group:{name:e.id,pull:!0,put:!0}}));e.element.querySelectorAll(".item-grid.group").forEach(((t,s)=>{this.sortableInstances.set(`${e.id}-group-${s}`,new Sortable(t,{...this.sortableConfig,group:{name:e.id,pull:!0,put:!0}}))}))}handleReorder(e){const t=e.to.closest(".field, .upload");if(!t)return;const s=t.closest("form");if(!s)return;const o=s.dataset.formId;if(o&&window.jvbForms){const e=window.jvbForms.forms?.get(o);e?.options.autosave&&window.jvbForms.scheduleSave(e,1e3)}window.jvbA11y&&window.jvbA11y.announce("Item reordered"),t.dispatchEvent(new CustomEvent("jvb-items-reordered",{detail:{from:e.from,to:e.to,oldIndex:e.oldIndex,newIndex:e.newIndex},bubbles:!0}))}handleExternalDragLeave(e){const t=e.target.closest(this.selectors.field.dropZone);t&&!t.contains(e.relatedTarget)&&t.classList.remove("dragover")}handleExternalDragEnter(e){if(!e.dataTransfer.types.includes("Files"))return;const t=e.target.closest(this.selectors.field.dropZone);t&&(e.preventDefault(),t.classList.add("dragover"))}handleExternalDragOver(e){if(!e.dataTransfer.types.includes("Files"))return;e.target.closest(this.selectors.field.dropZone)&&(e.preventDefault(),e.dataTransfer.dropEffect="copy")}handleExternalDrop(e){const t=e.target.closest(this.selectors.field.dropZone);if(!t)return;e.preventDefault(),t.classList.remove("dragover");const s=Array.from(e.dataTransfer.files);if(0===s.length)return;const o=this.getFieldIdFromElement(t);o?(this.processFiles(o,s),this.a11y.announce(`${s.length} file(s) dropped for upload`)):console.error("No field ID found for drop zone")}handleItemDrop(e,t){const s=t.classList.contains("preview");let o=t;if(t.classList.contains("empty-group")){const e=this.getFieldIdFromElement(t),s=this.createGroup(e);if(!s)return void console.error("Failed to create group");o=s.grid}e.forEach((e=>{s?this.removeFromGroup(e):this.addToGroup(e,o)}));const r=this.getFieldIdFromElement(t);this.schedulePersistance(r);const i=e.length>1?`Moved ${e.length} items`:"Moved item";this.a11y.announce(i)}handleClick(e){if(e.target.matches(this.selectors.field.dropZone)||e.target.closest(this.selectors.field.dropZone)){const t=e.target.closest(this.selectors.field.dropZone);if(t&&!e.target.matches("input, button, a")){const e=t.querySelector(this.selectors.field.input);e?.click()}}const t=e.target.closest("[data-action]");t&&this.handleAction(t)}handleChange(e){const t=this.getFieldIdFromElement(e.target);if(e.target.matches(this.selectors.field.input)){const t=this.getFieldIdFromElement(e.target),s=Array.from(e.target.files);s.length>0&&t&&this.processFiles(t,s)}t&&("post_group"===this.fields.get(t).config.destination?this.handleGroupMetaChange(e.target):this.queueUploadMeta(e))}getCurrentSelection(e){let t=[];for(let[s,o]of this.selectionHandlers)(e===s||s.includes(e))&&o.selectedItems.size>0&&(t=t.concat([...o.selectedItems]));return t}getSubtypeFromMime(e){return e.startsWith("image/")?"image":e.startsWith("video/")?"video":"document"}getStatusText(e){return this.statusMapping[e]||e}getStatusIcon(e){return window.getIcon(this.queue.icons[e])}getStatusProgress(e){switch(e){case"local_processing":return 28;case"queued":return 50;case"uploading":return 66;case"pending":return 75;case"processing":return 89;case"completed":return 100;default:return 0}}getModalType(e){if(void 0!==e._cachedModalType)return e._cachedModalType;if(!e||!e.element)return e._cachedModalType=null,null;const t=e.element.closest("dialog");if(!t)return e._cachedModalType=null,null;let s=null;return s=t.classList.contains("edit")?"edit":t.classList.contains("create")?"create":t.classList.contains("bulkEdit")?"bulkEdit":t.className,e._cachedModalType=s,s}handleAction(e){const t=e.dataset.action,s=this.getFieldIdFromElement(e);switch(t){case"add-to-group":this.handleAddToGroup(e);break;case"delete-group":this.handleDeleteGroup(e);break;case"delete-upload":case"remove-from-group":this.handleRemoveItem(e);break;case"upload":this.fields.get(s).element.closest("details").open=!1,document.body.classList.add("uploading"),this.submitUploads(s);break;case"restore":this.handleRestoreUploads().then((()=>{}));break;case"clear-cache":confirm("Save these uploads for later?")||this.cleanupStoredUploads(),this.cleanupRestore()}}handleAddToGroup(e){const t=e.closest(this.selectors.field.field),s=t?.dataset.uploader;if(!s)return;const o=this.selected.get(s);if(o&&0!==o.size){const e=this.createGroup(s);if(!e)return;o.forEach((t=>{this.addToGroup(t,e.grid)}));const t=this.selectionHandlers.get(s);t?.clearSelection(),this.a11y.announce(`Created group with ${o.size} items`)}else this.createGroup(s);this.schedulePersistance(s)}handleDeleteGroup(e){const t=e.closest(this.selectors.groups.container);if(!t)return;const s=t.dataset.groupId,o=this.getFieldIdFromElement(t);if(!confirm("Delete this group? Items will be moved back to the upload area."))return;t.querySelectorAll(this.selectors.items.item).forEach((e=>{const t=e.dataset.uploadId;this.removeFromGroup(t)})),this.deleteGroup(s),this.a11y.announce("Group deleted, items returned to upload area"),this.schedulePersistance(o)}handleRemoveItem(e){const t=e.closest(this.selectors.items.item);if(!t)return;const s=t.dataset.uploadId,o=this.getFieldIdFromElement(t);confirm("Remove this item?")&&(this.removeUpload(o,s),this.a11y.announce("Item removed"),this.schedulePersistance(o))}addFieldSelectionHandler(e){if(this.selectionHandlers.has(e))return this.selectionHandlers.get(e);const t=this.fields.get(e);if(!t)return;const s=t.ui.field;if(!s)return;const o=new window.jvbHandleSelection({container:s,ui:{selectAll:s.querySelector('[name="select-all-uploads"]'),bulkControls:s.querySelector(".selection-actions"),count:s.querySelector(".selection-count")},itemSelector:"[data-upload-id]",checkboxSelector:'[name*="select-item"]'});return o.subscribe(((t,s)=>{switch(t){case"item-selected":case"item-deselected":case"range-selected":this.selected.set(e,s.selectedItems);break;case"select-all":this.handleSelectAll(s.container,s.selected)}})),this.selectionHandlers.set(e,o),o}addGroupSelectionHandler(e,t){const s=`${e}_${t}`;if(this.selectionHandlers.has(s))return this.selectionHandlers.get(s);const o=this.groups.get(t);if(!o)return;const r=new window.jvbHandleSelection({container:o.element,ui:{selectAll:o.element.querySelector(this.selectors.groups.selectAll),bulkControls:o.element.querySelector(this.selectors.groups.actions),count:o.element.querySelector(this.selectors.groups.count)},itemSelector:"[data-upload-id]",checkboxSelector:'[name*="select-item"]'});return r.subscribe(((t,s)=>{switch(t){case"item-selected":case"item-deselected":case"range-selected":this.selected.set(e,s.selectedItems);break;case"select-all":this.handleSelectAll(s.container,s.selected)}})),this.selectionHandlers.set(s,r),r}handleSelectAll(e,t){}determineFieldId(e){return`${e.dataset.content||e.closest("dialog")?.dataset.content||e.closest("form")?.dataset.save||""}_${e.dataset.itemId||e.closest("dialog")?.dataset.itemId||""}_${e.dataset.field||""}`}getFromElement(e,t){const s={field:{selector:this.selectors.field.field,key:"uploader",store:this.fields},upload:{selector:this.selectors.items.item,key:"uploadId",store:this.uploads},group:{selector:this.selectors.groups.container,key:"groupId",store:this.groups}}[t];if(!s)return null;const o=e.closest(s.selector);if(!o)return null;const r=o.dataset[s.key];return s.store.get(r)}getFieldFromElement(e){return this.getFromElement(e,"field")}getUploadFromElement(e){return this.getFromElement(e,"upload")}getGroupFromElement(e){return this.getFromElement(e,"group")}getFieldIdFromElement(e){return this.getFromElement(e,"field")?.id??null}getUploadIdFromElement(e){return this.getFromElement(e,"upload")?.id??null}getGroupIdFromElement(e){return this.getFromElement(e,"group")?.id??null}async processFiles(e,t){const s=this.fields.get(e);if(!s)return;s.ui.dropZone&&(s.ui.dropZone.hidden=!0),s.ui.groups.display&&(s.ui.groups.display.hidden=!1);const o=t.length;let r=0;this.updateUploadProgress(e,0,o,"Processing files..."),s.uploads||(s.uploads=new Set);const i=Array.from(t).map((async(t,i)=>{try{const i=`upload_${Date.now()}_${Math.random().toString(36).substr(2,9)}`,a={id:i,attachment_id:null,fieldId:e,originalFile:t,processedFile:null,preview:null,status:"local_processing",element:null,location:null,meta:{originalName:t.name,size:t.size,type:t.type}};a.preview=this.createPreviewUrl(t),t.type.startsWith("image/")?a.processedFile=await this.processImage(t,s.subtype):a.processedFile=t,await this.uploadStore.saveBlob(i,a.processedFile||t);const l=this.getSubtypeFromMime(t.type);return a.element=this.createUploadElement({...a,subtype:l},"post_group"===s.config.destination),this.showUploadProgress(i,!0),this.updateUploadItemProgress(i,50,"local_processing"),s.ui.preview&&(s.ui.preview.appendChild(a.element),a.location=s.ui.preview),this.uploads.set(i,a),s.uploads.add(i),r++,this.updateUploadProgress(e,r,o,"Processing files..."),this.updateUploadItemProgress(i,100,"processed"),a.status="processed",setTimeout((()=>{this.showUploadProgress(i,!1)}),1e3),i}catch(s){return console.error("Error processing file:",t.name,s),r++,this.updateUploadProgress(e,r,o,"Processing files..."),null}}));await Promise.all(i),this.updateFieldState(e),await this.schedulePersistance(e),"post_group"!==s.config.destination&&(await this.queueUpload(e),this.maybeLockUploads(e))}updateFieldState(e){const t=this.fields.get(e);if(!t||!t.ui.field)return;const s=t.ui.field,o=t.uploads?.size||0,r=t.ui.groups?.container?.querySelectorAll(".upload-group").length>0;s.dataset.hasUploads=o>0?"true":"false",s.dataset.uploadCount=o.toString(),s.dataset.hasGroups=r?"true":"false",t.ui.preview&&t.ui.preview.setAttribute("aria-label",`Upload preview area with ${o} item${1!==o?"s":""}`)}updateUploadProgress(e,t,s,o){const r=this.fields.get(e);if(!r?.ui?.progress?.progress)return;const i=r.ui.progress,a=s>0?t/s*100:0;i.fill&&(i.fill.style.width=`${a}%`),i.text&&(i.text.textContent=o),i.count&&(i.count.textContent=`${t}/${s}`),i.progress.hidden=t===s}updateFieldStatus(e,t){const s=this.fields.get(e);s&&(s.state=t)}updateUploadStatus(e,t){const s=this.uploads.get(e);s&&(s.status=t,this.updateUploadUI(e))}updateUploadUI(e){const t=this.uploads.get(e);if(!t?.element)return;t.element.className=t.element.className.replace(/status-[\w-]+/g,""),t.element.classList.add(`status-${t.status}`);t.element.querySelector(".progress")&&this.updateUploadItemProgress(e,this.getStatusProgress(t.status),t.status)}showUploadProgress(e,t=!0){const s=this.uploads.get(e);if(!s||!s.element)return;const o=s.element.querySelector(".progress");o&&(t?(o.style.removeProperty("animation"),o.hidden=!1):(o.style.animation="fadeOut var(--transition-base)",setTimeout((()=>{o.hidden=!0}),300)))}updateUploadItemProgress(e,t,s=null){const o=this.uploads.get(e);if(!o||!o.element)return;const r=o.element.querySelector(".progress");if(!r)return;const i=r.querySelector(".fill"),a=r.querySelector(".details"),l=r.querySelector(".icon");i&&(i.style.width=`${t}%`),s&&a&&(a.textContent=this.getStatusText(s)),s&&l&&(l.innerHTML=this.getStatusIcon(s).outerHTML)}checkFieldLimits(e,t){const s=this.fields.get(e);if(!s)return!1;return(s.uploads?.size||0)+t<=s.maxFiles}validateFile(e,t){return this.settings.allowedTypes.includes(e.type)?!(e.size>this.settings.maxFileSize)||(this.notify(`File too large: ${this.formatBytes(e.size)}`,"error"),!1):(this.notify(`Invalid file type: ${e.type}`,"error"),!1)}formatBytes(e,t=2){if(0===e)return"0 Bytes";const s=t<0?0:t,o=Math.floor(Math.log(e)/Math.log(1024));return parseFloat((e/Math.pow(1024,o)).toFixed(s))+" "+["Bytes","KB","MB","GB"][o]}shouldProcessClientSide(e,t){return!("image"!==t||!e.type.startsWith("image/"))}async processImage(e,t){const s=this.worker.settings.timeout;return new Promise(((o,r)=>{let i,a=!1;i=setTimeout((()=>{a||(a=!0,this.worker.tasks.delete(t),this.worker.settings.restartAfterTimeout&&this.restartCompressionWorker(),r(new Error(`Processing timeout for ${e.name}`)))}),s),this.worker.tasks.set(t,{file:e,timeoutId:i}),this.handleProcess(e,t).then((e=>{a||(a=!0,clearTimeout(i),this.worker.tasks.delete(t),o(e))})).catch((e=>{a||(a=!0,clearTimeout(i),this.worker.tasks.delete(t),r(e))}))}))}async handleProcess(e,t){if(!e.type.startsWith("image/"))return e;const s=this.getMaxDimension();if(this.shouldUseWorker(e))try{if(this.worker.worker||this.initCompressionWorker(),this.worker.worker)return await this.processWithWorker(e,t,s,.85)}catch(e){console.warn("Worker processing failed, falling back to main thread:",e)}return await this.processOnMainThread(e,s,.85)}async processOnMainThread(e,t,s){return new Promise(((o,r)=>{const i=new Image,a=document.createElement("canvas"),l=a.getContext("2d");let n=null;const d=()=>{i.onload=null,i.onerror=null,n&&(URL.revokeObjectURL(n),n=null),a.width=1,a.height=1,l.clearRect(0,0,1,1)};i.onload=()=>{try{const{width:n,height:c}=this.calculateOptimalDimensions(i,t);a.width=n,a.height=c,l.imageSmoothingEnabled=!0,l.imageSmoothingQuality="high",l.drawImage(i,0,0,n,c);const u=this.getOptimalFormat(e),p=this.getOptimalQuality(e,s);a.toBlob((t=>{if(d(),t){const s=new File([t],this.getProcessedFileName(e,u),{type:u,lastModified:Date.now()});o(s)}else r(new Error("Canvas toBlob failed"))}),u,p)}catch(e){d(),r(new Error(`Canvas processing failed: ${e.message}`))}},i.onerror=()=>{d(),r(new Error(`Failed to load image: ${e.name}`))};try{n=this.createPreviewUrl(e),i.src=n}catch(e){d(),r(new Error(`Failed to create object URL: ${e.message}`))}}))}getOptimalFormat(e){return"image/gif"===e.type||"image/svg+xml"===e.type?e.type:this.supportsWebP()?"image/webp":"image/jpeg"}getOptimalQuality(e,t){return e.size<512e3?Math.max(t,.9):e.size<2097152?t:Math.min(t,.8)}getProcessedFileName(e,t){return e.name.replace(/\.[^/.]+$/,"")+({"image/webp":".webp","image/jpeg":".jpg","image/png":".png","image/gif":".gif"}[t]||".jpg")}getMaxDimension(){const e=window.screen.width,t=window.devicePixelRatio||1;return e*t>2560?2400:e*t>1920?1920:1200}shouldUseWorker(e){return this.worker.worker&&e.size>1048576&&"undefined"!=typeof OffscreenCanvas}async processWithWorker(e,t,s,o){return new Promise(((r,i)=>{if(!this.worker.worker)return void i(new Error("Worker not available"));const a=`${t}_${Date.now()}`,l=t=>{if(t.data.messageId===a)if(this.worker.worker.removeEventListener("message",l),this.worker.worker.removeEventListener("error",n),t.data.success){const s=new File([t.data.blob],this.getProcessedFileName(e,t.data.format||"image/webp"),{type:t.data.format||"image/webp",lastModified:Date.now()});r(s)}else i(new Error(t.data.error||"Worker processing failed"))},n=e=>{this.worker.worker.removeEventListener("message",l),this.worker.worker.removeEventListener("error",n),i(new Error(`Worker error: ${e.message}`))};this.worker.worker.addEventListener("message",l),this.worker.worker.addEventListener("error",n),this.worker.worker.postMessage({messageId:a,file:e,maxDimension:s,quality:o,outputFormat:this.getOptimalFormat(e)})}))}restartCompressionWorker(){this.worker.worker&&(this.worker.worker.terminate(),this.worker.worker=null),this.worker.tasks.clear(),this.worker.restart.count>=this.worker.restart.max?console.error("Max worker restarts reached, disabling worker"):(this.worker.restart.count++,this.initCompressionWorker())}initCompressionWorker(){if(!this.worker.worker&&"undefined"!=typeof Worker)try{const e=new Blob(["\n            self.onmessage = async function(e) {\n                const { messageId, file, maxDimension, quality, outputFormat } = e.data;\n\n                try {\n                    // Create ImageBitmap from file\n                    const bitmap = await createImageBitmap(file);\n\n                    // Calculate dimensions\n                    const scale = Math.min(maxDimension / bitmap.width, maxDimension / bitmap.height, 1);\n                    const width = Math.round(bitmap.width * scale);\n                    const height = Math.round(bitmap.height * scale);\n\n                    // Create OffscreenCanvas\n                    const canvas = new OffscreenCanvas(width, height);\n                    const ctx = canvas.getContext('2d');\n\n                    // Draw and resize\n                    ctx.imageSmoothingEnabled = true;\n                    ctx.imageSmoothingQuality = 'high';\n                    ctx.drawImage(bitmap, 0, 0, width, height);\n\n                    // Clean up bitmap\n                    bitmap.close();\n\n                    // Convert to blob\n                    const blob = await canvas.convertToBlob({\n                        type: outputFormat,\n                        quality: quality\n                    });\n\n                    self.postMessage({\n                        messageId,\n                        success: true,\n                        blob: blob,\n                        format: outputFormat\n                    });\n\n                } catch (error) {\n                    self.postMessage({\n                        messageId,\n                        success: false,\n                        error: error.message\n                    });\n                }\n            };\n        "],{type:"application/javascript"});this.worker.worker=new Worker(this.createPreviewUrl(e))}catch(e){console.warn("Failed to initialize compression worker:",e),this.worker.worker=null}}calculateOptimalDimensions(e,t){let{width:s,height:o}=e;if(s<=t&&o<=t)return{width:s,height:o};const r=Math.min(t/s,t/o);return{width:Math.round(s*r),height:Math.round(o*r)}}supportsWebP(){return 0===document.createElement("canvas").toDataURL("image/webp").indexOf("data:image/webp")}createPreviewUrl(e){const t=URL.createObjectURL(e);return this.previewUrls||(this.previewUrls=new Set),this.previewUrls.add(t),t}revokePreviewUrl(e){e?.startsWith("blob:")&&(URL.revokeObjectURL(e),this.previewUrls?.delete(e))}maybeLockUploads(e){const t=this.fields.get(e);if(!t?.ui?.dropZone)return;if("post_group"===t.config.destination)return;const s=t.uploads?.size||0,o=t.config?.maxFiles||999;t.ui.dropZone.hidden=s>=o,t.element.classList.toggle("at-max-uploads",s>=o)}createUploadElement(e,t=!1){let s=window.getTemplate("uploadItem");if(!s)return void console.error("Image template not found");s.dataset.uploadId=e.id,e.originalFile&&(s.dataset.subtype=this.getSubtypeFromMime(e.originalFile.type)),s.querySelector('[name="featured"]').value=e.id;let[o,r,i,a,l]=[s.querySelector('[name="featured"]'),s.querySelector("img"),s.querySelector("video"),s.querySelector("label > span"),s.querySelector("details")];switch([o.value,r.src,r.alt]=[e.id,e.preview,e.originalFile?.name??e.meta?.originalName??""],s.dataset.subtype){case"image":[r.src,r.alt]=[e.preview,e.originalFile?.name??e.meta?.originalName??""],i.remove(),a.remove();break;case"video":i.src=e.preview,r.remove(),a.remove();break;case"document":const t=e.originalFile?.name??e.meta?.originalName??"",s=t.split(".").pop()?.toLowerCase()??"",o={pdf:"file-pdf",csv:"file-csv",doc:"file-doc",docx:"file-doc",txt:"file-txt",xls:"file-xls",xlsx:"file-xls"},l=window.getIcon(o[s]||"file");a.innerText=e.originalFile.name,a.prepend(l),r.remove(),i.remove()}if(l){let e=window.getTemplate("uploadMeta");e&&l.append(e)}return s.draggable=t,s.querySelectorAll("input").forEach((t=>{let s=t.id;if(s){let o=s+e.id,r=t.parentNode.querySelector(`label[for="${s}"]`);t.id=o,r&&(r.htmlFor=o)}})),s}async submitUploads(e){const t=this.fields.get(e);if(!t?.uploads||0===t.uploads.size)return;let s=Array.from(t.uploads);if(0===s.length)return void this.error.log("No uploads to upload",{component:"UploadManager",action:"submitGroupedUploads",fieldId:e});const o=this.getFieldGroups(e);if(0===o.length)return void this.error.log("No groups created for post_group upload",{component:"UploadManager",action:"submitGroupedUploads",fieldId:e});const r=[],i=new FormData;let a=[];s=s.map((e=>this.uploads.get(e))),o.forEach(((e,t)=>{const o={images:[],fields:{}};for(let[t,s]of Object.entries(e.changes))o.fields[t]=s;s.filter((t=>t.groupId===e.id)).forEach((e=>{if(e){const t=e.processedFile||e.originalFile;if(t){i.append("files[]",t);const s={upload_id:e.id,index:a.length};o.images.push(s),a.push(e.id)}}})),r.push(o)})),s.filter((e=>!Object.hasOwn(e,"groupId"))).forEach((e=>{if(e){const t={images:[],fields:{}},s=e.processedFile||e.originalFile;if(s){i.append("files[]",s);const o={upload_id:e.id,index:a.length};t.images.push(o),a.push(e.id)}r.push(t)}})),i.append("content",t.config.content),i.append("user",t.config.itemID),i.append("posts",JSON.stringify(r)),i.append("upload_ids",JSON.stringify(a));for(const[e,t]of i.entries())console.log(e,t);const l={endpoint:"uploads/groups",method:"POST",data:i,title:`Creating ${r.length} ${t.config.content}${r.length>1?"s":""} from uploads...`,popup:`Creating ${r.length} post${r.length>1?"s":""}...`,canMerge:!1,headers:{action_nonce:jvbSettings.dash},append:"_upload"};try{const e=await this.queue.addToQueue(l);return s.forEach((t=>{let s=this.uploads.get(t);s&&(s.operationId=e,this.updateUploadStatus(t,"queued"))})),t.operationId=e,this.a11y.announce(`Creating ${r.length} post${r.length>1?"s":""} from your uploads`),e}catch(t){throw this.error.log(t,{component:"UploadManager",action:"submitGroupedUploads",fieldId:e}),t}finally{this.schedulePersistance(t.id)}}async queueUpload(e){const t=this.fields.get(e);if(!t?.uploads)return;const s=Array.from(t.uploads);if(0===s.length)return;const o=this.prepareUploadData(t,s);this.a11y.announce("Queuing for upload");let r=1===s.length?"file":"files";const i={endpoint:"uploads",method:"POST",data:o,title:`Uploading ${s.length} ${r} to server...`,popup:`Uploading ${s.length} ${r}...`,canMerge:!1,headers:{action_nonce:jvbSettings.dash},append:"_upload"};try{const e=await this.queue.addToQueue(i);return s.forEach((t=>{let s=this.uploads.get(t);s&&(s.operationId=e,this.updateUploadStatus(t,"queued"))})),t.operationId=e,e}catch(e){throw e}finally{this.schedulePersistance(t.id)}}prepareUploadData(e,t){const s=new FormData;s.append("content",e.config.content),s.append("mode",e.config.mode),s.append("field_name",e.config.name),s.append("fieldId",e.id),s.append("field_type",e.config.type),s.append("subtype",e.config.subtype),s.append("item_id",e.config.itemID),s.append("destination",e.config.destination||"meta");let o=[];const r=this.getFieldGroups(e.id);if("post_group"===e.config.destination&&r.length>0){let e=[],t=[],i=[];r.forEach((r=>{let a=[],l=null;r.uploads.forEach((e=>{let t=this.uploads.get(e);if(t){const e=t.processedFile||t.originalFile;if(e){s.append("files[]",e);o.length;o.push(t.id),a.push(t.id);const r=t.element?.querySelector('[name="featured"]');r?.checked&&(l=t.id)}}})),e.push(a),t.push(r.title||""),i.push(l)})),s.append("groups",JSON.stringify(e)),s.append("group_titles",JSON.stringify(t)),s.append("featured_images",JSON.stringify(i))}else t.forEach((e=>{let t=this.uploads.get(e);if(t){const e=t.processedFile||t.originalFile;e&&(s.append("files[]",e),o.push(t.id))}}));return s.append("upload_ids",JSON.stringify(o)),s}getFieldGroups(e){const t=[];return this.groups.forEach(((s,o)=>{if(s.fieldId===e){const r=this.fields.get(e),i=r?.ui?.groups?.groups?.get(o);t.push({id:o,uploads:Array.from(s.uploads||new Set),changes:s.changes||{},element:i||null})}})),t}async queueUploadMeta(e){const t=this.getUploadFromElement(e.target);if(!t)return;if(!this.fields.get(t.fieldId))return;if(!e.target.closest(".upload-meta"))return;let s={};s[e.target.name]=e.target.value,t.meta={...t.meta,...s};let o={};o[t.attachmentId??t.id]=t.meta;const r={endpoint:"uploads/meta",method:"POST",data:o,title:"Updating meta",canMerge:!0,headers:{action_nonce:jvbSettings.dash}};try{await this.queue.addToQueue(r)}catch(e){this.error.log(e,{component:"UploadManager",action:"sendMetaUpdate",uploadId:t.id})}}createGroup(e,t=null){const s=this.fields.get(e);if(!s)return console.error("Field not found:",e),null;t||(t=`group_${Date.now()}_${Math.random().toString(36).substr(2,9)}`);const o=this.createGroupElement(t,e);if(!o)return console.error("Failed to create group element"),null;s.ui.groups||(s.ui.groups={groups:new Map,container:null,empty:null,display:null}),s.ui.groups.groups.set(t,o),s.ui.groups.container&&s.ui.groups.empty?s.ui.groups.container.insertBefore(o,s.ui.groups.empty):s.ui.groups.container&&s.ui.groups.container.appendChild(o);const r={id:t,fieldId:e,element:o,grid:o.querySelector(".item-grid.group"),uploads:new Set,changes:{}};return this.groups.set(t,r),this.addGroupSelectionHandler(e,t),this.schedulePersistance(e),r}createGroupElement(e,t){let s=window.getTemplate("imageGroup");if(!s)return;s.dataset.groupId=e,s.dataset.fieldId=t;let o=window.getTemplate("groupMetadata");const r=s.querySelector(".fields");if(r&&o){r.append(o);const i=r.querySelector('[name="post_title"]'),a=r.querySelector('[name="post_excerpt"]');i&&(i.id=`${e}_title`,i.name=`${e}[post_title]`),a&&(a.id=`${e}_excerpt`,a.name=`${e}[post_excerpt]`);let l=this.fields.get(t);if(""!==l.config.content){s.querySelector("summary").textContent=l.config.content+" Fields"}}else s.querySelector("details").remove();const i=s.querySelector(".item-grid.group");return i&&(i.dataset.groupId=e),s}deleteGroup(e,t=!0){let s=this.groups.get(e);if(!s)return;let o=!0;t&&s.uploads&&s.uploads.size>0&&(o=!window.confirm("Delete uploads in group?")),t&&o&&s.uploads&&s.uploads.size>0&&Array.from(s.uploads).forEach((e=>{this.addImageToGroup(e,null,!1)})),this.groups.delete(e);let r=s.element;r&&(r.remove(),this.a11y.announce("Group removed")),this.schedulePersistance(s.fieldId)}addToGroup(e,t=null,s=!0){let o=this.uploads.get(e);if(!o)return;let r=this.fields.get(o.fieldId);if(!r)return;if(!t&&o.location===r.ui.preview||t===o.location)return;if(o.location){let t=o.location.dataset.groupId;if(t){let s=this.groups.get(t);s&&s.uploads&&(s.uploads.delete(e),0===s.uploads.size&&this.deleteGroup(t))}}const i=o.element.querySelector('[name*="select-item"]');i&&(i.checked=!1);let a=o.element.querySelector('[name="featured"]');if(a.hidden=!t,t){if(!t.classList.contains("item-grid")||!t.classList.contains("preview")){let s=t.dataset.groupId;a.name=s+"_"+a.name;let r=this.groups.get(s);r||(r=this.createGroup(o.fieldId),t=r.grid,s=r.id),r&&(r.uploads.add(e),o.groupId=s)}}else t=r.ui.preview,o.groupId=null;o.location=t,t.append(o.element),s&&this.schedulePersistance(r.id)}removeFromGroup(e){const t=this.uploads.get(e);if(!t)return;const s=this.fields.get(t.fieldId);if(!s)return;if(t.groupId){const s=this.groups.get(t.groupId);s?.uploads&&(s.uploads.delete(e),0===s.uploads.size&&this.deleteGroup(t.groupId,!1)),t.groupId=null}s.ui?.preview&&(s.ui.preview.appendChild(t.element),t.location=s.ui.preview);const o=t.element.querySelector('[name="featured"]');o&&(o.hidden=!0,o.checked=!1)}removeUpload(e,t){const s=this.fields.get(e),o=this.uploads.get(t);if(!s||!o)return;if(s.uploads?.delete(t),o.groupId){const e=this.groups.get(o.groupId);e&&e.uploads&&(e.uploads.delete(t),0===e.uploads.size&&this.removeGroup(o.groupId))}o.element?.remove(),this.clearUpload(t),this.updateFieldState(e),this.maybeLockUploads(e);const r=this.selectionHandlers.get(s.id);r&&r.deselect(t),this.a11y.announce("Upload removed")}schedulePersistance(e){const t=`persist_${e}`;window.debouncer.schedule(t,(()=>this.persistFieldState(e)),1e3)}async persistFieldState(e){const t=this.fields.get(e);if(!t)return;const s={...t,id:e,fieldId:e,uploads:Array.from(t.uploads||[]).map((e=>this.uploads.get(e))),groups:Array.from(this.groups.entries()).filter((([t,s])=>s.fieldId===e&&s.uploads&&s.uploads.size>0)).map((([e,t])=>({id:t.id,uploads:Array.from(t.uploads),changes:t.changes||{}}))),context:{url:this.normalizeUrl(window.location.href),fullUrl:window.location.href,modalType:this.getModalType(t),formId:t.formId,fieldSelector:`.field.upload[data-field="${t.config.name}"]`},timestamp:Date.now()};await this.fieldStore.save(s)}normalizeUrl(e){try{const t=new URL(e);return t.origin+t.pathname}catch(t){return e}}getFieldUploads(e,t=!1){const s=this.fields.get(e);return s&&s.uploads?Array.from(s.uploads).map((e=>{const s=this.uploads.get(e);return s?t?{id:s.id,fieldId:s.fieldId,status:s.status,attachmentId:s.attachmentId,operationId:s.operationId,groupId:s.groupId||null,changes:s.changes||{},meta:{originalName:s.meta?.originalName||s.originalFile?.name,size:s.meta?.size||s.originalFile?.size,type:s.meta?.type||s.originalFile?.type,title:s.meta?.title,alt:s.meta?.alt,caption:s.meta?.caption}}:s:null})).filter(Boolean):[]}async checkForStoredUploads(){if(!this.db)return;const e=this.db.transaction(["fieldStates"],"readonly").objectStore("fieldStates"),t=(await new Promise((t=>{const s=e.getAll();s.onsuccess=()=>t(s.result)}))).filter((e=>e.uploads.some((e=>!e.operationId&&("completed"===e.status||"processed"===e.status||"local_processing"===e.status||"processed-original"===e.status)))));0!==t.length&&this.showRecoveryNotification(t)}async handleRestoreUploads(){let e=document.querySelector("dialog.restore-uploads");if(!e)return;const t=this.getSelectedRestorationUploads(e);0!==t.length&&(await this.restoreSelectedUploads(t),this.cleanupRestore())}getSelectedRestorationUploads(e){let t=[];return e.querySelectorAll("[type=checkbox]:checked").forEach((e=>{const s=e.closest(".item");s&&t.push({uploadId:s.dataset.uploadId,fieldId:s.dataset.fieldId})})),t}handleGroupMetaChange(e){let t=this.getGroupFromElement(e);if(!t)return;Object.hasOwn(t,"changes")||(t.changes={});let s=e.name;if(s.includes("group")){let e=t.id+"_",o=t.id+"[";s=s.replace(e,"").replace(o,"").replace("]","")}t.changes[`${s}`]=e.value,this.groups.set(t.id,t),this.schedulePersistance(t.fieldId)}async showRecoveryNotification(e){const t=e.reduce(((e,t)=>e+t.uploads.length),0),s=e.reduce(((e,t)=>e+(t.groups?.length||0)),0);let o,r=window.getTemplate("restoreNotification");if(!r)return void console.error("Restore notification template not found");if(s>0){o=`${s} ${s>1?"groups":"group"} with ${t} ${t>1?"uploads":"upload"} can be restored.`}else o=`${t} upload(s) from ${e.length} field(s) can be recovered.`;const i=r.querySelector(".restore-details");i&&(i.textContent=o);for(const t of e){let e=window.getTemplate("restoreField");if(!e)continue;const s=e.querySelector("h3");s&&(s.textContent=t.config.name||"Unnamed Field");const o=e.querySelector(".item-grid.restore");for(const e of t.uploads){let s=window.getTemplate("uploadItem");if(!s)continue;const r=await this.uploadStore.getBlob(e.id);if(r)try{const o=new Blob([r.data],{type:r.type}),i=this.createPreviewUrl(o);let[a,l,n,d,c]=[s.querySelector('[name="featured"]'),s.querySelector("img"),s.querySelector("video"),s.querySelector("label > span"),s.querySelector("details")];s.dataset.uploadId=e.id,s.dataset.fieldId=t.id;let u=this.getSubtypeFromMime(r.type);switch(s.dataset.subtype=u,u){case"image":[l.src,l.alt]=[i,e.originalFile?.name??e.meta?.originalName??""],n.remove(),d.remove();break;case"video":n.src=i,l.remove(),d.remove();break;case"document":let t;switch(""){case"pdf":t=window.getIcon("file-pdf");break;case"csv":t=window.getIcon("file-csv");break;case"doc":t=window.getIcon("file-doc");break;case"txt":t=window.getIcon("file-txt");break;case"xls":t=window.getIcon("file-xls");break;default:t=window.getIcon("file")}d.innerText=e.originalFile.name,d.prepend(t),l.remove(),n.remove()}s.dataset.previewUrl=i}catch(t){console.warn("Failed to create preview for upload:",e.id,t)}const i=s.querySelector("summary span");i&&(i.textContent=e.meta?.originalName||"Unknown file");const a=s.querySelector("details");a&&e.meta&&(a.textContent=`${this.formatBytes(e.meta.size)} â€¢ ${e.meta.type}`),s.querySelectorAll("input").forEach((t=>{let s=t.id;if(s){let o=s+e.id,r=t.parentNode.querySelector(`label[for="${s}"]`);t.id=o,r&&(r.htmlFor=o)}})),o&&o.appendChild(s)}r.querySelector(".wrap").appendChild(o)}document.querySelector(".field.upload").appendChild(r),r=document.querySelector("dialog.restore-uploads"),this.restoreModal=new window.jvbModal(r),this.restoreSelection=new window.jvbHandleSelection({container:r,ui:{selectAll:r.querySelector("#select-all-restore"),count:r.querySelector(".selection-count")}}),this.restoreModal.handleOpen()}async restoreSelectedUploads(e){const t=new Map;if(e.forEach((e=>{t.has(e.fieldId)||t.set(e.fieldId,[]),t.get(e.fieldId).push(e.uploadId)})),!this.db)return;const s=this.db.transaction(["fieldStates"],"readonly").objectStore("fieldStates");for(const[e,o]of t.entries()){const t=s.get(e),r=await new Promise((e=>{t.onsuccess=()=>e(t.result),t.onerror=()=>e(null)}));r&&(r.uploads=r.uploads.filter((e=>o.includes(e.id))),await this.restoreField(r))}}async restoreField(e){const{config:t,context:s,uploads:o,groups:r,id:i}=e;s.modalType&&await this.openModalForRestore(s);let a=document.querySelector(`.field.upload[data-field="${t.name}"]`);if(!a){const e=`${t.content}_${t.itemID}_${t.name}`;a=document.querySelector(`.field.upload[data-uploader="${e}"]`)}if(!a)return void console.warn(`Field ${t.name} not found for restoration`,t);let l=a.dataset.uploader;l&&this.fields.has(l)||(l=this.registerUploader(a,t));const n=this.fields.get(l);if(n){n.state=e.state||"ready",n.ui=this.buildFieldUI(a),n.ui.groups?.display&&(n.ui.groups.display.hidden=!1),r&&r.length>0&&await this.restoreGroups(l,r);for(const e of o)await this.restoreUpload(n,e);this.updateFieldState(l),this.maybeLockUploads(l),"direct"===t.mode&&"post_group"!==t.destination&&await this.queueUpload(l)}else console.error("Failed to register field for restoration")}async restoreUpload(e,t){const s=await this.uploadStore.getBlob(t.id);if(!s)return void console.warn("Blob data not found for upload:",t.id);{const e=s.data instanceof File?s.data:new File([s.data],s.name,{type:s.type,lastModified:s.lastModified});t.originalFile=e,t.processedFile=e,t.preview=this.createPreviewUrl(e)}e.uploads||(e.uploads=new Set),e.uploads.add(t.id);const o=this.getSubtypeFromMime(t.originalFile.type);let r;if(t.element=this.createUploadElement({...t,subtype:o},"post_group"===e.config.destination),r=t.groupId&&e.ui.groups.groups.has(t.groupId)?e.ui.groups.groups.get(t.groupId).querySelector(".item-grid"):e.ui.preview,r&&(r.appendChild(t.element),t.location=r),this.uploads.set(t.id,t),t.groupId){const e=this.groups.get(t.groupId);e&&e.uploads&&e.uploads.add(t.id)}}async restoreGroups(e,t){for(const s of t){const t=this.createGroup(e,s.id);if(t&&(s.meta&&(t.meta={...s.meta}),s.changes&&(t.changes={...s.changes}),s.title)){const e=t.element.querySelector('[name*="post_title"]');e&&(e.value=s.title)}}}async openModalForRestore(e){const{modalType:t,formId:s}=e;let o=null;switch(t){case"create":o=document.querySelector('[data-action="create"]');break;case"edit":o=document.querySelector(`[data-action="edit"][data-id="${e.itemId}"]`);break;case"bulkEdit":o=document.querySelector('[data-action="bulk-edit"]')}o&&(o.click(),await new Promise((e=>setTimeout(e,300))))}handleFieldStoreEvent(e,t){switch(e){case"data-loaded":break;case"item-saved":console.log(`Field state saved: ${t.key}`)}}handleUploadStoreEvent(e,t){switch(e){case"data-loaded":this.checkForStoredUploads();break;case"item-saved":this.showSaveIndicator(t.key)}}async saveUpload(e){const t=e.processedFile||e.originalFile||e.file;if(t instanceof File||t instanceof Blob){await this.uploadStore.saveBlob(e.id,t);const{file:s,originalFile:o,processedFile:r,...i}=e;await this.uploadStore.save(i)}else await this.uploadStore.save(e)}async loadFields(){(await this.fieldStore.getAll()).forEach((e=>{e.uploads&&Array.isArray(e.uploads)&&(e.uploads=new Set(e.uploads.map((e=>e.id)))),this.fields.set(e.fieldId,e)}))}async loadUploads(){(await this.uploadStore.getAll()).forEach((e=>{this.uploads.set(e.id,e)}))}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}destroy(){document.removeEventListener("click",this.clickHandler),document.removeEventListener("change",this.changeHandler),document.removeEventListener("dragenter",this.dragEnterHandler),document.removeEventListener("dragleave",this.dragLeaveHandler),document.removeEventListener("dragover",this.dragOverHandler),document.removeEventListener("drop",this.dropHandler),this.dragController&&this.dragController.destroy(),this.selectionHandlers.forEach((e=>e.destroy())),this.selectionHandlers.clear(),this.cleanupAllPreviewUrls(),this.sortableInstances.forEach((e=>{e?.destroy&&e.destroy()})),this.sortableInstances.clear(),this.fields.clear(),this.uploads.clear(),this.groups.clear(),this.selected.clear(),this.subscribers.clear()}destroySortable(e){Array.from(this.sortableInstances.keys()).filter((t=>t.startsWith(e))).forEach((e=>{const t=this.sortableInstances.get(e);t?.destroy&&t.destroy(),this.sortableInstances.delete(e)}))}cleanupRestore(){this.restoreModal.handleClose(),this.restoreSelection.destroy(),this.restoreSelection=null,this.restoreModal.destroy(),this.restoreModal.modal.remove(),this.restoreModal=null}async cleanupStoredUploads(){this.fieldStore.clear(),this.uploadStore.clear()}async clearField(e){await this.fieldStore.delete(e);const t=this.fields.get(e);if(t?.uploads)for(const e of t.uploads)await this.uploadStore.delete(e);this.fields.delete(e)}async clearUpload(e,t=!0){const s=this.uploads.get(e);if(s){if(this.revokePreviewUrl(s.preview),s.element){const e=s.element.dataset.previewUrl;this.revokePreviewUrl(e),delete s.element.dataset.previewUrl}t&&await this.schedulePersistance(s.fieldId),this.uploads.delete(e),this.uploadStore.delete(e),this.uploadStore.delete(e,"blobs")}}cleanupAllPreviewUrls(){this.previewUrls&&(this.previewUrls.forEach((e=>{try{URL.revokeObjectURL(e)}catch(e){}})),this.previewUrls.clear())}}document.addEventListener("DOMContentLoaded",(()=>{window.jvbUploads=new e}))})();
assets/js/min/utility.min.js
@@ -1 +1 @@
(()=>{window.fade=function(t,e=!0){e?t.style.animation="fadeIn var(--transition-base)":(t.style.animation="fadeOut var(--transition-base)",window.debouncer.schedule(`remove-${t.dataset.id??t.id??t.className.replace(" ","-")}`,(()=>{t.remove()}),500))},window.formatTimeAgo=function(t){const e=t instanceof Date?t:new Date(t),n=new Date,i=Math.floor((n-e)/1e3),o=Math.floor(i/60),r=Math.floor(o/60),a=Math.floor(r/24);return r<24?0===r?0===o?"Just now":`${o} ${1===o?"minute":"minutes"} ago`:`${r} ${1===r?"hour":"hours"} ago`:a<7?`${a} ${1===a?"day":"days"} ago`:e.toLocaleDateString()},window.formatTimeSoon=function(t){const e=t instanceof Date?t:new Date(t),n=new Date;if(e<=n)return"Just now";const i=Math.floor((e-n)/1e3),o=Math.floor(i/60);return i<60?"In a moment":o<5?"In a few minutes":o<20?"Coming up soon":o<60?"In about half an hour":"Later today"},window.uppercaseFirst=function(t){return t.charAt(0).toUpperCase()+t.slice(1)},window.templates=new Map,document.addEventListener("DOMContentLoaded",(()=>{window.loadTemplates()})),window.loadTemplates=function(){document.querySelectorAll("template").forEach((t=>{const e=Array.from(t.classList);if(e.length>0){const n=t.content.cloneNode(!0).firstElementChild;e.forEach((t=>{window.templates.has(t)||window.templates.set(t,n)}))}}))},window.getTemplate=function(t){return 0===window.templates.size&&loadTemplates(),!!window.templates.has(t)&&window.templates.get(t).cloneNode(!0)},window.formatVote=function(t,e){let n=window.getTemplate("voteButton");n.dataset.itemId=t.id,n.dataset.content=t.content;let i=n.querySelector("button.up"),o=n.querySelector("button.down");return"up"===e&&i.classList.add("voted"),"down"===e&&o.classList.add("voted"),t.upvotes>0&&(i.querySelector(".count").textContent=t.upvotes),t.downvotes>0&&(o.querySelector(".count").textContent="-"+t.downvotes),n},window.checkVoteStatus=function(t,e){if(!jvbSettings.currentUser)return"";let n="";return window.userVotes&&window.userVotes[t]?.has(e)&&(n=window.userVotes[t].get(e)),n},window.getIcon=function(t){if(void 0===t)return"";if(window.jvbIcons||(window.jvbIcons=new Map),!window.jvbIcons.has(t)&&jvbSettings.icons[t]){let e=document.createElement("div");e.innerHTML=jvbSettings.icons[t],window.jvbIcons.set(t,e.firstElementChild.cloneNode(!0)),e.remove()}return window.jvbIcons.get(t)?.cloneNode(!0)},window.isEmptyObject=function(t){return 0===Object.keys(t).length},window.formatNumber=function(t){return t.toString().replace(/\B(?=(\d{3})+(?!\d))/g,",")},window.formatPrice=function(t,e="CAD"){return new Intl.NumberFormat("en-CA",{style:"currency",currency:e}).format(t)},window.escapeHtml=function(t){return t?("string"==typeof t||t instanceof String||(t=String(t)),t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")):""},window.truncateText=function(t,e=100){return!t||t.length<=e?t:t.substring(0,e)+"..."},window.removeChildren=function(t){if(0!==t.children.length)for(;t.firstChild;)t.removeChild(t.firstChild)},window.formatDateRange=function(t,e){const n=new Date(t),i=new Date(e);return n.toDateString()===i.toDateString()?n.toLocaleDateString("en-US",{year:"numeric",month:"short",day:"numeric"}):n.getMonth()===i.getMonth()&&n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-US",{month:"short",day:"numeric"})} - ${i.getDate()}, ${i.getFullYear()}`:n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-US",{month:"short",day:"numeric"})} - ${i.toLocaleDateString("en-US",{month:"short",day:"numeric"})}, ${i.getFullYear()}`:`${n.toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"})} - ${i.toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"})}`},window.debounce=function(t,e=300){let n;return function(...i){clearTimeout(n),n=setTimeout((()=>t.apply(this,i)),e)}},window.throttle=function(t,e){let n;return function(){const i=arguments,o=this;n||(t.apply(o,i),n=!0,setTimeout((()=>n=!1),e))}},window.throttle=function(t,e=300){let n;return function(...i){n||(t.apply(this,i),n=!0,setTimeout((()=>n=!1),e))}},window.uppercaseFirst=function(t){return t.charAt(0).toUpperCase()+t.slice(1)},window.sanitizeHtml=function(t){const e=document.createElement("div");return e.textContent=t,e.innerHTML},window.formatDate=function(t){if(!t)return"";const e=new Date(t),n=new Date,i=Math.floor((n-e)/864e5);return i<1?"Today":i<2?"Yesterday":i<7?`${i} days ago`:e.toLocaleDateString()},window.getPluralContent=function(t){return"artwork"===t?"artwork":t+"s"},window.showToast=function(t,e="success",n={}){window.jvbNotifications.showToast(t,e,n)},window.dateFormatter=new Intl.DateTimeFormat("en-CA",{year:"numeric",month:"long",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit",timeZoneName:"short"}),window.formatDate=function(t){return t instanceof Date&&!isNaN(t)||(t=new Date(t)),window.dateFormatter.format(t)},window.typeText=function(t,e,n=50){return t.classList.add("typeText"),new Promise((i=>{let o=0;t.textContent="";const r=setInterval((()=>{o<e.length?(t.textContent+=e.charAt(o),o++):(clearInterval(r),i())}),n)}))},window.eraseText=function(t,e=10){return new Promise((n=>{let i=t.textContent,o=i.length;const r=setInterval((()=>{o>0?(o--,t.textContent=i.substring(0,o)):(clearInterval(r),n())}),e)}))},window.typeLoop=function(t,e,n=50,i=10,o=1e3,r=250){let a=!0;return async function(){for(;a;)await window.typeText(t,e,n),await new Promise((t=>setTimeout(t,o))),await window.eraseText(t,i),await new Promise((t=>setTimeout(t,r)))}(),function(){a=!1}},window.toCamelCase=function(t){return t.replace(/-([a-z])/g,(function(t){return t[1].toUpperCase()}))},window.targetCheck=function(t,e){return"string"==typeof e&&(t.target.closest(e)??!1)},window.getDifferences={VALUE_CREATED:"created",VALUE_UPDATED:"updated",VALUE_DELETED:"deleted",VALUE_UNCHANGED:"unchanged",map:function(t,e){if(this.isFunction(t)||this.isFunction(e))throw"Invalid argument. Function given, object expected.";if(this.isFile(t)||this.isFile(e)){const n=this.compareFiles(t,e);return n===this.VALUE_UNCHANGED?null:{type:n,data:void 0===t?e:t}}if(this.isValue(t)||this.isValue(e)){const n=this.compareValues(t,e);if(n===this.VALUE_UNCHANGED)return null;let i;switch(n){case this.VALUE_CREATED:i=e;break;case this.VALUE_DELETED:i=this.getEmptyValue(t);break;case this.VALUE_UPDATED:default:i=e}return{type:n,data:i}}let n={},i=!1;for(let o in t)if(!this.isFunction(t[o])){let r;e&&void 0!==e[o]&&(r=e[o]);const a=this.map(t[o],r);null!==a&&(a.hasOwnProperty("type")&&a.hasOwnProperty("data")?n[o]=a.data:n[o]=a,i=!0)}if(e)for(let o in e)if(!this.isFunction(e[o])&&(void 0===t||void 0===t[o])){const t=this.map(void 0,e[o]);null!==t&&(t.hasOwnProperty("type")&&t.hasOwnProperty("data")?n[o]=t.data:n[o]=t,i=!0)}return i?n:null},getEmptyValue:function(t){return this.isArray(t)?[]:this.isObject(t)?{}:"number"==typeof t?0:"boolean"!=typeof t&&""},compareValues:function(t,e){return t===e||this.isDate(t)&&this.isDate(e)&&t.getTime()===e.getTime()?this.VALUE_UNCHANGED:void 0===t?this.VALUE_CREATED:void 0===e?this.VALUE_DELETED:this.VALUE_UPDATED},isFunction:function(t){return"[object Function]"===Object.prototype.toString.call(t)},isArray:function(t){return"[object Array]"===Object.prototype.toString.call(t)},isDate:function(t){return"[object Date]"===Object.prototype.toString.call(t)},isObject:function(t){return"[object Object]"===Object.prototype.toString.call(t)},isFile:function(t){return t instanceof File},isValue:function(t){return!this.isObject(t)&&!this.isArray(t)},compareFiles:function(t,e){return!this.isFile(t)&&this.isFile(e)?this.VALUE_CREATED:this.isFile(t)&&!this.isFile(e)?this.VALUE_DELETED:this.isFile(t)&&this.isFile(e)?t.name===e.name&&t.size===e.size&&t.type===e.type&&t.lastModified===e.lastModified?this.VALUE_UNCHANGED:this.VALUE_UPDATED:this.VALUE_UNCHANGED},merge:function(t,e){if(null==t)return e;if(null==e)return t;if(this.isFunction(t)||this.isFunction(e))return e;if(this.isFile(t)||this.isFile(e))return e;if(this.isValue(t)||this.isValue(e)||this.isArray(t)||this.isArray(e))return e;if(this.isObject(t)&&this.isObject(e)){let n={};for(let e in t)this.isFunction(t[e])||(n[e]=t[e]);for(let i in e)this.isFunction(e[i])||(void 0!==t[i]?n[i]=this.merge(t[i],e[i]):n[i]=e[i]);return n}return e}},window.deepMerge=function(t,e){return window.getDifferences.merge(t,e)},window.isInt=function(t){return!isNaN(parseInt(t))&&isFinite(t)},window.isNumeric=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},window.handleListField=function(t,e){if(!Array.isArray(e))return void t.remove();let n=t.querySelector("li");e.forEach((e=>{let i=n.cloneNode(!0);i.textContent=e,t.append(i)})),n.remove()},window.handleTextField=function(t,e){"string"==typeof e?t.textContent=e:t.remove()},window.handleImageField=function(t,e){if(!Array.isArray(e)||0===e)return void t.remove();let n="IMG"===t.tagName?t:t.querySelector("img");n?(n.alt=e.alt,n.src=e.thumbnail,n.dataset.small=e.small,n.dataset.medium=e.medium,n.dataset.large=e.full):t.remove()},window.handleGalleryField=function(t,e){if(!Array.isArray(e))return void t.remove();let n=t.querySelector("img");e.forEach((e=>{let i=n.cloneNode(!0);window.handleImageField(i,e),t.append(i)})),n.remove()},window.uiFromSelectors=function(t,e=null){let n={};for(let[i,o]of Object.entries(t))n[i]="object"==typeof o?window.uiFromSelectors(o,e):e?e.querySelector(o):document.querySelector(o);return n};window.debouncer=new class{constructor(){this.timeouts=new Map,window.addEventListener("beforeunload",(()=>this.cleanup()))}schedule(t,e,n=1e3){this.cancel(t),this.timeouts.set(t,setTimeout((()=>{e(),this.timeouts.delete(t)}),n))}cancel(t){this.timeouts.has(t)&&(clearTimeout(this.timeouts.get(t)),this.timeouts.delete(t))}cleanup(){for(let t of this.timeouts.values())clearTimeout(t);this.timeouts.clear()}}})();
(()=>{window.fade=function(t,e=!0){e?t.style.animation="fadeIn var(--transition-base)":(t.style.animation="fadeOut var(--transition-base)",window.debouncer.schedule(`remove-${t.dataset.id??t.id??t.className.replace(" ","-")}`,(()=>{t.remove()}),500))},window.formatTimeAgo=function(t){const e=t instanceof Date?t:new Date(t),n=new Date,i=Math.floor((n-e)/1e3),o=Math.floor(i/60),r=Math.floor(o/60),a=Math.floor(r/24);return r<24?0===r?0===o?"Just now":`${o} ${1===o?"minute":"minutes"} ago`:`${r} ${1===r?"hour":"hours"} ago`:a<7?`${a} ${1===a?"day":"days"} ago`:e.toLocaleDateString()},window.formatTimeSoon=function(t){const e=t instanceof Date?t:new Date(t),n=new Date;if(e<=n)return"Just now";const i=Math.floor((e-n)/1e3),o=Math.floor(i/60);return i<60?"In a moment":o<5?"In a few minutes":o<20?"Coming up soon":o<60?"In about half an hour":"Later today"},window.uppercaseFirst=function(t){return t.charAt(0).toUpperCase()+t.slice(1)},window.templates=new Map,document.addEventListener("DOMContentLoaded",(()=>{window.loadTemplates()})),window.loadTemplates=function(){document.querySelectorAll("template").forEach((t=>{const e=Array.from(t.classList);if(e.length>0){const n=t.content.cloneNode(!0).firstElementChild;e.forEach((t=>{window.templates.has(t)||window.templates.set(t,n)}))}}))},window.getTemplate=function(t){return 0===window.templates.size&&loadTemplates(),!!window.templates.has(t)&&window.templates.get(t).cloneNode(!0)},window.formatVote=function(t,e){let n=window.getTemplate("voteButton");n.dataset.itemId=t.id,n.dataset.content=t.content;let i=n.querySelector("button.up"),o=n.querySelector("button.down");return"up"===e&&i.classList.add("voted"),"down"===e&&o.classList.add("voted"),t.upvotes>0&&(i.querySelector(".count").textContent=t.upvotes),t.downvotes>0&&(o.querySelector(".count").textContent="-"+t.downvotes),n},window.checkVoteStatus=function(t,e){if(!jvbSettings.currentUser)return"";let n="";return window.userVotes&&window.userVotes[t]?.has(e)&&(n=window.userVotes[t].get(e)),n},window.icon=document.querySelector(".icon").cloneNode(!0),window.icon.className="icon",window.getIcon=function(t){if(void 0===t)return"";console.log("Getting icon: "+t);let e=window.icon.cloneNode(!0);return e.classList.add("icon-"+t),e},window.isEmptyObject=function(t){return 0===Object.keys(t).length},window.formatNumber=function(t){return t.toString().replace(/\B(?=(\d{3})+(?!\d))/g,",")},window.formatPrice=function(t,e="CAD"){return new Intl.NumberFormat("en-CA",{style:"currency",currency:e}).format(t)},window.escapeHtml=function(t){return t?("string"==typeof t||t instanceof String||(t=String(t)),t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#039;")):""},window.truncateText=function(t,e=100){return!t||t.length<=e?t:t.substring(0,e)+"..."},window.removeChildren=function(t){if(0!==t.children.length)for(;t.firstChild;)t.removeChild(t.firstChild)},window.formatDateRange=function(t,e){const n=new Date(t),i=new Date(e);return n.toDateString()===i.toDateString()?n.toLocaleDateString("en-US",{year:"numeric",month:"short",day:"numeric"}):n.getMonth()===i.getMonth()&&n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-US",{month:"short",day:"numeric"})} - ${i.getDate()}, ${i.getFullYear()}`:n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-US",{month:"short",day:"numeric"})} - ${i.toLocaleDateString("en-US",{month:"short",day:"numeric"})}, ${i.getFullYear()}`:`${n.toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"})} - ${i.toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"})}`},window.debounce=function(t,e=300){let n;return function(...i){clearTimeout(n),n=setTimeout((()=>t.apply(this,i)),e)}},window.throttle=function(t,e){let n;return function(){const i=arguments,o=this;n||(t.apply(o,i),n=!0,setTimeout((()=>n=!1),e))}},window.throttle=function(t,e=300){let n;return function(...i){n||(t.apply(this,i),n=!0,setTimeout((()=>n=!1),e))}},window.uppercaseFirst=function(t){return t.charAt(0).toUpperCase()+t.slice(1)},window.sanitizeHtml=function(t){const e=document.createElement("div");return e.textContent=t,e.innerHTML},window.formatDate=function(t){if(!t)return"";const e=new Date(t),n=new Date,i=Math.floor((n-e)/864e5);return i<1?"Today":i<2?"Yesterday":i<7?`${i} days ago`:e.toLocaleDateString()},window.getPluralContent=function(t){return"artwork"===t?"artwork":t+"s"},window.showToast=function(t,e="success",n={}){window.jvbNotifications.showToast(t,e,n)},window.dateFormatter=new Intl.DateTimeFormat("en-CA",{year:"numeric",month:"long",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit",timeZoneName:"short"}),window.formatDate=function(t){return t instanceof Date&&!isNaN(t)||(t=new Date(t)),window.dateFormatter.format(t)},window.typeText=function(t,e,n=50){return t.classList.add("typeText"),new Promise((i=>{let o=0;t.textContent="";const r=setInterval((()=>{o<e.length?(t.textContent+=e.charAt(o),o++):(clearInterval(r),i())}),n)}))},window.eraseText=function(t,e=10){return new Promise((n=>{let i=t.textContent,o=i.length;const r=setInterval((()=>{o>0?(o--,t.textContent=i.substring(0,o)):(clearInterval(r),n())}),e)}))},window.typeLoop=function(t,e,n=50,i=10,o=1e3,r=250){let a=!0;return async function(){for(;a;)await window.typeText(t,e,n),await new Promise((t=>setTimeout(t,o))),await window.eraseText(t,i),await new Promise((t=>setTimeout(t,r)))}(),function(){a=!1}},window.toCamelCase=function(t){return t.replace(/-([a-z])/g,(function(t){return t[1].toUpperCase()}))},window.targetCheck=function(t,e){return"string"==typeof e&&(t.target.closest(e)??!1)},window.getDifferences={VALUE_CREATED:"created",VALUE_UPDATED:"updated",VALUE_DELETED:"deleted",VALUE_UNCHANGED:"unchanged",map:function(t,e){if(this.isFunction(t)||this.isFunction(e))throw"Invalid argument. Function given, object expected.";if(this.isFile(t)||this.isFile(e)){const n=this.compareFiles(t,e);return n===this.VALUE_UNCHANGED?null:{type:n,data:void 0===t?e:t}}if(this.isValue(t)||this.isValue(e)){const n=this.compareValues(t,e);if(n===this.VALUE_UNCHANGED)return null;let i;switch(n){case this.VALUE_CREATED:i=e;break;case this.VALUE_DELETED:i=this.getEmptyValue(t);break;case this.VALUE_UPDATED:default:i=e}return{type:n,data:i}}let n={},i=!1;for(let o in t)if(!this.isFunction(t[o])){let r;e&&void 0!==e[o]&&(r=e[o]);const a=this.map(t[o],r);null!==a&&(a.hasOwnProperty("type")&&a.hasOwnProperty("data")?n[o]=a.data:n[o]=a,i=!0)}if(e)for(let o in e)if(!this.isFunction(e[o])&&(void 0===t||void 0===t[o])){const t=this.map(void 0,e[o]);null!==t&&(t.hasOwnProperty("type")&&t.hasOwnProperty("data")?n[o]=t.data:n[o]=t,i=!0)}return i?n:null},getEmptyValue:function(t){return this.isArray(t)?[]:this.isObject(t)?{}:"number"==typeof t?0:"boolean"!=typeof t&&""},compareValues:function(t,e){return t===e||this.isDate(t)&&this.isDate(e)&&t.getTime()===e.getTime()?this.VALUE_UNCHANGED:void 0===t?this.VALUE_CREATED:void 0===e?this.VALUE_DELETED:this.VALUE_UPDATED},isFunction:function(t){return"[object Function]"===Object.prototype.toString.call(t)},isArray:function(t){return"[object Array]"===Object.prototype.toString.call(t)},isDate:function(t){return"[object Date]"===Object.prototype.toString.call(t)},isObject:function(t){return"[object Object]"===Object.prototype.toString.call(t)},isFile:function(t){return t instanceof File},isValue:function(t){return!this.isObject(t)&&!this.isArray(t)},compareFiles:function(t,e){return!this.isFile(t)&&this.isFile(e)?this.VALUE_CREATED:this.isFile(t)&&!this.isFile(e)?this.VALUE_DELETED:this.isFile(t)&&this.isFile(e)?t.name===e.name&&t.size===e.size&&t.type===e.type&&t.lastModified===e.lastModified?this.VALUE_UNCHANGED:this.VALUE_UPDATED:this.VALUE_UNCHANGED},merge:function(t,e){if(null==t)return e;if(null==e)return t;if(this.isFunction(t)||this.isFunction(e))return e;if(this.isFile(t)||this.isFile(e))return e;if(this.isValue(t)||this.isValue(e)||this.isArray(t)||this.isArray(e))return e;if(this.isObject(t)&&this.isObject(e)){let n={};for(let e in t)this.isFunction(t[e])||(n[e]=t[e]);for(let i in e)this.isFunction(e[i])||(void 0!==t[i]?n[i]=this.merge(t[i],e[i]):n[i]=e[i]);return n}return e}},window.deepMerge=function(t,e){return window.getDifferences.merge(t,e)},window.isInt=function(t){return!isNaN(parseInt(t))&&isFinite(t)},window.isNumeric=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},window.handleListField=function(t,e){if(!Array.isArray(e))return void t.remove();let n=t.querySelector("li");e.forEach((e=>{let i=n.cloneNode(!0);i.textContent=e,t.append(i)})),n.remove()},window.handleTextField=function(t,e){"string"==typeof e?t.textContent=e:t.remove()},window.handleImageField=function(t,e){if(!Array.isArray(e)||0===e)return void t.remove();let n="IMG"===t.tagName?t:t.querySelector("img");n?(n.alt=e.alt,n.src=e.thumbnail,n.dataset.small=e.small,n.dataset.medium=e.medium,n.dataset.large=e.full):t.remove()},window.handleGalleryField=function(t,e){if(!Array.isArray(e))return void t.remove();let n=t.querySelector("img");e.forEach((e=>{let i=n.cloneNode(!0);window.handleImageField(i,e),t.append(i)})),n.remove()},window.uiFromSelectors=function(t,e=null){let n={};for(let[i,o]of Object.entries(t))n[i]="object"==typeof o?window.uiFromSelectors(o,e):e?e.querySelector(o):document.querySelector(o);return n};window.debouncer=new class{constructor(){this.timeouts=new Map,window.addEventListener("beforeunload",(()=>this.cleanup()))}schedule(t,e,n=1e3){this.cancel(t),this.timeouts.set(t,setTimeout((()=>{e(),this.timeouts.delete(t)}),n))}cancel(t){this.timeouts.has(t)&&(clearTimeout(this.timeouts.get(t)),this.timeouts.delete(t))}cleanup(){for(let t of this.timeouts.values())clearTimeout(t);this.timeouts.clear()}}})();
assets/js/min/view.min.js
@@ -1 +1 @@
window.jvbViews=class{constructor(e,t){this.a11y=window.jvbA11y,this.error=window.jvbError,this.container=e,this.initElements(),this.settings=window.jvbUserSettings,this.store=t,this.items={list:new Map,grid:new Map,table:new Map},this.currentView="grid",this.selectedItems=new Set,this.init()}initElements(){this.selectors={grid:".item-grid",table:{table:"table",body:"table body",selectedColumns:".all-filters .multi-select",columns:"thead th"},bulk:{count:".bulk-controls .selected-count",control:".bulk-controls .bulk-actions",select:".bulk-controls select",selectAll:".select-all"}},this.ui=window.uiFromSelectors(this.selectors,this.container)}init(){this.store.subscribe(((e,t)=>{switch(e){case"items-saved":case"item-saved":case"item-deleted":break;case"data-loaded":this.handleItemsUpdate()}})),this.setupViewSwitcher(),this.changeHandler=this.handleChange.bind(this),this.clickHandler=this.handleClick.bind(this),this.lastSelected=null,document.addEventListener("change",this.changeHandler),document.addEventListener("click",this.clickHandler)}handleClick(e){e.target.closest(".select-item-label")&&(e.shiftKey?(e.preventDefault(),this.handleRangeSelection(e.target)):this.lastSelected=e.target.closest(".item"))}handleRangeSelection(e){if(!this.lastSelected)return void(this.lastSelected=e.closest(".item"));const t=e.closest(".item"),i=Array.from(this.container.querySelectorAll(".item")),s=i.indexOf(this.lastSelected),l=i.indexOf(t);if(-1===s||-1===l)return void(this.lastSelected=t);const a=Math.min(s,l),r=Math.max(s,l);let d=0;for(let e=a;e<=r;e++){let t=i[e];this.selectedItems.add(t.dataset.id);let s=t.querySelector(".select-item");s&&!s.checked&&(s.checked=!0,d++)}this.updateSelectionUI(),window.jvbA11y.announce(`Selected ${d} items in range.`)}handleChange(e){e.target.closest(".select-all")?this.selectAll(e.target.checked):e.target.closest(".select-item")?this.toggleSelection(e.target.closest(".item").dataset.id):e.target.closest("details.multi-select")&&this.toggleColumns(e.target.id,e.target.checked)}toggleColumns(e,t){this.ui.table.columns.filter((t=>t.className===e))}setupViewSwitcher(){document.querySelectorAll("[data-view]").forEach((e=>{this.settings.addSetting(e),e.addEventListener("click",(()=>{this.currentView=e.dataset.view,this.render()}))}))}handleDataUpdate(e){console.log(e);const t=e.data?.items||e.items||[];this.render(t)}handleItemsUpdate(){console.log(this.store.data),this.render(this.store.data)}render(e=[]){if(this.store)if(0!==e.length){switch(this.currentView){case"grid":this.renderGrid(e);break;case"table":this.renderTable(e);break;case"list":this.renderList(e)}this.updateSelectionUI()}else this.renderEmpty();else console.error("No store connected to renderer")}renderEmpty(){this.toggleTable(!1),window.removeChildren(this.ui.grid);const e=window.getTemplate("emptyState");e&&(this.ui.grid.appendChild(e),this.a11y?.announce("No items found"))}renderGrid(e){this.toggleGrid(),this.toggleTable(!1),this.ui.grid.classList.remove("list-view"),this.ui.grid.classList.add("grid-view");const t=document.createDocumentFragment();e.forEach((e=>{let i;this.store.renderOrRetrieve?i=this.store.renderOrRetrieve(e,"grid",this.renderGridItem.bind(this)):this.items.grid.has(e.id)?i=this.items.grid.get(e.id):(i=this.renderGridItem(e),this.items.grid.set(e.id,i)),t.appendChild(i)})),this.ui.grid.appendChild(t)}renderGridItem(e){const t=window.getTemplate("gridView");t.dataset.id=e.id,e._pending&&t.classList.add("pending");let[i,s,l,a,r]=[t.querySelector("input"),t.querySelector("label"),t.querySelector("img"),t.querySelector('[data-action="edit"]'),t.querySelector('[data-action="trash"]')];return[i.value,i.id,i.checked,s.htmlFor,a.dataset.id,r.dataset.id]=[e.id,`select-${e.id}`,this.selectedItems.has(`${e.id}`),`select-${e.id}`,e.id,e.id],"progress"===this.store.config.storeName?[l.src,l.alt]=[e.images[e.fields.timeline[0].post_thumbnail]?.medium??"",e.images[e.fields.timeline[0].post_thumbnail]?.alt??""]:[l.src,l.alt]=[e.images[e.fields.post_thumbnail]?.medium??"",e.images[e.fields.post_thumbnail]?.alt??""],t}toggleTable(e){if(this.ui.table.selectedColumns.hidden=!e,e&&!this.ui.table.table){let e=window.getTemplate("contentTable");this.container.append(e),this.ui.table.table=this.container.querySelector("form.table"),this.ui.table.body=this.ui.table.table.querySelector("tbody"),this.ui.table.columns=this.container.querySelectorAll(this.selectors.table.columns)}this.ui.table.table&&(this.ui.table.table.hidden=!e,window.removeChildren(this.ui.table.body)),this.ui.table.selectedColumns.hidden=!e}toggleGrid(){window.removeChildren(this.ui.grid)}renderTable(e){this.toggleTable(!0),this.toggleGrid(),e.forEach((e=>{let t;this.items.table.has(e.id)?t=this.items.table.get(e.id):(t=this.store.renderOrRetrieve(e,"table",this.renderTableItem.bind(this)),this.items.table.set(e.id,t)),this.ui.table.body.append(t)})),window.jvbSelector.scanExistingFields()}renderTableItem(e){let t=["",0];const i=window.getTemplate("tableView");return i.dataset.id=e.id,[i.querySelector(".select-item").id,i.querySelector(".select-item").value,i.querySelector(".select-item").checked,i.querySelector(".select-item + label").htmlFor,i.querySelector(`input[name="post_status"][value="${e.status}"]`).checked]=[e.id,e.id,this.selectedItems.has(`${e.id}`),e.id,e.status],i.querySelectorAll("td[data-field]").forEach((i=>{let s,l=e.fields[i.dataset.field],a=i.querySelector("label"),r=t.includes(l);switch(i.dataset.fieldType){case"text":case"number":case"url":case"tel":case"email":r||(i.querySelector("input").value=l),a.remove();break;case"textarea":r||(i.querySelector("textarea").value=l),a.remove();break;case"taxonomy":a.remove(),r||(s=i.querySelector("input[type=hidden]"),s.value=l);break;case"image":if(!r){let t=window.getTemplate("uploadItem"),s=t.querySelector("img");[s.src,s.alt]=[e.images[l].medium??"",e.images[l].alt??""],i.querySelector(".item-grid").append(t),i.querySelector("input[type=hidden]").value=l}i.querySelectorAll(".progress,label,.upload-select,.status,details").forEach((e=>{e.remove()}));break;case"true_false":r||(i.querySelector("input").checked=1===parseInt(l)),i.querySelector(".toggle-label")?.remove();break;case"select":a.remove();case"radio":case"checkbox":i.querySelector(".label")?.remove(),r||(l=l.split(","),l.forEach((e=>{s=i.querySelector(`[value="${e}"]`),s&&(s.checked=!0)})));break;default:r||console.log(l)}})),i}renderList(e){this.toggleGrid(),this.toggleTable(!1),this.ui.grid.classList.remove("grid-view"),this.ui.grid.classList.add("list-view"),e.forEach((e=>{let t;this.items.list.has(e.id)?t=this.items.list.get(e.id):(t=this.store.renderOrRetrieve(e,"list",this.renderListItem.bind(this)),this.items.list.set(e.id,t)),this.ui.grid.appendChild(t)}))}renderListItem(e){const t=window.getTemplate("listView");t.dataset.id=e.id,e._pending&&t.classList.add("pending");let i=t.querySelector(".select-item"),s=t.querySelector(".select-item + label");[i.id,i.value,i.checked,s.htmlFor]=[e.id,e.id,this.selectedItems.has(`${e.id}`),e.id],t.querySelectorAll("[data-attr]").forEach((t=>{""!==e[t.dataset.attr]?t.textContent=e[t.dataset.attr]:t.remove()})),t.querySelectorAll("[data-field]").forEach((t=>{let i=e.fields[t.dataset.field];""!==i?"DIV"===t.tagName?t.innerHTML=i:t.textContent=i:t.remove()}));let l=t.querySelector("img");return l&&([l.src,l.alt]=[e.images[e.fields.post_thumbnail]?.medium??"",e.images[e.fields.post_thumbnail]?.alt??""]),t}toggleSelection(e){this.selectedItems.has(e)?this.selectedItems.delete(e):this.selectedItems.add(e),this.updateSelectionUI()}selectAll(e){const t=this.container.querySelectorAll(".item");e||(this.selectedItems.clear(),this.ui.bulk.selectAll.checked=!1,this.ui.bulk.select.value=""),t.forEach((t=>{e&&this.selectedItems.add(t.dataset.id),t.querySelector(".select-item").checked=e})),this.updateSelectionUI()}clearSelection(){this.selectAll(!1),this.ui.bulk.select.value=""}updateSelectionUI(){const e=this.selectedItems.size;if(this.ui.bulk.control&&(this.ui.bulk.control.hidden=0===e),this.ui.bulk.count){let t=1===e?"item":"items";this.ui.bulk.count.hidden=0===e,this.ui.bulk.count.textContent=0===e?"":`${e} ${t} selected`}}};
window.jvbViews=class{constructor(e,t){this.a11y=window.jvbA11y,this.error=window.jvbError,this.container=e,this.initElements(),this.settings=window.jvbUserSettings,this.store=t,this.items={list:new Map,grid:new Map,table:new Map},this.currentView="grid",this.selectedItems=new Set,this.init()}initElements(){this.selectors={grid:".item-grid",table:{table:"table",body:"table body",selectedColumns:".all-filters .multi-select",columns:"thead th"},bulk:{count:".bulk-controls .selected-count",control:".bulk-controls .bulk-actions",select:".bulk-controls select",selectAll:".select-all"}},this.ui=window.uiFromSelectors(this.selectors,this.container)}init(){this.store.subscribe(((e,t)=>{switch(e){case"items-saved":case"item-saved":case"item-deleted":break;case"data-loaded":this.handleItemsUpdate()}})),this.setupViewSwitcher(),this.changeHandler=this.handleChange.bind(this),this.clickHandler=this.handleClick.bind(this),this.lastSelected=null,document.addEventListener("change",this.changeHandler),document.addEventListener("click",this.clickHandler)}handleClick(e){e.target.closest(".select-item-label")&&(e.shiftKey?(e.preventDefault(),this.handleRangeSelection(e.target)):this.lastSelected=e.target.closest(".item"))}handleRangeSelection(e){if(!this.lastSelected)return void(this.lastSelected=e.closest(".item"));const t=e.closest(".item"),i=Array.from(this.container.querySelectorAll(".item")),s=i.indexOf(this.lastSelected),l=i.indexOf(t);if(-1===s||-1===l)return void(this.lastSelected=t);const a=Math.min(s,l),r=Math.max(s,l);let d=0;for(let e=a;e<=r;e++){let t=i[e];this.selectedItems.add(t.dataset.id);let s=t.querySelector(".select-item");s&&!s.checked&&(s.checked=!0,d++)}this.updateSelectionUI(),window.jvbA11y.announce(`Selected ${d} items in range.`)}handleChange(e){e.target.closest(".select-all")?this.selectAll(e.target.checked):e.target.closest(".select-item")?this.toggleSelection(e.target.closest(".item").dataset.id):e.target.closest("details.multi-select")&&this.toggleColumns(e.target.id,e.target.checked)}toggleColumns(e,t){this.ui.table.columns.filter((t=>t.className===e))}setupViewSwitcher(){document.querySelectorAll("[data-view]").forEach((e=>{this.settings.addSetting(e),e.addEventListener("click",(()=>{this.currentView=e.dataset.view,this.render()}))}))}handleDataUpdate(e){console.log(e);const t=e.data?.items||e.items||[];this.render(t)}handleItemsUpdate(){console.log(this.store.data),this.render(this.store.data)}render(e=[]){if(this.store)if(console.log(e),0!==e.length){switch(this.currentView){case"grid":this.renderGrid(e);break;case"table":this.renderTable(e);break;case"list":this.renderList(e)}this.updateSelectionUI()}else this.renderEmpty();else console.error("No store connected to renderer")}renderEmpty(){this.toggleTable(!1),window.removeChildren(this.ui.grid);const e=window.getTemplate("emptyState");e&&(this.ui.grid.appendChild(e),this.a11y?.announce("No items found"))}renderGrid(e){this.toggleGrid(),this.toggleTable(!1),this.ui.grid.classList.remove("list-view"),this.ui.grid.classList.add("grid-view");const t=document.createDocumentFragment();e.forEach((e=>{let i;this.store.renderOrRetrieve?i=this.store.renderOrRetrieve(e,"grid",this.renderGridItem.bind(this)):this.items.grid.has(e.id)?i=this.items.grid.get(e.id):(i=this.renderGridItem(e),this.items.grid.set(e.id,i)),t.appendChild(i)})),this.ui.grid.appendChild(t)}renderGridItem(e){const t=window.getTemplate("gridView");t.dataset.id=e.id,e._pending&&t.classList.add("pending");let[i,s,l,a,r]=[t.querySelector("input"),t.querySelector("label"),t.querySelector("img"),t.querySelector('[data-action="edit"]'),t.querySelector('[data-action="trash"]')];return[i.value,i.id,i.checked,s.htmlFor,a.dataset.id,r.dataset.id]=[e.id,`select-${e.id}`,this.selectedItems.has(`${e.id}`),`select-${e.id}`,e.id,e.id],[l.src,l.alt]=[e.images[e.fields.post_thumbnail]?.medium??"",e.images[e.fields.post_thumbnail]?.alt??""],t}toggleTable(e){if(this.ui.table.selectedColumns.hidden=!e,e&&!this.ui.table.table){let e=window.getTemplate("contentTable");this.container.append(e),this.ui.table.table=this.container.querySelector("form.table"),this.ui.table.body=this.ui.table.table.querySelector("tbody"),this.ui.table.columns=this.container.querySelectorAll(this.selectors.table.columns)}this.ui.table.table&&(this.ui.table.table.hidden=!e,window.removeChildren(this.ui.table.body)),this.ui.table.selectedColumns.hidden=!e}toggleGrid(){window.removeChildren(this.ui.grid)}renderTable(e){this.toggleTable(!0),this.toggleGrid(),e.forEach((e=>{let t;this.items.table.has(e.id)?t=this.items.table.get(e.id):(t=this.store.renderOrRetrieve(e,"table",this.renderTableItem.bind(this)),this.items.table.set(e.id,t)),this.ui.table.body.append(t)})),window.jvbSelector.scanExistingFields()}renderTableItem(e){let t=["",0];const i=window.getTemplate("tableView");return i.dataset.id=e.id,[i.querySelector(".select-item").id,i.querySelector(".select-item").value,i.querySelector(".select-item").checked,i.querySelector(".select-item + label").htmlFor,i.querySelector(`input[name="post_status"][value="${e.status}"]`).checked]=[e.id,e.id,this.selectedItems.has(`${e.id}`),e.id,e.status],i.querySelectorAll("td[data-field]").forEach((i=>{let s,l=e.fields[i.dataset.field],a=i.querySelector("label"),r=t.includes(l);switch(i.dataset.fieldType){case"text":case"number":case"url":case"tel":case"email":r||(i.querySelector("input").value=l),a.remove();break;case"textarea":r||(i.querySelector("textarea").value=l),a.remove();break;case"taxonomy":a.remove(),r||(s=i.querySelector("input[type=hidden]"),s.value=l);break;case"image":if(!r){let t=window.getTemplate("uploadItem"),s=t.querySelector("img");[s.src,s.alt]=[e.images[l].medium??"",e.images[l].alt??""],i.querySelector(".item-grid").append(t),i.querySelector("input[type=hidden]").value=l}i.querySelectorAll(".progress,label,.upload-select,.status,details").forEach((e=>{e.remove()}));break;case"true_false":r||(i.querySelector("input").checked=1===parseInt(l)),i.querySelector(".toggle-label")?.remove();break;case"select":a.remove();case"radio":case"checkbox":i.querySelector(".label")?.remove(),r||(l=l.split(","),l.forEach((e=>{s=i.querySelector(`[value="${e}"]`),s&&(s.checked=!0)})));break;default:r||console.log(l)}})),i}renderList(e){this.toggleGrid(),this.toggleTable(!1),this.ui.grid.classList.remove("grid-view"),this.ui.grid.classList.add("list-view"),e.forEach((e=>{let t;this.items.list.has(e.id)?t=this.items.list.get(e.id):(t=this.store.renderOrRetrieve(e,"list",this.renderListItem.bind(this)),this.items.list.set(e.id,t)),this.ui.grid.appendChild(t)}))}renderListItem(e){const t=window.getTemplate("listView");t.dataset.id=e.id,e._pending&&t.classList.add("pending");let i=t.querySelector(".select-item"),s=t.querySelector(".select-item + label");[i.id,i.value,i.checked,s.htmlFor]=[e.id,e.id,this.selectedItems.has(`${e.id}`),e.id],t.querySelectorAll("[data-attr]").forEach((t=>{""!==e[t.dataset.attr]?t.textContent=e[t.dataset.attr]:t.remove()})),t.querySelectorAll("[data-field]").forEach((t=>{let i=e.fields[t.dataset.field];""!==i?"DIV"===t.tagName?t.innerHTML=i:t.textContent=i:t.remove()}));let l=t.querySelector("img");return l&&([l.src,l.alt]=[e.images[e.fields.post_thumbnail]?.medium??"",e.images[e.fields.post_thumbnail]?.alt??""]),t}toggleSelection(e){this.selectedItems.has(e)?this.selectedItems.delete(e):this.selectedItems.add(e),this.updateSelectionUI()}selectAll(e){const t=this.container.querySelectorAll(".item");e||(this.selectedItems.clear(),this.ui.bulk.selectAll.checked=!1,this.ui.bulk.select.value=""),t.forEach((t=>{e&&this.selectedItems.add(t.dataset.id),t.querySelector(".select-item").checked=e})),this.updateSelectionUI()}clearSelection(){this.selectAll(!1),this.ui.bulk.select.value=""}updateSelectionUI(){const e=this.selectedItems.size;if(this.ui.bulk.control&&(this.ui.bulk.control.hidden=0===e),this.ui.bulk.count){let t=1===e?"item":"items";this.ui.bulk.count.hidden=0===e,this.ui.bulk.count.textContent=0===e?"":`${e} ${t} selected`}}};
base/_setup.php
@@ -2,6 +2,7 @@
require(JVB_DIR.'/base/site.php');
require(JVB_DIR.'/base/content.php');
require(JVB_DIR.'/base/email.php');
require(JVB_DIR.'/base/taxonomies.php');
require(JVB_DIR.'/base/users.php');
require(JVB_DIR.'/base/login.php');
base/email.php
New file
@@ -0,0 +1,97 @@
<?php
$defaults = [
    'colours'       => [
        'action-0'      => '#ff0080',
        'action-50'     => '#ff2492',
        'action-100'    => '#ff47a4',
        'action-200'    => '#ff6bb5',
        'secondary-0'   => '#D69121',
        'secondary-50'  => '#ffc421',
        'secondary-100' => '#ffcd44',
        'secondary-200' => '#ffd768',
        'light'         => '#efefef',
        'light-50'      => '#e2e2e2',
        'light-100'     => '#d5d5d5',
        'light-200'     => '#c9c9c9',
        'dark'          => '#151515',
        'dark-50'       => '#222222',
        'dark-100'      => '#2e2e2e',
        'dark-200'      => '#3b3b3b',
        'action-contrast'=> '',
        'secondary-contrast'=> '',
    ],
    'content'       => [
        'title'             => get_bloginfo('name'),
        'subjectPrefix'     => '['.get_bloginfo('name').']',
        'signature'         => '<p>&emsp; â€”  â™¡ the edmonton.ink crew</p>',
        'footer'    => [
            '<p>&copy; ' . date('Y') . ' edmonton.ink â€” Your tattoo scene on your screen.</p>',
            '<p><a href="' . get_home_url() . '" class="text-link">edmonton.ink</a></p>'
        ]
    ],
    'types' => [
        'newUser' => [
            'subject'       => 'Welcome to Legacy! Finish creating your account.',
            'showPrefix'    => true,
        ],
        'resetPass' => [
            'subject'   => 'Reset your Password'
        ],
        'emailChange'   => [
            'subject'   => 'Successfully Changed Email'
        ],
        'passwordChange'    => [
            'subject'   => 'Successfully Changed Password',
        ]
    ]
];
/*
 * EXTEND WITH:
 *      {string} $message: email content
*       {WP_User} $user: user
 * apply_filters
 *      jvbNewUserEmail         ->  New user email content
 *          add_filter('jvbNewUserEmail', 'customFunction', 10, 2);
 *      jvbNewUserAdminEmail    ->  New User notification for admin
 *          add_filter('jvbNewUserAdminEmail', 'customFunction', 10, 2);
 *      jvbPasswordResetEmail   ->  Password Reset Email
 *          {string} $message: email content
 *          {string} $key
 *          {string} $user_login,
 *          {WP_User} $user_data
 *          add_filter('jvbPasswordResetEmail', 'customFunction', 10, 4);
 *      jvbEmailChangeRequestEmail      ->  Request for email change
 *          {string} $message
 *          {array} $oldUser
 *          {array} newUser
 *          add_filter('jvbEmailChangeRequestEmail', 'customFunction', 10, 3);
 *      jvbEmailChangedEmail    ->  Notification that email successfully changed
 *          {string} $message
 *          {string} $confirm_url
            add_filter('jvbEmailChangedEmail', 'customFunction', 10, 2);
 *      jvbPasswordChangeEmail  ->  Request for password change
 *          {string} $message
 *          {array} $oldUser
 *          {array} $newUser
 *          add_filter('jvbPasswordChangeEmail', 'customFunction', 10, 3);
 *      jvbPersonalDataExport   ->  Request for User Data
 *          {string} $message
 *          {string} $request_type
 *          {string} $confirmation_url
 *          {array} $emailData, from the original filter
 *          add_filter('jvbPersonalDataExport', 'customFunction', 10, 4);
 *      jvbPersonalDataExported ->  Notification that data is ready for download
 *          {string} $message
 *          {string} $downloadURL
 *          {string} $expiresAt
 *          {array} $emailData, from the original filter
 *          add_filter('jvbPersonalDataExported', 'customFunction', 10, );
 *
 */
$jvbEmail = apply_filters('jvb_email', $defaults);
define('JVB_EMAIL', $jvbEmail);
build/feed/render.php
@@ -195,7 +195,7 @@
                        <div class="filter-actions">
                            <?= jvbRenderToggleTextField('match', 'Match', 'Filters', 'ALL', 'ANY', true) ?>
                            <button type="button" class="clear-filters row" hidden>
                                <?= jvbIcon('close', ['title'    => 'Clear']) ?>
                                <?= jvbIcon('x', ['title'    => 'Clear']) ?>
                                Clear All Filters
                            </button>
                        </div>
@@ -220,7 +220,7 @@
                            <input type="radio" id="order-random" class="btn" name="orderby" value="random">
                            <label for="order-random" title="Random Order">
                                <?= jvbIcon('random') ?>
                                <?= jvbIcon('shuffle') ?>
                                <span class="label">Random</span>
                            </label>
                        </div>
@@ -229,12 +229,12 @@
                    <div class="order-direction radio-group-label" data-for-order="date,title">
                        <input type="radio" id="order-desc" class="btn" name="order" value="desc" checked>
                        <label for="order-desc" title="Newest First">
                            <?= jvbIcon('desc') ?>
                            <?= jvbIcon('sort-descending') ?>
                        </label>
                        <input type="radio" id="order-asc" class="btn" name="order" value="asc">
                        <label for="order-asc" title="Oldest First">
                            <?= jvbIcon('asc') ?>
                            <?= jvbIcon('sort-ascending') ?>
                        </label>
                    </div>
                </div>
build/glossary/view.asset.php
@@ -1 +1 @@
<?php return array('dependencies' => array(), 'version' => '90747b206e7cdf47035c');
<?php return array('dependencies' => array(), 'version' => '2f66c620b1540459b263');
build/glossary/view.js
@@ -1 +1 @@
(()=>{class t{constructor(t="dl.glossary",e="nav.glossary-index"){this.glossary=document.querySelector(t),this.nav=document.querySelector(e),this.glossary&&this.nav&&(this.terms=this.glossary.querySelectorAll("dt[id]"),this.navList=this.nav.querySelector("ul"),this.activeClass="active",this.currentActive=null,this.breakpoint=768,this.init(),this.setupResizeHandler())}init(){const t={root:null,rootMargin:this.getRootMargin(),threshold:0};this.observer=new IntersectionObserver((t=>this.handleIntersection(t)),t),this.terms.forEach((t=>this.observer.observe(t))),this.handleScroll=this.debounce((()=>this.checkActiveTerm()),100),window.addEventListener("scroll",this.handleScroll,{passive:!0})}getRootMargin(){if(window.innerWidth<this.breakpoint){const t=parseFloat(getComputedStyle(document.documentElement).fontSize),e=Math.round(5*t);return`-${e}px 0px -${e}px 0px`}return"-50% 0px -50% 0px"}setupResizeHandler(){let t;window.addEventListener("resize",(()=>{clearTimeout(t),t=setTimeout((()=>{this.reinitialize()}),250)}))}reinitialize(){this.observer&&this.observer.disconnect(),this.init()}handleIntersection(t){const e=t.find((t=>t.isIntersecting));e&&this.setActive(e.target)}checkActiveTerm(){const t=4*parseFloat(getComputedStyle(document.documentElement).fontSize);let e=null,i=1/0;this.terms.forEach((s=>{const n=s.getBoundingClientRect();if(window.innerWidth<this.breakpoint?n.top>=t&&n.top<=window.innerHeight-t:n.top+n.height/2>=0&&n.top+n.height/2<=window.innerHeight){const o=window.innerWidth<this.breakpoint?t:window.innerHeight/2,r=Math.abs(n.top-o);r<i&&(i=r,e=s)}})),e&&this.setActive(e)}setActive(t){this.currentActive!==t&&(this.currentActive&&this.currentActive.classList.remove(this.activeClass),t.classList.add(this.activeClass),this.currentActive=t,this.updateNavigation(t.id))}updateNavigation(t){this.nav.querySelectorAll("a").forEach((t=>t.classList.remove(this.activeClass)));const e=this.nav.querySelector(`a[href="#${t}"]`);e&&(e.classList.add(this.activeClass),this.centerNavItem(e))}centerNavItem(t){const e=this.navList.getBoundingClientRect(),i=t.getBoundingClientRect(),s=this.navList.scrollTop,n=i.top-e.top,o=e.height/2-i.height/2;this.navList.scrollTo({top:s+n-o,behavior:"smooth"})}debounce(t,e){let i;return function(...s){clearTimeout(i),i=setTimeout((()=>{clearTimeout(i),t(...s)}),e)}}destroy(){this.observer&&this.observer.disconnect(),window.removeEventListener("scroll",this.handleScroll)}}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",(()=>{new t})):new t})();
(()=>{class t{constructor(t="dl.glossary",e="nav.glossary-index"){this.glossary=document.querySelector(t),this.nav=document.querySelector(e),this.glossary&&this.nav&&(this.terms=this.glossary.querySelectorAll("dt[id]"),this.navList=this.nav.querySelector("ul"),this.activeClass="active",this.currentActive=null,this.init(),this.setupResizeHandler())}init(){const t={root:null,rootMargin:this.getRootMargin(),threshold:0};this.observer=new IntersectionObserver((t=>this.handleIntersection(t)),t),this.terms.forEach((t=>this.observer.observe(t))),this.handleScroll=this.debounce((()=>this.checkActiveTerm()),100),window.addEventListener("scroll",this.handleScroll,{passive:!0})}getRootMargin(){return"-50% 0px -50% 0px"}setupResizeHandler(){let t;window.addEventListener("resize",(()=>{clearTimeout(t),t=setTimeout((()=>{this.reinitialize()}),250)}))}reinitialize(){this.observer&&this.observer.disconnect(),this.init()}handleIntersection(t){const e=t.find((t=>t.isIntersecting));e&&this.setActive(e.target)}checkActiveTerm(){parseFloat(getComputedStyle(document.documentElement).fontSize);let t=null,e=1/0;this.terms.forEach((i=>{const s=i.getBoundingClientRect();if(s.top+s.height/2>=0&&s.top+s.height/2<=window.innerHeight){const n=window.innerHeight/2,r=Math.abs(s.top-n);r<e&&(e=r,t=i)}})),t&&this.setActive(t)}setActive(t){this.currentActive!==t&&(this.currentActive&&this.currentActive.classList.remove(this.activeClass),t.classList.add(this.activeClass),this.currentActive=t,this.updateNavigation(t.id))}updateNavigation(t){this.nav.querySelectorAll("a").forEach((t=>t.classList.remove(this.activeClass)));const e=this.nav.querySelector(`a[href="#${t}"]`);e&&(e.classList.add(this.activeClass),this.centerNavItem(e))}centerNavItem(t){const e=this.navList.getBoundingClientRect(),i=t.getBoundingClientRect(),s=this.navList.scrollTop,n=i.top-e.top,r=e.height/2-i.height/2;this.navList.scrollTo({top:s+n-r,behavior:"smooth"})}debounce(t,e){let i;return function(...s){clearTimeout(i),i=setTimeout((()=>{clearTimeout(i),t(...s)}),e)}}destroy(){this.observer&&this.observer.disconnect(),window.removeEventListener("scroll",this.handleScroll)}}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",(()=>{new t})):new t})();
build/summary/render.php
@@ -1,4 +1,7 @@
<?php
use JVBase\managers\CacheManager;
if (!defined('ABSPATH')) {
    exit; // Exit if accessed directly
}
@@ -28,7 +31,7 @@
function jvbRenderArtistSummary():string
{
    $current = get_queried_object();
    $cache = new JVBase\managers\CacheManager('artists', WEEK_IN_SECONDS);
    $cache = CacheManager::for('artists', WEEK_IN_SECONDS);
    $key = 'artist-bio-'.$current->ID;
    $cached = $cache->get($key);
    $cached = false;
@@ -49,11 +52,11 @@
    <nav id="artist" class="on-this-page index">
        <label>Jump to:
            <button type="button" aria-label="Show Index" title="Show Index" class="toggle" aria-expanded="false">
                <?= jvbIcon('add')?>
                <?= jvbIcon('plus-square')?>
            </button>
        </label>
        <ul>
            <li><a href="#top" title="Back to Top"><?=jvbIcon('up')?></a></li>
            <li><a href="#top" title="Back to Top"><?=jvbIcon('caret-circle-up')?></a></li>
            <li><a href="#about">About</a></li>
            <li><a href="#styles">Styles</a></li>
            <li><a href="#contact">Contact</a></li>
@@ -155,7 +158,7 @@
{
    $current = get_queried_object();
    $cache = new JVBase\managers\CacheManager('shops', WEEK_IN_SECONDS);
    $cache = CacheManager::for('shops', WEEK_IN_SECONDS);
    $key = 'shop-bio-'.$current->term_id;
    $cached = $cache->get($key);
    $cached = false;
@@ -181,11 +184,11 @@
    <nav id="shop" class="on-this-page index">
        <label>Jump to:
            <button type="button" aria-label="Show Index" title="Show Index" class="toggle" aria-expanded="false">
                <?= jvbIcon('add')?>
                <?= jvbIcon('plus-square')?>
            </button>
        </label>
        <ul>
            <li><a href="#top" title="Back to Top"><?=jvbIcon('up')?></a></li> <?php
            <li><a href="#top" title="Back to Top"><?=jvbIcon('caret-circle-up')?></a></li> <?php
            if ($rating !== 'none') {
                ?>
                <li><a href="#rating">Rating</a></li>
@@ -286,8 +289,8 @@
function jvbRenderTermSummary()
{
    $current = get_queried_object();
    $cache = new JVBase\managers\CacheManager($current->taxonomy, WEEK_IN_SECONDS);
    $key = $current->taxonomy.'-'.$current->ID;
    $cache = CacheManager::for(jvbNoBase($current->taxonomy), WEEK_IN_SECONDS);
    $key = $current->ID;
    $cached = $cache->get($key);
    $cached = false;
    if ($cached !== false) {
cleanup.php
@@ -44,7 +44,9 @@
    }
    // Remove block-specific scripts and modules
    wp_deregister_script('heartbeat');
    if (!is_admin()) {
        wp_deregister_script('heartbeat');
    }
    wp_dequeue_script('wp-block-template-skip-link');
    // Remove WordPress 6.5+ script modules
icons.php
File was deleted
iconsOld.php
New file
@@ -0,0 +1,761 @@
<?php
namespace JVBase;
use JVBase\managers\CacheManager;
if (!defined('ABSPATH')) {
    exit; // Exit if accessed directly
}
class JVBIcons
{
    static array $processing = ['map' => [],'label' => []];
    protected string $style;
    protected array $styles = [
        'regular',
        'bold',
        'duotone',
        'fill',
        'light',
        'thin'
    ];
    protected array $labels = [
        'submenu'           => 'Toggle Menu',
        'dashboard'         => 'Dashboard',
        'arrow-fat-down'    => 'Downvote',
        'arrow-fat-up'      => 'Upvote',
        'hamburger'         => 'Menu',
        'check-square-offset'=> 'Approval Requests',
        'h1'                => 'Heading 1',
        'h2'                => 'Heading 2',
        'h3'                => 'Heading 3',
        'note-pencil'       => 'Notes',
        'sun-dim'           => 'Light Mode',
        'moon'              => 'Dark Mode',
        'grid'              => 'Grid View',
        'list'              => 'List View',
        'clock-clockwise'   => 'Upcoming Events',
        'clock-counter-clockwise'=>'Past Events',
        'repeat'            => 'Recurring Events',
        'floppy-disk'       => 'Save',
    ];
    protected array $map = [
        'time'          => 'clock',
        'back'          => 'arrow-u-up-left',
        'logo'          => 'logo.svg',
        'logo-basic'    => 'logo-basic.svg',
        'save'          => 'floppy-disk',
        'restore'       => 'arrow-counter-clockwise',
        'help'          => 'question',
        'upload'        => 'cloud-arrow-up',
        'download'      => 'cloud-arrow-down',
        'synced'        => 'cloud-check',
        'syncing'       => 'cloud-sync-thin.svg',
        'offline'       => 'cloud-slash',
        'error'         => 'cloud-warning',
        'cart'          => 'shopping-cart',
        'delete'        => 'trash',
        'edit'          => 'pencil-simple',
        'bio'           => 'user',
        'login'         => 'sign-in',
        'logout'        => 'sign-out',
        'future'        => 'clock-clockwise',
        'past'          => 'clock-counter-clockwise',
        'light'         => 'sun-dim',
        'dark'          => 'moon',
        'h1'            => 'text-h-one',
        'h2'            => 'text-h-two',
        'h3'            => 'text-h-three',
        'bold'          => 'text-b',
        'italic'        => 'text-italic',
        'underline'     => 'text-underline',
        'strike'        => 'text-strikethrough',
        'image'         => 'image-square',
        'reply'         => 'arrow-bend-up-left',
        'approval'      => 'check-square-offset',
        'response'      => 'chat-teardrop',
        'karma'         => 'scales',
        'show'          => 'eye',
        'publish'       => 'eye',
        'hide'          => 'eye-closed',
        'draft'         => 'eye-closed',
        'asc'           => 'sort-ascending',
        'desc'          => 'sort-descending',
        'all'           => 'infinity',
        'random'        => 'shuffle',
        'location'      => 'map-pin',
        'hours'         => 'clock',
        'favourite'     => 'heart',
        'email'         => 'envelope',
        'text'          => 'chat',
        'integrations'  => 'plugs-connected',
        'connected'     => 'plugs-connected',
        'disconnected'  => 'plugs',
        'umami'         => 'chart-line',
        'square-up'     => 'square-logo',
        'tiktok'        => 'tiktok-logo',
        'threads'       => 'threads-logo',
        'twitch'        => 'twitch-logo',
        'u'     => 'snapchat-logo',
        'linktree'      => 'linktree-logo',
        'fediverse'     => 'fediverse-logo',
        'mastadon'      => 'mastadon-logo',
        'youtube'       => 'youtube-logo',
        'twitter'       => 'twitter-logo',
        'messenger'     => 'messenger-logo',
        'facebook'      => 'facebook-logo',
        'instagram'     => 'instagram-logo',
        'dash'          => 'door',
        'event'         => 'calendar',
        'events'        => 'calendar',
        'settings'      => 'gear-six',
        'grid'          => 'squares-four',
        'list'          => 'rows',
        'pinned'        => 'push-pin',
        'search'            => 'magnifying-glass',
        'add'           => 'plus-square',
        'minus'         => 'minus-square',
        'support'       => 'question',
        'grab'          => 'dots-six-vertical',
        'checkout'      => 'receipt',
        'home'          => 'house',
        'elbow-right-down' => 'arrow-elbow-right-down',
        'elbow-right-up'=> 'arrow-elbow-right-up',
        'elbow-left-down'=> 'arrow-elbow-left-down',
        'elbow-left-up' => 'arrow-elbow-left-up',
        'menu'          => 'list',
        'submenu'       => 'caret-down',
        'close'         => 'x',
        'close-square'  => 'x-square',
        'dashboard'     => 'door',
        'system'        => 'gear-six',
        'options'       => 'gear-six',
        'news'          => 'newspaper',
        'metrics'       => 'chart-line',
        'prev'          => 'caret-left',
        'up'            => 'caret-circle-up',
        'right'         => 'caret-double-right',
        'left'          => 'caret-double-left',
        'next'          => 'caret-right',
        'refresh'       => 'arrows-clockwise',
        'pending'       => 'arrows-clockwise',
        'copy'          => 'copy-simple',
        'align-center'  => 'text-align-center',
        'align-left'    => 'text-align-left',
        'align-right'   => 'text-align-right',
    //  'alphabetical'  => 'alphabetical.svg',
        'date'          => 'calendar',
        'down'          => 'caret-double-down',
        'tattoo'        => 'drop-simple',
        'tattoos'       => 'drop-simple',
        'theme'         => 'folder-open',
        'arttheme'      => 'folder-open',
        'style'         => 'hash',
        'artstyle'      => 'hash',
        'colour'        => 'drop',
        'placement'     => 'person-arms-spread',
        'artmedia'      => 'squares-four',
        'artist'        => 'user',
        'client'        => 'user',
        'artists'       => 'users-three',
        'partner'       => 'currency-circle-dollar',
        'shop'          => 'storefront',
        'piercing'      => 'needle',
        'piercings'     => 'needle',
        'artwork'       => 'palette',
        'artform'       => 'shapes',
        'city'          => 'map-pin',
        'type'          => 'users-three',
        'pstyle'        => 'nut',
        'project'       => 'code',
        'map'           => 'map-trifold',
        'offer'         => 'gift',
        'referrals'     => 'hand-heart'
    ];
    private const ICON_GROUPS = [
        'navigation' => [
            'back'          => ['filename' => 'back', 'label' => 'Back', 'size' => 24],
            'home'          => ['filename' => 'house', 'label' => 'Home'],
            'right'         => ['filename' => 'arrow-fat-right', 'label' => ''],
            'elbow-right-down'=> ['filename' => 'arrow-elbow-right-down', 'label' => ''],
            'elbow-right-up'=> ['filename' => 'arrow-elbow-right-up', 'label' => ''],
            'elbow-left-down'=> ['filename' => 'arrow-elbow-left-down', 'label' => ''],
            'elbow-left-up'=> ['filename' => 'arrow-elbow-left-up', 'label' => ''],
            'menu'          => ['filename' => 'list', 'label' => 'Toggle Menu', 'size' => 32],
            'submenu'       => ['filename' => 'caret-down', 'label' => 'Toggle Submenu'],
            'close'         => ['filename' => 'x', 'label' => 'Close', 'size' => 32],
            'close-square'  => ['filename' => 'x-square', 'label' => 'Close'],
            'dashboard'     => ['filename' => 'door', 'label'=> 'Behind the Scenes', 'size' => 24],
            'dash'          => ['filename' => 'door', 'label'=> 'Behind the Scenes', 'size' => 24],
            'settings'      => ['filename' => 'gear-six', 'label'=> 'Settings', 'size' => 24],
            'system'      => ['filename' => 'gear-six', 'label'=> 'Settings', 'size' => 24],
            'options'       => ['filename' => 'gear-six', 'label'=> 'Options', 'size' => 24],
            'news'          => ['filename' => 'newspaper', 'label' => 'News', 'size' => 24],
            'metrics'       => ['filename' => 'chart-line', 'label' => 'Metrics', 'size' => 24],
            'prev'          => ['filename' => 'caret-left', 'label' => 'Previous', 'size' => 24],
            'up'            => ['filename' => 'caret-circle-up', 'label' => 'Up', 'size' => 24],
            'down'            => ['filename' => 'caret-double-down', 'label' => 'Down', 'size' => 24],
            'right'            => ['filename' => 'caret-double-right', 'label' => 'Right', 'size' => 24],
            'next'          => ['filename' => 'caret-right', 'label' => 'Next', 'size' => 24],
            'refresh'       => ['filename' => 'arrows-clockwise', 'label' => 'Refresh', 'size' => 24],
            'pending'       => ['filename' => 'arrows-clockwise', 'label' => 'Refresh', 'size' => 24],
            'clock'         => ['filename' => 'clock', 'label' => 'Hours'],
            'copy'          => ['filename' => 'copy-simple', 'label' => 'Duplicate'],
            'list-heart'    => ['filename' => 'list-heart', 'label'=> 'Lists'],
            'downvoted'      => ['filename' => 'arrow-fat-down-fill.svg', 'label'=> 'Downvote'],
            'upvoted'        => ['filename' => 'arrow-fat-up-fill.svg', 'label'=> 'Upvote'],
            'downvote'      => ['filename' => 'arrow-fat-down', 'label'=> 'Downvote'],
            'upvote'        => ['filename' => 'arrow-fat-up', 'label'=> 'Upvote'],
            'karma'         => ['filename' => 'scales', 'label'=> 'Karma'],
            'response'      => ['filename' => 'chat-teardrop', 'label' => 'Responses'],
            'reply'         => ['filename' => 'arrow-bend-up-left', 'label' => 'Reply'],
            'approval'      => ['filename' => 'check-square-offset', 'label' => 'Approval Requests'],
            'check'         => ['filename' => 'check-circle', 'label' => 'done'],
            'hamburger'         => ['filename' => 'hamburger', 'label' => 'menu']
        ],
        'formatting' => [
            'paragraph'     => ['filename' => 'paragraph', 'label' => 'Paragraph', 'size' => 24],
            'h1'            => ['filename' => 'text-h-one', 'label' => 'Heading 1', 'size' => 24],
            'h2'            => ['filename' => 'text-h-two', 'label' => 'Heading 2', 'size' => 24],
            'h3'            => ['filename' => 'text-h-three', 'label' => 'Heading 3', 'size' => 24],
            'bold'          => ['filename' => 'text-b-bold.svg', 'label' => 'Bold', 'size' => 24],
            'italic'        => ['filename' => 'text-italic', 'label' => 'Italic', 'size' => 24],
            'underline'     => ['filename' => 'text-underline', 'label' => 'Underline', 'size' => 24],
            'strike'        => ['filename' => 'text-strikethrough', 'label' => 'Strikethrough', 'size' => 24],
            'list-bullets'  => ['filename' => 'list-bullets', 'label' => 'Bulleted List', 'size' => 24],
            'list-numbers'  => ['filename' => 'list-numbers', 'label' => 'Numbered List', 'size' => 24],
            'image'         => ['filename' => 'image-square', 'label' => 'Image', 'size' => 24],
            'align-left'    => ['filename' => 'text-align-left', 'label' => 'Align Left', 'size' => 24],
            'align-right'   => ['filename' => 'text-align-right', 'label' => 'Align Right', 'size' => 24],
            'align-center'  => ['filename' => 'text-align-center', 'label' => 'Align Center', 'size' => 24],
            'link'          =>['filename' => 'link', 'label' => 'Link', 'size' => 24],
            'note'          => ['filename' => 'note-pencil', 'label' => 'Notes', 'size' => 24],
            'password'      => ['filename' => 'password', 'label' => 'Password']
        ],
        'theme' => [
            'light'     => ['filename' => 'sun-dim', 'label' => 'Light Mode'],
            'dark'      => ['filename' => 'moon', 'label' => 'Dark Mode'],
            'grid'      => ['filename' => 'squares-four', 'label' => 'Grid View'],
            'list'      => ['filename' => 'rows', 'label' => 'List View'],
        ],
        'content'   => [
            'offer'     => ['filename'=>'gift', 'label'=>'Offer'],
            'logo' => ['filename' => 'logo.svg', 'label' => 'North\'eh', 'size' => 20],
            'logo-basic' => ['filename' => 'logo-basic.svg', 'label' => 'North\'eh', 'size' => 20],
            'theme'     => ['filename' => 'folder-open', 'label' => 'Theme', 'size' => 20],
            'arttheme'  => ['filename' => 'folder-open', 'label' => 'Art Theme', 'size' => 20],
            'style'     => ['filename' => 'hash', 'label' => 'Style', 'size' => 20],
            'artstyle'  => ['filename' => 'hash', 'label' => 'Art Style', 'size' => 20],
            'colour'    => ['filename' => 'drop', 'label' => 'Colour', 'size' => 20],
            'placement' => ['filename' => 'person-arms-spread', 'label' => 'Placement', 'size' => 20],
            'artmedia'  => ['filename' => 'squares-four', 'label'   => 'Media', 'size' => 20],
            'artist'    => ['filename' => 'user', 'label'=> 'Artist', 'size' => 24],
            'client'    => ['filename' => 'user', 'label'=> 'Artist', 'size' => 24],
            'artists'   => ['filename' => 'users-three', 'label' => 'Artists'],
            'partner'   => ['filename' => 'currency-circle-dollar', 'label' => 'Partner'],
            'shop'      => ['filename' => 'storefront', 'label' => 'Shop', 'size' => 20],
            'tattoo'    => ['filename' => 'drop-simple', 'label' => 'Tattoo', 'size' => 20],
            'tattoos'   => ['filename' => 'drop-simple', 'label' => 'Your Tattoos', 'size' => 20],
            'event'     => ['filename' => 'calendar', 'label' => 'Event', 'size' => 20],
            'events'     => ['filename' => 'calendar', 'label' => 'Your Events', 'size' => 20],
            'piercing'  => ['filename' => 'needle', 'label' => 'Piercings', 'size' => 20],
            'piercings' => ['filename' => 'needle', 'label' => 'Your Piercings', 'size' => 20],
            'artwork'   => ['filename' => 'palette', 'label' => 'Artwork', 'size' => 20],
            'artform'   => ['filename' => 'shapes', 'label' => 'Art Form', 'size' => 20],
            'city'      => ['filename' => 'map-pin', 'label' => 'City', 'size' => 20],
            'type'      => ['filename' => 'users-three', 'label' => 'Artist Type', 'size' => 20],
            'pstyle'    => ['filename' => 'nut', 'label' => 'Body Modifications', 'size' => 20],
            'past'      => ['filename' => 'clock-counter-clockwise', 'label' => 'Past Events', 'size' => 20],
            'future'    => ['filename' => 'clock-clockwise', 'label' => 'Upcoming Events', 'size' => 20],
            'repeat'    => ['filename' => 'repeat', 'label' => 'Recurring Events', 'size' => 20],
            'project'   => ['filename' => 'code', 'label' => 'Project'],
            'gauge' => ['filename' => 'gauge', 'label'=> 'Dashboard'],
            'map'       => ['filename' => 'map-trifold', 'label' => 'Map'],
        ],
        'users' => [
            'new-user'  => ['filename' => 'user-circle-plus', 'label' => 'New Artist'],
            'user'      => ['filename' => 'user', 'label' => 'Artist', 'size' => 20],
            'bio'       => ['filename' => 'user', 'label' => 'Your Bio', 'size' => 20],
            'login'     => ['filename' => 'sign-in', 'label' => 'Login'],
            'logout'    => ['filename' => 'sign-out', 'label' => 'Logout']
        ],
        'actions'   => [
            'save'  => ['filename' => 'floppy-disk', 'label' => 'Save'],
            'restore'=> ['filename' => 'arrow-counter-clockwise', 'label' => 'Restore', 'size' => 32],
            'edit'  => ['filename' => 'pencil-simple', 'label' => 'Edit', 'size' => 24],
            'delete'=> ['filename' => 'trash', 'label' => 'Delete', 'size' => 32],
            'search'=> ['filename' => 'magnifying-glass', 'label' => 'Search', 'size' => 32],
            'add'   => ['filename' => 'plus-square', 'label' => 'Add New', 'size' => 32],
            'minus' => ['filename' => 'minus-square', 'label' => 'Collapse', 'size' => 32],
            'help'  => ['filename' => 'question', 'label' => 'Toggle Quick Help'],
            'support'  => ['filename' => 'question', 'label' => 'Toggle Quick Help'],
            'grab'  => ['filename' => 'dots-six-vertical', 'label'=> 'Grab'],
            'share' => ['filename' => 'share', 'label' => 'Share', 'size'=> 24],
            'cart' => ['filename' => 'shopping-cart', 'label' => 'Your Cart', 'size'=> 24],
            'checkout' => ['filename' => 'receipt', 'label' => 'Checkout', 'size'=> 24],
            'upload' => ['filename' => 'cloud-arrow-up', 'label' => 'Upload to Server', 'size'=> 24],
            'download' => ['filename' => 'cloud-arrow-down', 'label' => 'Downloading', 'size'=> 24],
            'synced' => ['filename' => 'cloud-check', 'label' => 'Synced', 'size'=> 24],
            'syncing' => ['filename' => 'cloud-sync-thin.svg', 'label' => 'Synced', 'size'=> 24],
            'cloud' => ['filename' => 'cloud', 'label' => 'Synced', 'size'=> 24],
//            'pending' => ['filename' => 'cloud', 'label' => 'Synced', 'size'=> 24],
            'offline' => ['filename' => 'cloud-slash', 'label' => 'Offline', 'size'=> 24],
            'error' => ['filename' => 'cloud-warning', 'label' => 'Upload Failed', 'size'=> 24],
        ],
        'status' => [
            'show'          => ['filename' => 'eye', 'label' => 'Show', 'size' => 20],
            'publish'       => ['filename' => 'eye', 'label' => 'Public', 'size' => 20],
            'hide'          => ['filename' => 'eye-closed', 'label' => 'Hide', 'size' => 20],
            'draft'         => ['filename' => 'eye-closed', 'label' => 'Hidden', 'size' => 20],
            'pinned'        => ['filename' => 'push-pin-simple', 'label' => 'Pin', 'size' => 32],
            'bell'          => ['filename' => 'bell', 'label' => 'Notification', 'size' => 32],
            'bell-ringing'  => ['filename' => 'bell-ringing', 'label' => 'Notification', 'size' => 32]
        ],
        'sorting' => [
            'table' => ['filename' => 'table', 'label' => 'Table View', 'size' => 20],
            'columns' => ['filename' => 'columns', 'label' => 'Show Columns', 'size' => 20],
            'alphabetical' => ['filename' => 'alphabetical', 'label' => 'Alphabetical', 'size' => 20],
            'calendar' => ['filename' => 'calendar-heart', 'label' => 'Date', 'size' => 20],
            'asc' => ['filename' => 'sort-ascending', 'label' => 'Sort Ascending', 'size' => 20],
            'ASC' => ['filename' => 'sort-ascending', 'label' => 'Sort Ascending', 'size' => 20],
            'desc' => ['filename' => 'sort-descending', 'label' => 'Sort Descending', 'size' => 20],
            'DESC' => ['filename' => 'sort-descending', 'label' => 'Sort Descending', 'size' => 20],
            'filter' => ['filename' => 'faders', 'label' => 'Filter'],
            'all' => ['filename' => 'infinity', 'label' => 'All', 'size' => 32],
            'random' => ['filename' => 'shuffle', 'label' => 'Random', 'size' => 20]
        ],
        'business' => [
            'location' => ['filename' => 'map-pin', 'label' => 'Location'],
            'hours' => ['filename' => 'clock', 'label' => 'Hours'],
            'star'  => ['filename' => 'star', 'label' => 'Empty Star'],
            'star-fill'  => ['filename' => 'star-fill.svg', 'label' => 'Star'],
            'star-half'  => ['filename' => 'star-half-fill.svg', 'label' => 'Half Star'],
        ],
        'social' => [
            'heart'     => ['filename' => 'heart', 'label' => 'Favourite', 'size' => 24],
            'favourite'     => ['filename' => 'heart', 'label' => 'Favourite', 'size' => 24],
            'favourites'=> ['filename' => 'heart', 'label' => 'Your Favourites', 'size' => 24],
            'heart-fill'=> ['filename' => 'heart-fill.svg', 'label' => 'Favourited', 'size' => 24],
            'share'     => ['filename' => 'share', 'label' => 'Share', 'size' => 32],
            'email'     => ['filename' => 'envelope','label'=> 'Email'],
            'text'      => ['filename' => 'chat','label'=> 'Text'],
            'phone'     => ['filename' => 'phone','label'=> 'Call'],
        ],
        'external'   => [
            'integrations' => ['filename' => 'plugs-connected', 'label' => 'Integrations'],
            'connected' => ['filename' => 'plugs-connected', 'label' => 'Connected'],
            'disconnected' => ['filename' => 'plugs', 'label' => 'Disconnected'],
            'instagram' => ['filename' => 'instagram-logo', 'label' => 'Instagram'],
            'facebook'  => ['filename' => 'facebook-logo', 'label' => 'Facebook'],
            'messenger' => ['filename' => 'messenger-logo', 'label' => 'Facebook Messenger'],
            'twitter'   => ['filename' => 'twitter-logo', 'label' => 'Twitter'],
            'x'   => ['filename' => 'x-logo', 'label' => 'X'],
            'youtube'   => ['filename' => 'youtube-logo', 'label' => 'YouTube'],
            'mastadon'   => ['filename' => 'mastadon-logo', 'label' => 'Mastadon'],
            'fediverse'   => ['filename' => 'fediverse-logo', 'label' => 'Fediverse'],
            'linktree'   => ['filename' => 'linktree-logo', 'label' => 'LinkTree'],
            'snapchat'   => ['filename' => 'snapchat-logo', 'label' => 'Snapchat'],
            'twitch'   => ['filename' => 'twitch-logo', 'label' => 'Twitch'],
            'threads'   => ['filename' => 'threads-logo', 'label' => 'Threads'],
            'tiktok'   => ['filename' => 'tiktok-logo', 'label' => 'TikTok'],
            'square-up'   => ['filename' => 'square-logo', 'label' => 'Square'],
            'umami'   => ['filename' => 'chart-line', 'label' => 'Umami'],
        ]
    ];
    private string $defaultStyle = '-thin';
    private int $defaultSize = 16;
    protected string $name;
    protected array $used;
    protected int $size = 20;
    protected CacheManager $cache;
    public function __construct()
    {
        $this->cache = new CacheManager('icons', 604800); //1 week in seconds
//      $this->cache->invalidateGroup('icons');
        $this->style = JVB_SITE['icons']??'regular';
        $this->used = get_option(BASE.'used_icons', [
            $this->style => [
                'heart',
                'hours',
                'random',
                'alphabetical',
                'calendar',
                'asc',
                'desc',
                'all',
                'paragraph',
                'h1',
                'h2',
                'h3',
                'bold',
                'italic',
                'underline',
                'strike',
                'list-bullets',
                'list-numbers',
                'image',
                'align-left',
                'align-right',
                'align-center',
                'link',
                'note',
                'password',
                'light',
                'left',
                'dark',
                'grid',
                'list',
                'save',
                'restore',
                'edit',
                'delete',
                'search',
                'add',
                'minus',
                'help',
                'support',
                'grab',
                'share',
                'cart',
                'checkout',
                'upload',
                'download',
                'synced',
                'syncing',
                'cloud',
                'offline',
                'error',
                'show',
                'publish',
                'hide',
                'draft',
                'pinned',
                'bell',
                'bell-ringing',
                'back',
                'home',
                'right',
                'elbow-right-down',
                'elbow-right-up',
                'elbow-left-down',
                'elbow-left-up',
                'menu',
                'submenu',
                'close',
                'close-square',
                'dashboard',
                'dash',
                'settings',
                'system',
                'options',
                'news',
                'metrics',
                'prev',
                'up',
                'down',
                'right',
                'copy',
                'next',
                'refresh',
                'pending',
                'clock',
                'copy',
                'list-heart',
                'karma',
                'response',
                'reply',
                'approval',
                'check',
                'hamburger',
                'location',
                'hours',
                'star',
                'star-half',
                'exclamation-mark'
            ],
            'fill'  => [
                'heart',
                'arrow-fat-down',
                'arrow-fat-up',
                'star'
            ]
        ]);
    }
    protected function updateUsed():void
    {
        $icons = [];
        foreach ($this->used as $style => $items) {
            $temp = array_unique($items);
            sort($temp);
            $icons[$style] = $temp;
        }
        update_option(BASE . 'used_icons', $icons);
    }
    protected function checkMap(string $name): string
    {
        $result = apply_filters('jvbIconMap', (array_key_exists($name, $this->map)) ? $this->map[$name] : $name, $name);
        return $result;
    }
    protected function checkName(string $name) {
        $name = $this->checkMap($name);
        $path = (str_contains($name, '.svg')) ? '/assets/icons/' : '/assets/phosphor-icons/regular/';
        $filename = (str_contains($name, '.svg')) ? $name : $name.'.svg';
        return file_exists(JVB_DIR . $path . $filename);
    }
    public function getIcon(string $name, array $options = []):?string
    {
        if (!$this->checkName($name)) {
            error_log('[Icons]Icon not found for: '.print_r($name, true));
            return '';
        }
        $style = (array_key_exists('style', $options) && in_array($options['style'], $this->styles)) ? $options['style'] : 'regular';
        $update = false;
        if (!array_key_exists($style, $this->used)) {
            $this->used[$style] = [];
            $update = true;
        }
        if (!in_array($name, $this->used[$style])) {
            $this->used[$style][] = $name;
            $update = true;
        }
        if ($update) {
            $this->updateUsed();
        }
        $name = $this->checkMap($name);
        $this->name = str_replace('.svg', '', $name);
        // Merge options with defaults and icon config
        $options = array_merge([
            'title' => $options['label']??$this->getIconLabel($name),
            'size' => $options['size'] ?? $this->size,
            'style' => $style,
            'class' => '',
            'wrap'  => true,
            'color' => 'currentColor'
        ], $options);
        return $this->cache->remember(
            array_merge($options, ['name' => $name]),
            function () use ($name, $options) {
                return $this->buildIcon($name, $options);
            }
        );
    }
    public function getIconsByGroup(string $group):array
    {
        if (!isset(self::ICON_GROUPS[$group])) {
            return [];
        }
        $icons = [];
        foreach (self::ICON_GROUPS[$group] as $name => $config) {
            $icons[$name] = $this->getIcon($name);
        }
        return array_filter($icons);
    }
    public function getAllIcons():array
    {
        $icons = [];
        foreach (self::ICON_GROUPS as $group => $groupIcons) {
            foreach ($groupIcons as $name => $config) {
                $icons[$name] = $this->getIcon($name);
            }
        }
        return array_filter($icons);
    }
    public function getGroups():array
    {
        return array_keys(self::ICON_GROUPS);
    }
    private function findIconConfig(string $name):?array
    {
        foreach (self::ICON_GROUPS as $groupIcons) {
            if (isset($groupIcons[$name])) {
                return $groupIcons[$name];
            }
        }
        return null;
    }
    private function buildIcon(string $name, array $options):?string
    {
        $filepath = $this->buildFilePath($name, $options['style']);
        if (!file_exists($filepath)) {
            error_log("Icon file not found: $filepath");
            return null;
        }
        $svg = file_get_contents($filepath);
        if ($svg === false) {
            return null;
        }
        return $this->formatSvg($svg, $options);
    }
    private function buildFilePath(string $filename, string $style = ''):string
    {
        $svg = false;
        if (str_contains($filename, '.svg')) {
            $svg = true;
            $nameExtra = '';
            $path = '/assets/icons/';
        } else {
            $nameExtra = ($style === 'regular') ? '' : '-'.$style;
            $path = '/assets/phosphor-icons/';
        }
        $filename = (str_contains($filename, '.svg')) ? $filename : $filename . $nameExtra . '.svg';
        $style = ($style === '') ? '' : $style .'/';
        $style = ($svg) ? '' : $style;
        return JVB_DIR . $path . $style . $filename;
    }
    private function formatSvg(string $svg, array $options): string
    {
        // Clean up SVG
        $svg = preg_replace("/([\n\t]+)/", ' ', $svg);
        $svg = preg_replace('/>\s*</', '><', $svg);
        // Add size attributes - FIXED: assign results back to $svg
        if ($options['size'] !== 32) {
            $svg = str_replace('width="32"', 'width="' . (int)$options['size'] . '"', $svg);
            $svg = str_replace('height="32"', 'height="' . (int)$options['size'] . '"', $svg);
        }
        // Add color if provided - FIXED: assign results back to $svg
        if ($options['color'] !== 'currentColor') {
            $svg = str_replace('currentColor', $options['color'], $svg);
        }
        // Add title if provided
        if (!empty($options['title'])) {
            $svg = str_replace('<svg', '<svg title="' . esc_attr($options['title']) . '"', $svg);
            $svg = str_replace('aria-hidden="true"', '', $svg);
        }
        $classes = trim('icon row ' . $this->name . ' ' . $options['class']);
        if (array_key_exists('wrap', $options) && $options['wrap'] === false) {
            return $svg;
        }
        return sprintf(
            '<i class="%s">%s</i>',
            esc_attr($classes),
            $svg
        );
    }
    /****************************
     * Converts to CSS icon
     ***************************/
    public function getCSSIcon(string $name, array $options = []): ?string
    {
        if (!$this->checkName($name)) {
            return '';
        }
        $name = $this->checkMap($name);
        $style = (array_key_exists('style', $options) && in_array($options['style'], $this->styles)) ? $options['style'] : 'regular';
        $this->name = str_replace('.svg', '', $name);
        // Merge options with defaults and icon config
        $options = array_merge([
            'title' => $options['label']??$this->getIconLabel($name),
            'size' => $options['size'] ?? $this->size,
            'style' => $style,
            'class' => '',
            'wrap'  => true,
            'color' => 'currentColor'
        ], $options);
        $svg = $this->cache->remember(
            array_merge($options, ['name' => $name, 'css' => true]),
            function() use ($name, $options) {
                return $this->buildRawSvg($name, $options);
            }
        );
        // Convert to base64 data URI
        $svg = ($svg) ? 'data:image/svg+xml;base64,' . base64_encode($svg) : null;
        return $svg;
    }
    private function buildRawSvg(string $filename, array $options): ?string
    {
        $filepath = $this->buildFilePath($filename, $options['style']);
        if (!file_exists($filepath)) {
            error_log("Icon file not found: $filepath");
            return null;
        }
        $svg = file_get_contents($filepath);
        if ($svg === false) {
            return null;
        }
        return $this->formatRawSvg($svg, $options);
    }
    private function formatRawSvg(string $svg, array $options): string
    {
        // Clean up SVG
        $svg = preg_replace("/([\n\t]+)/", ' ', $svg);
        $svg = preg_replace('/>\s*</', '><', $svg);
        // Add size attributes - FIXED: assign results back to $svg
        if ($options['size'] > 0) {
            $svg = str_replace('width="32"', 'width="' . (int)$options['size'] . '"', $svg);
            $svg = str_replace('height="32"', 'height="' . (int)$options['size'] . '"', $svg);
        }
        // Add color if provided and not currentColor - FIXED: assign results back to $svg
        if ($options['color'] !== 'currentColor') {
            $svg = str_replace('currentColor', $options['color'], $svg);
        }
        return $svg;
    }
    public function localizeIcons():array
    {
        if (empty($this->used)) {
            return [];
        }
        $used = [];
        foreach ($this->used as $style => $icons) {
            foreach ($icons as $icon) {
                $used[$icon] = $this->getIcon($icon, ['style' => $style]);
            }
        }
        return $used;
    }
    public function getIconLabel(string $name): string
    {
        $name = str_replace('.svg', '', $name);
        $label = (array_key_exists($name, $this->labels)) ? $this->labels[$name] : '';
        $result = apply_filters('jvbIconLabel', $label, $name);
        return $result;
    }
}
inc/blocks/CustomBlocks.php
@@ -20,7 +20,10 @@
        $this->cache = CacheManager::for('blocks', WEEK_IN_SECONDS);
        $this->imgCache = CacheManager::for('images', WEEK_IN_SECONDS);
        add_action('render_block', [$this, 'render'], 10, 3);
        add_filter('render_block', [$this, 'render'], 999, 3);
        add_action('init', [$this, 'registerBlockStyles']);
    }
@@ -67,8 +70,10 @@
    public function render(string $content, array $block, WP_Block $instance)
    {
        $method = 'render_'.$this->sanitizeBlockName($block);
        $blockName = $this->sanitizeBlockName($block);
        $method = 'render_'.$blockName;
        $function = BASE.$method;
        if (function_exists($function)) {
//          return $this->cache->remember(
//              $block,
@@ -77,8 +82,7 @@
//              }
//          );
            return $function($block, $content);
        }
        if (method_exists($this, $method)) {
        } else if (method_exists($this, $method)) {
            return $this->$method($block, $content);
            //TODO: Recache it
//          return $this->cache->remember(
@@ -94,9 +98,10 @@
                'core/post-title',
                'core/list-item',
                'core/site-title',
                'jvb/forms'
            ];
            if (!in_array($block['blockName'], $ignore)) {
                jvbDump('No method found for '.print_r($block['blockName'], true));
//              jvbDump('No method found for '.print_r($block['blockName'], true));
            }
        }
        if ($block['blockName'] === 'jvb/feed') {
@@ -220,6 +225,7 @@
        $attrs = $block['attrs'] ?? [];
        $innerContent = $this->innerBlocks($block);
        $position = 'object-position: center;';
        if (array_key_exists('focalPoint', $attrs)) {
            $x = (array_key_exists('x', $attrs['focalPoint'])) ? ($attrs['focalPoint']['x'] * 100).'%' : 'center';
            $y = (array_key_exists('y', $attrs['focalPoint'])) ? ($attrs['focalPoint']['y'] * 100).'%' : 'center';
@@ -231,8 +237,16 @@
        $backgroundType = $attrs['backgroundType'] ?? 'image';
        $background = '';
        if ($backgroundType === 'image' && isset($attrs['id'])) {
            $background .= str_replace('<img', '<img style="'.$position.'"', $this->image($attrs['id']));
        $ID = false;
        if (array_key_exists('useFeaturedImage', $attrs)) {
            global $post;
            $ID = get_post_thumbnail_id($post->ID);
        } else {
            $ID = (int)$attrs['id'];
        }
        if ($backgroundType === 'image' && $ID) {
            $background .= str_replace('<img', '<img style="'.$position.'"', $this->image($ID));
        } elseif ($backgroundType === 'video' && isset($attrs['url'])) {
            $background .= '<video style="'.$position.'"autoplay muted loop playsinline src="' . esc_url($attrs['url']) . '"></video>';
        }
@@ -471,8 +485,8 @@
            aria-label="Open Menu"
            aria-controls="navigation-' .$ID. '"
            aria-expanded="false">'.
            jvbIcon('menu', ['title'=>'Toggle Menu']).
            jvbIcon('close', ['title'=>'Toggle Menu']).
            jvbIcon('list', ['title'=>'Toggle Menu']).
            jvbIcon('x', ['title'=>'Toggle Menu']).
            '</button>';
        $class = ($toggle === '') ?
            $this->getClassesAndStyles($block['attrs'], ['mobile']) :
@@ -537,7 +551,7 @@
        $linkOpen = $this->build_navigation_link($block['attrs'], $aria);
        $content = '<li'.$classes.'>'.$linkOpen.$block['attrs']['label'].
                   '</a><button class="toggle" data-action="toggle-submenu" title="Toggle Submenu" aria-label="Open '.$block['attrs']['label'].' Submenu" aria-expanded="false" aria-controls="'.$id.'-submenu">'.
                   jvbIcon('submenu', ['title'=>'Toggle Submenu']).
                   jvbIcon('caret-down', ['title'=>'Toggle Submenu']).
                   '</button><ul class="submenu" id='.$id.'-submenu">';
        $content .= $this->innerBlocks($block);
@@ -607,8 +621,10 @@
    //core_post_excerpt
    protected function render_core_post_featured_image(array $block):string
    {
        global $post;
        $ID = get_post_thumbnail_id($post->ID);
        return '<figure'.$this->getClassesAndStyles($block['attrs']).'>'.
               $this->imageLink(true).
               $this->image($ID).
               '</figure>';
    }
    //core_post_navigation_link
@@ -767,12 +783,12 @@
        $check = ['header', 'footer'];
        $isHeaderTemplate = (
            (array_key_exists('slug', $block['attrs']) && str_contains($block['attrs']['slug'], 'header')) ||
            (array_key_exists('tagName', $block['attrs']) && str_contains($block['attrs']['tagName'], 'header'))
            (array_key_exists('slug', $block['attrs']) && str_contains(strtolower($block['attrs']['slug']), 'header')) ||
            (array_key_exists('tagName', $block['attrs']) && str_contains(strtolower($block['attrs']['tagName']), 'header'))
        ) ? 'header' : false;
        $isFooterTemplate = (
            (array_key_exists('slug', $block['attrs']) && str_contains($block['attrs']['slug'], 'footer')) ||
            (array_key_exists('tagName', $block['attrs']) && str_contains($block['attrs']['tagName'], 'footer'))
            (array_key_exists('slug', $block['attrs']) && str_contains(strtolower($block['attrs']['slug']), 'footer')) ||
            (array_key_exists('tagName', $block['attrs']) && str_contains(strtolower($block['attrs']['tagName']), 'footer'))
        ) ? 'footer' : false;
@@ -788,11 +804,12 @@
                }
                $checked = (is_user_logged_in() && current_user_can('prefers_dark_theme', true)) ? ' checked' : '';
                $title = ($checked == '') ? 'Toggle Dark Mode' : 'Toggle Light Mode';
                $themeSwitch = '<label title="'.$title.'" id="theme-switch" class="toggle-switch" for="theme-switcher">
                $showThemeSwitch = (bool)apply_filters('jvb_show_theme_switch', true);
                $themeSwitch = ($showThemeSwitch) ? '<label title="'.$title.'" id="theme-switch" class="toggle-switch" for="theme-switcher">
                    <input class="theme-switch row" id="theme-switcher" type="checkbox"'.$checked.' data-setting="theme" data-theme role="switch" name="dark-mode"><span class="slider">'.
                    jvbIcon('light', ['title'=> 'Light Mode']).
                    jvbIcon('dark', ['title'=>'Dark Mode']).
                    '</span></label>';
                    jvbIcon('sun-dim', ['title'=> 'Light Mode']).
                    jvbIcon('moon', ['title'=>'Dark Mode']).
                    '</span></label>' : '';
                $breadcrumbs = jvbBuildBreadcrumbs();
                $afterHeader = apply_filters('jvbBelowHeader', $afterHeader);
                if ($afterHeader !== '') {
@@ -859,8 +876,6 @@
                'id' => get_current_user_id()
            ] : null,
            'from'  => get_the_ID(),
            'icons' => jvbFeedBlockIcons(),
        ]);
    }
@@ -876,18 +891,22 @@
    public function innerBlocks(array $block, string $before = '', string $after = ''):string
    {
        $content = '';
        foreach ($block['innerBlocks'] as $b) {
            $method = 'render_'.$this->sanitizeBlockName($b);
            $content .= $before;
            if (method_exists($this, $method)) {
                $content .= $this->$method($b, '');
            } else {
                $content .= render_block($b);
            }
            $content .= $after;
        }
        return $content;
        $content = '';
        foreach ($block['innerBlocks'] as $b) {
            $method = 'render_'.$this->sanitizeBlockName($b);
            $function = BASE.$method;
            $content .= $before;
            if (function_exists($function)) {
                $content .= $function($b, '');
            } else if (method_exists($this, $method)) {
                $content .= $this->$method($b, '');
            } else {
                $content .= render_block($b);
            }
            $content .= $after;
        }
        return $content;
    }
    public function inside(array $block, mixed $tag = false, mixed $o = false):string
@@ -946,8 +965,8 @@
    public function imageID(int|string $ID, array $block = []):int|false
    {
        if ($ID === '' && !empty($block)) {
            if (($block['blockName'] === 'core/post-featured-image' ||
                 (!array_key_exists('attrs', $block) && !array_key_exists('id', $block['attrs'])))) {
                $ID = get_post_thumbnail_id();
@@ -982,7 +1001,11 @@
        if (!$ID) {
            return '';
        }
        $imgSlug = get_post($ID)->post_name;
        $img = get_post($ID);
        if (!$img) {
            return '';
        }
        $imgSlug = $img->post_name;
        $img = '<a class="open-gallery" target="_blank" rel="nofollow" data-opens="gallery-'.$postSlug.'" data-focus="'.$postSlug.'-'.$imgSlug.'">'.$image;
        if ($close) {
@@ -1167,7 +1190,6 @@
    }
    protected function getClass(string $key, string|bool|array|int $value, array $attrs):string|array
    {
        switch ($key) {
            //Any additional classes the user adds
            case 'className':
@@ -1177,6 +1199,7 @@
                    default => str_replace('is-style-', '', $value),
                };
            case 'contentPosition':
                $classes = [];
                $pos = explode(' ', $value);
                foreach($pos as $p) {
@@ -1229,14 +1252,14 @@
//
//              }
                if (!array_key_exists('justifyContent', $value) && !array_key_exists('contentPosition', $attrs)) {
                    $classes[] = 'start';
                    $classes[] = ($type === 'row') ? 'start' : 'a-start';
                }
                if (array_key_exists('justifyContent', $value)  && !array_key_exists('contentPosition', $attrs)) {
                    if (in_array($value['justifyContent'], ['left', 'right','space-between'])) {
//                      jvbDump($type);
                        switch ($value['justifyContent']) {
                            case 'right':
                                $classes[] = 'end';
                                $classes[] = ($type === 'row') ? 'end' : 'a-end';
                                break;
                            case 'space-between':
                                $classes[] = 'btw';
@@ -1412,6 +1435,7 @@
                return '';
            default:
                $ignore = [
                    'useFeaturedImage',
                    'opacity',
                    'borderColor',
                    'backgroundColor',
@@ -1453,8 +1477,8 @@
                ];
                if (!is_admin() &&!in_array($key, $ignore)) {
//                  TESTING
                    jvbDump($key, 'getClass');
                    jvbDump($attrs);
//                  jvbDump($key, 'getClass');
//                  jvbDump($attrs);
                }
                return '';
@@ -1674,6 +1698,7 @@
            // Any other attributes that need direct styling
            default:
                $ignore = [
                    'useFeaturedImage',
                    'opacity',
                    'textAlign',
                    'minHeightUnit',
@@ -1718,8 +1743,8 @@
                ];
                if (!is_admin() && !in_array($key, $ignore)) {
                    //TESTING
                    jvbDump($key, 'getStyle');
                    jvbDump($attrs);
//                  jvbDump($key, 'getStyle');
//                  jvbDump($attrs);
                }
                // No default inline styles
                break;
inc/blocks/FeedBlock.php
@@ -223,7 +223,7 @@
                        <div class="filter-actions row">
                            <?= jvbRenderToggleTextField('match', 'Match', 'Filters', 'ALL', 'ANY') ?>
                            <button type="button" class="clear-filters row">
                                <?= jvbIcon('close', ['title'    => 'Clear']) ?>
                                <?= jvbIcon('x', ['title'    => 'Clear']) ?>
                                Clear All Filters
                            </button>
                        </div>
@@ -247,7 +247,7 @@
                        <input type="radio" id="order-random" class="btn" name="orderby" value="random">
                        <label for="order-random" title="Random Order" class="row">
                            <?= jvbIcon('random') ?>
                            <?= jvbIcon('shuffle') ?>
                            <span class="label">Random</span>
                        </label>
                    </div>
@@ -256,12 +256,12 @@
                        <span class="label">ORDER:</span>
                        <input type="radio" id="order-desc" class="btn" name="order" value="desc" checked>
                        <label for="order-desc" title="Newest First" class="row">
                            <?= jvbIcon('desc') ?>
                            <?= jvbIcon('sort-descending') ?>
                        </label>
                        <input type="radio" id="order-asc" class="btn" name="order" value="asc">
                        <label for="order-asc" title="Oldest First" class="row">
                            <?= jvbIcon('asc') ?>
                            <?= jvbIcon('sort-ascending') ?>
                        </label>
                    </div>
                </div>
@@ -281,9 +281,9 @@
    {
        ?>
        <button type="button" class="load-more">
            <?= jvbIcon('elbow-left-down', ['title'    =>'More']) ?>
            <?= jvbIcon('arrow-elbow-left-down', ['title'    =>'More']) ?>
            Show Me More
            <?= jvbIcon('elbow-right-down', ['title'=> 'More']) ?>
            <?= jvbIcon('arrow-elbow-right-down', ['title'=> 'More']) ?>
        </button>
        <?= jvbLoadingScreen() ?>
inc/forms/PostSelectorOld.php
@@ -2,8 +2,6 @@
namespace JVBase\forms;
use JVBase\forms\TaxonomySelector;
use JVBase\JVBIcons;
use JVBase\managers\CacheManager;
use WP_REST_Request;
use WP_REST_Response;
use WP_Term;
inc/forms/TaxonomySelector.php
@@ -187,13 +187,13 @@
        </template>
        <template class="termChildrenToggle">
            <button type="button" class="toggle-children" aria-expanded="false">
                <?=jvbIcon('add')?>
                <?=jvbIcon('plus-square')?>
            </button>
        </template>
        <template class="selectedTerm">
            <div class="selected-item row">
                <span class="item-name"></span>
                <button type="button" class="remove-item row"><?=jvbIcon('close')?></button>
                <button type="button" class="remove-item row"><?=jvbIcon('x')?></button>
            </div>
        </template>
        <template class="termBreadcrumb">
@@ -237,7 +237,7 @@
                    <?= $hasAutocomplete ?>
                    title="Open <?= $this->singular ?> Selector"
                    aria-label="Select <?= esc_attr($this->plural) ?>">
                    <?= jvbIcon('add', ['title' => 'Add ' . $this->title]) ?>
                    <?= jvbIcon('plus-square', ['title' => 'Add ' . $this->title]) ?>
                </button>
                <input type="text" id="<?= $this->base ?><?= esc_attr($this->config['name']) ?>-autocomplete" autocomplete="off" data-ignore data-autocomplete>
                <ul class="autocomplete-dropdown" hidden>
@@ -320,7 +320,7 @@
            <button type="button"
                    class="remove-item row"
                    aria-label="Remove <?= esc_attr($term->name) ?>">
                <?= jvbIcon('close') ?>
                <?= jvbIcon('x') ?>
            </button>
        </div>
        <?php
inc/forms/TaxonomySelectorOld.php
@@ -1,8 +1,8 @@
<?php
namespace JVBase\forms;
use JVBase\JVBIcons;
use JVBase\managers\CacheManager;
use JVBase\managers\IconsManager;
use WP_REST_Request;
use WP_REST_Response;
use WP_Term;
@@ -21,7 +21,7 @@
    protected string $id;
    protected string $name;
    protected string $taxonomyName;
    protected JVBIcons $icon;
    protected IconsManager $icon;
    protected string $plural;
    protected string $taxonomy;
@@ -43,7 +43,7 @@
        $this->id = sanitize_key($id);
        $this->taxonomy = jvbCheckBase($taxonomy);
        $this->name = str_replace(BASE, '', $taxonomy);
        $this->icon = new JVBIcons();
        $this->icon = IconsManager::getInstance();
        $this->cache = CacheManager::for(jvbNoBase($taxonomy), WEEK_IN_SECONDS);
        $this->base = $config['base'] ?? '';
inc/helpers/all.php
@@ -13,7 +13,7 @@
require(JVB_DIR . '/inc/helpers/email.php');
require(JVB_DIR . '/inc/helpers/forms.php');
require(JVB_DIR . '/inc/helpers/formatting.php');
require(JVB_DIR . '/inc/helpers/icons.php');
//require(JVB_DIR . '/inc/helpers/icons.php');
require(JVB_DIR . '/inc/helpers/inlineStyles.php');
require(JVB_DIR . '/inc/helpers/legacy.php');
require(JVB_DIR . '/inc/helpers/media.php');
inc/helpers/breadcrumbs.php
@@ -23,7 +23,7 @@
    $crumbs = [];
    $crumbs[] = [
        'name'  => 'Home',
        'icon'  => jvbIcon('home'),
        'icon'  => jvbIcon('house'),
        'url'   => get_home_url(),
    ];
inc/helpers/crud.php
@@ -103,7 +103,7 @@
                    </button>
                </div>
            </div>',
            jvbIcon('close', ['title'=>'Cancel'])
            jvbIcon('x', ['title'=>'Cancel'])
        );
    }
    $out .= '</div>';
inc/helpers/formatting.php
@@ -147,13 +147,13 @@
        return [];
    }
    $image = [
        'tiny' => wp_get_attachment_image_src($imgID, 'tiny')[0],
        'small' => wp_get_attachment_image_src($imgID, 'medium')[0],
        'medium' => wp_get_attachment_image_src($imgID, 'large')[0],
        'large' => wp_get_attachment_image_src($imgID, 'full')[0],
        'alt'   => get_post_meta($imgID, '_wp_attachment_image_alt', true),
        'title' => get_the_title($imgID),
        'caption' => get_the_excerpt($imgID),
        'tiny'          => wp_get_attachment_image_src($imgID, 'tiny')[0],
        'small'         => wp_get_attachment_image_src($imgID, 'medium')[0],
        'medium'        => wp_get_attachment_image_src($imgID, 'large')[0],
        'large'         => wp_get_attachment_image_src($imgID, 'full')[0],
        'image-alt-text'=> get_post_meta($imgID, '_wp_attachment_image_alt', true),
        'image-title'   => get_the_title($imgID),
        'image-caption' => get_the_excerpt($imgID),
    ];
    $cache->set($imgID, $image);
    return $image;
inc/helpers/forms.php
@@ -21,7 +21,7 @@
            $meta->render('form', $field, $config);
        }
        ?>
        <?= (jvbCheck('submit', $options)) ? '<button type="submit">'.jvbIcon('save').'Save</button>' : '' ?>
        <?= (jvbCheck('submit', $options)) ? '<button type="submit">'.jvbIcon('floppy-disk').'Save</button>' : '' ?>
    </form>
    <?php
    $out = ob_get_clean();
inc/helpers/icons.php
File was deleted
inc/helpers/legacy.php
@@ -11,7 +11,9 @@
function jvbRandomFooterText():string
{
    return apply_filters('jvbRandomFooterText', '<p>©'.date('Y').' '.get_bloginfo('name').'</p><p>Built with â™¡ by <a href="https://jakevan.ca">Jake Van</a>');
    $privacy = get_privacy_policy_url();
    $privacy = ($privacy === '') ? '' : ' | <a href="'.$privacy.'">Privacy Policy</a>';
    return apply_filters('jvbRandomFooterText', '<p class="font-small">©'.date('Y').' <a href="'.get_home_url().'">'.get_bloginfo('name').'</a>'.$privacy.'</p><p class="font-small">Built with â™¡ by <a href="https://jakevan.ca">Jake Van</a></p>');
//
//  $aOpen = '<a href="https://legacytattooremoval.ca" title="Learn more about Legacy Tattoo Removal">';
//  $options = array(
inc/helpers/media.php
@@ -9,13 +9,13 @@
    ?>
    <dialog class="gallery" aria-modal="true" aria-label="Image Gallery">
        <div class="wrap">
            <button type="button" class="cancel" aria-label="Close Gallery"> <?= jvbIcon('close') ?></button>
            <button type="button" class="cancel" aria-label="Close Gallery"> <?= jvbIcon('x') ?></button>
            <button class="nav prev row" aria-label="Previous image">
                <?= jvbIcon('prev') ?>
                <?= jvbIcon('caret-left') ?>
            </button>
            <button class="nav next row" aria-label="Next image">
                <?= jvbIcon('next') ?>
                <?= jvbIcon('caret-right') ?>
            </button>
            <div class="content row">
inc/helpers/members.php
@@ -208,13 +208,13 @@
            return 'admin';
        }
    }
    $user = ($ID === 0) ? wp_get_current_user() : get_userdata($ID);
    if (user_can($user, 'manage_options')) {
    if ($ID > 0 && user_can($ID, 'manage_options')) {
        return 'admin';
    }
    $user = ($ID === 0) ? wp_get_current_user() : get_userdata($ID);
    error_log('Current User: '.print_r($user, true));
    return array_values(array_intersect(
        array_keys(JVB_USER),
        array_keys(array_merge(JVB_USER, ['administrator'])),
        array_map(function ($role) {
            return jvbNoBase($role);
        },
inc/helpers/renderFields.php
@@ -79,41 +79,41 @@
            $url = $link['url'];
            switch (true) {
                case strpos($url, 'facebook'):
                    $i = jvbIcon('facebook');
                    $i = jvbIcon('facebook-logo');
                    break;
                case strpos($url, 'instagram'):
                    $i = jvbIcon('instagram');
                    $i = jvbIcon('instagram-logo');
                    break;
                case strpos($url, 'tiktok'):
                    $i = jvbIcon('tiktok');
                    $i = jvbIcon('tiktok-logo');
                    break;
                case strpos($url, 'x.com'):
                    $i = jvbIcon('x');
                    break;
                case strpos($url, 'youtube'):
                    $i = jvbIcon('youtube');
                    $i = jvbIcon('youtube-logo');
                    break;
                case strpos($url, 'mastadon'):
                    $i = jvbIcon('mastadon');
                    $i = jvbIcon('mastadon-logo');
                    break;
                case strpos($url, 'fediverse'):
                case strpos($url, 'bluesky'):
                    $i = jvbIcon('fediverse');
                    $i = jvbIcon('fediverse-logo');
                    break;
                case strpos($url, 'linktree'):
                    $i = jvbIcon('linktree');
                    $i = jvbIcon('linktree-logo');
                    break;
                case strpos($url, 'snapchat'):
                    $i = jvbIcon('snapchat');
                    $i = jvbIcon('snapchat-logo');
                    break;
                case strpos($url, 'twitch'):
                    $i = jvbIcon('twitch');
                    $i = jvbIcon('twitch-logo');
                    break;
                case strpos($url, 'threads'):
                    $i = jvbIcon('threads');
                    $i = jvbIcon('threads-logo');
                    break;
                case strpos($url, 'twitter'):
                    $i = jvbIcon('twitter');
                    $i = jvbIcon('twitter-logo');
                    break;
                default:
                    $i = jvbIcon('link');
@@ -158,7 +158,7 @@
            switch ($p) {
                case 'text':
                    $link = 'sms:+1'.$phone.';?body='.rawurlencode('Hey! I found you on edmonton.ink, and I wanted to reach out.');
                    $label = jvbIcon('text').'<span>Text</span>';
                    $label = jvbIcon('chat').'<span>Text</span>';
                    break;
                case 'call':
                    $link = 'tel:+1'.$phone;
@@ -167,7 +167,7 @@
                case 'email':
                    $link = 'mailto:'.$meta->getValue('email').'?subject='.rawurlencode('Contact from edmonton.ink').'&body='.rawurlencode('Hey,
        I found you on edmonton.ink, and I wanted to reach out!');
                    $label = jvbIcon('email').'<span>Email</span>';
                    $label = jvbIcon('envelope').'<span>Email</span>';
                    break;
            }
            if ($link) {
@@ -373,11 +373,11 @@
                    </div>
                    <div class="group-actions">
                        <button type="button" data-action="add-to-group" title="Add selected uploads to this group">
                            <?= jvbIcon('add') ?>
                            <?= jvbIcon('plus-square') ?>
                            Add Here
                        </button>
                        <button type="button" data-action="delete-group" title="Delete group">
                            <?= jvbIcon('delete') ?>
                            <?= jvbIcon('trash') ?>
                            Delete Group
                        </button>
                    </div>
@@ -407,7 +407,7 @@
                </div>
                <button type="button" data-action="remove-from-group" title="Remove from Group">
                    <?=jvbIcon('delete')?>
                    <?=jvbIcon('trash')?>
                </button>
            </div>
        </template>
@@ -418,7 +418,7 @@
                </div>
                <div class="selection-controls">
                    <button type="button" data-action="add-to-group">
                        <?= jvbIcon('add') ?>
                        <?= jvbIcon('plus-square') ?>
                        New Group
                    </button>
                </div>
@@ -472,11 +472,11 @@
                        </div>
                        <div class="m-actions row nowrap">
                            <button type="button" data-action="restore">
                                <?= jvbIcon('restore') ?>
                                <?= jvbIcon('arrow-counter-clockwise') ?>
                                Restore Selected
                            </button>
                            <button type="button" data-action="clear-cache" title="Clear cache and close window">
                                <?= jvbIcon('close') ?>
                                <?= jvbIcon('x') ?>
                                Clear Cache
                            </button>
                        </div>
inc/helpers/terms.php
@@ -81,6 +81,10 @@
add_action('save_post', 'jvbClearListCache', 10, 2);
function jvbClearListCache(int $post_id, \WP_Post $post):void
{
    // SAFETY: Skip attachments and other non-content post types
    if (in_array($post->post_type, jvbIgnoredPostTypes())) {
        return;
    }
    if (jvbNoSaveIt($post_id, $post)) {
        return;
    }
inc/helpers/ui.php
@@ -21,7 +21,7 @@
            <div class="refresh row btw">
                <span class="countdown row" title="Will refresh again...">5</span>
                <button class="refreshNow row" title="Check now">
                    <?= jvbIcon('refresh', ['title'=> 'Check now']) ?>
                    <?= jvbIcon('arrows-clockwise', ['title'=> 'Check now']) ?>
                </button>
            </div>
            <div class="popup row"><span></span></div>
@@ -62,7 +62,7 @@
        </div>
    </aside>
    <button class="qtoggle row" title="Show Queue" aria-controls="queue" hidden>
        <?= jvbIcon('save') ?>
        <?= jvbIcon('floppy-disk') ?>
        <span class="screen-reader-text"></span>
        <span class="indicator"></span>
        <span class="count row"></span>
@@ -111,7 +111,7 @@
        ?>
        <li>
            <a href="<?=get_home_url(null, '/dash/')?>" title="Behind the Scenes">
                <?= jvbIcon('dashboard', ['title' => 'Behind the Scenes'])?>
                <?= jvbIcon('door', ['title' => 'Behind the Scenes'])?>
                <span class="screen-reader-text">Go Behind the Scenes</span>
            </a>
        </li>
@@ -180,7 +180,7 @@
        }
        $out = '<li class="has-submenu">
                <button class="toggle quick-help" type="button" title="Toggle Quick Help Menu" aria-expanded="false" aria-controls="quick-help" aria-label="Toggle Quick Help Menu">'.jvbIcon('help', ['title'=> 'Quick Help']).'</button>
                <button class="toggle quick-help" type="button" title="Toggle Quick Help Menu" aria-expanded="false" aria-controls="quick-help" aria-label="Toggle Quick Help Menu">'.jvbIcon('question', ['title'=> 'Quick Help']).'</button>
                <ul class="submenu">
                    '.$links.'
                </ul>
@@ -190,9 +190,9 @@
    }
    if (is_user_logged_in()) {
        $out .= '<li><a href="'.wp_logout_url(get_home_url()).'" title="Logout">'.jvbIcon('logout').'<span class="screen-reader-text">Logout</span></a></li>';
        $out .= '<li><a href="'.wp_logout_url(get_home_url()).'" title="Logout">'.jvbIcon('sign-out').'<span class="screen-reader-text">Logout</span></a></li>';
    } else {
        $out .= '<li><a href="'.wp_login_url(null, true).'" title="Login">'.jvbIcon('login').'<span class="screen-reader-text">Login</span></a></li>';
        $out .= '<li><a href="'.wp_login_url(null, true).'" title="Login">'.jvbIcon('sign-in').'<span class="screen-reader-text">Login</span></a></li>';
    }
    return $out;
}
@@ -213,7 +213,7 @@
    aria-label="Clear search"
    onclick="this.previousElementSibling.value = \'\'; this.previousElementSibling.focus();"
  >'.jvbIcon('x', ['title'=> 'Clear Search']).'</button>
  <button type="button" title="Search" class="toggle search" aria-label="Toggles search input visually" onclick="this.parentNode.classList.toggle(\'open\');this.previousElementSibling.previousElementSibling.focus();">'.jvbIcon('search').'</button>
  <button type="button" title="Search" class="toggle search" aria-label="Toggles search input visually" onclick="this.parentNode.classList.toggle(\'open\');this.previousElementSibling.previousElementSibling.focus();">'.jvbIcon('magnifying-glass').'</button>
</div>';
}
@@ -222,8 +222,8 @@
function jvbModalActions()
{
    return '<div class="m-actions row">
        <button type="button" class="cancel">'.jvbIcon('close').'<span class="screen-reader-text">Cancel</span></button>
        <button type="submit" class="save">'.jvbIcon('save').'<span class="screen-reader-text">Save</span></button>
        <button type="button" class="cancel">'.jvbIcon('x').'<span class="screen-reader-text">Cancel</span></button>
        <button type="submit" class="save">'.jvbIcon('floppy-disk').'<span class="screen-reader-text">Save</span></button>
    </div>';
}
@@ -278,7 +278,7 @@
            <div class="toast-content row btw">
                <p></p>
                <button type="button" class="close-toast" aria-label="Close">
                    <?= jvbIcon('close') ?>
                    <?= jvbIcon('x') ?>
                </button>
            </div>
        </div>
@@ -304,13 +304,13 @@
    <nav id="<?=$id?>" class="on-this-page index">
        <label>Jump to:
            <button type="button" aria-label="Show Index" title="Show Index" class="toggle" aria-expanded="false">
                <?= jvbIcon('add') ?>
                <?= jvbIcon('plus-square') ?>
            </button>
        </label>
        <ul>
            <li>
                <a href="#top" title="Back to Top">
                    <?= jvbIcon('up') ?>
                    <?= jvbIcon('caret-circle-up') ?>
                </a>
            </li>
            <?php
@@ -336,7 +336,7 @@
        <div class="col">
            <div class="spinner"></div>
            <div class="status col">
                <div class="icon">'.apply_filters('jvbLoadingIcon', jvbIcon('tattoo')).'</div>
                <div class="icon">'.apply_filters('jvbLoadingIcon', jvbIcon('drop-simple')).'</div>
                <h3>Loading</h3>
                <p class="typeText">Please wait...</p>
            </div>
@@ -449,7 +449,7 @@
                    <span>Start Over</span>
                </button>
                <button type="button" data-action="dismiss-restore">
                    '.jvbIcon('close').'
                    '.jvbIcon('x').'
                    <span>Dismiss</span>
                </button>
            </div>
inc/integrations/Facebook.php
@@ -563,7 +563,7 @@
    /**
     * Helper: Get user's Facebook pages
     */
    private function getUserPages(string $access_token = null): array
    private function getUserPages(?string $access_token = null): array
    {
        $token = $access_token ?: $this->credentials['access_token'];
inc/integrations/GoogleMyBusiness.php
@@ -10,9 +10,12 @@
class GoogleMyBusiness extends Integrations
{
    private string $access_token;
    private ?string $access_token = null;
    protected string $readMask = 'name,title,storefrontAddress,metadata,openInfo,storeCode,categories,phoneNumbers,labels,specialHours';
    private ?string $location = null;
    private ?string $refresh_token = null;
    private ?string $client_id = null;
    private ?string $client_secret = null;
    private ?string $account_id = null;
    public function __construct(?int $userID = null)
@@ -134,12 +137,12 @@
        if (empty($this->credentials)) {
            $this->loadCredentials();
        }
        $this->access_token = $this->credentials['access_token'] ?? '';
        $this->refresh_token = $this->credentials['refresh_token'] ?? '';
        $this->client_id = $this->credentials['client_id'] ?? '';
        $this->client_secret = $this->credentials['client_secret'] ?? '';
        $this->location = $this->credentials['location'] ?? null;
        $this->account_id = $this->credentials['account'] ?? null;
        $this->access_token = (array_key_exists('access_token', $this->credentials)) ? $this->credentials['access_token'] : null;
        $this->refresh_token = (array_key_exists('refresh_token', $this->credentials)) ? $this->credentials['refresh_token'] : null;
        $this->client_id = (array_key_exists('client_id', $this->credentials)) ? $this->credentials['client_id'] : null;
        $this->client_secret = (array_key_exists('client_secret', $this->credentials)) ? $this->credentials['client_secret'] : null;
        $this->location = (array_key_exists('location', $this->credentials)) ? $this->credentials['location'] : null;
        $this->account_id = (array_key_exists('account', $this->credentials)) ? $this->credentials['account'] : null;
        if ($this->account_id) {
            $this->apiEndpoints[] = "/v1/{$this->account_id}/locations";
inc/integrations/Helcim.php
@@ -240,7 +240,7 @@
        }
        ?>
        <button type="button" class="toggle-cart row" title="Your Cart" data-action="toggle-cart" aria-label="Open Cart" aria-controls="checkout" aria-expanded="false" hidden>
            <?= jvbIcon('cart')?><span class="abs"></span><span class="abs count"></span>
            <?= jvbIcon('shopping-cart')?><span class="abs"></span><span class="abs count"></span>
        </button>
        <aside id="cart">
            <form id="checkout" data-form-id="checkout" data-save="checkout">
@@ -461,7 +461,7 @@
                    <label for="quantity"></label>
                    <div class="quantity field" data-min="0" data-max="50" data-step="1" data-price="" data-id="">
                        <button type="button" class="decrease" aria-label="Decrease quantity">
                            <?= jvbIcon('minus') ?>
                            <?= jvbIcon('minus-square') ?>
                        </button>
                        <input type="number" name="quantity" value="1" min="0" max="50">
                        <button type="button" class="increase" aria-label="Increase quantity">
inc/integrations/Integrations.php
@@ -3073,7 +3073,7 @@
                    switch ($action) {
                        case 'save_credentials':
                            $title = $label;
                            $label = jvbIcon('save');
                            $label = jvbIcon('floppy-disk');
                            break;
                        case 'clear_credentials':
                            $title = $label;
inc/integrations/PostMark.php
@@ -15,6 +15,12 @@
 */
class PostMark extends Integrations
{
    protected ?string $server_token = null;
    protected string $message_stream = 'outbound';
    protected string $from_email;
    protected string $from_name;
    protected bool $track_open;
    protected bool $track_links;
    /**
     * Constructor
     */
@@ -22,9 +28,10 @@
    {
        $this->service_name = 'postmark';
        $this->title = 'PostMark';
        $this->description = 'Professional transactional email delivery service';
        $this->icon = 'envelope-simple';
        $this->cacheName = ($userID)? 'gmb_'.$userID : 'postmark';
        // PostMark API configuration
        $this->apiBase = 'https://api.postmarkapp.com';
        $this->apiVersion = 'v1';
@@ -48,12 +55,13 @@
        ];
        // Required credentials
        $this->requiredCredentials = [
        $this->fields = [
            'server_token' => [
                'label' => 'Server API Token',
                'type' => 'password',
                'type' => 'text',
                'subtype'=> 'password',
                'placeholder' => 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
                'description' => 'Your PostMark Server API Token'
                'hint' => 'Your PostMark Server API Token'
            ],
            'message_stream' => [
                'label' => 'Message Stream ID',
@@ -101,7 +109,7 @@
        parent::__construct($userID);
        // Hook into WordPress mail system if integration is healthy
        if ($this->is_healthy && $this->hasValidCredentials()) {
        if ($this->is_healthy && $this->isSetUp()) {
            $this->initializeMailHooks();
        }
    }
@@ -271,7 +279,7 @@
            return true;
        } catch (\Exception $e) {
            $this->handleException($e, 'email');
            $this->logError('[POSTMARK]', ['method' => 'sendEmail', 'error' => $e->getMessage()]);
            return new WP_Error(
                'postmark_send_failed',
                $e->getMessage()
@@ -459,22 +467,15 @@
    /**
     * Override makeRequest to add PostMark headers
     */
    protected function makeRequest(
        string $method,
        string $endpoint,
        array $params = [],
        array $headers = []
    ): mixed {
    protected function getRequestHeaders(): array {
        $credentials = $this->getCredentials();
        // Add PostMark specific headers
        $headers = array_merge([
        return [
            'Accept' => 'application/json',
            'Content-Type' => 'application/json',
            'X-Postmark-Server-Token' => $credentials['server_token'] ?? ''
        ], $headers);
        return parent::makeRequest($method, $endpoint, $params, $headers);
        ];
    }
    /**
@@ -500,7 +501,7 @@
                return $stats;
            }
        } catch (\Exception $e) {
            $this->handleException($e, 'stats/outbound');
            $this->logError('stats/outbound', ['error' => $e->getMessage()]);
        }
        return [];
@@ -517,7 +518,7 @@
                'offset' => $offset
            ]);
        } catch (\Exception $e) {
            $this->handleException($e, 'bounces');
            $this->logError('[POSTMARK]', ['method' => 'getBounces', 'error' => $e->getMessage()]);
            return [];
        }
    }
@@ -634,11 +635,14 @@
    protected function initialize(): void
    {
        // TODO: Implement initialize() method.
    }
    protected function getRequestHeaders(): array
    {
        // TODO: Implement getRequestHeaders() method.
        if (empty($this->credentials)) {
            $this->loadCredentials();
        }
        $this->server_token = (array_key_exists('server_token', $this->credentials)) ? $this->credentials['server_token'] : null;
        $this->from_email = (array_key_exists('from_email', $this->credentials)) ? $this->credentials['from_email'] : get_bloginfo('admin_email');
        $this->from_name = (array_key_exists('from_name', $this->credentials)) ? $this->credentials['from_name'] : get_bloginfo('name');
        $this->message_stream = (array_key_exists('message_stream', $this->credentials)) ? $this->credentials['message_stream'] : 'outbound';
        $this->track_open = (array_key_exists('track_open', $this->credentials)) ? $this->credentials['track_open'] : false;
        $this->track_links = (array_key_exists('track_links', $this->credentials)) ? $this->credentials['track_links'] : false;
    }
}
inc/integrations/Square.php
@@ -949,11 +949,11 @@
                    <label for="quantity"></label>
                    <div class="quantity field" data-min="0" data-max="50" data-step="1" data-price="17" data-id="">
                        <button type="button" class="decrease"aria-label="Decrease Add to Order">'.jvbIcon('minus').'</button>
                        <button type="button" class="decrease"aria-label="Decrease Add to Order">'.jvbIcon('minus-square').'</button>
                        <input type="number" id="quantity" name="quantity" value="0" min="0" max="50" step="1" class="quantity-input">
                        <button type="button" class="increase" aria-label="Increase Add to Order">'.jvbIcon('add').'</button>
                        <button type="button" class="increase" aria-label="Increase Add to Order">'.jvbIcon('plus-square').'</button>
                    </div>
                </td>
                <td class="price">
@@ -977,7 +977,7 @@
        $actions[] = [
            'button' =>     '<button type="button" class="toggle-cart row" title="Your Cart" data-action="toggle-cart" aria-label="Open Cart" aria-controls="checkout" aria-expanded="false">
                    '.jvbIcon('cart').'<span class="abs"></span><span class="abs count"></span>
                    '.jvbIcon('shopping-cart').'<span class="abs"></span><span class="abs count"></span>
                </button>',
            'content' =>    $form
        ];
inc/managers/AdminPages.php
@@ -417,7 +417,7 @@
                <a data-action="<?=$action['slug']?>" class="jvb-action">
                    <?= jvbIcon($action['icon']); ?>
                    <span class="jvb-link-title"><?= esc_html($action['label'])?></span>
                    <span class="loader"><?=jvbIcon('refresh')?><?=jvbIcon('check')?></span>
                    <span class="loader"><?=jvbIcon('arrows-clockwise')?><?=jvbIcon('check')?></span>
                </a>
                <?php
            }
inc/managers/CRUDManager.php
@@ -28,6 +28,13 @@
    protected array $sections;
    protected array $stuck;
    //For Timeline-specific posts
    protected bool $isTimeline = false;
    protected array $nonTimelineFields = [];
    protected array $timelineSharedFields = [];
    protected array $timelineUniqueFields = [];
    protected bool $userCanPublish = false;
    public function __construct(string $content)
@@ -43,6 +50,7 @@
        $this->plural = $this->config['plural'];
        $this->content = $content;
        $this->fields = jvbGetFields($this->content, 'post');
        $this->maybeSetupTimeline();
        $this->sections = jvbGetSections($this->content, 'post');
        $this->stuck = [
            'post_title',
@@ -67,6 +75,34 @@
    }
    protected function maybeSetupTimeline():void {
        $this->isTimeline = Features::forContent($this->content)->has('is_timeline');
        if (!$this->isTimeline) {
            return;
        }
        $this->timelineSharedFields = array_keys(array_filter($this->fields, function ($field) {
            if (!array_key_exists('for_all', $field) || $field['for_all'] === false){
                return true;
            }
            return false;
        }));
        array_unshift($this->timelineSharedFields, 'post_thumbnail');
        array_unshift($this->timelineSharedFields, 'post_title');
        $this->timelineUniqueFields = array_keys(array_filter($this->fields, function ($field) {
            if (array_key_exists('for_all', $field) && $field['for_all'] === true) {
                return true;
            }
            return false;
        }));
        $all = array_merge($this->timelineUniqueFields, $this->timelineSharedFields);
        $this->nonTimelineFields = array_filter($this->fields, function ($field) use ($all) {
            return !in_array($field, $all);
        }, ARRAY_FILTER_USE_KEY);
    }
    protected function initTaxonomies():void
    {
        $this->taxonomies = array_filter(JVB_TAXONOMY, function ($config) {
@@ -84,41 +120,41 @@
                ],
                'future'=> [
                    'label' => 'Upcoming',
                    'icon'  => 'future',
                    'icon'  => 'clock-clockwise',
                ],
                'past'  => [
                    'label' => 'Past',
                    'icon'  => 'past',
                    'icon'  => 'clock-counter-clockwise',
                ],
                'repeat'=> [
                    'label' => 'Recurring',
                    'icon'  => 'repeat',
                ],
                'draft' => [
                    'icon'  => 'hide',
                    'icon'  => 'eye-closed',
                    'label' => 'Hidden',
                ],
                'trash' => [
                    'label' => 'Scrapped',
                    'icon'  => 'delete',
                    'icon'  => 'trash',
                ],
            ] :
            [
                'all'   => [
                    'icon'  => 'all',
                    'icon'  => 'infinity',
                    'label' => 'Everything',
                ],
                'publish'=> [
                    'icon'  => 'publish',
                    'icon'  => 'eye',
                    'label' => 'Live',
                ],
                'draft' => [
                    'icon'  => 'hide',
                    'icon'  => 'eye-closed',
                    'label' => 'Hidden',
                ],
                'trash' => [
                    'label' => 'Scrapped',
                    'icon'  => 'delete',
                    'icon'  => 'trash',
                ],
            ];
    }
@@ -147,7 +183,7 @@
        foreach ($this->taxonomies as $taxonomy=> $config) {
            $this->filters['taxonomy'][$taxonomy] = [
                'label' => $config['singular'],
                'icon'  => $taxonomy
                'icon'  => $config['icon']??'folder'
            ];
        }
    }
@@ -248,9 +284,9 @@
        ob_start();
        ?>
        <div class="empty-state">
            <h3><?=jvbIcon($this->content)?>Nothing here<?=jvbIcon($this->content)?></h3>
            <h3><?=jvbIcon($this->config['icon'])?>Nothing here<?=jvbIcon($this->config['icon'])?></h3>
            <p>It doesn't look like you have any <?=$this->config['plural'] ?> yet.</p>
            <p><small><i>Add many by uploading images above.</i>, or click the "<?=jvbIcon('add')?>" button to add one at a time.</small></p>
            <p><small><i>Add many by uploading images above.</i>, or click the "<?=jvbIcon('plus-square')?>" button to add one at a time.</small></p>
        </div>
        <?php
        return ob_get_clean();
@@ -278,7 +314,7 @@
                    $this->renderDateFilters();
                ?>
                <button type="button" class="clear-filters row" hidden>
                    <?= jvbIcon('close', ['title'    => 'Clear']); ?>
                    <?= jvbIcon('x', ['title'    => 'Clear']); ?>
                    Clear All Filters
                </button>
            </div>
@@ -295,12 +331,12 @@
            <?php
                $order = [
                    'orderby' => [
                        'date' => 'Order by date created',
                        'calendar' => 'Order by date created',
                        'alphabetical' => 'Order alphabetically'
                    ],
                    'order' => [
                        'asc' => 'In ascending order (Z-A, oldest to newest)',
                        'desc' => 'In descending order (A-Z, newest to oldest)'
                        'sort-ascending' => 'In ascending order (Z-A, oldest to newest)',
                        'sort-descending' => 'In descending order (A-Z, newest to oldest)'
                    ]
                ];
@@ -360,8 +396,8 @@
            <?php
            $views = [
                'grid' => ['icon' => 'grid', 'label' => 'Grid View'],
                'list' => ['icon' => 'list', 'label' => 'List View'],
                'grid' => ['icon' => 'squares-four', 'label' => 'Grid View'],
                'list' => ['icon' => 'rows', 'label' => 'List View'],
                'table' => ['icon' => 'table', 'label' => 'Table View']
            ];
@@ -605,7 +641,7 @@
    {
        ob_start();
        ?>
        <form class="edit-form" data-save="content" data-form-id="edit-<?=$this->content?>">
        <form class="edit-form" data-save="content" data-form-id="edit-<?=$this->content?>" data-autosave<?= ($this->isTimeline) ? ' data-timeline' : ''?>>
            <?= jvbFormStatus() ?>
            <input type="hidden" name="form-id" value="<?=uniqid('new-')?>" />
            <input type="hidden" name="content" value="<?=$this->content?>" />
@@ -637,11 +673,9 @@
                    $tabs = false;
                }
                $isTimeline = Features::forContent($this->content)->has('is_timeline');
                $fields = $this->fields;
                if (!$isTimeline) {
                if (!$this->isTimeline) {
                    $first = ['post_thumbnail', 'post_title', 'price'];
                    foreach ($first as $f) {
@@ -657,13 +691,11 @@
                    }
                }
                if ($isTimeline) {
                if ($this->isTimeline) {
                    $temp = array_filter($fields, function ($field) {
                        if (array_key_exists('for_all', $field) && $field['for_all'] === true) {
                            return true;
                        }
                        return false;
                    });
                        return in_array($field, $this->timelineUniqueFields);
                    }, ARRAY_FILTER_USE_KEY);
                    $config = [
                        'type'      => 'gallery',
                        'subtype'   => 'timeline',
@@ -673,21 +705,16 @@
                    ];
                    $content = '';
                    foreach ($fields as $slug=> $field) {
                        if (!array_key_exists('for_all', $field) || $field['for_all'] === false) {
                        if (in_array($slug, $this->timelineSharedFields)) {
                            $content .= $this->form->render($slug, null, $field, false, true);
                        }
                    }
                    $content .= $this->meta->render('form', 'timeline', $config, false,true);
                    $tabs['progression']['content'] = $content;
                    $this->fields = array_filter($fields, function ($field) {
                        if (array_key_exists('for_all', $field) && $field['for_all'] === true) {
                            return false;
                        }
                        return true;
                    });
                    $fields = $this->fields;
                    $fields = $this->nonTimelineFields;
                }
                foreach ($fields as $n => $config) {
                    if ($tabs) {
@@ -731,7 +758,7 @@
            </div>
            <button type="button" class="add-repeater-row btn secondary">
                <?= jvbIcon('add') ?>
                <?= jvbIcon('plus-square') ?>
                <span>Add Progress Step</span>
            </button>
        </div>
@@ -769,7 +796,7 @@
                if ($status === 'future') {
                    $status = 'publish';
                    $config = [
                        'icon'  => 'publish',
                        'icon'  => 'eye',
                        'label' => 'Live',
                    ];
                } else {
@@ -870,16 +897,13 @@
        $this->renderGridView();
        $this->renderTableView();
        $this->renderTableRow();
        if (Features::forContent($this->content)->has('is_timeline')) {
        if ($this->isTimeline) {
            $temp = array_filter($this->fields, function ($field) {
                if (array_key_exists('for_all', $field) && $field['for_all'] === true) {
                    return true;
                }
                return false;
            });
                return in_array($field, $this->timelineUniqueFields);
            }, ARRAY_FILTER_USE_KEY);
            $form = new MetaForm();
            echo '<template class="uploadTimeline">';
            $form->renderImagePreview(null,$temp);
            echo '<template class="timelineItem">';
            $form->renderImagePreview(null,['fields' => $temp]);
            echo '</template>';
        }
        echo jvbGetEmptyStateTemplate();
@@ -917,11 +941,11 @@
        ?>
        <div class="item-actions">
            <button type="button" class="action" data-action="edit" title="Edit <?= $this->singular ?>">
                <?=jvbIcon('edit')?>
                <?=jvbIcon('pencil-simple')?>
                <span class="screen-reader-text">Edit <?= $this->singular ?></span>
            </button>
            <button type="button" class="action" data-action="trash" title="Scrap <?= $this->singular ?>">
                <?=jvbIcon('delete')?>
                <?=jvbIcon('trash')?>
                <span class="screen-reader-text">Scrap <?= $this->singular ?></span>
            </button>
<!--            <button type="button" class="action" data-action="toggle-status">-->
@@ -1060,7 +1084,7 @@
                if ($status === 'future') {
                    $status = 'publish';
                    $config = [
                        'icon' => 'publish',
                        'icon' => 'eye',
                        'label' => 'Live'
                    ];
                } elseif ($status === 'past') {
@@ -1118,12 +1142,12 @@
                'vertical',
                'TAB NAV:',
                '',
                jvbIcon('down'),
                jvbIcon('right')
                jvbIcon('caret-double-down'),
                jvbIcon('caret-double-right')
            ) ?>
            <button type="button" class="add-row" title="Add new row">
                <?= jvbIcon('add') ?>
                <?= jvbIcon('plus-square') ?>
                <span>Add Row</span>
            </button>
        </div>
@@ -1137,7 +1161,7 @@
        $this->renderCreateModal();
        $content = ob_get_clean();
        $create = [
            'button'    => '<button type="button" class="create-item row" title="Create New '.$this->singular.'">'.jvbIcon('add').'<span class="screen-reader-text">Create New '.$this->singular.'</span></button>',
            'button'    => '<button type="button" class="create-item row" title="Create New '.$this->singular.'">'.jvbIcon('plus-square').'<span class="screen-reader-text">Create New '.$this->singular.'</span></button>',
            'content'   => $content,
        ];
        $actions[] = $create;
inc/managers/DashboardManager.php
@@ -23,13 +23,12 @@
    public function __construct()
    {
        $this->cache = CacheManager::for('dashboard', WEEK_IN_SECONDS);
        $this->cache->invalidate();
        add_action('init', [$this, 'registerDashboard']);
        if (!$this->isRegistered()) {
            add_action('init', [$this, 'buildDashboard']);
        }
        $this->user = wp_get_current_user();
        $this->role = jvbUserRole();
        $this->role = jvbUserRole($this->user->ID);
        $this->userLink = (int)get_user_meta($this->user->ID, BASE.'link', true);
@@ -307,6 +306,12 @@
        // Get current page/section
        $page = $this->getCurrentPage();
        $config = $this->getConfig($page);
        if(!empty($config)) {
            add_filter('jvbLoadingIcon', function() use ($config) {
                return jvbIcon($config['icon']);
            });
        }
        $integrationSlugs = array_map(function($name) {
            return sanitize_title(str_replace('_', '-', $name));
        }, array_keys(JVB()->getAvailableServices(false)));
@@ -316,13 +321,14 @@
            // Pass along to the Integrations template handler which knows to check for subpages
            $page = 'integrations';
        }
        echo $this->cache->remember(
            $page,
            function() use ($page) {
                return $this->renderDashboard($page);
            }
        );
        echo $this->renderDashboard($page);
        //TODO: Reenable
//      echo $this->cache->remember(
//          $page,
//          function() use ($page) {
//              return $this->renderDashboard($page);
//          }
//      );
        return '';
    }
@@ -614,8 +620,8 @@
            $title = ($checked == '') ? 'Toggle Dark Mode' : 'Toggle Light Mode';
            echo '<label title="'.$title.'" id="theme-switch" class="toggle-switch" for="theme-switcher">
                    <input class="theme-switch row" id="theme-switcher" type="checkbox"'.$checked.' data-setting="theme" data-theme role="switch" name="dark-mode"><span class="slider">'.
                    jvbIcon('light', ['title'=> 'Light Mode']).
                    jvbIcon('dark', ['title'=>'Dark Mode']).
                    jvbIcon('sun-dim', ['title'=> 'Light Mode']).
                    jvbIcon('moon', ['title'=>'Dark Mode']).
                    '</span></label>';
            ?>
            <p class="title">
@@ -630,7 +636,7 @@
                        }
                    }
                    if ($out == '') {
                        $out =jvbIcon('home');
                        $out =jvbIcon('house');
                    }
                    ?><?= $out ?>
                </a>
@@ -659,7 +665,6 @@
                <?php
                $current_page = $this->getCurrentPage();
                $pages = $this->getUserAllowedPages()?:[];
                error_log('PageS: '.print_r($pages, true));
                echo '<ul>';
                foreach ($pages as $page) {
@@ -712,7 +717,7 @@
        echo '<p>Welcome back!</p>';
        $pages = $this->getUserAllowedPages();
        error_log('Pages: '.print_r($pages, true));
        echo '<h2>What would you like to do today?</h2>';
@@ -722,7 +727,7 @@
                continue;
            }
            $title = $this->getTitle($page);
            $url = sanitize_title($title);
            $description = $this->getDescription($page);
            $icon = $page;
            if (!is_numeric($slug)) {
@@ -730,10 +735,12 @@
                if (array_key_exists('icon', $config)) {
                    $icon = $config['icon'];
                }
            } else {
                $slug = sanitize_title($page);
            }
            if ($title !== '') {
                echo '<li><p><a href="'.get_home_url(null, '/dash/'.$url.'/').'"
                    data-page="'.$url.'" data-dash>'.jvbIcon($icon).ucwords($title).'</a></p></li>';
                echo '<li><p><a href="'.get_home_url(null, '/dash/'.$slug.'/').'"
                    data-page="'.$slug.'" data-dash>'.jvbIcon($icon).ucwords($title).'</a></p></li>';
            }
        }
@@ -781,7 +788,7 @@
        if ($content !== '') {
            echo $content;
        } else {
        jvbRenderSections($this->user->ID, 'user', jvbUserRole());
            jvbRenderSections($this->user->ID, 'user', jvbUserRole());
        }
    }
@@ -851,16 +858,16 @@
        <div class="approvals container">
            <nav class="tabs row start" role="tablist">
                <button type="button" class="tab active" data-tab="summary" role="tab" aria-selected="true">
                    <h2><?= jvbIcon('all')?>All</h2>
                    <h2><?= jvbIcon('infinity')?>All</h2>
                </button>
                <button type="button" class="tab" data-tab="artists" role="tab" aria-selected="false">
                    <h2><?= jvbIcon('artists')?>Artists</h2>
                    <h2><?= jvbIcon('users-three')?>Artists</h2>
                </button>
                <button type="button" class="tab" data-tab="terms" role="tab" aria-selected="false">
                    <h2><?= jvbIcon('style')?>Terms</h2>
                    <h2><?= jvbIcon('hash')?>Terms</h2>
                </button>
                <button type="button" class="tab" data-tab="yours" role="tab" aria-selected="false">
                    <h2><?= jvbIcon('artist')?>Yours</h2>
                    <h2><?= jvbIcon('user')?>Yours</h2>
                </button>
            </nav>
        </div>
@@ -964,8 +971,8 @@
            'vertical',
            'TAB NAV:',
            '',
            jvbIcon('down'),
            jvbIcon('right'))?>
            jvbIcon('caret-double-down'),
            jvbIcon('caret-double-right'))?>
    </div>
    <div class="items-container">
@@ -1024,18 +1031,18 @@
        <template class="<?= $type ?>Row">
            <tr>
                <td>
                     <?= jvbIcon('grab') ?>
                     <?= jvbIcon('dots-six-vertical') ?>
                 </td>
                 <td data-id="actions" class="col">
                     <?= jvbRenderToggleTextField(
                         'public',
                         '',
                         '',
                         jvbIcon('show'),
                         jvbIcon('hide'))
                         jvbIcon('eye'),
                         jvbIcon('eye-closed'))
                     ?>
                     <button type="button" data-action="edit">
                         <?= jvbIcon('edit') ?>
                         <?= jvbIcon('pencil-simple') ?>
                    </button>
                </td>
                <?php
inc/managers/DirectoryManager.php
@@ -47,7 +47,7 @@
                'not_found' => "No {$plural} found",
                'not_found_in_trash' => "No {$plural} found in Trash"
            ],
            'menu_icon'             => jvbCSSIcon('asc'),
            'menu_icon'             => jvbCSSIcon('sort-ascending'),
            'public'                => true,
            'publicly_queryable'    => true,
            'show_in_menu'          => true,
inc/managers/EmailManager.php
@@ -24,17 +24,15 @@
class EmailManager
{
    // Brand colors
    private string $action_color = '#FF0080';
    private string $bg_light = '#F9F9F9';
    private string $bg_dark = '#1B1B1B';
    private string $text_color = '#151515';
    private string $text_light = '#F9F9F9';
   private array $colours = JVB_EMAIL['colours'];
   private string $title = JVB_EMAIL['content']['title'];
   private string $prefix = JVB_EMAIL['content']['subjectPrefix'];
   private string $signature = JVB_EMAIL['content']['signature'];
   private string $footer;
    // Site info
    private string $site_name;
    private string $site_url;
    private string $signature;
    /**
     * Constructor - sets up all filters
@@ -43,7 +41,7 @@
    {
        $this->site_name = wp_specialchars_decode(get_option('blogname'), ENT_QUOTES);
        $this->site_url = get_site_url();
        $this->signature = jvbSignature();
        $this->footer = (is_array(JVB_EMAIL['content']['footer'])) ? implode('', JVB_EMAIL['content']['footer']) : JVB_EMAIL['content']['footer'];
        add_filter('wp_mail_content_type', [$this, 'setHtmlContentType']);
        // User registration emails
@@ -109,12 +107,11 @@
     */
    private function getEmailTemplate(string $content, string $header = ''):string
    {
        // Get logo URL (replace with your actual logo path)
        $logo_url = esc_url(wp_get_attachment_image_url(get_theme_mod('custom_logo'), 'medium'));
        $logo = get_custom_logo();
        // Default header if none provided
        if (empty($header)) {
            $header = 'edmonton.ink';
            $header = $this->title;
        }
        return '<!DOCTYPE html>
@@ -122,17 +119,17 @@
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>' . $this->site_name . '</title>
            <title>' . $this->title . ' | ' . $this->site_name .'</title>
            <style>
                body, table, td, p, a, li, blockquote {
                    line-height: 1.5;
                }
                body {
                    background-color: ' . $this->bg_light . ';
                    background-color: ' . $this->colours['light'] . ';
                    margin: 0;
                    padding: 0;
                    color: ' . $this->text_color . ';
                    color: ' . $this->colours['dark'] . ';
                }
                .email-container {
@@ -147,10 +144,10 @@
                }
                .header {
                    background-color: ' . $this->bg_dark . ';
                    background-color: ' . $this->colours['dark'] . ';
                    padding: 5px;
                    text-align: center;
                    color: ' . $this->text_light . ';
                    color: ' . $this->colours['light'] . ';
                    font-size: 24px;
                    font-weight: 900;
                    letter-spacing: 1.5px;
@@ -169,7 +166,7 @@
                }
                .content {
                    background-color: #ffffff;
                    background-color: ' . $this->colours['light-50'] . ';
                    padding: 30px;
                    border-bottom-left-radius: 5px;
                    border-bottom-right-radius: 5px;
@@ -178,8 +175,8 @@
                .button {
                    display: inline-block;
                    padding: 12px 24px;
                    background-color: ' . $this->action_color . ';
                    color: ' . $this->text_light . ' !important;
                    background-color: ' . $this->colours['action-0'] . ';
                    color: ' . $this->colours['action-contrast'] . ' !important;
                    text-decoration: none;
                    border-radius: 3px;
                    font-weight: bold;
@@ -189,7 +186,7 @@
                }
                .text-link {
                    color: ' . $this->action_color . ';
                    color: ' . $this->colours['action-0'] . ';
                    text-decoration: underline;
                }
@@ -197,13 +194,14 @@
                    text-align: center;
                    margin-top: 20px;
                    font-size: 12px;
                    color: #666666;
                    backgorund-color: ' . $this->colours['light-200'] . ';
                    color: ' . $this->colours['dark-200'] . ';
                    padding: 10px;
                    line-height: 1.6;
                }
                .divider {
                    border-top: 1px solid #dddddd;
                    border-top: 1px solid ' . $this->colours['dark-200'] . ';
                    margin: 25px 0;
                }
@@ -282,25 +280,23 @@
/* Dark mode support for clients that support it */
@media (prefers-color-scheme: dark) {
    .email-container {
        background-color: #1B1B1B !important;
    .body {
        background-color: ' . $this->colours['dark'] . ';
        color: ' . $this->colours['light'] . ';
    }
    .header {
        background-color: ' . $this->colours['light'] . ';
        color: ' . $this->colours['dark'] . ';
    }
    .content {
        background-color: #2A2A2A !important;
        color: #F9F9F9 !important;
    }
    h1, h2, h3, p, li, a.text-link {
        color: #F9F9F9 !important;
        background-color: ' . $this->colours['dark-50'] . ';
    }
    .footer {
        color: #999999 !important;
    }
    .divider {
        border-color: #444444 !important;
        background-color: ' . $this->colours['dark-200'] . ';
        color: ' . $this->colours['light-200'] . ';
    }
}
            </style>
@@ -310,7 +306,7 @@
                <div class="header">
                    <a href="' . $this->site_url . '">
                        <img src="' . $logo_url . '" alt="Edmonton Ink">
                        ' . $logo . '
                    </a>
                    <p>' . $header . '</p>
                </div>
@@ -318,8 +314,7 @@
                    ' . $content . '
                </div>
                <div class="footer">
                    <p>&copy; ' . date('Y') . ' edmonton.ink â€” Your tattoo scene on your screen.</p>
                    <p><a href="' . $this->site_url . '" class="text-link">edmonton.ink</a></p>
                    ' . $this->footer . '
                </div>
            </div>
        </body>
@@ -331,54 +326,24 @@
     */
    public function customizeNewUserEmail($wp_new_user_notification_email, $user, $blogname)
    {
        $user_login = $user->user_login;
        $message = sprintf(
            '<p>Hi %s!</p>
                    <p>Thanks for signing up for an account on %s.</p>
                    <p><b>Your username:</b> %s</p>
                    <p><b>Your password:</b> <i>Your chosen password.</i></p>',
            $user->display_name,
            get_bloginfo('name'),
            $user->user_login,
        );
        $message = apply_filters('jvbNewUserEmail', $message, $user);
        // Only create the password key if the user can change their password
        $key = get_password_reset_key($user);
        if (!is_wp_error($key)) {
            $reset_url = network_site_url("wp-login.php?action=rp&key=$key&login=" . rawurlencode($user_login), 'login');
            $message = sprintf('<p>Hey %s!</p>', $user->first_name);
            $message .= '<p>Thanks for joining Edmonton\'s tattoo scene. Here\'s your login information:</p>';
            $message .= sprintf('<p><strong>Username:</strong> %s</p>', $user_login);
            $message .= '<p>To set your password and access your account, click the button below:</p>';
            $message .= sprintf('<p style="text-align: center;"><a href="%s" class="button">Set Your Password</a></p>', $reset_url);
            $message .= '<p>Or copy and paste this link into your browser:</p>';
            $message .= sprintf('<p style="user-select:all;">%s</p>', $reset_url);
            $message .= '<p>This link will expire in 24 hours, for security reasons.</p>';
            if (in_array('jvb_artist', array_values($user->roles))) {
                $message .= '<div class="divider"></div>';
                $message .= '<h3>NOTE:</h3>
                    <p>Once you set your password, you\'ll have access to your custom dashboard where you can:</p>
                    <ul>
                        <li>Manage your profile information</li>
                        <li>Upload tattoos/piercings, and artwork</li>
                    </ul>
                    <p>Nothing will be published until you\'ve been approved by 3 already approved artists, or the admin.</p>
                    <p>Admins check every day or three, but, if you are in a rush, you can contact us directly by replying to this email, or texting us at 825-925-9916.</p>';
            } elseif (in_array('jvb_partner', array_values($user->roles))) {
                $message .= '<div class="divider"></div>';
                $message .= '<h3>NOTE:</h3>
                    <p>Once you set your password, you\'ll have access to your custom dashboard where you can:</p>
                    <ul>
                        <li>Manage your profile information</li>
                        <li>Create offers for enthusiasts or partners or both</li>
                    </ul>
                    <p>Nothing will be published until you\'ve been approved by the admin.</p>
                    <p>Admins check every day or three, but, if you are in a rush, you can contact us directly by replying to this email, or texting us at 825-925-9916.</p>
                    <p><strong>Note:</strong>Even after approval by admin, your ability to publish depends on your karmic standing by artists. Artists each have a vote they can cast (UP or DOWN) - if your karmic score dips too far in the negative, you account is subject to reconsideration or even a ban.</p>';
            }
            $message .= '<div class="divider"></div>';
            $message = apply_filters(BASE.'new_user_email_content', $message, $user);
            $message .= '<p>If you didn\'t create this account, please ignore this email and the link will expire.</p>';
            $message .= sprintf('<p>Ink on, %s</p>', $user->first_name);
            $message .= $this->signature;
            $wp_new_user_notification_email['message'] = $this->getEmailTemplate($message, 'Welcome to edmonton.ink');
        }
        $message .= $this->signature;
        $wp_new_user_notification_email['message'] = $this->getEmailTemplate($message, JVB_EMAIL['subjects']['newUser']);
        // Change the subject line
        $wp_new_user_notification_email['subject'] = sprintf('Welcome to %s! Confirm your account', $this->site_name);
        $prefix = JVB_EMAIL['types']['newUser']['showPrefix']??true;
        $prefix = ($prefix) ? $this->prefix : '';
        $wp_new_user_notification_email['subject'] = $prefix.JVB_EMAIL['types']['newUser']['subject'];
        return $wp_new_user_notification_email;
    }
@@ -394,12 +359,12 @@
    public function customizeNewUserEmailAdmin(array $emailData, WP_User $user, string $blogname):array
    {
        $message = '<p>A new user has registered on <strong>' . $this->site_name . '</strong>:</p>';
        $message .= '<p><strong>Username:</strong> ' . $user->user_login . '<br>';
        $message .= '<strong>Email:</strong> ' . $user->user_email . '<br>';
        $message .= '<strong>Role:</strong> ' . str_replace(BASE, '', array_values($user->roles)[0]).'</p>';
        $message .= '<p><strong>Username:</strong> ' . $user->user_login . '</p>';
        $message .= '<p><strong>Email:</strong> ' . $user->user_email . '</p>';
        $message = apply_filters('jvbNewUserAdminEmail', $message, $user);
        $message .= $this->signature;
        $emailData['message'] = $this->getEmailTemplate($message, 'New User Registration');
        $emailData['subject'] = '[' . $this->site_name . '] New ' . str_replace(BASE, '', array_values($user->roles)[0]).': '.$user->display_name;
        $emailData['subject'] = $this->prefix .'New ' . str_replace(BASE, '', array_values($user->roles)[0]).': '.$user->display_name;
        return $emailData;
    }
@@ -413,21 +378,27 @@
     *
     * @return string
     */
    public function customizePasswordResetEmail(string $message, string $key, string $user_login, WP_User $user_data):string
    public function customizePasswordResetEmail(string $message, string $key, string $user_login, WP_User $user):string
    {
        $reset_url = network_site_url("wp-login.php?action=rp&key=$key&login=" . rawurlencode($user_login), 'login');
        $content = '<p>Hey bud,</p>';
        $content = '<p>We received a request to reset the password for an account associated with this email:</p>';
        $content .= '<p><strong>Username:</strong> ' . $user_login . '</p>';
        $content .= '<p>If you didn\'t make this request, you can safely ignore this email and nothing will happen to your account.</p>';
        $content .= '<p>To reset your password, click the button below:</p>';
        $content .= '<p style="text-align: center;"><a href="' . $reset_url . '" class="button">Reset Password</a></p>';
        $content .= '<p>Or copy and paste this link into your browser:</p>';
        $content .= '<p style="user-select:all;">' . $reset_url . '</p>';
        $content .= '<div class="divider"></div>';
        $content .= '<p>This password reset link is only valid for 24 hours.</p>';
            $content .= $this->signature;
        $reset_url = network_site_url("wp-login.php?action=rp&key=$key&login=" . rawurlencode($user_login), 'login');
        $content = sprintf(
            '<p>Hi %s!</p>
            <p>We received a request to reset the password for an account associated with this email:</p>
            <p><strong>Username:</strong> %s</p>
            <p>If you didn\'t make this request, you can safely ignore this email and nothing will happen to your account.</p>
            <p>To reset your password, click the button below:</p>
            %s
            <p>Or copy and paste this link into your browser:</p>
            %s
            <div class="divider"></div>
            <p>This password reset link is only valid for 24 hours.</p>',
            $user->display_name,
            $user_login,
            jvbMailButton($reset_url,'Reset Password'),
            jvbEmailLink($reset_url)
        );
        $content = apply_filters('jvbPasswordResetEmail', $content, $user_login, $user, $reset_url);
        $content .= $this->signature;
        return $this->getEmailTemplate($content, 'Password Reset');
    }
@@ -440,29 +411,42 @@
     */
    public function customizePasswordResetTitle(string $title, string $user_login, WP_User $user_data):string
    {
        return '[' . $this->site_name . '] Password Reset Request';
    }
        $prefix = JVB_EMAIL['types']['resetPass']['showPrefix']??true;
        $prefix = ($prefix) ? $this->prefix : '';
        return $prefix.JVB_EMAIL['types']['resetPass']['subject'];
    }
    /**
     * Email change notification to admin
     * @param array $email_change_email
     * @param array $user
     * @param array $userdata
     * @param array $oldUser
     * @param array $newUser
     *
     * @return array
     */
    public function customizeEmailChangeEmail(array $email_change_email, array $user, array $userdata):array
    public function customizeEmailChangeEmail(array $email_change_email, array $oldUser, array $newUser):array
    {
        $content = sprintf('<p>Hi %s,</p>', $user['first_name']);
        $content .= '<p>Ideally you already know this: you asked to change your email, and here we are.</p>';
        $content .= '<p><strong>Old Email:</strong> ' . $email_change_email['to'] . '<br>';
        $content .= '<strong>New Email:</strong> ' . $user['user_email'] . '</p>';
        $content .= '<p>If this is news, or you did not change your email, please contact us immediately. You can <a href="sms:+18258239916">text us</a> or reply to this email."></a></p>';
        $content .= '<p style="text-align: center;"><a href="' . wp_login_url() . '" class="button">Log In to Your Account</a></p>';
        $content = sprintf(
            '<p>Hi %s,</p>
        <p>Ideally you already know this: someone asked to change the email for your account.</p>
        <p><strong>Old Email:</strong> %s</p>
        <p><strong>New Email:</strong> %s</p>
        <div class="divider"></div>
        <p>If this is news to you, or you did not request this - please contact us immediately. You can <a href="sms:+18258239916">text us</a> or reply to this email."></a></p>
        %s',
            $newUser['first_name'],
            $oldUser['user_email'],
            $newUser['user_email'],
            jvbMailButton(wp_login_url(), 'Log In To Your Account')
        );
        $content = apply_filters('jvbEmailChangeRequestEmail', $content, $oldUser, $newUser);
        $content .= $this->signature;
        $email_change_email['message'] = $this->getEmailTemplate($content, 'Email Address Changed');
        $email_change_email['subject'] = '[' . $this->site_name . '] Email Address Changed';
        $prefix = JVB_EMAIL['types']['emailChange']['showPrefix']??true;
        $prefix = ($prefix) ? $this->prefix : '';
        $email_change_email['subject'] = $prefix.JVB_EMAIL['types']['emailChange']['subject'];
        return $email_change_email;
    }
@@ -474,21 +458,23 @@
     *
     * @return string
     */
    public function customizeNewUserEmailContent(string $email_text, array $user):string
    public function customizeNewUserEmailContent(string $email_text, array $new_user_email):string
    {
        $confirm_url = esc_url( add_query_arg('newuseremail', $new_user_email['hash'], self_admin_url('profile.php')));
        $content = sprintf(
            '<p>Hey,</p>
            <p>There was a request to change the email address associated with your account to this one.</p>
            <p>This is just a friendly email to ensure you would like this change.</p>
            <p>You can confirm this change by clicking the button below:</p>
            %s
            <p>Or copy and paste this link into your browser:</p>
            %s',
            jvbMailButton($confirm_url, 'Confirm this Email'),
            jvbEmailLink($confirm_url)
        );
        // Extract the confirmation link from the default message
        $pattern = '/###CONFIRM_URL### (.*?)(?=\n|\r|$)/';
                                                                         preg_match($pattern, $email_text, $matches);
        $confirm_url = isset($matches[1]) ? $matches[1] : admin_url('profile.php?newuseremail=' . $user['hash']);
        $content = apply_filters('jvbEmailChangedEmail', $content, $confirm_url);
        $content = '<p>Hey human,</p>';
        $content .= '<p>Seems you want to change the email associated with your account.</p>';
        $content .= '<p>You\'d like to change your email to: <strong>' . $user['newemail'] . '</strong></p>';
        $content .= '<p>Please confirm this change by clicking the button below:</p>';
        $content .= '<p style="text-align: center;"><a href="' . $confirm_url . '" class="button">Confirm Email Change</a></p>';
        $content .= '<p>Or copy and paste this link into your browser:</p>';
        $content .= '<p style="user-select:all">' . $confirm_url . '</p>';
        $content .= '<div class="divider"></div>';
        $content .= '<p>If you did not request this change, you can safely ignore this email and nothing will change.</p>';
        $content .= $this->signature;
@@ -499,23 +485,29 @@
        /**
         * Password change notification
         * @param array $pass_change_email
         * @param array $user
         * @param array $userdata
         * @param array $oldUser
         * @param array $newUser
         *
         * @return array
     */
    public function customizePasswordChangeEmail(array $pass_change_email, array $user, array $userdata):array
    public function customizePasswordChangeEmail(array $pass_change_email, array $oldUser, array $newUser):array
    {
        $content = '<p>Hey bud,</p>';
        $content .= '<p>Ideally, you\'re expecting this email. You wanted to change your password, and this is to let you know that it\'s definitely updated.</p>';
        $content .= '<p>If you did not change your password, please contact us immediately.</p>';
        $content .= '<p>You can <a href="sms:+18259257398">text us</a>, or reply to this email.</p>';
        $content .= '<p style="text-align: center;"><a href="' . wp_login_url() . '" class="button">Log In to Your Account</a></p>';
        $content = sprintf(
            '<p>Hi %s,</p>
            <p>This is a confirmation email to let you know your password has successfully been changed.</p>
            <p>If you\'re not expecting this email, and did not change your password - please <strong>contact us immediately</strong></p>
            <p>You can <a href="sms:+18259257398">text us</a>, or reply to this email.</p>
            %s',
            $oldUser['first_name'],
            jvbMailButton(wp_login_url(), 'Log In to Your Account')
        );
        $content = apply_filters('jvbPasswordChangeEmail', $content, $oldUser, $newUser);
        $content .= $this->signature;
        $pass_change_email['message'] = $this->getEmailTemplate($content, 'Password Changed');
        $pass_change_email['subject'] = '[' . $this->site_name . '] Password Changed';
        $prefix = JVB_EMAIL['types']['passwordChange']['showPrefix']??true;
        $prefix = ($prefix) ? $this->prefix : '';
        $pass_change_email['subject'] = $prefix.JVB_EMAIL['types']['passwordChange']['subject'];
        return $pass_change_email;
    }
@@ -527,16 +519,11 @@
     *
     * @return string
     */
    public function customizeUserRequestEmail(string $content, array $request_data):string
    public function customizeUserRequestEmail(string $content, array $email_data):string
    {
        // Extract the confirmation URL from the content
        $pattern = '/<a href="([^"]+)"[^>]*>/';
        preg_match($pattern, $content, $matches);
        $confirm_url = isset($matches[1]) ? $matches[1] : '';
        $request_type = $request_data['action_name'];
        $request_name = '';
        $confirm_url = $email_data['confirm_url'];
        $request_type = $email_data['action_name'];
        switch ($request_type) {
            case 'export_personal_data':
@@ -549,13 +536,20 @@
                $request_name = 'Data Request';
        }
        $message = '<p>Hi privacy enthusiast,</p>';
        $message .= '<p>A request has been made to perform the following action on your account:</p>';
        $message .= '<p><strong>' . $request_name . '</strong></p>';
        $message .= '<p>If you made this request, you can confirm it by clicking the button below:</p>';
        $message .= '<p style="text-align: center;"><a href="' . $confirm_url . '" class="button">Confirm Request</a></p>';
        $message .= '<p>Or copy and paste this link into your browser:</p>';
        $message .= '<p style="user-select:all;">' . $confirm_url . '</p>';
        $message = sprintf(
            '<p>Hi,</p>
            <p>You\'re receiving this email because a request has been made to <strong>%s</strong></p>
            <p>If you\'re the one who made this request, you can confirm it by clicking the button below:</p>
            %s
            <p>Or copy and paste this link into your browser:</p>
            %s',
            $request_name,
            jvbMailButton($confirm_url, 'Confirm'),
            jvbEmailLink($confirm_url)
        );
        $message = apply_filters('jvbPersonalDataExport', $message, $request_type, $confirm_url, $email_data);
        $message .= '<div class="divider"></div>';
        $message .= '<p>If you did not make this request, you can safely ignore this email.</p>';
        $message .= $this->signature;
@@ -573,19 +567,23 @@
     */
    public function customizePersonalDataEmail(string $content, int $request_id, array $email_data):string
    {
        $download_url = $email_data['export_file_url'];
        $expiresAt = $email_data['expiration_date'];
        // Extract the download link
        $pattern = '/<a href="([^"]+)"[^>]*>/';
        preg_match($pattern, $content, $matches);
        $download_url = isset($matches[1]) ? $matches[1] : '';
        $message = '<p>Your request for an export of personal data has been completed.</p>';
        $message .= '<p>You can download your personal data by clicking the button below.</p>';
        $message .= '<p style="text-align: center;"><a href="' . $download_url . '" class="button">Download Personal Data</a></p>';
        $message .= '<p>Or copy and paste this link into your browser:</p>';
        $message .= '<p style="user-select:all;">' . $download_url . '</p>';
        $message .= '<div class="divider"></div>';
        $message .= '<p><strong>Important:</strong> For privacy reasons, this link will expire in ' . $email_data['expiration'] . '.</p>';
        $message = sprintf(
            '<p>Hi,</p>
            <p>You\'re receiving this email because you requested an export of your personal data.</p>
            <p>You can download your personal data by clicking the button below:</p>
            %s
            <p>Or you can copy and paste this link into your browser:</p>
            %s
            <div class="divider"></div>
            <p><strong>Important:</strong> For privacy and security, this link will expire at %s.</p>',
            jvbMailButton($download_url, 'Download Your Data'),
            jvbEmailLink($download_url),
            $expiresAt
        );
        $message = apply_filters('jvbPersonalDataExported', $message, $download_url, $expiresAt, $email_data);
        $message .= $this->signature;
        return $this->getEmailTemplate($message, 'Your Personal Data Export');
inc/managers/IconsManager.php
New file
@@ -0,0 +1,434 @@
<?php
namespace JVBase\managers;
use JVBase\utility\Features;
if (!defined('ABSPATH')) {
    exit;
}
class IconsManager
{
    protected static ?IconsManager $instance = null;
    protected CacheManager $cache;
    protected string $style = 'regular';
    protected array $styles = ['regular', 'bold', 'duotone', 'fill', 'light', 'thin'];
    // Custom icons registered via filter
    protected array $customIcons = [];
    protected array $usedIcons = [];
    protected array $map = [];
    /**
     * Get singleton instance
     */
    public static function getInstance(): IconsManager
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    private function __construct()
    {
        $this->cache = CacheManager::for('icons', WEEK_IN_SECONDS);
        $this->style = (array_key_exists('icons', JVB_SITE) && in_array(JVB_SITE['icons'], $this->styles))
            ? JVB_SITE['icons']
            : 'regular';
        $this->addMap();
        // Allow custom icon registration
        $this->customIcons = apply_filters('jvbRegisterCustomIcons', [
            'syncing'       => JVB_DIR .'/assets/icons/cloud-sync-thin.svg',
            'alphabetical'  => JVB_DIR.'/assets/icons/alphabetical.svg'
        ]);
        $this->usedIcons = get_option(BASE.'used_icons', []);
        $this->includeIcons();
        // Register hooks only once
        $this->registerHooks();
    }
    /**
     * Include icons via filter (for JS usage, etc.)
     */
    protected function includeIcons():array
    {
        $icons = get_option(BASE.'includeIcons');
//      $icons = false;
        if (!$icons) {
            $icons = $this->addIncludeIcons();
        }
        $include = apply_filters('jvbIncludeIcons', []);
        $add = array_filter($include, function($addIt) use ($icons) {
            return !in_array($addIt, $icons);
        });
        error_log('Adding icons: '.print_r($add, true));
        if (!empty($add)) {
            $this->addIncludeIcons($add);
        }
        return $icons;
    }
    protected function addIncludeIcons(array $add = []) {
        $icons = get_option(BASE.'includeIcons');
        if (!$icons) {
            $icons = [
                'check-circle',
                'close-circle',
                'cloud-slash',
                'exclamation-mark',
                'cloud-arrow-down',
                'cloud-arrow-up',
                'cloud-check',
                'cloud-slash',
                'cloud-warning',
                'syncing',
                'cloud-x',
                'arrows-clockwise',
                'share-fat',
                'trash',
                'star',
                ['name' => 'star-half', 'style' => 'fill'],
                ['name' => 'star', 'style' => 'fill'],
                //FORMATTING
                'copy',
                'paragraph',
                'text-h-one',
                'text-h-two',
                'text-h-three',
                'text-h-four',
                'text-h-five',
                'text-h-six',
                ['name' =>'text-b', 'style' => 'fill'],
                'text-italic',
                'text-underline',
                'text-strikethrough',
                'list-dashes',
                'list-numbers',
                'text-align-left',
                'text-align-center',
                'text-align-right',
//          'text-align-justify',
                'link',
                //FILE ICONS
                'file-pdf',
                'file-csv',
                'file-doc',
                'file-txt',
                'file-xls',
            ];
            $check = [JVB_CONTENT, JVB_TAXONOMY, JVB_USER];
            foreach ($check as $constant) {
                foreach ($constant as $key => $value) {
                    if (array_key_exists('icon', $value) && !in_array($value['icon'], $icons)) {
                        $icons[] = $value['icon'];
                    }
                }
            }
            $icons = apply_filters('jvbIncludeIcons', $icons);
            update_option(BASE.'includeIcons', $icons);
            update_option(BASE.'icons_needs_update', true);
        }
        $add = apply_filters('jvbIncludeIcons', $add);
        $add = array_filter($add, function($addIt) use ($icons) {
            return !in_array($addIt, $icons);
        });
        if (!empty($add)) {
            $icons = array_merge($add, $icons);
            update_option(BASE.'usedIcons', $icons);
            update_option(BASE.'icons_needs_update', true);
        }
        return $icons;
    }
    protected function addMap():void
    {
        $map = get_option(BASE.'iconMap');
        if (!$map) {
            $map = [];
            if (Features::forSite()->has('referrals')){
                $map['referrals'] = 'hand-heart';
            }
            if (Features::forSite()->has('dashboard')){
                $map['dash'] = 'door';
            }
            if (Features::forSite()->has('magicLink')){
                $map['magicLink'] = 'magic-wand';
            }
            if (Features::hasAnyIntegration()) {
                $map['integrations'] = 'plugs-connected';
            }
            update_option(BASE.'iconMap', $map);
        }
        $this->map = apply_filters('jvbMapIcons', $map);
    }
    /**
     * Register WordPress hooks
     */
    protected function registerHooks(): void
    {
        add_action('init', [$this, 'checkCSS']);
        add_action('wp_enqueue_scripts', [$this, 'enqueueIconStyles']);
        add_action('admin_enqueue_scripts', [$this, 'enqueueIconStyles']);
    }
    public function checkCSS():void
    {
//      update_option(BASE.'icons_needs_update', true);
        if (get_option(BASE.'icons_needs_update', false)) {
            error_log('Regenerating CSS');
            delete_option(BASE.'icons_needs_update');
            $this->regenerateCSS();
        }
    }
    protected function regenerateCSS(): void
    {
        error_log('[IconsManager]:regenerateCSS');
        $css = $this->generateIconCSS();
        $css_path = JVB_DIR.'/assets/css/icons.css';
        if (file_put_contents($css_path, $css) !== false) {
            CacheManager::updateTimestamp('icons');
        } else {
            error_log('[IconsManager]Could not write css.');
        }
    }
    /**
     * Prevent cloning
     */
    private function __clone() {}
    /**
     * Prevent unserialization
     */
    public function __wakeup()
    {
        throw new \Exception("Cannot unserialize singleton");
    }
    /**
     * Get an icon element
     *
     * @param string $name Icon name (e.g., 'heart', 'calendar')
     * @param array $options Options array:
     *   - 'style' => 'regular'|'bold'|'fill'|etc.
     *   - 'label' => 'Accessible label' (for standalone icons)
     *   - 'decorative' => true (for icons next to text)
     *   - 'class' => 'additional classes'
     *   - 'size' => 24 (for custom sizing via inline style)
     * @return string HTML icon element
     */
    public function getIcon(string $name, array $options = []): string
    {
        if (!array_key_exists('style', $options)) {
            $options['style'] = $this->style;
        }
        $name = (array_key_exists($name, $this->map)) ? $this->map[$name] : $name;
        // Validate icon exists
        if (!$this->iconExists($name, $options['style'])) {
            error_log('[IconsManager] Icon not found: ' . $name);
            return '';
        }
        $style = $options['style'] ?? $this->style;
        // Track icon usage
        $this->trackIconUsage($name, $style);
        // Build classes
        $classes = ['icon', 'icon-' . $name];
        if (!empty($options['class'])) {
            $classes[] = $options['class'];
        }
        $attrs = ['class="' . esc_attr(implode(' ', $classes)) . '"'];
        $attrs[] = 'aria-hidden="true"';
        return '<i ' . implode(' ', $attrs) . '></i>';
    }
    /**
     * Track icon usage for CSS generation
     */
    protected function trackIconUsage(string $name, string $style): void
    {
        if (!array_key_exists($style, $this->usedIcons)) {
            $this->usedIcons[$style] = [];
        }
        if (!in_array($name, $this->usedIcons[$style])) {
            $this->usedIcons[$style][] = $name;
            update_option(BASE.'used_icons', $this->usedIcons);
            // Flag for regeneration on next init
            update_option(BASE.'icons_needs_update', true);
            // Clear cache
            $this->cache->delete('icon_styles_css');
        }
    }
    /**
     * Check if icon file exists
     */
    protected function iconExists(string $name, ?string $style = null): bool
    {
        if (!$style) {
            $style = $this->style;
        }
        // Check custom icons first
        if (array_key_exists($name, $this->customIcons)) {
            return file_exists($this->customIcons[$name]);
        }
        // Check standard icons
        $filepath = $this->buildFilePath($name, $style);
        return file_exists($filepath);
    }
    /**
     * Build file path for icon
     */
    protected function buildFilePath(string $name, ?string $style = null): string
    {
        if (!$style) {
            $style = $this->style;
        }
        // Custom icons (absolute path provided)
        if (array_key_exists($name, $this->customIcons)) {
            return $this->customIcons[$name];
        }
        // Standard SVG icons in /assets/icons/
        if (str_ends_with($name, '.svg')) {
            return JVB_DIR . '/assets/icons/' . $name;
        }
        $name = ($style === 'regular') ? $name : $name . '-' . $style;
        // Phosphor icons with style variants
        return JVB_DIR . '/assets/phosphor-icons/' . $style . '/' . $name . '.svg';
    }
    /**
     * Get raw SVG content for CSS mask-image
     */
    protected function getRawSvg(string $name, ?string $style = null): ?string
    {
        if (!$style) {
            $style = $this->style;
        }
        $filepath = $this->buildFilePath($name, $style);
        if (!file_exists($filepath)) {
            return null;
        }
        $svg = file_get_contents($filepath);
        if ($svg === false) {
            return null;
        }
        // Clean up SVG for CSS usage
        $svg = preg_replace("/([\n\t]+)/", ' ', $svg);
        $svg = preg_replace('/>\s*</', '><', $svg);
        $svg = trim($svg);
        return $svg;
    }
    /**
     * Enqueue icon styles via REST endpoint
     */
    public function enqueueIconStyles(): void
    {
        $timestamp = CacheManager::getTimestamp('icons');
        wp_enqueue_style(
            'jvb-icons',
            JVB_URL.'assets/css/icons.css',
            [],
            $timestamp
        );
    }
    /**
     * Generate CSS from icon list
     */
    protected function generateIconCSS(): string
    {
        $css = '';
        foreach ($this->usedIcons as $style => $icons) {
            $styleClass = ($style !== $this->style) ? '-'.substr($style, 0,2) : '';
            foreach ($icons as $icon) {
                $svg = $this->getEncodedSVG($icon, $style);
                if ($svg !== '') {
                    $css .= ".icon-{$icon}{$styleClass}{";
                    $css .= "--icon:url('data:image/svg+xml;base64,{$svg}');";
                    $css .= "}";
                }
            }
        }
        return $this->minifyCss($css);
    }
    protected function minifyCSS(string $css): string
    {
        // Remove comments
        $css = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $css);
        // Remove whitespace
        $css = preg_replace('/\s+/', ' ', $css);
        // Remove spaces around specific characters
        $css = preg_replace('/\s*([:;{}])\s*/', '$1', $css);
        return trim($css);
    }
    public function getCSSIcon(string $icon, ?string $style=null):string
    {
        if (!$style) {
            $style = $this->style;
        }
        $svg = $this->getEncodedSVG($icon, $style);
        if ($svg !== '') {
            return "data:image/svg+xml;base64,{$svg}";
        }
        return '';
    }
    public function getEncodedSVG(string $icon, ?string $style = null):string
    {
        if (!$style) {
            $style = $this->style;
        }
        return $this->cache->remember($style.$icon,
        function () use ($icon, $style) {
            $svg = $this->getRawSvg($icon, $style);
            if ($svg) {
                return base64_encode($svg);
            }
            return '';
        });
    }
    /**
     * Clear icon cache (useful for development/debugging)
     */
    public function clearIconCache(): void
    {
        delete_option(BASE . 'icon_usage_list'); // Clear DB option
        $this->cache->delete('icon_styles_css');
        CacheManager::updateTimestamp('icons');
    }
}
inc/managers/LoginManager.php
@@ -21,6 +21,7 @@
    protected ?MetaForm $metaForm = null;
    protected EmailManager $emailManager;
    protected AjaxRateLimiter $rateLimiter;
    protected CacheManager $cache;
    protected array $forms =[];
@@ -30,7 +31,6 @@
    protected string $title = '';
    // Token handlers registry
    protected array $tokenHandlers = [];
    protected array $messageHandlers = [];
    private array $allowed_file_types = [
@@ -44,12 +44,10 @@
    public function __construct()
    {
        $this->siteFeatures = Features::forSite();
        $this->metaForm = new MetaForm();
        $this->emailManager = new EmailManager();
        $this->rateLimiter = new AjaxRateLimiter();
        // Register default token handlers
        $this->registerDefaultHandlers();
        $this->cache = CacheManager::for('login');
        // Initialize magic link support if enabled
        if ($this->siteFeatures->has('magicLink')) {
@@ -66,12 +64,6 @@
        add_action('wp_enqueue_scripts', [$this, 'enqueueScripts'], 15);
        // Handle form submissions via AJAX
        add_action('wp_ajax_nopriv_jvb_login', [$this, 'handleAjaxLogin']);
        add_action('wp_ajax_nopriv_jvb_register', [$this, 'handleAjaxRegister']);
        add_action('wp_ajax_nopriv_jvb_lostpassword', [$this, 'handleAjaxLostPassword']);
        add_action('wp_ajax_nopriv_jvb_resetpass', [$this, 'handleAjaxResetPassword']);
        // Login success handling
        add_action('wp_login', [$this, 'handleSuccessfulLogin'], 10, 2);
@@ -88,6 +80,11 @@
     */
    public function redirectToCustomLogin(): void
    {
        // Handle interim login
        if (isset($_GET['interim-login'])) {
            // Don't redirect - let WP handle it
            return;
        }
        // Don't redirect if AJAX or REST
        if ((defined('DOING_AJAX') && DOING_AJAX) || (defined('REST_REQUEST') && REST_REQUEST)) {
            return;
@@ -115,19 +112,27 @@
            $select = [];
            //Basic fields, for any
            $fields = [
                'name'  => [
                'user_name' => [
                    'type'      => 'text',
                    'required'  => true,
                    'label'     => 'Your Name',
                    'placeholder'=> 'Mister Meseeks'
                ],
                'email' => [
                'user_email'    => [
                    'type'      => 'email',
                    'required'  => true,
                    'label'     => 'Your Email',
                    'placeholder'=> 'look@me.com'
                ]
            ];
            if (Features::forSite()->has('referrals')) {
                $fields['referral_code'] = [
                    'type'  => 'text',
                    'required'=> false,
                    'label' => 'Referral Code',
                    'hint'  => 'Have a referral code? Paste it here!'
                ];
            }
            if (count(JVB_USER) > 1) {
                foreach (JVB_USER as $slug => $config) {
                    if (!array_key_exists('can_register', $config) || !$config['can_register']) {
@@ -184,6 +189,7 @@
                $fields = $this->getRegistrationFormFields();
                break;
            case 'lostpassword':
            case 'magic':
                $fields = [
                    'user_email' => [
                        'type' => 'email',
@@ -297,108 +303,6 @@
        return $self->isLoginPage();
    }
    /**************************************************************************
        TOKEN & MESSAGE HANDLERS
            Extensible by other classes
    **************************************************************************/
    public function registerTokenHandler(string $token_key, callable $handler, int $priority = 10): void
    {
        if (!isset($this->tokenHandlers[$priority])) {
            $this->tokenHandlers[$priority] = [];
        }
        $this->tokenHandlers[$priority][$token_key] = $handler;
        ksort($this->tokenHandlers);
    }
    public function registerMessageHandler(string $type, callable $handler, ?callable $condition = null): void
    {
        $this->messageHandlers[$type] = [
            'handler' => $handler,
            'condition' => $condition
        ];
    }
    protected function registerDefaultHandlers(): void
    {
        // Invitation handler
        if ($this->siteFeatures->has('invitations')) {
            $this->registerTokenHandler('invite', function($token, $email, $user_id) {
                if (isset($_POST['invite_token'])) {
                    JVB()->routes('invites')->acceptInvitation(
                        sanitize_text_field($_POST['invite_token']),
                        sanitize_email($_POST['invite_email']),
                        $user_id
                    );
                }
            });
            $this->registerMessageHandler('invitation',
                function() {
                    $data = JVB()->routes('invites')->verifyInvitation(
                        sanitize_text_field($_GET['invite']),
                        sanitize_email($_GET['email'])
                    );
                    $name = $data->name;
                    $inviters = json_decode($data->inviters, true);
                    $names = [];
                    foreach ($inviters as $inviter) {
                        $artist = jvbContentFromUser((int)$inviter['user_id']);
                        $names[] = ($artist['name'] === '') ? $artist['display_name'] : $artist['name'];
                    }
                    $message = (count($names) > 1)
                        ? 'are already here, and have invited you to join in!'
                        : ' is already here, and invited you to join in!';
                    return '<h2>Join the Scene, '.$name.'</h2>
                    <p style="text-align:center;">'.jvbCommaList($names).$message.'</p>';
                },
                function() {
                    return isset($_GET['invite']) && isset($_GET['email']);
                }
            );
        }
        // List sharing handler (Favourites)
        if ($this->siteFeatures->has('favourites')) {
            $this->registerTokenHandler('list_token', function($token, $email, $user_id) {
                if (!empty($_GET['list_token']) && !empty($_GET['email'])) {
                    JVB()->routes('favourites')->acceptListInvitation(
                        sanitize_text_field($_GET['list_token']),
                        sanitize_email($_GET['email']),
                        $user_id
                    );
                }
            });
            $this->registerMessageHandler('favourites',
                function() {
                    return '<h2>'.(JVB_LOGIN['login_from_favourite_header'] ?? 'Save your Favourites').'</h2>';
                },
                function() {
                    return isset($_GET['type']) && $_GET['type'] === 'favourites';
                }
            );
        }
        // Referral handler - FIXED VERSION
        $this->registerTokenHandler('referral_code', function($code, $email, $user_id) {
            // $code is already sanitized from processTokenHandlers
            if (session_status() === PHP_SESSION_NONE) {
                session_start();
            }
            $_SESSION[BASE . 'referral_code'] = $code;
            setcookie(
                BASE . 'referral_code',
                $code,
                time() + (86400 * 30),
                '/'
            );
        }, 5);
    }
    protected function initMagicLinkSupport(): void
    {
        if (!Features::forSite()->has('magicLink')) {
@@ -407,8 +311,6 @@
        $this->magicLink = new MagicLinkManager();
    }
    /*********************************************************************
        RENDERING
    *********************************************************************/
@@ -418,6 +320,18 @@
            return $template;
        }
        $this->setup();
        $page = $this->cache->remember(
                $this->getAction(),
                function() {
                    return $this->renderPage();
                },
                5
            );
        echo $page;
        return '';
    }
    protected function renderPage() {
        ob_start();
        jvbInlineStyles('nav');
        jvbInlineStyles('dash');
@@ -428,11 +342,10 @@
        $this->renderForms();
        $this->renderFooter();
        echo  ob_get_clean();
        return '';
        return ob_get_clean();
    }
    protected function setup():void
    protected function getAction():string
    {
        if (array_key_exists('action', $_GET)) {
            switch ($_GET['action']){
@@ -450,8 +363,23 @@
        } else {
            $action = 'login';
        }
        return $action;
    }
        $this->action = $action;
    protected function setup():void
    {
        $this->action = $this->getAction();
        if (in_array($this->action, ['logout']) || array_key_exists('loggedout', $_GET)) {
            wp_logout();
            wp_redirect(esc_attr($_GET['redirect_to'] ?? get_home_url()));
            exit;
        }
        if (in_array($this->action, ['rp', 'resetpass']) && !is_user_logged_in()) {
            wp_redirect(wp_login_url());
            exit;
        } elseif (is_user_logged_in()) {
            wp_redirect(get_home_url(null, '/dash/'));
        }
        $this->setupLabels();
        $this->setupFields();
        $this->setupTitle();
@@ -496,6 +424,12 @@
                justify-content: center;
                position: relative;
            }
            .login .fstatus.fstatus {
                --wrap: nowrap;
                top:0;
                bottom:unset;
                right: 0;
            }
            .login main::before {
                background-size: 20vw;
                inset: 0;
@@ -567,21 +501,23 @@
    protected function renderForms():void
    {
        $this->metaForm = new MetaForm();
        $form = $this->action.'form';
        ?>
        <section class="login-box col btw">
            <h1><?=$this->labels['title']?></h1>
            <?= $this->labels['description'] ?>
            <form name="<?=$form?>" method="post" data-action="jvb_<?=$this->action?>">
                <?= jvbFormStatus() ?>
                <?php wp_nonce_field('jvb_'.$this->action, '_wpnonce'); ?>
                <input type="hidden" name="action" value="jvb_<?=$this->action?>">
                <input type="hidden" name="redirect_to" value="<?= esc_attr($_GET['redirect_to'] ?? '') ?>">
                <input type="hidden" name="request_id" value="<?= wp_generate_password(16, false) ?>">
                <?= ($this->action === 'magic') ? '<input type="hidden" name="type" value="login">' : '' ?>
                <?php
                $this->addHiddenTokenFields();
                do_action('jvb_add_token_inputs', $this->action);
                foreach ($this->fields as $name => $config) {
                    $this->metaForm->render($name, '', $config);
@@ -590,7 +526,7 @@
                $this->maybeTurnstile();
                 ?>
                 <div class="row btw nowrap">
                    <button type="submit" class="button button-primary button-large">Log In</button>
                    <button type="submit" class="button button-primary button-large"><?=$this->labels['submit']?></button>
                    <?php $this->maybeMagicLink(); ?>
                </div>
            </form>
@@ -620,7 +556,8 @@
                        <a href="<?= add_query_arg('action', 'lostpassword', get_the_permalink()) ?>">Forgot Password?</a>
                        <?php
                        break;
                    case 'lostpassword': ?>
                    case 'lostpassword':
                    case 'magic': ?>
                        <a href="<?= get_the_permalink() ?>">Login Instead</a>
                        <a href="<?= add_query_arg('action', 'register', get_the_permalink()) ?>">Create Account</a>
                        <?php
@@ -661,8 +598,8 @@
            $title = ($checked == '') ? 'Toggle Dark Mode' : 'Toggle Light Mode';
            echo '<label title="'.$title.'" id="theme-switch" class="toggle-switch" for="theme-switcher">
                    <input class="theme-switch row" id="theme-switcher" type="checkbox"'.$checked.' data-setting="theme" data-theme role="switch" name="dark-mode"><span class="slider">'.
                    jvbIcon('light', ['title'=> 'Light Mode']).
                    jvbIcon('dark', ['title'=>'Dark Mode']).
                    jvbIcon('sun-dim', ['title'=> 'Light Mode']).
                    jvbIcon('moon', ['title'=>'Dark Mode']).
                    '</span></label>';
            ?>
            <p class="title">
@@ -677,7 +614,7 @@
                        }
                    }
                    if ($out == '') {
                        $out =jvbIcon('home');
                        $out =jvbIcon('house');
                    }
                    ?><?= $out ?>
                </a>
@@ -709,339 +646,6 @@
        <?php
    }
    protected function addHiddenTokenFields(): void
    {
        foreach ($this->tokenHandlers as $priority => $handlers) {
            foreach ($handlers as $token_key => $handler) {
                if (isset($_GET[$token_key])) {
                    $value = sanitize_text_field($_GET[$token_key]);
                    echo '<input type="hidden" name="' . esc_attr($token_key) . '" value="' . esc_attr($value) . '">';
                }
            }
        }
        if (isset($_GET['email'])) {
            echo '<input type="hidden" name="token_email" value="' . esc_attr(sanitize_email($_GET['email'])) . '">';
        }
    }
    /*************************************************************************
        AJAX HANDLERS
    *************************************************************************/
    public function handleAjaxLogin(): void
    {
        check_ajax_referer('jvb_login', '_wpnonce');
        // Rate limiting
        if (!$this->checkAjaxRateLimit('login')) {
            wp_send_json_error([
                'message' => 'Too many attempts. Please wait a moment.',
                'code' => 'rate_limit'
            ], 429);
        }
        // Duplicate submission check
        if (!$this->checkRequestId()) {
            wp_send_json_error([
                'message' => 'Duplicate request detected',
                'code' => 'duplicate_request'
            ], 409);
        }
        $email = sanitize_email($_POST['user_email'] ?? '');
        $password = $_POST['user_password'] ?? '';
        $remember = !empty($_POST['remember_me']);
        if (empty($email) || empty($password)) {
            wp_send_json_error([
                'message' => 'Please fill in all fields',
                'field' => empty($email) ? 'user_email' : 'user_password',
                'code' => 'missing_fields'
            ]);
        }
        // Verify Turnstile if enabled
        if (!$this->verifyTurnstile()) {
            wp_send_json_error([
                'message' => 'Security verification failed',
                'code' => 'turnstile_failed'
            ]);
        }
        $user = get_user_by('email', $email);
        if (!$user) {
            wp_send_json_error([
                'message' => 'Unknown email address',
                'field' => 'user_email',
                'code' => 'invalid_email'
            ]);
        }
        $user = wp_authenticate($user->user_login, $password);
        if (is_wp_error($user)) {
            wp_send_json_error([
                'message' => $user->get_error_message(),
                'field' => 'user_password',
                'code' => $user->get_error_code()
            ]);
        }
        wp_clear_auth_cookie();
        wp_set_current_user($user->ID);
        wp_set_auth_cookie($user->ID, $remember);
        do_action('wp_login', $user->user_login, $user);
        $redirect = $_POST['redirect_to'] ?? home_url('/dash');
        wp_send_json_success(['redirect' => $redirect]);
    }
    public function handleAjaxRegister(): void
    {
        check_ajax_referer('jvb_register', '_wpnonce');
        // Rate limiting
        if (!$this->checkAjaxRateLimit('register')) {
            wp_send_json_error([
                'message' => 'Too many attempts. Please wait a moment.',
                'code' => 'rate_limit'
            ], 429);
        }
        // Duplicate submission check
        if (!$this->checkRequestId()) {
            wp_send_json_error([
                'message' => 'Duplicate request detected',
                'code' => 'duplicate_request'
            ], 409);
        }
        // Verify Turnstile
        if (!$this->verifyTurnstile()) {
            wp_send_json_error([
                'message' => 'Security verification failed',
                'code' => 'turnstile_failed'
            ]);
        }
        $name = sanitize_text_field($_POST['name'] ?? '');
        $email = sanitize_email($_POST['email'] ?? '');
        $user_type = sanitize_text_field($_POST['user_select'] ?? 'subscriber');
        // Spam prevention - if subscriber is selected and there are other options
        if ($user_type === 'subscriber' && count(JVB_USER) > 0) {
            $registerable = array_filter(JVB_USER, fn($config) => $config['can_register'] ?? false);
            if (!empty($registerable)) {
                wp_send_json_error([
                    'message' => 'Please select a valid account type',
                    'field' => 'user_select',
                    'code' => 'invalid_user_type'
                ]);
            }
        }
        // Validate fields
        if (empty($name)) {
            wp_send_json_error([
                'message' => 'Name is required',
                'field' => 'name',
                'code' => 'missing_name'
            ]);
        }
        if (empty($email)) {
            wp_send_json_error([
                'message' => 'Email is required',
                'field' => 'email',
                'code' => 'missing_email'
            ]);
        }
        // Check if role can register
        if ($user_type !== 'subscriber') {
            if (!isset(JVB_USER[$user_type]) || empty(JVB_USER[$user_type]['can_register'])) {
                wp_send_json_error([
                    'message' => 'Invalid account type',
                    'field' => 'user_select',
                    'code' => 'invalid_user_type'
                ]);
            }
        }
        // Check if email exists
        if (email_exists($email)) {
            wp_send_json_error([
                'message' => 'Email already registered',
                'field' => 'email',
                'code' => 'duplicate_email'
            ]);
        }
        // Create user
        $user_id = wp_create_user($email, wp_generate_password(), $email);
        if (is_wp_error($user_id)) {
            wp_send_json_error([
                'message' => $user_id->get_error_message(),
                'code' => 'user_creation_failed'
            ]);
        }
        // Update user data
        wp_update_user([
            'ID' => $user_id,
            'display_name' => $name,
            'first_name' => $name
        ]);
        // Set role
        $user = new WP_User($user_id);
        if ($user_type === 'subscriber') {
            $user->set_role('subscriber');
        } else {
            $role = JVB_USER[$user_type]['role'] ?? 'subscriber';
            $user->set_role($role);
            // Check if needs approval
            if (Features::forMembership()->has('memberVerified') &&
                in_array($role, JVB_MEMBERSHIP['memberVerified'] ?? [])) {
                $user->add_cap('skip_moderation', false);
                update_user_meta($user_id, BASE . 'pending_approval', true);
            }
        }
        // Save additional fields
        update_user_meta($user_id, BASE . 'user_type', $user_type);
        // Process additional fields from form
        foreach ($_POST as $key => $value) {
            if (in_array($key, ['name', 'email', 'action', '_wpnonce', 'request_id', 'user_select'])) {
                continue;
            }
            update_user_meta($user_id, BASE . $key, sanitize_text_field($value));
        }
        // Handle token handlers
        $this->processTokenHandlers($user_id, $email);
        // Send welcome email with password setup link
        $this->sendWelcomeEmail($user_id);
        // Trigger registration action for other systems
        do_action('jvbAfterUserRegistration', $user_id, $user_type, $_POST);
        wp_send_json_success([
            'message' => 'Registration successful! Check your email.',
            'title' => $this->labels['successTitle'] ?? 'Success!',
            'description' => $this->labels['successDescription'] ?? 'Check your email for next steps',
            'user_id' => $user_id // Important for file upload dependencies!
        ]);
    }
    public function handleAjaxLostPassword(): void
    {
        check_ajax_referer('jvb_lostpassword', '_wpnonce');
        // Rate limiting
        if (!$this->checkAjaxRateLimit('lostpassword')) {
            wp_send_json_error([
                'message' => 'Too many attempts. Please wait a moment.',
                'code' => 'rate_limit'
            ], 429);
        }
        $email = sanitize_email($_POST['user_email'] ?? '');
        if (empty($email)) {
            wp_send_json_error([
                'message' => 'Email required',
                'field' => 'user_email',
                'code' => 'missing_email'
            ]);
        }
        // Verify Turnstile
        if (!$this->verifyTurnstile()) {
            wp_send_json_error([
                'message' => 'Security verification failed',
                'code' => 'turnstile_failed'
            ]);
        }
        // Use WordPress's built-in function
        $result = retrieve_password($email);
        if (is_wp_error($result)) {
            wp_send_json_error([
                'message' => $result->get_error_message(),
                'code' => $result->get_error_code()
            ]);
        }
        wp_send_json_success(['message' => 'Check your email for reset link']);
    }
    public function handleAjaxResetPassword(): void
    {
        check_ajax_referer('jvb_resetpass', '_wpnonce');
        // Rate limiting
        if (!$this->checkAjaxRateLimit('resetpass')) {
            wp_send_json_error([
                'message' => 'Too many attempts. Please wait a moment.',
                'code' => 'rate_limit'
            ], 429);
        }
        $key = sanitize_text_field($_POST['key'] ?? $_GET['key'] ?? '');
        $login = sanitize_text_field($_POST['login'] ?? $_GET['login'] ?? '');
        $pass1 = $_POST['pass1'] ?? '';
        $pass2 = $_POST['pass2'] ?? '';
        if (empty($key) || empty($login)) {
            wp_send_json_error([
                'message' => 'Invalid reset link',
                'code' => 'invalid_key'
            ]);
        }
        if (empty($pass1) || empty($pass2)) {
            wp_send_json_error([
                'message' => 'Please enter a password',
                'field' => empty($pass1) ? 'pass1' : 'pass2',
                'code' => 'missing_password'
            ]);
        }
        if ($pass1 !== $pass2) {
            wp_send_json_error([
                'message' => 'Passwords do not match',
                'field' => 'pass2',
                'code' => 'password_mismatch'
            ]);
        }
        // Verify reset key
        $user = check_password_reset_key($key, $login);
        if (is_wp_error($user)) {
            wp_send_json_error([
                'message' => 'Invalid or expired reset link',
                'code' => 'invalid_key'
            ]);
        }
        // Reset password
        reset_password($user, $pass1);
        wp_send_json_success([
            'message' => 'Password reset successfully',
            'redirect' => home_url('/login')
        ]);
    }
    /**********************************************************************
        TOKEN PROCESSING
    **********************************************************************/
@@ -1057,40 +661,6 @@
        }
    }
    /***********************************************************************
        EMAIL SENDING
    ***********************************************************************/
    protected function sendWelcomeEmail(int $user_id): void
    {
        $user = get_userdata($user_id);
        if (!$user) {
            return;
        }
        // Generate password reset key
        $key = get_password_reset_key($user);
        if (is_wp_error($key)) {
            error_log('Failed to generate password reset key: ' . $key->get_error_message());
            return;
        }
        $reset_url = add_query_arg([
            'action' => 'rp',
            'key' => $key,
            'login' => rawurlencode($user->user_login)
        ], home_url('/login'));
        $subject = $this->labels['email'] ?? 'Welcome to ' . get_bloginfo('name');
        $message = '<h2>Welcome, ' . esc_html($user->display_name) . '!</h2>';
        $message .= '<p>Your account has been created. Click the button below to set your password and get started:</p>';
        $message .= jvbMailButton($reset_url, 'Set Your Password');
        $message .= '<p>This link expires in 24 hours.</p>';
        $this->emailManager->sendEmail($user->user_email, $subject, $message);
    }
    /*************************************************************************
    *   SECURITY & VALIDATION
    *************************************************************************/
@@ -1151,18 +721,35 @@
    protected function setupLabels(): void
    {
        $default = $this->getDefaultLabels();
        $this->labels = apply_filters('jvbLoginLabels', $default, $_GET);
        $default = apply_filters('jvbLoginLabels', $default, $_GET);
        foreach (['description', 'footer', 'extra'] as $location) {
            $text = (!is_array($this->labels[$location])) ? [$this->labels[$location]] : $this->labels[$location];
            if (!empty($text)) {
                $this->labels[$location] = '<div class="'.$location.'">';
                foreach ($text as $d) {
                    $this->labels[$location] .= '<p>'.$d.'</p>';
        if(array_key_exists('type', $_GET) && $_GET['type'] === 'favourites') {
            if (array_key_exists('favourites', JVB_LOGIN)) {
                foreach (JVB_LOGIN['favourites'] as $key => $value) {
                    $default[$key] = $value;
                }
                $this->labels[$location] .= '</div>';
            }
        }
        foreach (['description', 'footer', 'extra'] as $location) {
            if ($default[$location] === '') {
                continue;
            }
            if (empty($default[$location])) {
                $default[$location] = '';
                continue;
            }
            $text = (!is_array($default[$location])) ? [$default[$location]] : $default[$location];
            if (!empty($text)) {
                $default[$location] = '<div class="'.$location.'">';
                foreach ($text as $d) {
                    $default[$location] .= '<p>'.$d.'</p>';
                }
                $default[$location] .= '</div>';
            }
        }
        $this->labels = $default;
    }
    protected function getDefaultLabels(): array
@@ -1197,6 +784,23 @@
                    'footer' => JVB_LOGIN['reset_pass']['footer'] ?? '',
                    'submit' => JVB_LOGIN['reset_pass']['submit'] ?? 'Reset Password',
                ];
            case 'logout':
                return [
                    'title' => JVB_LOGIN['logout']['title'] ?? 'Logged Out!',
                    'description' => JVB_LOGIN['logout']['description'] ?? [],
                    'extra' => JVB_LOGIN['logout']['extra'] ?? [],
                    'footer' => JVB_LOGIN['logout']['footer'] ?? '',
                    'submit' => JVB_LOGIN['logout']['submit'] ?? '',
                ];
            case 'magic':
                return [
                    'title' => JVB_LOGIN['magic']['title'] ?? 'Log in with Magic Link',
                    'description' => JVB_LOGIN['magic']['description'] ?? ['Enter your email.','You\'ll get an email with a magic link.','Click it, and you\'re logged in!'],
                    'extra' => JVB_LOGIN['magic']['extra'] ?? [],
                    'footer' => JVB_LOGIN['magic']['footer'] ?? '',
                    'submit' => JVB_LOGIN['magic']['submit'] ?? jvbIcon('magic-wand').'Send Magic Link',
                ];
            case 'login':
            default:
                return [
@@ -1215,33 +819,10 @@
            return;
        }
        ?>
        <button type="button" id="magic-link-btn" class="button button-secondary button-large">
            <?= jvbIcon('email', ['size' => 20]); ?>
            Get Login Link
        </button>
        <script type="text/javascript">
            document.getElementById('magic-link-btn')?.addEventListener('click', function(e) {
                e.preventDefault();
                const email = document.querySelector('input[name="user_email"]')?.value;
                if (!email) {
                    alert('Please enter your email address first');
                    return;
                }
                fetch('<?= rest_url('jvb/v1/magic-link'); ?>', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-WP-Nonce': '<?= wp_create_nonce('wp_rest') ?>'
                    },
                    body: JSON.stringify({ email: email, type: 'login' })
                })
                .then(r => r.json())
                .then(data => {
                    alert(data.success ? 'Check your email!' : (data.message || 'Failed to send link'));
                });
            });
        </script>
        <a class="button" href="<?= add_query_arg('action', 'magic', wp_login_url()) ?>" title="Email yourself a link to log you in auto-magically!">
            <?= jvbIcon('magic-wand'); ?>
            Magic Link
        </a>
        <?php
    }
@@ -1257,20 +838,159 @@
        $this->maybeTurnstileScripts();
        wp_enqueue_script('jvb-form');
        $script = "
        $action = $this->getAction();
        ob_start();
        ?>
        <script type="text/javascript">
        window.checkedEmails = new Set();
        document.addEventListener('DOMContentLoaded', () => {
            const form = document.querySelector('.login form');
            if (form && window.jvbForm) {
                let controller = new window.jvbForm();
                controller.registerForm(form, {
                    autosave: false,
                    endpoint: false
                });
            } else if (form && !window.jvbForm) {
            if (!form) return;
            if (!window.jvbForm) {
                console.error('jvbForm not loaded');
                return;
            }
        });";
            window.LoginController = new window.jvbForm();
            window.LoginController.registerForm(form, {
                autosave: false,
                endpoint: <?= "'{$action}'" ?>,
                formStatus: false,
                cache: false,
            });
            window.LoginController.subscribe((event, data) => {
                if (event === 'form-submit') {
                    handleFormSubmission(data);
                } else if (event === 'field-validated' && data.name === 'user_email') {
                    if (!window.checkedEmails.has(data.value)){
                        checkEmail(data.value, data);
                    }
                }
            });
            async function handleFormSubmission(data) {
                const { formId, config, data: formData } = data;
                const form = config.element;
                const submit = form.querySelector('[type=submit]');
                let oldText = submit.textContent;
                // Show uploading status
                window.LoginController.showFormStatus(formId, 'uploading');
                try {
                    submit.disabled = true;
                    submit.textContent = 'Loading...';
                    const response = await fetch(`${jvbSettings.api}<?=($action === 'magic') ? $action : 'auth/'.$action?>`, {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json',
                            'X-WP-Nonce': jvbSettings.nonce
                        },
                        body: JSON.stringify(formData)
                    });
                    const result = await response.json();
                    // Handle errors
                    if (!response.ok) {
                        window.LoginController.showFormStatus(formId, 'error');
                        window.LoginController.handleFormError(form, result);
                        return;
                    }
                    // Handle success
                    window.LoginController.showFormStatus(formId, 'submitted');
                    // Show success message briefly before redirect
                    if (result.message) {
                        window.LoginController.handleFormSuccess(form, result);
                    }
                    // Handle redirect
                    if (result.redirect) {
                        setTimeout(() => {
                            window.location.href = result.redirect;
                        }, 500); // Brief delay to show success message
                    }
                } catch (error) {
                    console.error('Form submission error:', error);
                    window.LoginController.showFormStatus(formId, 'error');
                    window.LoginController.handleFormError(form, {
                        message: 'Network error. Please check your connection and try again.',
                        code: 'network_error'
                    });
                } finally {
                    submit.textContent = oldText;
                    submit.disabled = false;
                }
            }
            async function checkEmail(email, input) {
                window.checkedEmails.add(email);
                let wrapper = input.closest('.field');
                let submit = input.closest('form').querySelector('[type=submit]');
                try {
                    submit.disabled = true;
                    window.LoginController.showSuccess(wrapper, 'Checking our records...');
                    const response = await fetch(`${jvbSettings.api}auth/email`, {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json',
                            'X-WP-Nonce': jvbSettings.nonce
                        },
                        body: JSON.stringify({ email: email })
                    });
                    const result = await response.json();
                    if (!response.ok) {
                        return; // On error, allow to proceed (fail open)
                    }
                    if (result.exists) {
                        <?php
                        switch ($action) {
                            case 'register':
                                echo 'window.LoginController.showError(wrapper,\'This email is already registered. Log in instead?\');';
                                break;
                            case 'login':
                            case 'lostpassword':
                            case 'magic':
                                echo 'window.LoginController.showSuccess(wrapper,\'Email exists in our system.\');';
                                break;
                        }
                        ?>
                    } else {
                        <?php
                        switch ($action) {
                            case 'register':
                                echo 'window.LoginController.showSuccess(wrapper,\'Email is available!\');';
                                break;
                            case 'login':
                            case 'lostpassword':
                            case 'magic':
                                echo 'window.LoginController.showError(wrapper,\'This email doesn\\\'t seem to exist in our system. Create account instead?\');';
                                break;
                        }
                        ?>
                    }
                    return true; // Email is available
                } catch (error) {
                    console.error('Email check failed:', error);
                    return true; // On network error, allow to proceed
                }finally {
                    submit.disabled = false;
                }
            }
        });
        </script>
        <?php
        $script = ob_get_clean();
        wp_add_inline_script('jvb-form', $script);
    }
inc/managers/LoginManagerOld.php
@@ -97,7 +97,7 @@
        <script type="text/javascript">
            document.addEventListener('DOMContentLoaded', function() {
                let loginLabel = document.querySelector('label[for="user_login"');
                loginLabel.innerHTML = '<?= jvbIcon('email', ['size' => 20]); ?> Your Email';
                loginLabel.innerHTML = '<?= jvbIcon('envelope', ['size' => 20]); ?> Your Email';
                let passwordLabel = document.querySelector('label[for="user_pass"');
                passwordLabel.innerHTML = '<?= jvbIcon('password', ['size' => 20]); ?> Your Password';
@@ -580,9 +580,9 @@
            <input type="radio" id="enthusiast" name="user_type" value="enthusiast" required <?= ($this->fromFavourites()) ? 'checked' : '' ?>>
            <label for="enthusiast"><?=jvbIcon('heart', ['title' =>'Enthusiast', 'size'=>40])?><h4>Enthusiast</h4><p>Start here.</p></label>
            <input type="radio" id="artist" name="user_type" value="artist" required>
            <label for="artist"><?=jvbIcon('tattoo', ['title'=> 'Artist', 'size'=> 40])?><h4>Artist</h4><p>Show your talent.</p></label>
            <label for="artist"><?=jvbIcon('drop-simple', ['title'=> 'Artist', 'size'=> 40])?><h4>Artist</h4><p>Show your talent.</p></label>
            <input type="radio" id="partner" name="user_type" value="partner" required>
            <label for="partner"><?=jvbIcon('partner', ['title'=>'Partner', 'size' => 40])?><h4>Partner</h4><p>Support the community.</p></label>
            <label for="partner"><?=jvbIcon('currency-circle-dollar', ['title'=>'Partner', 'size' => 40])?><h4>Partner</h4><p>Support the community.</p></label>
            <p class="enthusiast">Save your favourites. Get notified.</p>
            <p class="artist">Show off your work.</p>
            <p class="partner">Support the community.</p>
inc/managers/MagicLinkManager.php
@@ -38,9 +38,7 @@
        // Hook into WordPress auth flow
        add_action('template_redirect', [$this, 'handleMagicLinkClick']);
        add_action('wp_login_failed', [$this, 'handleFailedLogin']);
        // NOTE: LoginManager now handles the login form UI
        // If magic_link integration is enabled, LoginManager will call addMagicLinkOption()
        add_action('jvb_process_login_tokens', [$this, 'processRegistrationToken'], 10, 3);
    }
    /**
@@ -105,7 +103,7 @@
    /**
     * Verify a token
     */
    protected function verifyToken(string $token, string $email): array|WP_Error
    public function verifyToken(string $token, string $email): array|WP_Error
    {
        $token_data = $this->cache->get($token);
@@ -171,7 +169,7 @@
        $magic_url = add_query_arg([
            'magic_token' => $token,
            'email' => urlencode($email),
            'email' => rawurlencode($email),
            'action' => 'magic_login'
        ], home_url('/'));
@@ -207,7 +205,7 @@
        $magic_url = add_query_arg([
            'magic_token' => $token,
            'email' => urlencode($email),
            'email' => rawurlencode($email),
            'action' => 'magic_signup'
        ], home_url('/'));
@@ -238,7 +236,7 @@
        $magic_url = add_query_arg([
            'magic_token' => $token,
            'email' => urlencode($email),
            'email' => rawurlencode($email),
            'action' => 'magic_referral'
        ], home_url('/'));
@@ -269,7 +267,7 @@
        $magic_url = add_query_arg([
            'magic_token' => $token,
            'email' => urlencode($email),
            'email' => rawurlencode($email),
            'action' => 'magic_reset'
        ], home_url('/'));
inc/managers/NotificationManager.php
@@ -435,6 +435,10 @@
     */
    public function trackContentCreation(int $post_id, WP_POST $post, bool $update):void
    {
        // SAFETY: Skip attachments and other non-content post types
        if (in_array($post->post_type, jvbIgnoredPostTypes())) {
            return;
        }
        // Skip if not a published post
        if ($post->post_status !== 'publish') {
            return;
inc/managers/OperationQueue.php
@@ -3,6 +3,7 @@
use JVBase\managers\CacheManager;
use Exception;
use JVBase\utility\Features;
use WP_Error;
use WP_REST_Response;
use WP_REST_Request;
@@ -598,7 +599,9 @@
    }
    protected function updateLastModified(int $user_id) {
        JVB()->routes('queue')->updateUserQueueTimestamp($user_id);
        if (Features::forSite()->has('dashboard')) {
            JVB()->routes('queue')->updateUserQueueTimestamp($user_id);
        }
    }
    protected function deepMerge(array $existing, array $new): array
inc/managers/ReferralManager.php
@@ -24,6 +24,7 @@
    protected MagicLinkManager $magic_link;
    protected CacheManager $cache;
    protected string $referrals_table;
    protected ?int $referralPage = null;
    protected string $rewards_table;
    // Default reward settings
@@ -36,6 +37,8 @@
        'referee_reward_applies_to' => 'first_order'  // 'first_order' or 'all_orders'
    ];
    protected array $settings;
    public function __construct()
    {
        global $wpdb;
@@ -45,8 +48,13 @@
        $this->rewards_table = BASE . 'referral_rewards';
        $this->magic_link = new MagicLinkManager();
        // Hook into user registration to track referrals
        add_action('user_register', [$this, 'processReferral'], 10, 1);
        $this->referralPage = $this->getReferralPageId();
        $this->settings = $this->getRewardSettings();
        add_action('jvbUserRegistered', [$this, 'processRegistrationToken'], 10, 3);
        add_action('jvb_add_token_inputs', [$this, 'addLoginInputs'], 10, 1);
        add_action('jvbUserRegistered', [$this, 'processReferral'], 10, 1);
        // Add meta boxes for admin to manage referrals
        add_action('show_user_profile', [$this, 'displayUserReferralInfo']);
@@ -56,7 +64,7 @@
        add_action('personal_options_update', [$this, 'saveUserReferralCode']);
        add_action('edit_user_profile_update', [$this, 'saveUserReferralCode']);
        add_filter(BASE.'new_user_email_content', [$this, 'addReferralToWelcomeEmail'], 99, 2);
        add_filter('jvbNewUserEmail', [$this, 'addReferralToWelcomeEmail'], 99, 2);
        add_filter('jvbAdditionalActions', [$this, 'outputShareWidget']);
@@ -66,7 +74,7 @@
        $this->registerCronJobs();
        // Register admin subpage
        add_filter('jvbAdminSubpages', [$this, 'addSubpage'], 10, 1);
//      add_filter('jvbAdminSubpages', [$this, 'addSubpage'], 10, 1);
        // Add admin bar label for referral page
        add_action('admin_bar_menu', [$this, 'addReferralPageLabel'], 999);
@@ -81,6 +89,37 @@
        add_action('admin_init', [$this, 'registerSettings']);
    }
    public function addLoginInputs(string $action):void
    {
        if (array_key_exists('ref', $_GET)) {
            echo '<input type="hidden" name="referral_code" value="'.$_GET['ref'].'">';
        }
    }
    public function modifyLoginText(array $defaults):array
    {
        if (!array_key_exists('ref', $_GET)){
            return $defaults;
        }
        $code = $_GET['ref'];
        $user = $this->getUserByReferralCode($code);
        $desc = ($user) ? strtok($user->display_name, ' ') . ' invited you ' : 'You\'ve been invited ';
        $defaults['title'] = 'Register your Account. Get Your Reward.';
        $defaults['description'] = [
            $desc.' to see the difference with us.',
            'Oh, and you\'ll get 20% off your first treatment!',
            'Finish this account creation (you\'ll still need to make an account on Jane App to book), or let us know when you come in for your appointment.'
        ];
        $defaults['extra'] = [
            'Once you\'re in, you\'ll get your own code you can share.',
            'Get a $25 credit for <b>each</b> person you send our way who comes in for their first treatment!'
        ];
        return $defaults;
    }
    public function enqueueScripts():void
    {
        $requirements = [
@@ -205,17 +244,47 @@
        return !empty($users);
    }
    public function processRegistrationToken(int $user_id, string $email, array $data): void
    {
        // Check for referral code in data
        $code = $data['referral_code'] ?? '';
        if (empty($code)) {
            return;
        }
        // Store in session/cookie for processReferral to pick up
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }
        $_SESSION[BASE . 'referral_code'] = sanitize_text_field($code);
        setcookie(
            BASE . 'referral_code',
            sanitize_text_field($code),
            time() + (86400 * 30),
            '/'
        );
    }
    /**
     * Track a new referral when user registers
     *
     * @param int $user_id
     */
    public function processReferral(int $user_id): void
    public function processReferral(int $user_id, string $email, array $data): void
    {
        // Check if user was created via referral magic link
        $referral_code = get_user_meta($user_id, BASE . 'pending_referral_code', true);
        // Try to get code from multiple sources
        $referral_code = $data['referral_code'] ??
        get_user_meta($user_id, BASE . 'pending_referral_code', true);
        if (!$referral_code) {
        // Check session/cookie if not in data
        if (empty($referral_code)) {
            if (session_status() === PHP_SESSION_NONE) {
                session_start();
            }
            $referral_code = $_SESSION[BASE . 'referral_code'] ?? $_COOKIE[BASE . 'referral_code'] ?? '';
        }
        if (empty($referral_code)) {
            return;
        }
@@ -254,7 +323,7 @@
     * @param string $code
     * @return int|false
     */
    protected function createReferral(int $referrer_id, int $referee_id, string $code)
    public function createReferral(int $referrer_id, int $referee_id, string $code)
    {
        $user = get_user_by('ID', $referee_id);
@@ -282,13 +351,17 @@
     */
    public function getUserByReferralCode(string $code): ?WP_User
    {
        $users = get_users([
            'meta_key' => BASE . 'referral_code',
            'meta_value' => $code,
            'number' => 1
        ]);
        return $users[0] ?? null;
        return $this->cache->remember(
            $code,
            function () use ($code) {
                $users = get_users([
                    'meta_key' => BASE . 'referral_code',
                    'meta_value' => $code,
                    'number' => 1
                ]);
                return $users[0] ?? null;
            }
        );
    }
    /**
@@ -353,8 +426,6 @@
            return;
        }
        $settings = $this->getRewardSettings();
        // Create referrer reward
        $this->wpdb->insert(
            $this->rewards_table,
@@ -362,7 +433,7 @@
                'referral_id' => $referral_id,
                'user_id' => $referral->referrer_id,
                'reward_type' => 'referrer',
                'amount' => $settings['referrer_reward_amount'],
                'amount' => $this->settings['referrer_reward_amount'],
                'status' => 'available',
                'created_at' => current_time('mysql')
            ],
@@ -370,9 +441,9 @@
        );
        // Create referee reward
        $referee_amount = $settings['referee_reward_type'] === 'percentage'
            ? $settings['referee_reward_amount']  // Store as percentage
            : $settings['referee_reward_amount'];  // Store as fixed amount
        $referee_amount = $this->settings['referee_reward_type'] === 'percentage'
            ? $this->settings['referee_reward_amount']  // Store as percentage
            : $this->settings['referee_reward_amount'];  // Store as fixed amount
        $this->wpdb->insert(
            $this->rewards_table,
@@ -381,7 +452,7 @@
                'user_id' => $referral->referee_id,
                'reward_type' => 'referee',
                'amount' => $referee_amount,
                'reward_calculation' => $settings['referee_reward_type'],
                'reward_calculation' => $this->settings['referee_reward_type'],
                'status' => 'available',
                'created_at' => current_time('mysql')
            ],
@@ -998,9 +1069,8 @@
            return '';
        }
        $settings = $this->getRewardSettings();
        $reward_amount = $settings['referee_reward_amount'] ?? 20;
        $reward_type = $settings['referee_reward_type'] ?? 'percentage';
        $reward_amount = $this->settings['referee_reward_amount'] ?? 20;
        $reward_type = $this->settings['referee_reward_type'] ?? 'percentage';
        $reward_text = $reward_type === 'percentage'
            ? $reward_amount . '% off'
@@ -1036,7 +1106,7 @@
        return ob_get_clean();
    }
    function getLoggedInReferral(int $user_id):string
    public function getLoggedInReferral(int $user_id):string
    {
        // Logged-in user widget
        $referral_code = get_user_meta($user_id, BASE . 'referral_code', true);
@@ -1049,7 +1119,7 @@
            }
        }
        $share_url = home_url('/?ref=' . $referral_code);
        $share_url = $this->getShareURL($referral_code);
        ob_start();
        ?>
@@ -1069,14 +1139,14 @@
               target="_blank"
               rel="noopener noreferrer"
               class="share-btn facebook-share">
                <?php echo jvbIcon('facebook', ['size' => 20]); ?>
                <?php echo jvbIcon('facebook-logo', ['size' => 20]); ?>
                Facebook
            </a>
            <a href="https://twitter.com/intent/tweet?url=<?php echo urlencode($share_url); ?>&text=<?php echo urlencode('Check this out!'); ?>"
               target="_blank"
               rel="noopener noreferrer"
               class="share-btn twitter-share">
                <?php echo jvbIcon('twitter', ['size' => 20]); ?>
                <?php echo jvbIcon('twitter-logo', ['size' => 20]); ?>
                Twitter
            </a>
        </div>
@@ -1129,27 +1199,52 @@
            return $content;
        }
        $settings = get_option(BASE . 'referral_settings', []);
        $reward_amount = $settings['referee_reward_amount'] ?? 20;
        $reward_type = $settings['referee_reward_type'] ?? 'percentage';
        $reward_amount = $this->settings['referee_reward_amount'] ?? 20;
        $reward_type = $this->settings['referee_reward_type'] ?? 'percentage';
        $reward_text = $reward_type === 'percentage'
            ? $reward_amount . '% off'
            : '$' . number_format($reward_amount, 2) . ' off';
        $bonus_content = '<div style="background: #e7f5ff; padding: 20px; border-radius: 8px; margin: 20px 0;">';
        $bonus_content .= '<h3 style="margin-top: 0; color: #2271b1;">🎉 Welcome Bonus!</h3>';
        $bonus_content .= '<p>Since you were referred by a friend, you\'ve earned <strong>' . $reward_text . '</strong> your first booking!</p>';
        $bonus_content .= '<p>Your reward will be automatically applied when you book.</p>';
        $bonus_content .= '</div>';
        $bonus_content = sprintf(
            '
            <h3>Thanks for the â™¡</h3>
            <p>Since you were referred by a friend, you\'ve earned <strong>%s</strong> your first booking!</p>
            <p>Your reward will be automatically applied when you book.</p>',
            $reward_text,
        );
        // Insert bonus content after the first paragraph
        $parts = explode('</p>', $content, 2);
        if (count($parts) === 2) {
            return $parts[0] . '</p>' . $bonus_content . $parts[1];
        $code = $this->getUserReferralCode($user->ID);
        $yourCode = '';
        if (!is_wp_error($code)) {
            $share_url = $this->getShareURL($code);
            $yourCode = sprintf(
                '<div class="callout">
            <h3>Share the â™¡ with Friends</h3>
            <p>If you find you love what we can do for you, you can share your own code!</p>
            <p>Your Referral Code: <strong>%s</strong></p>
            <p>Or click the button below:</p>
            %s
            </div>',
                jvbEmailLink($code),
                jvbMailButton($share_url, 'Share Your Code')
            );
        }
        return $content . $bonus_content;
        return $content . $bonus_content . $yourCode;
    }
    public function getShareURL(string $code):string
    {
        return add_query_arg(
            [
                'ref' => $code,
                'action'    => 'register'
            ],
            wp_login_url()
        );
    }
    /**
@@ -1197,10 +1292,9 @@
        }
        // Get reward text for email
        $settings = $this->getRewardSettings();
        $reward_text = $settings['referee_reward_type'] === 'percentage'
            ? "Get {$settings['referee_reward_amount']}% off your first treatment!"
            : "Get \${$settings['referee_reward_amount']} off your first treatment!";
        $reward_text = $this->settings['referee_reward_type'] === 'percentage'
            ? "Get {$this->settings['referee_reward_amount']}% off your first treatment!"
            : "Get \${$this->settings['referee_reward_amount']} off your first treatment!";
        // Record the invitation attempt (for tracking)
        $this->recordInvitationAttempt($user_id, $invitee_email, $invitee_name);
@@ -1555,9 +1649,6 @@
            echo '<div class="notice notice-success is-dismissible"><p>Settings saved successfully.</p></div>';
        }
        $referral_page_id = $this->getReferralPageId();
        $settings = $this->getRewardSettings();
        echo $this->renderAdminHTML();
    }
@@ -1585,7 +1676,7 @@
                                wp_dropdown_pages([
                                    'name' => BASE . 'referral_page_id',
                                    'id' => BASE . 'referral_page_id',
                                    'selected' => $referral_page_id,
                                    'selected' => $this->referralPage,
                                    'show_option_none' => __('— Select â€”', 'jvbase'),
                                    'option_none_value' => '0'
                                ]);
@@ -1611,8 +1702,8 @@
                            </th>
                            <td>
                                <select name="referrer_reward_type" id="referrer_reward_type">
                                    <option value="fixed" <?php selected($settings['referrer_reward_type'], 'fixed'); ?>>Fixed Amount</option>
                                    <option value="percentage" <?php selected($settings['referrer_reward_type'], 'percentage'); ?>>Percentage</option>
                                    <option value="fixed" <?php selected($this->settings['referrer_reward_type'], 'fixed'); ?>>Fixed Amount</option>
                                    <option value="percentage" <?php selected($this->settings['referrer_reward_type'], 'percentage'); ?>>Percentage</option>
                                </select>
                            </td>
                        </tr>
@@ -1624,7 +1715,7 @@
                                <input type="number"
                                       name="referrer_reward_amount"
                                       id="referrer_reward_amount"
                                       value="<?= esc_attr($settings['referrer_reward_amount']) ?>"
                                       value="<?= esc_attr($this->settings['referrer_reward_amount']) ?>"
                                       step="0.01"
                                       min="0">
                                <p class="description">Amount in dollars or percentage</p>
@@ -1636,8 +1727,8 @@
                            </th>
                            <td>
                                <select name="referrer_reward_applies_to" id="referrer_reward_applies_to">
                                    <option value="per_user" <?php selected($settings['referrer_reward_applies_to'], 'per_user'); ?>>Per User Referred</option>
                                    <option value="flat_total" <?php selected($settings['referrer_reward_applies_to'], 'flat_total'); ?>>Flat Total</option>
                                    <option value="per_user" <?php selected($this->settings['referrer_reward_applies_to'], 'per_user'); ?>>Per User Referred</option>
                                    <option value="flat_total" <?php selected($this->settings['referrer_reward_applies_to'], 'flat_total'); ?>>Flat Total</option>
                                </select>
                            </td>
                        </tr>
@@ -1651,8 +1742,8 @@
                            </th>
                            <td>
                                <select name="referee_reward_type" id="referee_reward_type">
                                    <option value="percentage" <?php selected($settings['referee_reward_type'], 'percentage'); ?>>Percentage</option>
                                    <option value="fixed" <?php selected($settings['referee_reward_type'], 'fixed'); ?>>Fixed Amount</option>
                                    <option value="percentage" <?php selected($this->settings['referee_reward_type'], 'percentage'); ?>>Percentage</option>
                                    <option value="fixed" <?php selected($this->settings['referee_reward_type'], 'fixed'); ?>>Fixed Amount</option>
                                </select>
                            </td>
                        </tr>
@@ -1664,7 +1755,7 @@
                                <input type="number"
                                       name="referee_reward_amount"
                                       id="referee_reward_amount"
                                       value="<?= esc_attr($settings['referee_reward_amount']) ?>"
                                       value="<?= esc_attr($this->settings['referee_reward_amount']) ?>"
                                       step="0.01"
                                       min="0">
                                <p class="description">Amount in dollars or percentage</p>
@@ -1676,8 +1767,8 @@
                            </th>
                            <td>
                                <select name="referee_reward_applies_to" id="referee_reward_applies_to">
                                    <option value="first_order" <?php selected($settings['referee_reward_applies_to'], 'first_order'); ?>>First Order Only</option>
                                    <option value="all_orders" <?php selected($settings['referee_reward_applies_to'], 'all_orders'); ?>>All Orders</option>
                                    <option value="first_order" <?php selected($this->settings['referee_reward_applies_to'], 'first_order'); ?>>First Order Only</option>
                                    <option value="all_orders" <?php selected($this->settings['referee_reward_applies_to'], 'all_orders'); ?>>All Orders</option>
                                </select>
                            </td>
                        </tr>
@@ -1744,16 +1835,14 @@
            return;
        }
        $referral_page_id = $this->getReferralPageId();
        if (!$referral_page_id) {
        if (!$this->referralPage) {
            return;
        }
        global $pagenow, $post;
        // Check if we're editing the referral page
        if ('post.php' === $pagenow && $post && $post->ID === $referral_page_id) {
        if ('post.php' === $pagenow && $post && $post->ID === $this->referralPage) {
            $wp_admin_bar->add_node([
                'id' => 'referral-page',
                'parent' => 'top-secondary',
@@ -1787,9 +1876,7 @@
            return;
        }
        $referral_page_id = $this->getReferralPageId();
        if ($post->ID === $referral_page_id) {
        if ($post->ID === $this->referralPage) {
            echo '<div class="notice notice-info">';
            echo '<p>' . __('This page is designated as the <strong>Referral Page</strong>.', 'jvbase') . '</p>';
            echo '</div>';
inc/managers/RegisterManager.php
@@ -746,9 +746,9 @@
            <input type="radio" id="enthusiast" name="user_type" value="enthusiast" required <?= ($this->fromFavourites()) ? 'checked' : '' ?>>
            <label for="enthusiast"><?=jvbIcon('heart', ['title' =>'Enthusiast', 'size'=>40])?><h4>Enthusiast</h4><p>Start here.</p></label>
            <input type="radio" id="artist" name="user_type" value="artist" required>
            <label for="artist"><?=jvbIcon('tattoo', ['title'=> 'Artist', 'size'=> 40])?><h4>Artist</h4><p>Show your talent.</p></label>
            <label for="artist"><?=jvbIcon('drop-simple', ['title'=> 'Artist', 'size'=> 40])?><h4>Artist</h4><p>Show your talent.</p></label>
            <input type="radio" id="partner" name="user_type" value="partner" required>
            <label for="partner"><?=jvbIcon('partner', ['title'=>'Partner', 'size' => 40])?><h4>Partner</h4><p>Support the community.</p></label>
            <label for="partner"><?=jvbIcon('currency-circle-dollar', ['title'=>'Partner', 'size' => 40])?><h4>Partner</h4><p>Support the community.</p></label>
            <p class="enthusiast">Save your favourites. Get notified.</p>
            <p class="artist">Show off your work.</p>
            <p class="partner">Support the community.</p>
inc/managers/TaxonomyRelationships.php
@@ -4,6 +4,7 @@
use JVBase\JVB;
use JVBase\managers\CacheManager;
use WP_Error;
use WP_Post;
if (!defined('ABSPATH')) {
    exit; // Exit if accessed directly
@@ -42,8 +43,8 @@
     */
    public function init():void
    {
        add_action('save_post', [$this, 'updatePostRelationships']);
        add_action('before_delete_post', [$this, 'updatePostRelationships']);
        add_action('save_post', [$this, 'updatePostRelationships'], 10, 2);
        add_action('before_delete_post', [$this, 'updatePostRelationships'], 10, 2);
        add_action('delete_term', [$this, 'deleteTermRelationships']);
        add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
@@ -57,13 +58,13 @@
     *
     * @return void
     */
    public function updatePostRelationships(int $post_id):void
    public function updatePostRelationships(int $post_id, WP_Post $post):void
    {
        $this->cache->invalidate();
        $post_type = get_post_type($post_id);
        if (in_array($post_type, [BASE.'directory', BASE.'dash'])) {
        $post_type = $post->post_type;
        if (in_array($post_type, jvbIgnoredPostTypes())) {
            return;
        }
        $this->cache->invalidate();
        // Get all taxonomies for this post type
        $taxonomies = get_object_taxonomies($post_type);
inc/managers/UserTermsManager.php
@@ -64,6 +64,10 @@
        if (wp_is_post_autosave($post_id) || wp_is_post_revision($post_id)) {
            return;
        }
        // SAFETY: Skip attachments and other non-content post types
        if (in_array($post->post_type, jvbIgnoredPostTypes())) {
            return;
        }
        // Skip non-custom post types
        $post_type = get_post_type($post);
@@ -409,12 +413,13 @@
        $this->wpdb->query("TRUNCATE TABLE {$this->table_name}");
        // Get all users with posts
        $users = $this->wpdb->get_col("
            SELECT DISTINCT post_author
            FROM {$this->wpdb->posts}
            WHERE post_status = 'publish'
            AND post_type LIKE '" . BASE . "%'
        ");
        $users = $this->wpdb->get_col($this->wpdb->prepare(
            "SELECT DISTINCT post_author
            FROM {$this->wpdb->posts}
            WHERE post_status = 'publish'
            AND post_type LIKE %s",
            $this->wpdb->esc_like(BASE) . '%'
        ));
        JVB()->queue()->queueOperation(
            'rebuild_user_term_index',
@@ -490,14 +495,14 @@
                $this->clearUserCache($user_id);
                // Get all the user's published posts
                $posts = $this->wpdb->get_col($this->wpdb->prepare(
                    "SELECT ID FROM {$this->wpdb->posts}
            WHERE post_author = %d
            AND post_status = 'publish'
            AND post_type LIKE '%s'",
                    $user_id,
                    BASE
                ));
                $posts = $this->wpdb->get_col($this->wpdb->prepare(
                    "SELECT ID FROM {$this->wpdb->posts}
                    WHERE post_author = %d
                    AND post_status = 'publish'
                    AND post_type LIKE %s",
                    $user_id,
                    $this->wpdb->esc_like(BASE) . '%'
                ));
                $processed_terms = 0;
inc/managers/_setup.php
@@ -1,8 +1,9 @@
<?php
use JVBase\utility\Features;
require(JVB_DIR . '/inc/managers/CacheManager.php');
require(JVB_DIR . '/icons.php');
require(JVB_DIR . '/inc/managers/IconsManager.php');
require(JVB_DIR . '/inc/managers/ErrorHandler.php');
require(JVB_DIR . '/inc/managers/OperationQueue.php');
require(JVB_DIR . '/inc/managers/EmailManager.php');
@@ -15,9 +16,9 @@
//IF SITE HAS DASHBOARD AND FEED BLOCK
if (Features::forSite()->hasAny(['dashboard', 'feed_block'])) {
//if (Features::forSite()->hasAny(['dashboard', 'feed_block'])) {
    require(JVB_DIR . '/inc/forms/TaxonomySelector.php');
}
//}
//IF SITE HAS DASHBOARD
if (Features::forSite()->has('dashboard')) {
inc/meta/MetaForm.php
@@ -426,7 +426,7 @@
                        class="decrease"
                        title="<?= array_key_exists('remove', $field) ? $field['remove'] : 'Decrease amount' ?>"
                        aria-label="Decrease <?= esc_attr($field['label']) ?>">
                    <?= jvbIcon('minus') ?>
                    <?= jvbIcon('minus-square') ?>
                </button>
                <input type="number"
@@ -445,7 +445,7 @@
                        class="increase"
                        title="<?= array_key_exists('add', $field) ? $field['add'] : 'Increase amount' ?>"
                        aria-label="Increase <?= esc_attr($field['label']) ?>">
                    <?= jvbIcon('add') ?>
                    <?= jvbIcon('plus-square') ?>
                </button>
            </div>
@@ -678,11 +678,11 @@
        <div class="repeater-row" data-index="<?= esc_attr($index); ?>">
            <details <?= (is_string($index)) ? 'open' : ''; ?>>
                <summary class="repeater-row-header row btw">
                    <span class="drag-handle"><?= jvbIcon('grab'); ?></span>
                    <span class="drag-handle"><?= jvbIcon('dots-six-vertical'); ?></span>
                    <span class="row-number">#<?= esc_html($display_number); ?></span>
                    <span class="row-title"><?= esc_html($this->getRowTitle($fields, $values, $rowTitle)); ?></span>
                    <button type="button" class="remove-row" title="Remove">
                        <?= jvbIcon('delete', ['title'=>'Remove']); ?>
                        <?= jvbIcon('trash', ['title'=>'Remove']); ?>
                    </button>
                </summary>
                <div class="repeater-row-content">
@@ -931,18 +931,18 @@
                            <div class="selection-actions row btw" hidden>
                                <button type="button" data-action="add-to-group">
                                    <?= jvbIcon('add') ?>
                                    <?= jvbIcon('plus-square') ?>
                                    Group
                                </button>
                                <button type="button" data-action="delete-upload">
                                    <?= jvbIcon('delete') ?>
                                    <?= jvbIcon('trash') ?>
                                    Delete
                                </button>
                            </div>
                        </div>
                        <button type="button" data-action="upload" class="submit-uploads">
                            <?= jvbIcon('upload') ?> Upload <?= esc_html($plural ?? 'Content'); ?>
                            <?= jvbIcon('cloud-arrow-up') ?> Upload <?= esc_html($plural ?? 'Content'); ?>
                        </button>
                    </div>
                    <?php endif; ?>
@@ -959,7 +959,7 @@
                    </div>
                    <?php if ($destination === 'post_group') : ?>
                    <p class="hint"><?= jvbIcon('elbow-left-up') ?>  These will become individual <?= $plural ?>  <?= jvbIcon('elbow-right-up')?></p>
                    <p class="hint"><?= jvbIcon('arrow-elbow-left-up') ?>  These will become individual <?= $plural ?>  <?= jvbIcon('arrow-elbow-right-up')?></p>
                </div>
                <div class="sidebar flex col">
                    <div class="header">
@@ -971,7 +971,7 @@
                            <p>Drag here to create a new <?= $singular ?>!</p>
                        </div>
                    </div>
                    <p class="hint"><?= jvbIcon('elbow-left-up') ?>  Each group will become its own <?= $singular ?>  <?= jvbIcon('elbow-right-up')?></p>
                    <p class="hint"><?= jvbIcon('arrow-elbow-left-up') ?>  Each group will become its own <?= $singular ?>  <?= jvbIcon('arrow-elbow-right-up')?></p>
                </div>
            </div>
        <?php endif; ?>
@@ -1087,12 +1087,12 @@
                        </div>
                        <button type="button" data-action="delete-upload" title="Remove from Group">
                            <?=jvbIcon('delete')?>
                            <?=jvbIcon('trash')?>
                        </button>
                    </div>
                </div>
                <details>';
                    <summary class="row btw"><?=jvbIcon('edit')?><span>Edit Info</span></summary>
                <details>
                    <summary class="row btw"><?=jvbIcon('pencil-simple')?><span>Edit Info</span></summary>
        <?php
@@ -1127,7 +1127,10 @@
            ]
        ], $fields);
        $this->render('upload_data', null, $fields);
        $meta = new MetaManager($id);
        foreach ($fields as $field => $config) {
            $meta->render('form', $field, $config);
        }
        ?>
                </details>
        </div>
@@ -1162,12 +1165,12 @@
                        </div>
                        <button type="button" data-action="delete-upload" title="Remove from Group">
                            <?=jvbIcon('delete')?>
                            <?=jvbIcon('trash')?>
                        </button>
                    </div>
                </div>
                <details>';
                    <summary class="row btw"><?=jvbIcon('edit')?><span>Edit Info</span></summary>
                    <summary class="row btw"><?=jvbIcon('pencil-simple')?><span>Edit Info</span></summary>
        <?php
        $fields = array_key_exists('fields', $config) ? $config['fields'] : [];
@@ -1235,12 +1238,12 @@
                        </div>
                        <button type="button" data-action="delete-upload" title="Remove from Group">
                            <?=jvbIcon('delete')?>
                            <?=jvbIcon('trash')?>
                        </button>
                    </div>
                </div>
                <details>';
                    <summary class="row btw"><?=jvbIcon('edit')?><span>Edit Info</span></summary>
                    <summary class="row btw"><?=jvbIcon('pencil-simple')?><span>Edit Info</span></summary>
        <?php
        $fields = array_key_exists('fields', $config) ? $config['fields'] : [];
inc/meta/MetaFormOld.php
@@ -267,7 +267,7 @@
                         class="decrease"
                         title="<?= array_key_exists('remove', $field) ? $field['remove'] : 'Decrease amount' ?>"
                         aria-label="Decrease <?= esc_attr($field['label']); ?>">
                     <?= jvbIcon('minus')?>
                     <?= jvbIcon('minus-square')?>
                 </button>
                 <input type="number"
@@ -286,7 +286,7 @@
                         class="increase"
                         title="<?= array_key_exists('add', $field) ? $field['add'] : 'Increase amount' ?>"
                         aria-label="Increase <?= esc_attr($field['label']); ?>">
                     <?= jvbIcon('add')?>
                     <?= jvbIcon('plus-square')?>
                 </button>
             </div>
             <?php $this->renderDescription($description, $name);  ?>
@@ -469,11 +469,11 @@
        <div class="repeater-row" data-index="<?= esc_attr($index); ?>">
            <details <?= (is_string($index)) ? 'open' : ''; ?>>
                <summary class="repeater-row-header row btw">
                    <span class="drag-handle"><?= jvbIcon('grab'); ?></span>
                    <span class="drag-handle"><?= jvbIcon('dots-six-vertical'); ?></span>
                    <span class="row-number">#<?= esc_html($display_number); ?></span>
                    <span class="row-title"><?= esc_html($this->getRowTitle($fields, $values, $rowTitle)); ?></span>
                    <button type="button" class="remove-row" title="Remove">
                        <?= jvbIcon('delete', ['title'=>'Remove']); ?>
                        <?= jvbIcon('trash', ['title'=>'Remove']); ?>
                    </button>
                </summary>
                <div class="repeater-row-content">
@@ -605,7 +605,7 @@
                <button title="Add <?= esc_attr(ucfirst($field['post_type'])) ?>"
                        class="add-item-btn"
                        type="button">
                    <?= jvbIcon('add', ['title' => "Add " . ucfirst($field['post_type'])]) ?>
                    <?= jvbIcon('plus-square', ['title' => "Add " . ucfirst($field['post_type'])]) ?>
                </button>
            </div>
@@ -843,7 +843,7 @@
                        <?php endif; ?>
                        <button type="button" class="search-button">
                            <?= jvbIcon('search', ['title' => 'Search']); ?>
                            <?= jvbIcon('magnifying-glass', ['title' => 'Search']); ?>
                        </button>
                    </div>
                </div>
@@ -925,7 +925,7 @@
                                        <span class="item-title"><?= esc_html($item_title); ?></span>
                                        <span class="item-type"><?= esc_html(ucfirst($item_object)); ?></span>
                                        <button type="button" class="remove-item row">
                                            <?= jvbIcon('close', ['title' => 'Remove']); ?>
                                            <?= jvbIcon('x', ['title' => 'Remove']); ?>
                                        </button>
                                    </li>
                                    <?php
@@ -1049,7 +1049,7 @@
                            const removeBtn = document.createElement('button');
                            removeBtn.type = 'button';
                            removeBtn.className = 'remove-item';
                            removeBtn.innerHTML = '<?= jvbIcon('close', ['title' => 'Remove']); ?>';
                            removeBtn.innerHTML = '<?= jvbIcon('x', ['title' => 'Remove']); ?>';
                            removeBtn.addEventListener('click', function() {
                                removeItem(id, clone);
                            });
@@ -1434,18 +1434,18 @@
                            <div class="selection-actions row btw" hidden>
                                <button type="button" data-action="add-to-group">
                                    <?= jvbIcon('add') ?>
                                    <?= jvbIcon('plus-square') ?>
                                    Group
                                </button>
                                <button type="button" data-action="delete-upload">
                                    <?= jvbIcon('delete') ?>
                                    <?= jvbIcon('trash') ?>
                                    Delete
                                </button>
                            </div>
                        </div>
                        <button type="button" data-action="upload" class="submit-uploads">
                            <?= jvbIcon('upload') ?> Upload <?= esc_html($plural ?? 'Content'); ?>
                            <?= jvbIcon('cloud-arrow-up') ?> Upload <?= esc_html($plural ?? 'Content'); ?>
                        </button>
                    </div>
                    <?php endif; ?>
@@ -1462,7 +1462,7 @@
                    </div>
                    <?php if ($destination === 'post_group') : ?>
                    <p class="hint"><?= jvbIcon('elbow-left-up') ?>  These will become individual <?= $plural ?>  <?= jvbIcon('elbow-right-up')?></p>
                    <p class="hint"><?= jvbIcon('arrow-elbow-left-up') ?>  These will become individual <?= $plural ?>  <?= jvbIcon('arrow-elbow-right-up')?></p>
                </div>
                <div class="sidebar flex col">
                    <div class="header">
@@ -1474,7 +1474,7 @@
                            <p>Drag here to create a new <?= $singular ?>!</p>
                        </div>
                    </div>
                    <p class="hint"><?= jvbIcon('elbow-left-up') ?>  Each group will become its own <?= $singular ?>  <?= jvbIcon('elbow-right-up')?></p>
                    <p class="hint"><?= jvbIcon('arrow-elbow-left-up') ?>  Each group will become its own <?= $singular ?>  <?= jvbIcon('arrow-elbow-right-up')?></p>
                </div>
            </div>
        <?php endif; ?>
@@ -1764,18 +1764,18 @@
                            <!-- Selection actions (hidden by default) -->
                            <div class="selection-actions" hidden>
                                <button type="button" class="create-from-selection">
                                    <?= jvbIcon('add') ?>
                                    <?= jvbIcon('plus-square') ?>
                                    Create New <?= $singular ?>
                                </button>
                                <button type="button" class="remove-selection">
                                    <?= jvbIcon('delete') ?>
                                    <?= jvbIcon('trash') ?>
                                    Remove
                                </button>
                            </div>
                        </div>
                        <button type="button" class="submit-uploads">
                            <?= jvbIcon('upload') ?> Upload <?= esc_html($plural ?? 'Content'); ?>
                            <?= jvbIcon('cloud-arrow-up') ?> Upload <?= esc_html($plural ?? 'Content'); ?>
                        </button>
                    </div>
                    <?php endif; ?>
@@ -1789,7 +1789,7 @@
                    </div>
                    <?php if ($groupable) : ?>
                    <p class="hint"><?= jvbIcon('elbow-left-up') ?>  These will become individual <?= $plural ?>  <?= jvbIcon('elbow-right-up')?></p>
                    <p class="hint"><?= jvbIcon('arrow-elbow-left-up') ?>  These will become individual <?= $plural ?>  <?= jvbIcon('arrow-elbow-right-up')?></p>
                </div>
                <div class="sidebar">
                    <div class="header">
@@ -1798,7 +1798,7 @@
                        <p class="hint">Select multiple images and click "Add to <?= $singular?>" or create new <?= $plural ?>.</p>
                    </div>
                    <button type="button" class="create-group-from-selection">
                        <?= jvbIcon('add') ?>
                        <?= jvbIcon('plus-square') ?>
                        Create New <?= $singular ?>
                    </button>
                    <div class="item-grid groups">
@@ -1806,7 +1806,7 @@
                            <p>Drag here to create a new <?= $singular ?>!</p>
                        </div>
                    </div>
                    <p class="hint"><?= jvbIcon('elbow-left-up') ?>  Each group will become its own <?= $singular ?>  <?= jvbIcon('elbow-right-up')?></p>
                    <p class="hint"><?= jvbIcon('arrow-elbow-left-up') ?>  Each group will become its own <?= $singular ?>  <?= jvbIcon('arrow-elbow-right-up')?></p>
                </div>
            </div>
        <?php endif; ?>
@@ -1963,7 +1963,7 @@
                    <?= !empty($field['required']) ? 'required' : ''; ?>
                    data-format="<?= esc_attr($format); ?>"
                >
                <?= jvbIcon('event') ?>
                <?= jvbIcon('calendar') ?>
            </div>
             <?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
             <?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
@@ -2061,7 +2061,7 @@
                    <?= !empty($field['max']) ? 'max="' . esc_attr($field['max']) . '"' : ''; ?>
                    <?= !empty($field['step']) ? 'step="' . esc_attr($field['step']) . '"' : ''; ?>
                >
                <?= jvbIcon('event') ?>
                <?= jvbIcon('calendar') ?>
            </div>
            <?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); }  ?>
            <?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); }  ?>
@@ -2107,7 +2107,7 @@
    {
        $id = $name.'-help';
        $out = '<div class="has-tooltip">
        <span class="tt-toggle">'.jvbIcon('help').'</span>
        <span class="tt-toggle">'.jvbIcon('question').'</span>
        <div role="tooltip" id="'.$id.'"><p>'.$description.'</p></div>
        </div>';
        echo $out;
inc/meta/MetaManager.php
@@ -763,7 +763,7 @@
        }
        echo (jvbCheck('submit', $options)) ? '<button type="submit">'.jvbIcon('save').'Save</button>' : '';
        echo (jvbCheck('submit', $options)) ? '<button type="submit">'.jvbIcon('floppy-disk').'Save</button>' : '';
        echo '</form>';
        $out = ob_get_clean();
@@ -807,7 +807,7 @@
                $out = '<p class="'.$name.'">'.jvbIcon('calendar').'<span></span></p>';
                break;
            case 'time':
                $out = '<p class="'.$name.'">'.jvbIcon('time').'<time></time></p>';
                $out = '<p class="'.$name.'">'.jvbIcon('clock').'<time></time></p>';
                break;
            case 'true_false':
                $out = '<p class="'.$name.'"></p>';
inc/meta/MetaRenderer.php
@@ -59,7 +59,7 @@
        $link = 'mailto:'.$value.'?subject='.rawurlencode('Contact from edmonton.ink').'&body='.rawurlencode('Hey,
        I found you on edmonton.ink, and I wanted to reach out!');
        return '<a href="'.$link.'" title="Send an Email">'.jvbIcon('email').'<span>Email</span></a>';
        return '<a href="'.$link.'" title="Send an Email">'.jvbIcon('envelope').'<span>Email</span></a>';
    }
    protected function renderUrlField(string $name, string $value, array $field):string
@@ -422,7 +422,7 @@
        // Add icon if configured
        $show_icon = $field['show_icon'] ?? true;
        $icon_html = $show_icon ? jvbIcon('time') : '';
        $icon_html = $show_icon ? jvbIcon('clock') : '';
        return sprintf(
            '<div class="time-field-display">%s<span class="time-value">%s</span></div>',
@@ -458,7 +458,7 @@
        // Add icon if configured
        $show_icon = $field['show_icon'] ?? true;
        $icon_html = $show_icon ? jvbIcon('event') : '';
        $icon_html = $show_icon ? jvbIcon('calendar') : '';
        return sprintf(
            '<div class="datetime-field-display">%s<span class="datetime-value">%s</span></div>',
@@ -484,7 +484,7 @@
        // Add icon if configured
        $show_icon = $field['show_icon'] ?? true;
        $icon_html = $show_icon ? jvbIcon('event') : '';
        $icon_html = $show_icon ? jvbIcon('calendar') : '';
        return sprintf(
            '<div class="date-field-display">%s<span class="date-value">%s</span></div>',
inc/meta/_setup.php
@@ -1,6 +1,4 @@
<?php
if (!empty(JVB_CONTENT) || !empty(JVB_TAXONOMY) || !empty(JVB_USER)) {
    require(JVB_DIR . '/inc/meta/MetaTypeManager.php');
    require(JVB_DIR . '/inc/meta/MetaManager.php');
    require(JVB_DIR . '/inc/meta/MetaRegistry.php');
@@ -8,4 +6,3 @@
    require(JVB_DIR . '/inc/meta/MetaValidator.php');
    require(JVB_DIR . '/inc/meta/MetaRenderer.php');
    require(JVB_DIR . '/inc/meta/MetaForm.php');
}
inc/registry/CheckCustomTables.php
@@ -128,9 +128,9 @@
            }
            try {
                if (array_key_exists('dashboard', $this->JVB_SITE) && $this->JVB_SITE['dashboard']) {
//              if (array_key_exists('dashboard', $this->JVB_SITE) && $this->JVB_SITE['dashboard']) {
                    $tables = array_merge($tables, $this->queueTables(), $this->errorLogTables());
                }
//              }
            } catch (Exception $e) {
                error_log("JVB: Error in dashboard section: " . $e->getMessage());
            }
@@ -144,7 +144,7 @@
            }
            // RE-ENABLE favourites tables
            try {
                if ($this->JVB_SITE['favourites']) {
                if (array_key_exists('favourites', $this->JVB_SITE)) {
                    error_log('JVB: Creating favourites tables...');
                    $favouriteTables = $this->favouriteTables();
                    error_log('JVB: Favourites tables created: ' . count($favouriteTables));
inc/rest/RestRouteManager.php
@@ -114,6 +114,19 @@
        return true;
    }
    public function checkRateLimit(WP_REST_Request $request):bool|WP_Error
    {
        if (!$this->rate_limiter->checkLimit($request)) {
            error_log('Rate Limit Reached');
            return new WP_Error(
                'rate_limit',
                'Too many attempts. Please wait a moment before trying again.',
                ['status' => 429]
            );
        }
        return true;
    }
    protected function checkContent(string $content, bool $bool = false):string|bool
    {
        $result = JVB_CONTENT[$content]??JVB_TAXONOMY[$content]??JVB_USER[$content]??'';
@@ -520,6 +533,379 @@
        return $this->checkHeaders($request, $types, ['user_id' => $user_id]);
    }
    /**
     * Helper to return error response
     */
    protected function error(string $message, string $code, int $status = 400, ?string $field = null): WP_REST_Response
    {
        $data = [
            'message' => $message,
            'code' => $code
        ];
        if ($field) {
            $data['field'] = $field;
        }
        return new WP_REST_Response($data, $status);
    }
    /**
     * Helper to return success response
     */
    protected function success(array $data, int $status = 200): WP_REST_Response
    {
        return new WP_REST_Response($data, $status);
    }
    /************************************************************
        SESSION FINGERPRINT
     ************************************************************/
    /**
     * Store session fingerprint for hijacking detection
     */
    protected function storeSessionFingerprint(int $user_id, WP_REST_Request $request): void
    {
        if (!defined('JVB_SESSION_FINGERPRINT') || !JVB_SESSION_FINGERPRINT) {
            return;
        }
        $fingerprint = $this->generateSessionFingerprint($request);
        update_user_meta($user_id, BASE . 'session_fingerprint', $fingerprint);
        update_user_meta($user_id, BASE . 'session_timestamp', time());
    }
    /**
     * Generate session fingerprint for hijacking detection
     *
     * @param WP_REST_Request $request The REST request
     * @return string Hashed fingerprint
     */
    protected function generateSessionFingerprint(WP_REST_Request $request): string
    {
        return hash('sha256', implode('|', [
            $request->get_header('User-Agent') ?? '',
            // Use IP class instead of full IP to allow for mobile network changes
            $this->getIPClass(
                $request->get_header('X-Forwarded-For')
                    ?: $request->get_header('X-Real-IP')
                    ?: $_SERVER['REMOTE_ADDR'] ?? ''
            )
        ]));
    }
    /**
     * Get IP class (first 3 octets) for session validation
     * This allows for minor IP changes (common with mobile networks)
     *
     * @param string $ip IP address
     * @return string First 3 octets
     */
    protected function getIPClass(string $ip): string
    {
        $parts = explode('.', $ip);
        return implode('.', array_slice($parts, 0, 3));
    }
    /**
     * Validate session fingerprint against stored value
     *
     * @param int $user_id User ID to validate
     * @param WP_REST_Request $request Current request
     * @return bool True if valid, false if potential hijacking
     */
    protected function validateSessionFingerprint(int $user_id, WP_REST_Request $request): bool
    {
        // Only enforce if enabled in config
        if (!defined('JVB_SESSION_FINGERPRINT') || !JVB_SESSION_FINGERPRINT) {
            return true;
        }
        $stored = get_user_meta($user_id, BASE . 'session_fingerprint', true);
        $current = $this->generateSessionFingerprint($request);
        if (empty($stored)) {
            // First request - store fingerprint
            update_user_meta($user_id, BASE . 'session_fingerprint', $current);
            update_user_meta($user_id, BASE . 'session_timestamp', time());
            return true;
        }
        // Compare using timing-safe comparison
        return hash_equals($stored, $current);
    }
    /**
     * Clear session fingerprint (call on logout)
     *
     * @param int $user_id User ID
     * @return void
     */
    protected function clearSessionFingerprint(int $user_id): void
    {
        delete_user_meta($user_id, BASE . 'session_fingerprint');
        delete_user_meta($user_id, BASE . 'session_timestamp');
    }
    /******************************************************************
     *  CSRF PROTECTION
    ******************************************************************/
    /**
     * Generate CSRF token for a user
     *
     * @param int $user_id User ID
     * @return string CSRF token
     */
    protected function generateCSRFToken(int $user_id): string
    {
        $token = wp_generate_password(32, false);
        set_transient(BASE . 'csrf_' . $user_id, $token, HOUR_IN_SECONDS);
        return $token;
    }
    /**
     * Validate CSRF token from request header
     *
     * @param WP_REST_Request $request The REST request
     * @return bool True if valid or not required
     */
    protected function validateCSRFToken(WP_REST_Request $request): bool
    {
        // Only for authenticated requests
        if (!is_user_logged_in()) {
            return true;
        }
        // Only for state-changing operations
        if (in_array($request->get_method(), ['GET', 'HEAD', 'OPTIONS'])) {
            return true;
        }
        $user_id = get_current_user_id();
        $token = $request->get_header('X-CSRF-Token');
        $stored = get_transient(BASE . 'csrf_' . $user_id);
        if (empty($stored) || empty($token)) {
            return false;
        }
        return hash_equals($stored, $token);
    }
    /**
     * Get current CSRF token for user (for frontend to use)
     *
     * @param int $user_id User ID
     * @return string|null CSRF token or null
     */
    protected function getCSRFToken(int $user_id): ?string
    {
        $token = get_transient(BASE . 'csrf_' . $user_id);
        if (!$token) {
            $token = $this->generateCSRFToken($user_id);
        }
        return $token;
    }
    /***********************************************************
     * REQUEST SIGNATURE VERIFICATION
    ***********************************************************/
    /**
     * Verify request signature for sensitive operations
     * Adds an additional layer of security for critical endpoints
     *
     * @param WP_REST_Request $request The REST request
     * @param array $sensitive_params Params to include in signature
     * @return bool True if valid signature
     */
    protected function verifyRequestSignature(WP_REST_Request $request, array $sensitive_params): bool
    {
        $signature = $request->get_header('X-Request-Signature');
        if (empty($signature)) {
            return false;
        }
        // Get user's signing key (rotated on each login)
        $user_id = get_current_user_id();
        if (!$user_id) {
            return false;
        }
        $signing_key = get_user_meta($user_id, BASE . 'signing_key', true);
        if (empty($signing_key)) {
            // Generate new key if missing
            $signing_key = wp_generate_password(64, false);
            update_user_meta($user_id, BASE . 'signing_key', $signing_key);
        }
        // Build signature from sensitive params
        ksort($sensitive_params);
        $message = json_encode($sensitive_params);
        $expected = hash_hmac('sha256', $message, $signing_key);
        return hash_equals($expected, $signature);
    }
    /**
     * Rotate signing key (call on login)
     *
     * @param int $user_id User ID
     * @return string New signing key
     */
    protected function rotateSigningKey(int $user_id): string
    {
        $new_key = wp_generate_password(64, false);
        update_user_meta($user_id, BASE . 'signing_key', $new_key);
        return $new_key;
    }
    /**********************************************************
     * AUDIT LOGGING
    **********************************************************/
    /**
     * Log security-relevant events
     *
     * @param string $event Event name
     * @param array $data Additional event data
     * @return void
     */
    protected function auditLog(string $event, array $data = []): void
    {
        // Add standard context
        $context = array_merge($data, [
            'timestamp' => current_time('mysql'),
            'user_id' => get_current_user_id() ?: 0,
            'ip' => $_SERVER['REMOTE_ADDR'] ?? '',
            'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
        ]);
        // Use existing error handler
        try {
            JVB()->error()->log(
                'security_audit',
                $event,
                $context,
                'info'
            );
        } catch (\Exception $e) {
            // Fallback to error_log if handler fails
            error_log("Security Audit: {$event} - " . json_encode($context));
        }
    }
    /***************************************************************
     * SANITIZATION HELPERS
    ***************************************************************/
    /**
     * Sanitize array of IDs
     *
     * @param array $ids Array of IDs
     * @return array Sanitized array of integers
     */
    protected function sanitizeIDs(array $ids): array
    {
        return array_map('absint', array_filter($ids, function($id) {
            return is_numeric($id) && $id > 0;
        }));
    }
    /***************************************************************
     * RESPONSE HELPERS
    ***************************************************************/
    /**
     * Return validation error response
     *
     * @param array $errors Validation errors (field => message)
     * @return WP_REST_Response 422 response with errors
     */
    protected function validationError(array $errors): WP_REST_Response
    {
        return new WP_REST_Response([
            'success' => false,
            'message' => 'Validation failed',
            'errors' => $errors
        ], 422);
    }
    /**
     * Return unauthorized error response
     *
     * @param string $message Error message
     * @return WP_REST_Response 401 response
     */
    protected function unauthorized(string $message = 'Unauthorized'): WP_REST_Response
    {
        return new WP_REST_Response([
            'success' => false,
            'message' => $message
        ], 401);
    }
    /**
     * Return forbidden error response
     *
     * @param string $message Error message
     * @return WP_REST_Response 403 response
     */
    protected function forbidden(string $message = 'Forbidden'): WP_REST_Response
    {
        return new WP_REST_Response([
            'success' => false,
            'message' => $message
        ], 403);
    }
    /**
     * Return not found error response
     *
     * @param string $message Error message
     * @return WP_REST_Response 404 response
     */
    protected function notFound(string $message = 'Not found'): WP_REST_Response
    {
        return new WP_REST_Response([
            'success' => false,
            'message' => $message
        ], 404);
    }
    /*****************************************************************
     * CONCURRENT CHECKS
    *****************************************************************/
    /**
     * Prevent concurrent requests for the same operation
     * Useful for preventing double-submissions
     *
     * @param string $operation_key Unique key for this operation
     * @param int $lock_duration Lock duration in seconds
     * @return bool True if lock acquired, false if already locked
     */
    protected function acquireOperationLock(string $operation_key, int $lock_duration = 5): bool
    {
        $lock_key = 'op_lock_' . md5($operation_key);
        // Try to acquire lock
        $locked = get_transient($lock_key);
        if ($locked) {
            return false; // Already locked
        }
        // Set lock
        set_transient($lock_key, true, $lock_duration);
        return true;
    }
    /**
     * Release operation lock
     *
     * @param string $operation_key Unique key for this operation
     * @return void
     */
    protected function releaseOperationLock(string $operation_key): void
    {
        $lock_key = 'op_lock_' . md5($operation_key);
        delete_transient($lock_key);
    }
}
//
//Simple example:
@@ -605,3 +991,37 @@
//  $response = new WP_REST_Response($data);
//  return $this->addCacheHeaders($response);
//}
/**
 * Use operation lock in your methods like this:
 *
 * public function updateContent(WP_REST_Request $request): WP_REST_Response
 * {
 *     $user_id = get_current_user_id();
 *     $content_id = $request->get_param('content_id');
 *
 *     // Prevent concurrent updates
 *     $lock_key = "update_{$user_id}_{$content_id}";
 *     if (!$this->acquireOperationLock($lock_key)) {
 *         return $this->error(
 *             'An update is already in progress. Please wait.',
 *             'concurrent_operation',
 *             409
 *         );
 *     }
 *
 *     try {
 *         // Do your operation
 *         $result = $this->doUpdate($content_id);
 *
 *         $this->releaseOperationLock($lock_key);
 *         return $this->success($result);
 *
 *     } catch (\Exception $e) {
 *         $this->releaseOperationLock($lock_key);
 *         return $this->error($e->getMessage(), 'operation_failed');
 *     }
 * }
 */
inc/rest/_setup.php
@@ -23,14 +23,14 @@
    require(JVB_DIR . '/inc/rest/routes/TermRoutes.php');
}
require(JVB_DIR . '/inc/rest/routes/QueueRoutes.php');
require(JVB_DIR . '/inc/rest/routes/ErrorRoutes.php');
require(JVB_DIR . '/inc/rest/routes/UploadRoutes.php');
require(JVB_DIR . '/inc/rest/routes/SettingsRoutes.php');
if (Features::forSite()->has('dashboard')) {
    require(JVB_DIR . '/inc/rest/routes/QueueRoutes.php');
    require(JVB_DIR . '/inc/rest/routes/ErrorRoutes.php');
    require(JVB_DIR . '/inc/rest/routes/AdminRoutes.php');
    require(JVB_DIR . '/inc/rest/routes/ContentRoutes.php');
    require(JVB_DIR . '/inc/rest/routes/UploadRoutes.php');
    require(JVB_DIR . '/inc/rest/routes/BioRoutes.php');
    require(JVB_DIR . '/inc/rest/routes/SettingsRoutes.php');
    require(JVB_DIR . '/inc/rest/routes/ShopRoutes.php');
}
if (Features::forMembership()->has('forum')) {
@@ -61,3 +61,4 @@
require(JVB_DIR .'/inc/rest/routes/OptionsRoutes.php');
require(JVB_DIR .'/inc/rest/routes/FormRoutes.php');
require(JVB_DIR .'/inc/rest/routes/IntegrationsRoutes.php');
require(JVB_DIR .'/inc/rest/routes/LoginRoutes.php');
inc/rest/routes/ApprovalRoutes.php
@@ -5,6 +5,7 @@
use JVBase\JVB;
use JVBase\rest\RestRouteManager;
use JVBase\managers\CacheManager;
use JVBase\utility\Features;
use WP_User;
use WP_REST_Request;
use WP_REST_Response;
@@ -23,15 +24,17 @@
    protected array $voteTables;
    protected int $expiryDays = 7;
    protected bool $hasMemberApproval = false;
    public function __construct()
    {
        $this->cache_name = 'approvals';
        $this->hasMemberApproval = Features::forMembership()->has('member_verified');
        parent::__construct();
        $this->initTypes();
        if (jvbSiteHasMemberApproval()) {
        if ($this->hasMemberApproval) {
            add_action('user_register', [$this, 'handleNewUserRegistration'], 10, 2);
        }
@@ -43,7 +46,7 @@
        $approvals = jvbApprovalTypes();
        $this->userTypes = [];
        $this->termTypes = [];
        if (jvbSiteHasMemberApproval()) {
        if ($this->hasMemberApproval) {
            $this->userTypes = array_filter(
                array_keys($approvals),
                function ($item) {
inc/rest/routes/ContentRoutes.php
@@ -25,14 +25,18 @@
    protected string $post_type = '';
    protected string $user_id = '';
    //For Timeline-specific posts
    protected array $timelineSharedFields = [];
    protected array $timelineUniqueFields = [];
    //TODO: Ensure we are handling the bulk operations for all processes
    //TODO: be sure to clear cache ($this->>cache->invalidateGroup($this->>cache_name)) on content update/create
    //TODO: Also invalidate feed caches on updates!!
    public function __construct()
    {
        $this->cache_name = 'user_content_'.get_current_user_id();
        parent::__construct();
        $this->action = 'dash-';
        $this->operation_type = 'content_update';
        add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
@@ -75,6 +79,32 @@
        ]);
    }
    protected function initTimelineFields(string $content):void
    {
        $content = jvbNoBase($content);
        if (!Features::forContent($content)->has('is_timeline')){
            return;
        }
        $config = Features::getConfig($content);
        $this->fields = $config['fields'];
        $this->timelineSharedFields = array_keys(array_filter($this->fields, function ($field) {
            if (!array_key_exists('for_all', $field) || $field['for_all'] === false){
                return true;
            }
            return false;
        }));
        array_unshift($this->timelineSharedFields, 'post_thumbnail');
        array_unshift($this->timelineSharedFields, 'post_title');
        $this->timelineUniqueFields = array_keys(array_filter($this->fields, function ($field) {
            if (array_key_exists('for_all', $field) && $field['for_all'] === true) {
                return true;
            }
            return false;
        }));
    }
    /**
     * Handle content update/creation
     * @param WP_REST_Request $request
@@ -187,9 +217,6 @@
        }
        $post_type = str_replace('-', '_',jvbCheckBase($params['content']));
        $config = Features::getConfig($params['content']);
        // Build query args
        $args = [
@@ -396,14 +423,12 @@
            }
            CacheManager::invalidateGroup($post_data['content']);
            CacheManager::for($post_data['content'])->clear();
            if (jvbSiteUsesFeedBlock()) {
                CacheManager::invalidateGroup($post_data['feed']);
                CacheManager::for('feed')->clear();
            }
        }
        CacheManager::invalidateGroup('user_content');
        if (jvbSiteHasNotifications()) {
            $this->notifications = JVB()->notification();
            $this->notifications->addNotification(
@@ -433,83 +458,109 @@
            return ['success' => false, 'message' => 'No permission'];
        }
        $rows = $post_data['fields'] ?? [];
        if (empty($rows)) {
            return ['success' => false, 'message' => 'No data'];
        }
        $this->fields = jvbGetFields($post_data['content']);
        $this->initTimelineFields($post_data['content']);
        error_log('Received Data: '.print_r($post_data, true));
        $fields = jvbGetFields($post_data['content']);
        // First row = parent post
        $parent_row = array_shift($rows);
        if (($parent_row['id'] ?? null) != $parent_id) {
            return ['success' => false, 'message' => 'Parent ID mismatch'];
        }
        $allowedFields = array_filter($parent_row, function($key) use ($fields) {
            return array_key_exists($key, $fields);
        // First, process the main fields that will apply to all posts
        $sharedData = array_filter($post_data, function ($key) {
            return in_array($key, $this->timelineSharedFields);
        }, ARRAY_FILTER_USE_KEY);
        $parentMeta = new MetaManager($parent_id, 'post');
        $parentMeta->setAll($allowedFields);
        //Next, process any individual posts, including any menu order changes
        if (array_key_exists('timeline', $post_data) && is_array($post_data['timeline'])) {
            $sharedTaxonomies = $sharedData;
            unset($sharedTaxonomies['post_title']);
        // Get existing children to track deletions
        $existing_children = get_children([
            'post_parent' => $parent_id,
            'post_type' => jvbCheckBase($post_data['content']),
            'fields' => 'ids'
        ]);
        $processed_ids = [];
        // Process remaining rows as children
        foreach ($rows as $index => $row_data) {
            $row_id = $row_data['id'] ?? null;
            // New child post
            if (!$row_id || str_starts_with($row_id, 'new')) {
                $child_id = wp_insert_post([
                    'post_type' => jvbCheckBase($post_data['content']),
                    'post_parent' => $parent_id,
                    'post_author' => $this->user_id,
                    'post_status' => $post_data['status'] ?? 'draft',
                    'menu_order' => $index
                ]);
            //Ensure the parent post exists and is still first in the array
            $index = array_search((string)$parent_id, array_column($post_data['timeline'], 'id'));
            error_log('Found index: '.print_r($index, true));
            if ($index === false) {
                return [
                    'success' => false,
                    'message'   => 'Missing parent id. This should not have happened'
                ];
            } elseif ($index !== 0) {
                // Move that element to the start of the array
                $item = $post_data['timeline'][$index];
                unset($post_data['timeline'][$index]);
                array_unshift($post_data['timeline'], $item);
            }
            // Existing child post
            else {
                $child_id = (int) $row_id;
            $errors = [];
            $success = [];
            // Get existing children to track deletions
            $existing_children = get_children([
                'post_parent' => $parent_id,
                'post_type' => jvbCheckBase($post_data['content']),
                'fields' => 'ids'
            ]);
            //Iterate through the timeline posts
            foreach($post_data['timeline'] as $order => $timeline) {
                $allowedFields = array_filter($timeline, function($key) {
                    return in_array($key, $this->timelineUniqueFields);
                }, ARRAY_FILTER_USE_KEY);
                // Verify ownership via parent
                if (!in_array($child_id, $existing_children)) {
                    continue; // Skip if not actually a child of this parent
                $allowedFields['post_title'] = $allowedFields['post_title'] ?? $sharedData['post_title'].' - Treatment #'.$order;
                $allowedFields = array_merge($allowedFields, $sharedTaxonomies);
                if (!array_key_exists('id', $timeline) || !is_numeric($timeline['id'])) {
                    $newChild = wp_insert_post([
                        'post_author'   => $this->user_id,
                        'post_type'     => jvbCheckBase($post_data['content']),
                        'post_title'    => $allowedFields['post_title'],
                        'post_parent'   => $parent_id,
                        'menu_order'    => $order
                    ]);
                    if (!$newChild || is_wp_error($newChild)) {
                        $errors[] = [
                            'message'   => 'Could not create child post',
                            'data'      => $timeline
                        ];
                        continue;
                    }
                    $timeline['id'] = $newChild;
                }
                // Update menu_order (position may have changed)
                wp_update_post([
                    'ID' => $child_id,
                    'menu_order' => $index
                ]);
                if (in_array((int)$timeline['id'], $existing_children)) {
                    unset($existing_children[array_search((int)$timeline['id'], $existing_children)]);
                }
                //Determine which fields to update
                $meta = new MetaManager($timeline['id'], 'post');
                $oldValues = $meta->getAll(array_keys($allowedFields));
                $updateValues = array_filter($allowedFields, function($value, $key) use ($oldValues) {
                    return (!array_key_exists($key, $oldValues) || $value !== $oldValues[$key]);
                }, ARRAY_FILTER_USE_BOTH);
                $meta->setAll($updateValues);
                //Update Menu Order, if applicable
                if ((int) $timeline['id'] !== $parent_id) {
                    $post = get_post((int) $timeline['id']);
                    if ($post && $post->menu_order !== $order) {
                        $updated = wp_update_post([
                            'ID'            => $post->ID,
                            'menu_order'    => $order,
                        ]);
                        if (!$updated || is_wp_error($updated)) {
                            $errors[] = [
                                'message'   => 'Could not update timeline order for post',
                                'data'      => $timeline
                            ];
                        }
                    }
                }
                $success[] = $timeline['id'];
            }
            // Update child meta
            $allowedChildFields = array_filter($row_data, function($key) use ($fields) {
                return array_key_exists($key, $fields) && $key !== 'id' && $key !== 'draggable';
            }, ARRAY_FILTER_USE_KEY);
            $childMeta = new MetaManager($child_id, 'post');
            $childMeta->setAll($allowedChildFields);
            $processed_ids[] = $child_id;
        }
        //Delete any remaining children that no longer exist
        if (!empty($existing_children)) {
            foreach ($existing_children as $ID) {
                wp_delete_post($ID);
            }
        }
        // Delete removed children
        $deleted_ids = array_diff($existing_children, $processed_ids);
        foreach ($deleted_ids as $delete_id) {
            wp_delete_post($delete_id, true);
        }
        return ['success' => true, 'processed' => $processed_ids];
        return ['success' => true, 'data' => [
            'success'   => $success,
            'errors'    => $errors
        ]];
    }
    /**
@@ -590,9 +641,10 @@
     *
     * @return array
     */
    protected function prepareItem(WP_Post $post, $skip = false):array
    protected function prepareItem(WP_Post $post, bool $skip = false, bool $fields = true):array
    {
        if (!$skip && Features::forContent($post->post_type)->has('is_timeline')) {
            $this->initTimelineFields($post->post_type);
            return $this->formatTimeline($post);
        }
        $this->meta = new MetaManager($post->ID, 'post');
@@ -605,7 +657,7 @@
            'alt'       => get_post_meta(get_post_thumbnail_id(), '_wp_attachment_image_alt', true),
            'icon'      => $this->post_type,
            'taxonomies'=> [],
            'fields'    => $this->meta->getAll(),
            'fields'    => ($fields) ? $this->meta->getAll() : [],
            'images'    => [],
        ];
@@ -633,12 +685,13 @@
        return $data;
    }
    protected function extractImages():array
    protected function extractImages(array $fields = []):array
    {
        //Extract images
        $images = [];
        $get = [];
        foreach ($this->fields as $field => $config) {
        $fields = (empty($fields)) ? $this->fields : $fields;
        foreach ($fields as $field => $config) {
            if ($config['type'] === 'gallery' || $config['type'] === 'image' || $field === 'post_thumbnail') {
                $get[] = $field;
            }
@@ -660,36 +713,29 @@
    protected function formatTimeline(WP_Post $post):array
    {
        $data = $this->prepareItem($post, true);
        $firstRow = $data['fields'];
        $firstRow['id'] = $post->ID;
        $firstRow['draggable'] = false;
        $fields = [$firstRow];
        $item = $this->prepareItem($post, true, false);
        //Step 1: Get the fields that apply to all posts
        $mainMeta = new MetaManager($post->ID, 'post');
        $item['fields'] = $mainMeta->getAll($this->timelineSharedFields);
        $children = get_children(['post_parent' => $post->ID, 'orderby' => 'menu_order']);
        $allImages = [];
        //Step 2: Get the fields for each individual posts
        $children = get_children(['post_parent' => $post->ID, 'orderby' => 'menu_order', 'post_status' => ['publish', 'draft'], 'fields'=> 'ids']);
        array_unshift($children, $post->ID);
        $subFields = [];
        $images = [];
        foreach ($children as $child) {
            $this->meta = new MetaManager($child->ID, 'post');
            $row = $this->meta->getAll();  // Store in variable first
            $row['id'] = $child->ID;       // Add ID to the row
            $row['draggable'] = true;      // Mark as draggable
            $fields[] = $row;              // Then append to fields
            $meta = new MetaManager($child, 'post');
            $f = $meta->getAll($this->timelineUniqueFields);
            $f =  ['id' => $child] + $f;
            $subFields[$f['post_thumbnail']] = $f;
            $images = $this->extractImages();
            if (!empty($images)) {
                $allImages = $allImages + $images;
            }
            $images[$f['post_thumbnail']] = jvbImageData((int) $f['post_thumbnail']);
        }
        $item['fields']['timeline'] = $subFields;
        $item['images'] = $item['images'] + $images;
        if (!empty($allImages)) {
            if (!array_key_exists('images', $data)) {
                $data['images'] = [];
            }
            $data['images'] = $data['images'] + $allImages;
        }
        $data['fields']['timeline'] = $fields;
        return $data;
        return $item;
    }
    /**
@@ -839,9 +885,8 @@
                    }
                    //Clear cache
                    CacheManager::invalidateGroup($data['content']);
                    CacheManager::invalidateGroup('feed');
                    CacheManager::invalidateGroup('user_content');
                    CacheManager::for($data['content'])->clear();
                    CacheManager::for('feed')->clear();
                }
                return [
inc/rest/routes/FavouritesRoutes.php
@@ -33,6 +33,7 @@
        add_action('before_delete_post', [$this, 'cleanupPostFavourites']);
        add_action('delete_term', [$this, 'cleanupTermFavourites'], 10, 3);
        add_action('jvbUserRegistered', [$this, 'maybeAcceptListInvite'], 10, 3);
        // Register cleanup scheduler
        add_action('jvb_cleanupOrphanedFavourites', [$this, 'cleanupOrphanedFavourites']);
@@ -3110,6 +3111,13 @@
        }
    }
    public function maybeAcceptListInvite(int $user_id, string $email, array $data):void
    {
        if (array_key_exists('list_token', $data) && !empty($data['list_token'])) {
            $this->acceptListInvitation($data['list_token'], $email);
        }
    }
    /**
     * Get the owner ID for a content item
     *
inc/rest/routes/Invitations.php
@@ -48,20 +48,8 @@
        // Add hooks for processing accepted invitations
        add_action('user_register', [$this, 'checkInvitation'], 10, 1);
        add_action('jvbLoginManagerInit', function($loginManager) {
            $loginManager->registerTokenHandler('invite', function($token, $email, $user_id) {
                JVB()->routes('invites')->acceptInvitation($token, $email, $user_id);
            });
            $loginManager->registerMessageHandler('invitation',
                function() {
                    return '<h2>You\'ve been invited!</h2><p>Create your account to accept.</p>';
                },
                function() {
                    return isset($_GET['invite']);
                }
            );
        });
        add_filter('jvbLoginLabels', [$this, 'modifyLoginLabels'], 10, 2);
@@ -1361,4 +1349,47 @@
            ];
        }
    }
    public function modifyLoginLabels(array $labels, array $get_params): array
    {
        // Only modify if invitation params present
        if (!array_key_exists('invite', $get_params) || !array_key_exists('email', $get_params)) {
            return $labels;
        }
        $email = sanitize_email($get_params['email']);
        $token = sanitize_text_field($get_params['invite']);
        $user = email_exists($email);
        if (!$user) {
            return $labels;
        }
        $role = jvbUserRole($user);
        // Get invitation data
        $data = $this->verifyInvitation(
            $token,
            $email,
            $role,
        );
        if (!$data) {
            return $labels;
        }
        // Build custom message
        $inviters = json_decode($data->inviters, true);
        $name = $data->name;
        $names = array_map(function($inviter) {
            $artist = jvbContentFromUser((int)$inviter['user_id']);
            return $artist['name'] ?: $artist['display_name'];
        }, $inviters);
        $message = count($names) > 1
            ? 'are already here, and have invited you to join in!'
            : ' is already here, and invited you to join in!';
        // Modify labels
        $labels['title'] = 'Join the Scene, ' . $data->name;
        $labels['description'] = [jvbCommaList($names) . ' ' . $message];
        return $labels;
    }
}
inc/rest/routes/LoginRoutes.php
New file
@@ -0,0 +1,667 @@
<?php
namespace JVBase\rest\routes;
use JVBase\managers\EmailManager;
use JVBase\managers\LoginManager;
use JVBase\managers\MagicLinkManager;
use JVBase\rest\RestRouteManager;
use JVBase\utility\Features;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
use WP_User;
if (!defined('ABSPATH')) {
    exit;
}
class LoginRoutes extends RestRouteManager
{
    protected EmailManager $emailManager;
    protected MagicLinkManager $magic_link;
    protected LoginManager $loginManager;
    protected ?string $requestId = null;
    public function __construct()
    {
        $this->cache_name = 'auth';
        $this->cache_ttl = WEEK_IN_SECONDS;
        $this->emailManager = new EmailManager();
        if (Features::forSite()->has('magicLink')) {
            $this->magic_link = new MagicLinkManager();
        }
        parent::__construct();
    }
    public function registerRoutes(): void
    {
        // Login endpoint
        register_rest_route($this->namespace, '/auth/login', [
            'methods' => 'POST',
            'callback' => [$this, 'handleLogin'],
            'permission_callback' => [$this, 'checkRateLimit'],
            'args' => [
                'user_email' => [
                    'required' => true,
                    'type' => 'string',
                    'sanitize_callback' => 'sanitize_email'
                ],
                'user_password' => [
                    'required' => true,
                    'type' => 'string'
                ],
                'remember_me' => [
                    'required' => false,
                    'type' => 'boolean',
                    'default' => false
                ]
            ]
        ]);
        // Logout endpoint
        register_rest_route($this->namespace, '/auth/logout', [
            'methods' => 'POST',
            'callback' => [$this, 'handleLogout'],
            'permission_callback' => 'is_user_logged_in'
        ]);
        // Check auth status
        register_rest_route($this->namespace, '/auth/status', [
            'methods' => 'GET',
            'callback' => [$this, 'getAuthStatus'],
            'permission_callback' => '__return_true'
        ]);
        // Request password reset
        register_rest_route($this->namespace, '/auth/lostpassword', [
            'methods' => 'POST',
            'callback' => [$this, 'requestPasswordReset'],
            'permission_callback' => [$this, 'checkRateLimit'],
            'args' => [
                'user_email' => [
                    'required' => true,
                    'type' => 'string',
                    'format' => 'email',
                    'sanitize_callback' => 'sanitize_email'
                ]
            ]
        ]);
        // Complete password reset (with token)
        register_rest_route($this->namespace, '/auth/reset-password', [
            'methods' => 'POST',
            'callback' => [$this, 'resetPassword'],
            'permission_callback' => [$this, 'checkRateLimit'],
            'args' => [
                'token' => [
                    'required' => true,
                    'type' => 'string',
                    'sanitize_callback' => 'sanitize_text_field'
                ],
                'user_email' => [
                    'required' => true,
                    'type' => 'string',
                    'format' => 'email',
                    'sanitize_callback' => 'sanitize_email'
                ],
                'password' => [
                    'required' => true,
                    'type' => 'string'
                ]
            ]
        ]);
        register_rest_route($this->namespace, '/auth/register', [
            'method'    => 'POST',
            'callback'  => [$this, 'handleRegister'],
            'permission_callback' => [$this, 'checkRateLimit'],
        ]);
        // Refresh session
        register_rest_route($this->namespace, '/auth/refresh', [
            'methods' => 'POST',
            'callback' => [$this, 'refreshSession'],
            'permission_callback' => 'is_user_logged_in'
        ]);
        register_rest_route($this->namespace, '/auth/email', [
            'methods'   => 'POST',
            'callback'  => [$this, 'checkEmailExists'],
            'permission_callback' => [$this, 'checkRateLimit'],
        ]);
    }
    public function handleLogin(WP_REST_Request $request): WP_REST_Response
    {
        // Verify Turnstile
        if (!$this->verifyTurnstile($request->get_param('cf-turnstile-response') ?? '')) {
            return $this->error('Security verification failed', 'turnstile_failed', 403);
        }
        $username = $request->get_param('user_email');
        $password = $request->get_param('user_password');
        $remember = (bool)$request->get_param('remember_me');
        // Check for account lockout
        $lockout = $this->checkAccountLockout($username);
        if (is_wp_error($lockout)) {
            return $this->error(
                $lockout->get_error_message(),
                'account_locked',
                429
            );
        }
        // Attempt login
        $user = wp_signon([
            'user_login'    => $username,
            'user_email'    => $username,
            'user_password' => $password,
            'remember' => $remember
        ], false);
        if (is_wp_error($user)) {
            // Track failed attempt
            $this->trackFailedLogin($username);
            return $this->error(
                'Invalid username or password',
                'login_failed',
                401
            );
        }
        // Clear failed attempts on success
        $this->clearFailedAttempts($username);
        // Set auth cookie with remember me flag
        wp_set_current_user($user->ID);
        wp_set_auth_cookie($user->ID, $remember);
        // Store session fingerprint for hijacking protection
        $this->storeSessionFingerprint($user->ID, $request);
        // Trigger WordPress login action
        do_action('wp_login', $user->user_login, $user);
        // Audit log
        $this->auditLog('user_login', [
            'user_id' => $user->ID,
            'remember' => $remember
        ]);
        return $this->success([
            'message' => 'Login successful',
            'user' => $this->formatUserData($user),
            'redirect' => $this->getLoginRedirect($user)
        ]);
    }
    /**
     * Handle logout request
     */
    public function handleLogout(WP_REST_Request $request): WP_REST_Response
    {
        $user_id = get_current_user_id();
        // Clear session fingerprint
        $this->clearSessionFingerprint($user_id);
        // Audit log
        $this->auditLog('user_logout', ['user_id' => $user_id]);
        // WordPress logout
        wp_logout();
        return $this->success([
            'message' => 'Logged out successfully'
        ]);
    }
    /**
     * Get current authentication status
     */
    public function getAuthStatus(WP_REST_Request $request): WP_REST_Response
    {
        if (!is_user_logged_in()) {
            return $this->success([
                'authenticated' => false
            ]);
        }
        $user = wp_get_current_user();
        return $this->success([
            'authenticated' => true,
            'user' => $this->formatUserData($user)
        ]);
    }
    /**
     * Request password reset
     */
    public function requestPasswordReset(WP_REST_Request $request): WP_REST_Response
    {
        $email = $request->get_param('user_email')??'';
        $email = sanitize_email($email);
        $token = $request->get_param('cf-turnstile-response')??'';
        if (!$this->verifyTurnstile($token)){
            return $this->error('Security verification failed', 'turnstile_failed', 403);
        }
        // Use WordPress's built-in function
        $result = retrieve_password($email);
        // Log but don't expose
        if (is_wp_error($result)) {
            $this->auditLog('password_reset_failed', [
                'email' => $email,
                'reason' => $result->get_error_code()
            ]);
        } else {
            $this->auditLog('password_reset_sent', [
                'email' => $email
            ]);
        }
        return $this->success([
            'message' => 'If an account exists with this email, you will receive a password reset link.'
        ]);
    }
    /**
     * Complete password reset with token
     */
    public function resetPassword(WP_REST_Request $request): WP_REST_Response
    {
        $key = sanitize_text_field($request->get_param('key'));
        $login = sanitize_text_field($request->get_param('login'));
        $password = $request->get_param('password');
        if (empty($key) || empty($login)) {
            return $this->error('Invalid reset link', 'invalid_key', 400);
        }
        // Use WordPress's native key verification
        $user = check_password_reset_key($key, $login);
        if (is_wp_error($user)) {
            return $this->error(
                'Invalid or expired reset link',
                'invalid_token',
                400
            );
        }
        // Validate password strength
        $validation = $this->validatePassword($password);
        if (is_wp_error($validation)) {
            return $this->error(
                $validation->get_error_message(),
                'weak_password',
                400
            );
        }
        // Reset the password
        reset_password($user, $password);
        // Log them in
        wp_set_current_user($user->ID);
        wp_set_auth_cookie($user->ID, true);
        // Store session fingerprint
        $this->storeSessionFingerprint($user->ID, $request);
        // Audit log
        $this->auditLog('password_reset_complete', [
            'user_id' => $user->ID
        ]);
        return $this->success([
            'message' => 'Password reset successfully',
            'redirect' => home_url('/dash')
        ]);
    }
    /**
     * Refresh session (extends session duration)
     */
    public function refreshSession(WP_REST_Request $request): WP_REST_Response
    {
        $user_id = get_current_user_id();
        // Validate session fingerprint
        if (!$this->validateSessionFingerprint($user_id, $request)) {
            wp_logout();
            return $this->unauthorized('Session validation failed');
        }
        // Refresh auth cookie
        wp_set_auth_cookie($user_id, true);
        return $this->success([
            'message' => 'Session refreshed'
        ]);
    }
    public function handleRegister(WP_REST_Request $request): WP_REST_Response
    {
        $data = $request->get_json_params();
        // Duplicate submission check
        if (!$this->checkRequestId($data['request_id'] ?? '')) {
            return $this->error('Duplicate request detected', 'duplicate_request', 409);
        }
        // Verify Turnstile
        if (!$this->verifyTurnstile($data['cf-turnstile-response'] ?? '')) {
            return $this->error('Security verification failed', 'turnstile_failed', 403);
        }
        $name = sanitize_text_field($data['name'] ?? '');
        $email = sanitize_email($data['email'] ?? '');
        $user_type = sanitize_text_field($data['user_select'] ?? 'subscriber');
        // Validate fields
        if (empty($name)) {
            return $this->error('Name is required', 'missing_name', 400, 'name');
        }
        if (empty($email)) {
            return $this->error('Email is required', 'missing_email', 400, 'email');
        }
        // Spam prevention
        if ($user_type === 'subscriber' && count(JVB_USER) > 0) {
            $registerable = array_filter(JVB_USER, fn($config) => $config['can_register'] ?? false);
            if (!empty($registerable)) {
                return $this->error('Please select a valid account type', 'invalid_user_type', 400, 'user_select');
            }
        }
        // Check if role can register
        if ($user_type !== 'subscriber') {
            if (!isset(JVB_USER[$user_type]) || empty(JVB_USER[$user_type]['can_register'])) {
                return $this->error('Invalid account type', 'invalid_user_type', 400, 'user_select');
            }
        }
        // Check if email exists
        if (email_exists($email)) {
            return $this->error('Email already registered', 'duplicate_email', 400, 'email');
        }
        // Allow WP plugins to add registration errors
        $errors = new WP_Error();
        $errors = apply_filters('registration_errors', $errors, $email, $email);
        if ($errors->has_errors()) {
            return $this->error(
                $errors->get_error_message(),
                $errors->get_error_code(),
                400
            );
        }
        // Create user
        $user_id = wp_create_user($email, wp_generate_password(), $email);
        if (is_wp_error($user_id)) {
            return $this->error($user_id->get_error_message(), 'user_creation_failed', 500);
        }
        // Update user data
        wp_update_user([
            'ID' => $user_id,
            'display_name' => $name,
            'first_name' => strtok($name,' ')
        ]);
        // Set role
        $user = new WP_User($user_id);
        if ($user_type === 'subscriber') {
            $user->set_role('subscriber');
        } else {
            $role = JVB_USER[$user_type]['role'] ?? 'subscriber';
            $user->set_role($role);
            // Check if needs approval
            if (Features::forMembership()->has('memberVerified') &&
                in_array($role, JVB_MEMBERSHIP['memberVerified'] ?? [])) {
                $user->add_cap('skip_moderation', false);
                update_user_meta($user_id, BASE . 'pending_approval', true);
            }
        }
        if (Features::forUser($user_type)->has('namedDirectory')) {
            $upload_dir = wp_upload_dir();
            $user_directory = $user_type.'/'.$user_id;
            $target_dir = $upload_dir['basedir'].'/'.$user_directory;
            wp_mkdir_p($target_dir);
        }
        // Save additional fields
        update_user_meta($user_id, BASE . 'user_type', $user_type);
        // Process additional fields from form
        foreach ($data as $key => $value) {
            if (in_array($key, ['name', 'email', 'action', 'request_id', 'user_select', 'cf-turnstile-response'])) {
                continue;
            }
            update_user_meta($user_id, BASE . $key, sanitize_text_field($value));
        }
        // Handle token handlers
        do_action('jvbUserRegistered', $user_id, $email, $data);
        return $this->success([
            'message' => 'Registration successful! Check your email.',
            'user_id' => $user_id
        ]);
    }
    /**************************************************************
        HELPERS
     **************************************************************/
    /**
     * Format user data for response
     */
    protected function formatUserData(WP_User $user): array
    {
        return [
            'id' => $user->ID,
            'username' => $user->user_login,
            'email' => $user->user_email,
            'display_name' => $user->display_name,
            'roles' => $user->roles,
            'capabilities' => [
                'manage_options' => user_can($user, 'manage_options'),
                'skip_moderation' => user_can($user, 'skip_moderation'),
                // Add other relevant capabilities
            ]
        ];
    }
    /**
     * Get login redirect URL based on user role
     */
    protected function getLoginRedirect(WP_User $user): string
    {
        if (user_can($user, 'manage_options')) {
            return admin_url();
        }
        // Redirect to dashboard for members
        if (function_exists('isOurPeople') && isOurPeople()) {
            return home_url('/dash');
        }
        return home_url();
    }
    /**
     * Validate password strength
     */
    protected function validatePassword(string $password): bool|WP_Error
    {
        if (strlen($password) < 8) {
            return new WP_Error(
                'weak_password',
                'Password must be at least 8 characters long'
            );
        }
        // Add additional strength requirements as needed
        // - Must contain uppercase
        // - Must contain number
        // - Must contain special character
        return true;
    }
    /**
     * Check if account is locked out due to failed attempts
     */
    protected function checkAccountLockout(string $username): bool|WP_Error
    {
        if (!defined('JVB_MAX_LOGIN_ATTEMPTS')) {
            return true;
        }
        $cache_key = 'login_attempts_' . md5($username);
        $attempts = $this->cache->get($cache_key);
        if (!$attempts || !isset($attempts['count'])) {
            return true;
        }
        if ($attempts['count'] >= JVB_MAX_LOGIN_ATTEMPTS) {
            $lockout_duration = defined('JVB_LOCKOUT_DURATION')
                ? JVB_LOCKOUT_DURATION
                : 15 * MINUTE_IN_SECONDS;
            $time_remaining = $lockout_duration - (time() - $attempts['timestamp']);
            if ($time_remaining > 0) {
                return new WP_Error(
                    'account_locked',
                    sprintf(
                        'Too many failed login attempts. Please try again in %d minutes.',
                        ceil($time_remaining / 60)
                    )
                );
            }
            // Lockout expired - clear attempts
            $this->cache->delete($cache_key);
            return true;
        }
        return true;
    }
    /**
     * Track failed login attempt
     */
    protected function trackFailedLogin(string $username): void
    {
        $cache_key = 'login_attempts_' . md5($username);
        $attempts = $this->cache->get($cache_key) ?: [
            'count' => 0,
            'timestamp' => time()
        ];
        $attempts['count']++;
        $attempts['timestamp'] = time();
        $lockout_duration = defined('JVB_LOCKOUT_DURATION')
            ? JVB_LOCKOUT_DURATION
            : 15 * MINUTE_IN_SECONDS;
        $this->cache->set($cache_key, $attempts, $lockout_duration);
        // Audit log
        $this->auditLog('failed_login', [
            'username' => $username,
            'attempts' => $attempts['count']
        ]);
    }
    /**
     * Clear failed login attempts on successful login
     */
    protected function clearFailedAttempts(string $username): void
    {
        $cache_key = 'login_attempts_' . md5($username);
        $this->cache->delete($cache_key);
    }
    public function checkEmailExists(WP_REST_Request $request): WP_REST_Response
    {
        $data = $request->get_json_params();
        $email = sanitize_email($data['email'] ?? '');
        return $this->success([
            'exists' => is_email($email) && email_exists($email)
        ]);
    }
    /***********************************************************************
     * HELPER METHODS
     ***********************************************************************/
    protected function checkRequestId(string $request_id): bool
    {
        if (empty($request_id)) {
            return true;
        }
        $cache_key = 'request_' . $request_id;
        if (get_transient($cache_key)) {
            return false;
        }
        set_transient($cache_key, true, 60);
        return true;
    }
    protected function verifyTurnstile(string $token): bool
    {
        if (!Features::hasIntegration('cloudflare') || !JVB()->connect('cloudflare')->isSetUp()) {
            return true;
        }
        if (empty($token)) {
            return false;
        }
        return JVB()->connect('cloudflare')->verifyTurnstile($token);
    }
    /**
     * Helper to return error response
     */
    protected function error(string $message, string $code, int $status = 400, ?string $field = null): WP_REST_Response
    {
        if ($this->requestId) {
            delete_transient('request_'.$this->requestId);
            $this->requestId = null;
        }
        return parent::error($message, $code, $status, $field);
    }
}
inc/rest/routes/MagicLinkRoutes.php
@@ -32,38 +32,38 @@
    public function registerRoutes(): void
    {
        // Send magic link
        register_rest_route($this->namespace, '/magic-link', [
        register_rest_route($this->namespace, '/magic', [
            'methods' => 'POST',
            'callback' => [$this, 'sendMagicLink'],
            'permission_callback' => '__return_true', // Public endpoint
            'args' => [
                'email' => [
                    'required' => true,
                    'type' => 'string',
                    'format' => 'email',
                    'validate_callback' => function($param) {
                        return is_email($param);
                    }
                ],
                'type' => [
                    'required' => false,
                    'type' => 'string',
                    'default' => 'login',
                    'enum' => ['login', 'signup', 'referral', 'reset']
                ],
                'context' => [
                    'required' => false,
                    'type' => 'object',
                    'default' => []
                ]
            ]
            'permission_callback' => [$this, 'checkRateLimit'],
//          'args' => [
//              'email' => [
//                  'required' => true,
//                  'type' => 'string',
//                  'format' => 'email',
//                  'validate_callback' => function($param) {
//                      return is_email($param);
//                  }
//              ],
//              'type' => [
//                  'required' => false,
//                  'type' => 'string',
//                  'default' => 'login',
//                  'enum' => ['login', 'signup', 'referral', 'reset']
//              ],
//              'context' => [
//                  'required' => false,
//                  'type' => 'object',
//                  'default' => []
//              ]
//          ]
        ]);
        // Resend magic link
        register_rest_route($this->namespace, '/magic-link/resend', [
        register_rest_route($this->namespace, '/magic/resend', [
            'methods' => 'POST',
            'callback' => [$this, 'resendMagicLink'],
            'permission_callback' => '__return_true',
            'permission_callback' => [$this, 'checkRateLimit'],
            'args' => [
                'email' => [
                    'required' => true,
@@ -78,7 +78,7 @@
        ]);
        // Check token validity (useful for frontend)
        register_rest_route($this->namespace, '/magic-link/verify', [
        register_rest_route($this->namespace, '/magic/verify', [
            'methods' => 'POST',
            'callback' => [$this, 'verifyToken'],
            'permission_callback' => '__return_true',
@@ -104,7 +104,7 @@
     */
    public function sendMagicLink(WP_REST_Request $request): WP_REST_Response
    {
        $email = sanitize_email($request->get_param('email'));
        $email = sanitize_email($request->get_param('email')??$request->get_param('user_email')??'');
        $type = sanitize_text_field($request->get_param('type'));
        $context = $request->get_param('context') ?? [];
@@ -120,6 +120,16 @@
            ], 400);
        }
        // Check if email exists
        $exists = email_exists($email);
        if (!$exists) {
            return new WP_REST_Response([
                'success' => false,
                'message' => 'User account not found'
            ], 400);
        }
        // Send the magic link
        $result = $this->magic_link->sendMagicLink($email, $type, $context);
        error_log('Result: '.print_r($result, true));
@@ -163,34 +173,30 @@
        $token = sanitize_text_field($request->get_param('token'));
        $email = sanitize_email($request->get_param('email'));
        $cache_key = 'magic_token_' . $token;
        $token_data = get_transient($cache_key);
        // This returns array|WP_Error - check for error first
        $token_data = $this->magic_link->verifyToken($token, $email);
        if (!$token_data) {
        if (is_wp_error($token_data)) {
            return new WP_REST_Response([
                'valid' => false,
                'message' => 'Token expired or invalid'
                'message' => $token_data->get_error_message()
            ], 400);
        }
        if ($token_data['email'] !== $email) {
        // Now check the data
        if (!isset($token_data['email']) || $token_data['email'] !== $email) {
            return new WP_REST_Response([
                'valid' => false,
                'message' => 'Invalid token'
            ], 400);
        }
        if (time() > $token_data['expires_at']) {
            return new WP_REST_Response([
                'valid' => false,
                'message' => 'Token expired'
            ], 400);
        }
        // Check expiration - but your cache-based system doesn't store expires_at
        // If token wasn't expired, it wouldn't have been returned from cache
        // So just return valid:
        return new WP_REST_Response([
            'valid' => true,
            'type' => $token_data['type'],
            'expires_in' => $token_data['expires_at'] - time()
            'type' => $token_data['type'] ?? 'unknown'
        ], 200);
    }
inc/rest/routes/ReferralRoutes.php
@@ -1,6 +1,7 @@
<?php
namespace JVBase\rest\routes;
use JVBase\managers\ReferralManager;
use JVBase\rest\RestRouteManager;
use WP_REST_Request;
use WP_REST_Response;
@@ -15,7 +16,7 @@
 */
class ReferralRoutes extends RestRouteManager
{
    protected ReferralManager $manager;
    public function __construct()
    {
        $this->route = 'referrals';
@@ -31,10 +32,10 @@
            'permission_callback' => [$this, 'checkPermission']
        ]);
        register_rest_route($this->namespace, '/referrals/register', [
        register_rest_route($this->namespace, "/{$this->route}/register", [
            'methods' => 'POST',
            'callback' => [$this, 'registerWithReferral'],
            'permission_callback' => '__return_true',
            'permission_callback' => [$this, 'checkRateLimit'],
            'args' => [
                'name' => [
                    'required' => true,
@@ -62,7 +63,7 @@
        register_rest_route($this->namespace, '/referrals/check-code', [
            'methods' => 'POST',
            'callback' => [$this, 'checkReferralCode'],
            'permission_callback' => '__return_true',
            'permission_callback' => [$this, 'checkRateLimit'],
            'args' => [
                'code' => [
                    'required' => true,
@@ -85,7 +86,7 @@
        register_rest_route($this->namespace, "/{$this->route}/track", [
            'methods' => 'POST',
            'callback' => [$this, 'trackReferralClick'],
            'permission_callback' => '__return_true',
            'permission_callback' => [$this, 'checkRateLimit'],
            'args' => [
                'code' => [
                    'required' => true,
@@ -181,6 +182,8 @@
            ]
        ]);
        // Get top referrers (admin only)
        register_rest_route($this->namespace, "/{$this->route}/leaderboard", [
            'methods' => 'GET',
@@ -217,6 +220,21 @@
                }
            ]
        ]);
        register_rest_route($this->namespace, "/{$this->route}/add-code", [
            'methods'   => 'POST',
            'callback'  => [$this, 'addReferralCodeAfterRegistration'],
            'permission_callback'   => [$this, 'checkRateLimit'],
            'args'  => [
                'code'  => [
                    'required'  => true,
                    'type'      => 'string',
                    'sanitize_callback' => function ($code) {
                        return strtoupper(sanitize_text_field($code));
                    }
                ]
            ]
        ]);
    }
    public function checkPermission(WP_REST_Request $request): bool
@@ -588,6 +606,9 @@
                'message' => 'Invalid referral code'
            ], 404);
        }
        if (is_user_logged_in() && get_current_user_id() === $referrer->ID) {
            return $this->error('You cannot use your own referral code', 'self_referral', 400);
        }
        // Return basic referrer info (no sensitive data)
        return new WP_REST_Response([
@@ -596,4 +617,38 @@
            'referrer_name' => $referrer->display_name,
        ], 200);
    }
    public function addReferralCodePostRegistration(WP_REST_Request $request): WP_REST_Response
    {
        if (!$this->manager) {
            $this->manager = JVB()->referrals();
        }
        $user_id = get_current_user_id();
        $code = $request->get_param('code');
        // Check if user already has a referral (can't change)
        $existing = $this->manager->getReferralByReferee($user_id);
        if ($existing) {
            return $this->error('You already have a referral code applied', 'already_referred', 400);
        }
        // Validate the code exists
        $referrer = $this->manager->getUserByReferralCode($code);
        if (!$referrer) {
            return $this->error('Invalid referral code', 'invalid_code', 400);
        }
        // Create the referral
        $user = wp_get_current_user();
        $result = $this->manager->createReferral($referrer->ID, $user_id, $code);
        if ($result) {
            return $this->success([
                'message' => 'Referral code applied successfully!',
                'referrer_name' => $referrer->display_name
            ]);
        }
        return $this->error('Failed to apply referral code', 'creation_failed', 500);
    }
}
inc/templates.php
@@ -44,7 +44,7 @@
                    '.$textarea.'
                </div>
                <div class="actions row">
                    <button type="button" class="cancel">'.jvbIcon('close', ['title'=>'Cancel']).'</button>
                    <button type="button" class="cancel">'.jvbIcon('x', ['title'=>'Cancel']).'</button>
                    <button type="submit" class="create">Reply</button>
                </div>
            </div>
@@ -86,8 +86,8 @@
            <div class="upload-status">
                <div class="upload-progress"></div>
            </div>
            <button type="button" class="remove-preview" title="Remove Image">'.jvbIcon('delete').'</button>
            <button type="button" class="move-image" title="Reorder Image">'.jvbIcon('grab').'</button>
            <button type="button" class="remove-preview" title="Remove Image">'.jvbIcon('trash').'</button>
            <button type="button" class="move-image" title="Reorder Image">'.jvbIcon('dots-six-vertical').'</button>
        </div>
    </template>';
}
inc/users/UserSettings.php
@@ -535,7 +535,7 @@
            <section class="invite-artists">
                <div class="invited-artist">
                    <button type="button" onclick="removeArtist(this)" title="Remove Invite"><?=jvbIcon('delete')?></button>
                    <button type="button" onclick="removeArtist(this)" title="Remove Invite"><?=jvbIcon('trash')?></button>
                    <div>
                        <label for="invited-artist-name-1">First Name</label>
                        <input type="text" id="invited-artist-name-1" name="invited-artist-name-1">
@@ -551,13 +551,13 @@
                </div>
            </section>
            <div class="actions">
                <button type="button" onclick="addArtist(this)" class="add-artist"><?= jvbIcon('add')?>Add Artist</button>
                <button type="button" onclick="sendInvites(this)" class="send-invites"><?=jvbIcon('email')?>Send Invites</button>
                <button type="button" onclick="addArtist(this)" class="add-artist"><?= jvbIcon('plus-square')?>Add Artist</button>
                <button type="button" onclick="sendInvites(this)" class="send-invites"><?=jvbIcon('envelope')?>Send Invites</button>
            </div>
            <template class="artistInvite">
                <div class="invited-artist">
                    <button type="button" onclick="removeArtist(this)" title="Remove Invite"><?=jvbIcon('delete')?></button>
                    <button type="button" onclick="removeArtist(this)" title="Remove Invite"><?=jvbIcon('trash')?></button>
                    <div>
                        <label for="invited-artist-name">First Name</label>
                        <input type="text" id="invited-artist-name" name="invited-artist-name">
@@ -608,11 +608,11 @@
                        </td>
                        <td class="actions">
                            <button type="button" data-action="revoke" title="Revoke Invitation" onclick="revokeInvite(this)">
                                <?=jvbIcon('delete')?>
                                <?=jvbIcon('trash')?>
                                <span class="screen-reader-text">Revoke invite</span>
                            </button>
                            <button type="button" data-action="resend"<?=($invite['status'] === 'exipired') ? '' : ' disabled'?> title="Resend Invitation" onclick="resendInvite(this)">
                                <?=jvbIcon('refresh')?>
                                <?=jvbIcon('arrows-clockwise')?>
                                <span class="screen-reader-text">Try Again</span>
                            </button>
                        </td>
jvb.php
@@ -10,6 +10,7 @@
*/
use JVBase\JVB;
use JVBase\managers\IconsManager;
use JVBase\utility\Checker;
use JVBase\utility\Features;
@@ -18,14 +19,82 @@
    exit;
}
/**
 * Track REST API errors by wrapping request execution
 */
add_filter('rest_pre_dispatch', function($result, $server, $request) {
    // Store request details globally for error logging
    $GLOBALS['jvb_rest_request'] = [
        'route' => $request->get_route(),
        'method' => $request->get_method(),
        'params' => $request->get_params(),
        'time' => microtime(true),
    ];
    // Set up error handler for this request
    set_error_handler(function($errno, $errstr, $errfile, $errline) {
        if (!(error_reporting() & $errno)) {
            return false;
        }
        $context = array_merge($GLOBALS['jvb_rest_request'] ?? [], [
            'error_type' => $errno,
            'error_message' => $errstr,
            'file' => $errfile,
            'line' => $errline,
            'user_id' => get_current_user_id(),
        ]);
        error_log('JVB REST Error: ' . json_encode($context));
        return false; // Let PHP handle it normally too
    });
    return $result;
}, 10, 3);
add_filter('rest_post_dispatch', function($result, $server, $request) {
    // Restore default error handler
    restore_error_handler();
    // Check for fatal errors that occurred during request
    $error = error_get_last();
    if ($error && in_array($error['type'], [E_ERROR, E_USER_ERROR, E_PARSE, E_COMPILE_ERROR])) {
        $context = array_merge($GLOBALS['jvb_rest_request'] ?? [], [
            'fatal_error' => $error,
            'user_id' => get_current_user_id(),
        ]);
        error_log('JVB REST Fatal: ' . json_encode($context));
    }
    // Clean up
    unset($GLOBALS['jvb_rest_request']);
    return $result;
}, 10, 3);
const JVB_LOCAL = 'northeh.test';
function jvbIgnoredPostTypes():array
{
    return [BASE.'directory', BASE.'dash', 'attachment', 'revision', 'nav_menu_item'];
}
add_filter('show_admin_bar', '__return_false');
const JVB_TESTING = true;
const JVB_DIR = WP_PLUGIN_DIR . '/jvb';
define('JVB_URL', plugin_dir_url(__FILE__));
// Session Security
define('JVB_SESSION_FINGERPRINT', true);
// Login Security
define('JVB_MAX_LOGIN_ATTEMPTS', 5);
define('JVB_LOCKOUT_DURATION', 15 * MINUTE_IN_SECONDS);
require(JVB_DIR.'/base/_setup.php');
//error_log('###############################################');
//error_log('Registered Base');
@@ -54,6 +123,7 @@
global $jvb_everything;
$jvb_everything = array_merge(JVB_CONTENT, JVB_TAXONOMY);
require(JVB_DIR . '/inc/registry/_setup.php');
require(JVB_DIR . '/activate.php');
@@ -61,6 +131,15 @@
require(JVB_DIR . '/inc/helpers/all.php');
require(JVB_DIR . '/inc/meta/_setup.php');
require(JVB_DIR . '/inc/managers/_setup.php');
function jvbIcon($name, $options = []) {
    return IconsManager::getInstance()->getIcon($name, $options);
}
function jvbCSSIcon($name, $options = []) {
    $style = array_key_exists('style', $options) ? $options['style'] : null;
    return IconsManager::getInstance()->getCSSIcon($name, $style);
}
require(JVB_DIR . '/inc/integrations/_setup.php');
require(JVB_DIR . '/inc/rest/_setup.php');
@@ -804,12 +883,12 @@
    wp_enqueue_script('jvb-navigation');
//    wp_enqueue_script('jvb-ui');
    wp_enqueue_script('jvb-media');
    wp_enqueue_script('jvb-gallery');
    wp_enqueue_script('jvb-cache');
    $userID = get_current_user_id();
    $icons = new \JVBase\JVBIcons();
    $queue = (is_user_logged_in()) ?
        [
            'api'             => rest_url('jvb/v1/'),
@@ -818,7 +897,6 @@
            'dash'            => wp_create_nonce('dash-'.$userID),
            'favourites'    => wp_create_nonce('favourites-'.$userID),
            'notifications'    => wp_create_nonce('notifications-'.$userID),
            'icons'            => $icons->localizeIcons(),
            'labels'            => jvbGetLabels(),
        ] :
        [
@@ -826,7 +904,6 @@
            'nonce'       => wp_create_nonce('wp_rest'),
            'currentUser' => false,
            'redirect'    => wp_login_url(home_url(add_query_arg(null, null))), // Current URL as redirect
            'icons'       => $icons->localizeIcons(),
            'labels'            => jvbGetLabels(),
        ];
@@ -1012,7 +1089,27 @@
//add_action('wp_head', 'jvbDumpIt');
function jvbDumpIt()
{
    jvbDump(get_post_type_archive_link(BASE.'faq'));
}
add_action('after_setup_theme', 'jvbImageSize');
function jvbImageSize():void
{
    add_theme_support('post-thumbnails');
    add_image_size( 'tiny', 50, 50, false );
}
//
//add_action('rest_api_init', function() {
//  error_log('User ' . get_current_user_id() . ' can edit pages: ' . (current_user_can('edit_pages') ? 'yes' : 'no'));
//  error_log('User roles: ' . print_r(wp_get_current_user()->roles, true));
//});
//
//add_filter('rest_pre_dispatch', function($result, $server, $request) {
//  if (strpos($request->get_route(), '/wp/v2/pages') !== false) {
//      error_log('Pages request - User: ' . get_current_user_id());
//      error_log('Can edit pages: ' . (current_user_can('edit_pages') ? 'yes' : 'no'));
//      error_log('Nonce: ' . $request->get_header('X-WP-Nonce'));
//  }
//  return $result;
//}, 10, 3);
src/feed/render.php
@@ -195,7 +195,7 @@
                        <div class="filter-actions">
                            <?= jvbRenderToggleTextField('match', 'Match', 'Filters', 'ALL', 'ANY', true) ?>
                            <button type="button" class="clear-filters row" hidden>
                                <?= jvbIcon('close', ['title'    => 'Clear']) ?>
                                <?= jvbIcon('x', ['title'    => 'Clear']) ?>
                                Clear All Filters
                            </button>
                        </div>
@@ -220,7 +220,7 @@
                            <input type="radio" id="order-random" class="btn" name="orderby" value="random">
                            <label for="order-random" title="Random Order">
                                <?= jvbIcon('random') ?>
                                <?= jvbIcon('shuffle') ?>
                                <span class="label">Random</span>
                            </label>
                        </div>
@@ -229,12 +229,12 @@
                    <div class="order-direction radio-group-label" data-for-order="date,title">
                        <input type="radio" id="order-desc" class="btn" name="order" value="desc" checked>
                        <label for="order-desc" title="Newest First">
                            <?= jvbIcon('desc') ?>
                            <?= jvbIcon('sort-descending') ?>
                        </label>
                        <input type="radio" id="order-asc" class="btn" name="order" value="asc">
                        <label for="order-asc" title="Oldest First">
                            <?= jvbIcon('asc') ?>
                            <?= jvbIcon('sort-ascending') ?>
                        </label>
                    </div>
                </div>
src/glossary/view.js
@@ -14,7 +14,6 @@
        this.navList = this.nav.querySelector('ul');
        this.activeClass = 'active';
        this.currentActive = null;
        this.breakpoint = 768; // Adjust this to match your small screen breakpoint
        this.init();
        this.setupResizeHandler();
@@ -42,13 +41,6 @@
    }
    getRootMargin() {
        if (window.innerWidth < this.breakpoint) {
            // On small screens: 5rem from top and bottom
            // Convert rem to pixels
            const remInPixels = parseFloat(getComputedStyle(document.documentElement).fontSize);
            const margin = Math.round(remInPixels * 5); // 5rem in pixels
            return `-${margin}px 0px -${margin}px 0px`;
        }
        // On larger screens: centered (50% from top and bottom)
        return '-50% 0px -50% 0px';
    }
@@ -94,16 +86,11 @@
        this.terms.forEach(term => {
            const rect = term.getBoundingClientRect();
            // Check if term is within the trigger zone (4rem from top or bottom on small screens)
            const isInZone = window.innerWidth < this.breakpoint
                ? rect.top >= margin && rect.top <= window.innerHeight - margin
                : rect.top + rect.height / 2 >= 0 && rect.top + rect.height / 2 <= window.innerHeight;
            const isInZone = rect.top + rect.height / 2 >= 0 && rect.top + rect.height / 2 <= window.innerHeight;
            if (isInZone) {
                // Find closest to the trigger point
                const triggerPoint = window.innerWidth < this.breakpoint
                    ? margin  // Top edge of zone on small screens
                    : window.innerHeight / 2;  // Center on large screens
                const triggerPoint = window.innerHeight / 2;
                const distance = Math.abs(rect.top - triggerPoint);
src/summary/render.php
@@ -52,11 +52,11 @@
    <nav id="artist" class="on-this-page index">
        <label>Jump to:
            <button type="button" aria-label="Show Index" title="Show Index" class="toggle" aria-expanded="false">
                <?= jvbIcon('add')?>
                <?= jvbIcon('plus-square')?>
            </button>
        </label>
        <ul>
            <li><a href="#top" title="Back to Top"><?=jvbIcon('up')?></a></li>
            <li><a href="#top" title="Back to Top"><?=jvbIcon('caret-circle-up')?></a></li>
            <li><a href="#about">About</a></li>
            <li><a href="#styles">Styles</a></li>
            <li><a href="#contact">Contact</a></li>
@@ -184,11 +184,11 @@
    <nav id="shop" class="on-this-page index">
        <label>Jump to:
            <button type="button" aria-label="Show Index" title="Show Index" class="toggle" aria-expanded="false">
                <?= jvbIcon('add')?>
                <?= jvbIcon('plus-square')?>
            </button>
        </label>
        <ul>
            <li><a href="#top" title="Back to Top"><?=jvbIcon('up')?></a></li> <?php
            <li><a href="#top" title="Back to Top"><?=jvbIcon('caret-circle-up')?></a></li> <?php
            if ($rating !== 'none') {
                ?>
                <li><a href="#rating">Rating</a></li>
templates/dashboard/sections/favourites.php
@@ -35,7 +35,7 @@
            <div class="controls">
                <div class="type-filters">
                    <button type="button" class="type-filter active" data-type="all">
                        <?= jvbIcon('all'); ?>
                        <?= jvbIcon('infinity'); ?>
                        <span>Everything</span>
                        <span class="count">(<?= array_sum($counts); ?>)</span>
                    </button>
@@ -62,10 +62,10 @@
                <div class="view-controls">
                    <button type="button" class="view-toggle" data-view="grid" aria-pressed="true">
                        <?= jvbIcon('grid'); ?>
                        <?= jvbIcon('squares-four'); ?>
                    </button>
                    <button type="button" class="view-toggle" data-view="list" aria-pressed="false">
                        <?= jvbIcon('list'); ?>
                        <?= jvbIcon('rows'); ?>
                    </button>
                </div>
            </div>
@@ -89,7 +89,7 @@
                </select>
                <button type="button" class="apply-bulk">Apply</button>
                <button type="button" class="cancel-bulk">
                    <?= jvbIcon('close', ['title'=>'Cancel']); ?>
                    <?= jvbIcon('x', ['title'=>'Cancel']); ?>
                </button>
            </div>
        </div>
@@ -99,7 +99,7 @@
        <div class="tab-content" data-tab="lists" role="tabpanel" aria-hidden="true">
            <div class="lists-header">
                <button type="button" class="create-list-btn" onclick="window.userFavouritesManager.showCreateListModal()">
                    <?= jvbIcon('add', ['title'=>'Create List']); ?>
                    <?= jvbIcon('plus-square', ['title'=>'Create List']); ?>
                    <span>Create List</span>
                </button>
            </div>
@@ -122,11 +122,11 @@
                        <span>Share</span>
                    </button>
                    <button type="button" class="duplicate-list" title="Duplicate List">
                        <?= jvbIcon('copy')?>
                        <?= jvbIcon('copy-simple')?>
                        <span>Duplicate</span>
                    </button>
                    <button type="button" class="delete-list" title="Delete List">
                        <?= jvbIcon('delete')?>
                        <?= jvbIcon('trash')?>
                        <span>Delete</span>
                    </button>
                </div>
@@ -148,7 +148,7 @@
                        </select>
                        <button type="button" class="apply-bulk">Apply</button>
                        <button type="button" class="cancel-bulk">
                            <?= jvbIcon('close', ['title'=>'Cancel']); ?>
                            <?= jvbIcon('x', ['title'=>'Cancel']); ?>
                        </button>
                    </div>
                </div>
@@ -223,7 +223,7 @@
            <div class="input-with-button">
                <input type="email" id="share-email" name="share_email" placeholder="Enter email address">
                <button type="button" class="add-email">
                    <?= jvbIcon('add', ['title'=>'Add share access'])?>
                    <?= jvbIcon('plus-square', ['title'=>'Add share access'])?>
                </button>
            </div>
        </div>
templates/dashboard/sections/news.php
@@ -37,7 +37,7 @@
    <!-- Tab navigation -->
    <div class="tabs" role="tablist">
        <button type="button" class="tab active" data-tab="news" role="tab" aria-selected="true">
            <h2><?= jvbIcon('news')?>News</h2>
            <h2><?= jvbIcon('newspaper')?>News</h2>
        </button>
        <button type="button" class="tab" data-tab="mine" role="tab" aria-selected="false">
            <h2>Yours</h2>
@@ -47,7 +47,7 @@
        </button>
        <button type="button" class="add-item-btn">
            <?= jvbIcon('add', ['title' =>'Add News']) ?>
            <?= jvbIcon('plus-square', ['title' =>'Add News']) ?>
            <span>Post</span>
        </button>
    </div>
@@ -119,7 +119,7 @@
    <details class="type-filters">
        <summary class="row btw">Filters:
            <button class="clear-filters row">
                <?= jvbIcon('close', ['title' => 'Clear Filters'])?>
                <?= jvbIcon('x', ['title' => 'Clear Filters'])?>
                <span>Clear Filters</span>
                </span>
            </button>
@@ -202,13 +202,13 @@
                        <input type="radio" id="order-karma" class="btn" name="orderby" value="karma">
                        <label for="order-karma" title="Order by Standing" class="row">
                            <?= jvbIcon('karma') ?>
                            <?= jvbIcon('scales') ?>
                            <span class="label">Standing</span>
                        </label>
                        <input type="radio" id="order-random" class="btn" name="orderby" value="random">
                        <label for="order-random" title="Random Order" class="row">
                            <?= jvbIcon('random') ?>
                            <?= jvbIcon('shuffle') ?>
                            <span class="label">Random</span>
                        </label>
                    </div>
@@ -217,12 +217,12 @@
                <div class="order-direction radio-group-label" data-for-order="date,title,karma">
                    <input type="radio" id="order-desc" class="btn" name="order" value="DESC" checked>
                    <label for="order-desc" title="Newest First" class="row">
                        <?= jvbIcon('desc') ?>
                        <?= jvbIcon('sort-descending') ?>
                    </label>
                    <input type="radio" id="order-asc" class="btn" name="order" value="ASC">
                    <label for="order-asc" title="Oldest First" class="row">
                        <?= jvbIcon('asc') ?>
                        <?= jvbIcon('sort-ascending') ?>
                    </label>
                </div>
            </div>
@@ -297,11 +297,11 @@
            </div>
            <div class="item-actions">
                <button type="button" class="action" data-action="edit" title="Edit tattoo">
                    <?= jvbIcon('edit') ?>
                    <?= jvbIcon('pencil-simple') ?>
                    <span class="screen-reader-text">Edit news item</span>
                </button>
                <button type="button" class="action" data-action="trash" title="Scrap tattoo">
                    <?= jvbIcon('delete') ?>
                    <?= jvbIcon('trash') ?>
                    <span class="screen-reader-text">Scrap news item</span>
                </button>
                <button type="button" class="action" data-action="toggle-status">